Source code for pennylane.labs.transforms.decomp_selectpaulirot_phase_gradient
# Copyright 2026 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""
Decomposition rule for SelectPauliRot in terms of `phase gradient states <https://pennylane.ai/compilation/phase-gradient/d-multiplex-rotations>`__
"""
import pennylane as qml
from pennylane.decomposition import (
adjoint_resource_rep,
change_op_basis_resource_rep,
controlled_resource_rep,
resource_rep,
)
from pennylane.ops import Prod
from .rot_to_phase_gradient import _select_pauli_rot_phase_gradient
[docs]
def make_selectpaulirot_to_phase_gradient_decomp(angle_wires, phase_grad_wires, work_wires):
r"""
Custom decomposition rule for :class:`~.SelectPauliRot` gates
This is a temporary workaround as long as capture does not work, which blocks usage of dynamic allocation.
Here, we explicitly provide the necessary wires for the `phase gradient decomposition of SelectPauliRot <https://pennylane.ai/compilation/phase-gradient/d-multiplex-rotations>`__.
This way, this function can be used in a workflow context that explicitly uses those wires to generate this decomposition rule, which can then be used
as ``alt_decomps`` or ``fixed_decomp`` within :func:`~decompose`.
Parameters:
angle_wires (Wires): wires that encode the binary representation of the rotation angle
phase_grad_wires (Wires): wires that carry a phase gradient state
work_wires (Wires): additional work wires for :class:`~SemiAdder` decomposition
Returns:
func: decomposition rule to be used within :func:`~decompose`.
.. seealso:: :func:`~make_rz_to_phase_gradient_decomp`
**Example**
In this example we decompose a circuit containing only a single :class:`~RZ` gate using the custom decomposition rule
that we generate from within the context of the example, where all auxiliary wires exist.
.. code-block:: python
import pennylane as qp
from pennylane.labs.transforms import make_selectpaulirot_to_phase_gradient_decomp
import numpy as np
qp.decomposition.enable_graph()
prec = 3
angles = np.random.rand(2**3)
angle_wires = qp.wires.Wires([f"aux_{i}" for i in range(prec)])
phase_grad_wires = qp.wires.Wires([f"qft_{i}" for i in range(prec)])
work_wires = qp.wires.Wires([f"work_{i}" for i in range(prec - 1)])
custom_decomp = make_selectpaulirot_to_phase_gradient_decomp(angle_wires, phase_grad_wires, work_wires)
@qp.transforms.decompose(
gate_set={"QROM", "Adjoint(QROM)", "SemiAdder", "CNOT", "X", "Adjoint(X)", "GlobalPhase"},
fixed_decomps={qp.SelectPauliRot: custom_decomp}
)
@qp.qnode(qp.device("null.qubit"))
def circuit(angles):
qp.SelectPauliRot(angles, control_wires=range(3), target_wire=3)
return qp.state()
specs = qp.specs(circuit)(angles)["resources"].gate_types
The resulting circuit corresponds to the `phase gradient decomposition <https://pennylane.ai/compilation/phase-gradient/d-multiplex-rotations>`__ of RZ,
containing two CNOT fanouts corresponding to the binary representation of the angle (111 in this case), the :class:`~SemiAdder`, and a :class:`~GlobalPhase`.
>>> specs
{'ChangeOpBasis': 1}
>>> print(qp.draw(circuit, wire_order=[0, 1, 2, 3] + angle_wires + phase_grad_wires + work_wires)(angles))
0: ─╭QROM(M0)───────────────────────────────────────╭QROM(M0)†─┤ State
1: ─├QROM(M0)───────────────────────────────────────├QROM(M0)†─┤ State
2: ─├QROM(M0)───────────────────────────────────────├QROM(M0)†─┤ State
3: ─│─────────╭●────╭●────╭●───────────────╭●─╭●─╭●─│──────────┤ State
aux_0: ─├QROM(M0)─│─────│─────│─────╭SemiAdder─│──│──│──├QROM(M0)†─┤ State
aux_1: ─├QROM(M0)─│─────│─────│─────├SemiAdder─│──│──│──├QROM(M0)†─┤ State
aux_2: ─╰QROM(M0)─│─────│─────│─────├SemiAdder─│──│──│──╰QROM(M0)†─┤ State
qft_0: ───────────╰X──X─│─────│─────├SemiAdder─│──│──╰X──X─────────┤ State
qft_1: ─────────────────╰X──X─│─────├SemiAdder─│──╰X──X────────────┤ State
qft_2: ───────────────────────╰X──X─├SemiAdder─╰X──X───────────────┤ State
work_0: ─────────────────────────────├SemiAdder─────────────────────┤ State
work_1: ─────────────────────────────╰SemiAdder─────────────────────┤ State
"""
def _resource_fn(num_wires, rot_axis):
# decomposition costs, using information about angle_wires etc from the outer scope
num_control_wires = num_wires - 1
if num_control_wires == 0:
match rot_axis:
case "X":
return {resource_rep(qml.RX): 1}
case "Y":
return {resource_rep(qml.RY): 1}
case "Z":
return {resource_rep(qml.RZ): 1}
# 1. QROM compressed rep
qrom_rep = resource_rep(
qml.QROM,
clean=True,
num_bitstrings=2**num_control_wires,
num_control_wires=num_control_wires,
num_target_wires=len(angle_wires),
num_work_wires=num_control_wires - 1,
)
# 2. ctrl(X, control=target_wire, control_values=[0])
# -> Controlled X with 1 control, 1 zero-ctrl
ctrl_x_rep = controlled_resource_rep(
qml.X, base_params={}, num_control_wires=1, num_zero_control_values=1
)
# 3. Prod: MUST be a dict {CompressedResourceOp: count}
prod_res = {
qrom_rep: 1,
ctrl_x_rep: len(phase_grad_wires),
}
prod_rep = resource_rep(Prod, resources=prod_res)
# 4. SemiAdder as the target_op
semi_adder_rep = resource_rep(qml.SemiAdder, num_y_wires=len(phase_grad_wires))
# 5. change_op_basis(compute_op, target_op)
# compute_op = prod (the QROM + ctrl-X product)
# target_op = SemiAdder
change_basis_rep = change_op_basis_resource_rep(
compute_op=prod_rep,
target_op=semi_adder_rep,
uncompute_op=prod_rep,
)
# 6. Basis adaptation depending on rot_axis
match rot_axis:
case "X":
change_basis_rep_basis_adapted = change_op_basis_resource_rep(
resource_rep(qml.Hadamard),
change_basis_rep,
resource_rep(qml.Hadamard),
)
case "Y":
comp_rep = resource_rep(
Prod,
resources={
resource_rep(qml.Hadamard): 1,
adjoint_resource_rep(qml.S): 1,
},
)
uncomp_rep = resource_rep(
Prod,
resources={
resource_rep(qml.S): 1,
resource_rep(qml.Hadamard): 1,
},
)
change_basis_rep_basis_adapted = change_op_basis_resource_rep(
comp_rep, change_basis_rep, uncomp_rep
)
case "Z":
change_basis_rep_basis_adapted = change_basis_rep
return {change_basis_rep_basis_adapted: 1}
@qml.register_resources(_resource_fn)
def _decomp_fn(angles, control_wires, target_wire, rot_axis, **_):
if len(control_wires) == 0:
assert len(angles) == 1
match rot_axis:
case "X":
qml.RX(angles[0], target_wire)
case "Y":
qml.RY(angles[0], target_wire)
case "Z":
qml.RZ(angles[0], target_wire)
return
with qml.QueuingManager.stop_recording():
pg_op = _select_pauli_rot_phase_gradient(
angles,
rot_axis,
control_wires=control_wires,
target_wire=target_wire,
angle_wires=angle_wires,
phase_grad_wires=phase_grad_wires,
work_wires=work_wires,
)
qml.apply(pg_op)
return _decomp_fn
_modules/pennylane/labs/transforms/decomp_selectpaulirot_phase_gradient
Download Python script
Download Notebook
View on GitHub