From 7b37f5bd8ef070a448587f18c2cbfa18a1385f22 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 14:56:32 -0400 Subject: [PATCH 01/16] [Decomposition] Clean up custom logic for adjoint and pow --- .../decomposition/decomposition_graph.py | 123 +++---- pennylane/decomposition/decomposition_rule.py | 6 + pennylane/decomposition/resources.py | 2 +- .../decomposition/symbolic_decomposition.py | 318 +++++++----------- pennylane/ops/functions/assert_valid.py | 4 + pennylane/ops/identity.py | 17 + pennylane/ops/op_math/controlled.py | 26 +- pennylane/ops/op_math/controlled_ops.py | 49 ++- pennylane/ops/qubit/arithmetic_ops.py | 3 + pennylane/ops/qubit/matrix_ops.py | 100 ++++-- pennylane/ops/qubit/non_parametric_ops.py | 168 ++++++++- .../ops/qubit/parametric_ops_multi_qubit.py | 47 ++- .../ops/qubit/parametric_ops_single_qubit.py | 46 ++- pennylane/ops/qubit/qchem_ops.py | 21 ++ tests/decomposition/conftest.py | 32 +- .../decomposition/test_decomposition_graph.py | 71 +--- tests/decomposition/test_resources.py | 2 +- .../test_symbolic_decomposition.py | 245 ++++++++------ 18 files changed, 788 insertions(+), 492 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 66a63a42b23..9947d0ebb7e 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -40,17 +40,19 @@ controlled_global_phase_decomp, controlled_x_decomp, ) -from .decomposition_rule import DecompositionRule, list_decomps +from .decomposition_rule import DecompositionRule, list_decomps, null_decomp from .resources import CompressedResourceOp, Resources, resource_rep from .symbolic_decomposition import ( - AdjointDecomp, - adjoint_controlled_decomp, - adjoint_pow_decomp, + adjoint_rotation, cancel_adjoint, + decompose_to_base, + flip_pow_adjoint, + make_adjoint_decomp, merge_powers, + pow_of_self_adjoint, + pow_rotation, repeat_pow_base, - same_type_adjoint_decomp, - same_type_adjoint_ops, + self_adjoint, ) from .utils import DecompositionError, DecompositionNotApplicable, translate_op_alias @@ -164,10 +166,24 @@ def _get_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRul decomps = self._alt_decomps.get(op_name, []) + list_decomps(op_name) - if issubclass(op.op_type, qml.ops.Adjoint): + if ( + issubclass(op.op_type, qml.ops.Adjoint) + and self_adjoint not in decomps + and adjoint_rotation not in decomps + ): + # Only when the base op is not self-adjoint or a rotation with a single rotation + # angle that could be trivially negated do we need to apply adjoint to each of + # the base op's decomposition rules. decomps.extend(self._get_adjoint_decompositions(op)) - elif issubclass(op.op_type, qml.ops.Pow): + elif ( + issubclass(op.op_type, qml.ops.Pow) + and pow_rotation not in decomps + and pow_of_self_adjoint not in decomps + ): + # Only when the operator is not self-adjoint or a rotation with a single rotation + # angle that could be trivially multiplied with the power do we need to apply the + # power to each of the base op's decomposition rules. decomps.extend(self._get_pow_decompositions(op)) elif op.op_type in (qml.ops.Controlled, qml.ops.ControlledOp): @@ -180,10 +196,10 @@ def _construct_graph(self, operations): for op in operations: if isinstance(op, Operator): op = resource_rep(type(op), **op.resource_params) - idx = self._recursively_add_op_node(op) + idx = self._add_op_node(op) self._original_ops_indices.add(idx) - def _recursively_add_op_node(self, op_node: CompressedResourceOp) -> int: + def _add_op_node(self, op_node: CompressedResourceOp) -> int: """Recursively adds an operation node to the graph. An operator node is uniquely defined by its operator type and resource parameters, which @@ -202,56 +218,63 @@ def _recursively_add_op_node(self, op_node: CompressedResourceOp) -> int: return op_node_idx for decomposition in self._get_decompositions(op_node): - self._add_decomp_rule_to_op(decomposition, op_node, op_node_idx) + self._add_decomp(decomposition, op_node, op_node_idx) return op_node_idx - def _add_decomp_rule_to_op( - self, rule: DecompositionRule, op_node: CompressedResourceOp, op_node_idx: int - ): - """Adds a special decomposition rule to the graph.""" + def _add_decomp(self, rule: DecompositionRule, op: CompressedResourceOp, op_idx: int): + """Adds a decomposition rule to the graph.""" try: - decomp_resource = rule.compute_resources(**op_node.params) - d_node_idx = self._recursively_add_decomposition_node(rule, decomp_resource) - self._graph.add_edge(d_node_idx, op_node_idx, 0) + decomp_resource = rule.compute_resources(**op.params) + d_node = _DecompositionNode(rule, decomp_resource) + d_node_idx = self._graph.add_node(d_node) + if not decomp_resource.gate_counts: + # If an operator decomposes to nothing (e.g., a Hadamard raised to a + # power of 2), we must still connect something to this decomposition + # node so that it is accounted for. + self._graph.add_edge(self._start, d_node_idx, 0) + for op in decomp_resource.gate_counts: + op_node_idx = self._add_op_node(op) + self._graph.add_edge(op_node_idx, d_node_idx, (op_node_idx, d_node_idx)) + self._graph.add_edge(d_node_idx, op_idx, 0) except DecompositionNotApplicable: pass # ignore decompositions that are not applicable to the given op params. - def _get_adjoint_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: - """Retrieves a list of decomposition rules for an adjoint operator.""" + def _get_adjoint_decompositions(self, op) -> list[DecompositionRule]: + """Gets the decomposition rules for the adjoint of an operator.""" base_class, base_params = op.params["base_class"], op.params["base_params"] + # Special case: adjoint of an adjoint cancels out if issubclass(base_class, qml.ops.Adjoint): return [cancel_adjoint] - if ( - issubclass(base_class, qml.ops.Pow) - and base_params["base_class"] in same_type_adjoint_ops() - ): - return [adjoint_pow_decomp] - - if base_class in same_type_adjoint_ops(): - return [same_type_adjoint_decomp] - - if ( - issubclass(base_class, qml.ops.Controlled) - and base_params["base_class"] in same_type_adjoint_ops() - ): - return [adjoint_controlled_decomp] - - base_rep = resource_rep(base_class, **base_params) - return [AdjointDecomp(base_rule) for base_rule in self._get_decompositions(base_rep)] + # General case: apply adjoint to each of the base op's decomposition rules. + base = resource_rep(base_class, **base_params) + return [make_adjoint_decomp(base_decomp) for base_decomp in self._get_decompositions(base)] @staticmethod - def _get_pow_decompositions(op: CompressedResourceOp) -> list[DecompositionRule]: - """Retrieves a list of decomposition rules for a power operator.""" + def _get_pow_decompositions(op) -> list[DecompositionRule]: + """Gets the decomposition rules for the power of an operator.""" base_class = op.params["base_class"] + # Special case: power of zero + if op.params["z"] == 0: + return [null_decomp] + + if op.params["z"] == 1: + return [decompose_to_base] + + # Special case: power of a power if issubclass(base_class, qml.ops.Pow): return [merge_powers] + # Special case: power of an adjoint + if issubclass(base_class, qml.ops.Adjoint): + return [flip_pow_adjoint] + + # General case: repeat the operator z times return [repeat_pow_base] def _get_controlled_decompositions(self, op: CompressedResourceOp) -> list[DecompositionRule]: @@ -277,28 +300,6 @@ def _get_controlled_decompositions(self, op: CompressedResourceOp) -> list[Decom base_rep = resource_rep(base_class, **op.params["base_params"]) return [ControlledBaseDecomposition(rule) for rule in self._get_decompositions(base_rep)] - def _recursively_add_decomposition_node( - self, rule: DecompositionRule, decomp_resource: Resources - ) -> int: - """Recursively adds a decomposition node to the graph. - - A decomposition node is defined by a decomposition rule and a first-order resource estimate - of this decomposition as computed with resource params passed from the operator node. - - """ - - d_node = _DecompositionNode(rule, decomp_resource) - d_node_idx = self._graph.add_node(d_node) - if not decomp_resource.gate_counts: - # If an operator decomposes to nothing (e.g., a Hadamard raised to a - # power of 2), we must still connect something to this decomposition - # node so that it is accounted for. - self._graph.add_edge(self._start, d_node_idx, 0) - for op in decomp_resource.gate_counts: - op_node_idx = self._recursively_add_op_node(op) - self._graph.add_edge(op_node_idx, d_node_idx, (op_node_idx, d_node_idx)) - return d_node_idx - def solve(self, lazy=True): """Solves the graph using the Dijkstra search algorithm. diff --git a/pennylane/decomposition/decomposition_rule.py b/pennylane/decomposition/decomposition_rule.py index 9625c4973de..79787656af9 100644 --- a/pennylane/decomposition/decomposition_rule.py +++ b/pennylane/decomposition/decomposition_rule.py @@ -381,3 +381,9 @@ def has_decomp(op_type: Type[Operator] | str) -> bool: op_type = op_type.__name__ op_type = translate_op_alias(op_type) return op_type in _decompositions and len(_decompositions[op_type]) > 0 + + +@register_resources({}) +def null_decomp(*_, **__): + """A decomposition rule that does nothing.""" + return diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py index d4f71c06aaa..9c84258c7dd 100644 --- a/pennylane/decomposition/resources.py +++ b/pennylane/decomposition/resources.py @@ -326,7 +326,7 @@ def controlled_resource_rep( num_control_wires += base_params["num_control_wires"] num_zero_control_values += base_params["num_zero_control_values"] num_work_wires += base_params["num_work_wires"] - base_params = base_params["base"].resource_params + base_params = {"num_wires": base_params["num_target_wires"]} return CompressedResourceOp( qml.ops.Controlled, diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index dc4acf40084..2b576e69373 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -16,238 +16,166 @@ from __future__ import annotations -import functools +import numpy as np import pennylane as qml -from .controlled_decomposition import base_to_custom_ctrl_op from .decomposition_rule import DecompositionRule, register_resources from .resources import adjoint_resource_rep, pow_resource_rep, resource_rep +from .utils import DecompositionNotApplicable -class AdjointDecomp(DecompositionRule): # pylint: disable=too-few-public-methods - """The adjoint version of a decomposition rule.""" +def make_adjoint_decomp(base_decomposition: DecompositionRule): + """Create a decomposition rule for the adjoint of a decomposition rule.""" - def __init__(self, base_decomposition: DecompositionRule): - self._base_decomposition = base_decomposition - super().__init__(self._get_impl(), self._get_resource_fn()) + def _resource_fn(base_class, base_params): # pylint: disable=unused-argument + base_resources = base_decomposition.compute_resources(**base_params) + return { + adjoint_resource_rep(decomp_op.op_type, decomp_op.params): count + for decomp_op, count in base_resources.gate_counts.items() + } - def _get_impl(self): - """The implementation of the adjoint of a gate.""" + @register_resources(_resource_fn) + def _impl(*params, wires, base, **__): + # pylint: disable=protected-access + qml.adjoint(base_decomposition._impl)(*params, wires=wires, **base.hyperparameters) - def _impl(*params, wires, base, **__): - qml.adjoint(self._base_decomposition._impl)( # pylint: disable=protected-access - *params, wires, **base.hyperparameters - ) + return _impl - return _impl - def _get_resource_fn(self): - """The resource function of the adjoint of a gate.""" +def _cancel_adjoint_resource(*_, base_params, **__): + # The base of a nested adjoint is an adjoint, so the base of the base is the original operator, + # and the "base_params" of base_params are the resource params of the original operator. + base_class, base_params = base_params["base_class"], base_params["base_params"] + return {resource_rep(base_class, **base_params): 1} - def _resource_fn(base_class, base_params): # pylint: disable=unused-argument - base_resources = self._base_decomposition.compute_resources(**base_params) - return { - adjoint_resource_rep(decomp_op.op_type, decomp_op.params): count - for decomp_op, count in base_resources.gate_counts.items() - } - return _resource_fn +# pylint: disable=protected-access,unused-argument +@register_resources(_cancel_adjoint_resource) +def cancel_adjoint(*params, wires, base): + """Decompose the adjoint of the adjoint of an operator.""" + _, struct = base.base._flatten() + new_struct = (wires, *struct[1:]) + base.base._unflatten(params, new_struct) -def _same_type_adjoint_resource(base_class, base_params): - """Resources of the adjoint of a gate whose adjoint is an instance of its own type.""" - # This assumes that the adjoint of the gate has the same resources as the gate itself. +def _adjoint_rotation(base_class, base_params, **__): return {resource_rep(base_class, **base_params): 1} -@register_resources(_same_type_adjoint_resource) -def same_type_adjoint_decomp(*_, base, **__): - """Decompose the adjoint of a gate whose adjoint is an instance of its own type.""" - base.adjoint() +@register_resources(_adjoint_rotation) +def adjoint_rotation(phi, wires, base, **__): + """Decompose the adjoint of a rotation operator by negating the angle.""" + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten((-phi,), new_struct) -def _adjoint_adjoint_resource(*_, base_params, **__): - """Resources of the adjoint of the adjoint of a gate.""" - # The base of a nested adjoint is an adjoint, so the base of the base is - # the original gate, and the "base_params" of base_params is the parameters - # of the original gate. - base_class, base_params = base_params["base_class"], base_params["base_params"] - return {resource_rep(base_class, **base_params): 1} +def is_integer(x): + """Checks if x is an integer.""" + return isinstance(x, int) or np.issubdtype(getattr(x, "dtype", None), np.integer) -@register_resources(_adjoint_adjoint_resource) -def cancel_adjoint(*params, wires, base): # pylint: disable=unused-argument - """Decompose the adjoint of the adjoint of a gate.""" - _, [_, metadata] = base.base._flatten() # pylint: disable=protected-access - new_struct = wires, metadata - base.base._unflatten(params, new_struct) # pylint: disable=protected-access +def _repeat_pow_base_resource(base_class, base_params, z): + if (not is_integer(z)) or z < 0: + raise DecompositionNotApplicable + return {resource_rep(base_class, **base_params): z} -def _adjoint_controlled_resource(base_class, base_params): - """Resources of the adjoint of a controlled gate whose base has adjoint.""" +@register_resources(_repeat_pow_base_resource) +def repeat_pow_base(*params, wires, base, z, **__): + """Decompose the power of an operator by repeating the base operator. Assumes z + is a non-negative integer.""" - num_control_wires = base_params["num_control_wires"] - controlled_base_class = base_params["base_class"] + @qml.for_loop(0, z) + def _loop(i): + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten(params, new_struct) - # Handle controlled-X gates, the adjoint is just themselves - if controlled_base_class is qml.X: - if num_control_wires == 1: - return {resource_rep(qml.CNOT): 1} - if num_control_wires == 2: - return {resource_rep(qml.Toffoli): 1} - return { - resource_rep( - qml.MultiControlledX, - num_control_wires=num_control_wires, - num_zero_control_values=base_params["num_zero_control_values"], - num_work_wires=base_params["num_work_wires"], - ): 1 - } + _loop() - # Handle custom controlled gates. The adjoint of a general controlled operator that - # is equivalent to a custom controlled operator should just be the custom controlled - # operator given that its base has_adjoint. - custom_op_type = base_to_custom_ctrl_op().get((controlled_base_class, num_control_wires)) - if custom_op_type is not None: - # All gates in base_to_custom_ctrl_op do not have resource params. - return {resource_rep(custom_op_type): 1} - - # Handle the general case, here we assume that the adjoint of a controlled gate - # whose base has an adjoint that is of its own type, should have the same resource - # rep as the controlled gate itself. For example, Adjoint(Controlled(O)) should - # have the same resources as Controlled(O) if the adjoint of O is another O. - return {resource_rep(base_class, **base_params): 1} +def _merge_powers_resource(base_class, base_params, z): # pylint: disable=unused-argument + return { + pow_resource_rep( + base_params["base_class"], + base_params["base_params"], + z * base_params["z"], + ): 1 + } -@register_resources(_adjoint_controlled_resource) -def adjoint_controlled_decomp(*_, base, **__): - """Decompose the adjoint of a controlled gate whose base has adjoint. - Precondition: - - isinstance(base, qml.ops.Controlled) and base.base.has_adjoint +@register_resources(_merge_powers_resource) +def merge_powers(*params, wires, base, z, **__): + """Decompose nested powers by combining them.""" + _, struct = base.base._flatten() + new_struct = (wires, *struct[1:]) + base_op = base.base._unflatten(params, new_struct) + qml.pow(base_op, z * base.z) - """ - qml.ctrl( - base.base.adjoint(), - control=base.control_wires, - control_values=base.control_values, - work_wires=base.work_wires, - ) +def _flip_pow_adjoint_resource(base_class, base_params, z): # pylint: disable=unused-argument + # base class is adjoint, and the base of the base is the target class + target_class, target_params = base_params["base_class"], base_params["base_params"] + return { + adjoint_resource_rep( + qml.ops.Pow, {"base_class": target_class, "base_params": target_params, "z": z} + ): 1 + } -def _adjoint_pow_resource(base_class, base_params): # pylint: disable=unused-argument - """Resources of the adjoint of the power of a gate whose adjoint is of the same type.""" - base, base_params, z = base_params["base_class"], base_params["base_params"], base_params["z"] - # The adjoint of the base is assumed to be of the same type as the base. - return {pow_resource_rep(base, base_params, z): 1} +@register_resources(_flip_pow_adjoint_resource) +def flip_pow_adjoint(*params, wires, base, z, **__): + """Decompose the power of an adjoint by power to the base of the adjoint and + then taking the adjoint of the power.""" + _, struct = base.base._flatten() + new_struct = (wires, *struct[1:]) + base_op = base.base._unflatten(params, new_struct) + qml.adjoint(qml.pow(base_op, z)) -@register_resources(_adjoint_pow_resource) -def adjoint_pow_decomp(*_, base, **__): - """Decompose the adjoint of the power of a gate that has its own adjoint.""" - qml.pow(base.base.adjoint(), z=base.z) +def _pow_self_adjoint_resource(base_class, base_params, z): # pylint: disable=unused-argument + if (not is_integer(z)) or z < 0: + raise DecompositionNotApplicable + return {resource_rep(base_class, **base_params): z % 2} -def _pow_resource(base_class, base_params, z): - """Resources of the power of a gate.""" - if not isinstance(z, int) or z < 0: - raise NotImplementedError("Non-integer or negative powers are not supported yet.") - return {resource_rep(base_class, **base_params): z} +@register_resources(_pow_self_adjoint_resource) +def pow_of_self_adjoint(*params, wires, base, z, **__): + """Decompose the power of a self-adjoint operator, assumes z is an integer.""" -@register_resources(_pow_resource) -def repeat_pow_base(*_, base, z, **__): - """Decompose the power of a gate.""" - assert isinstance(z, int) and z >= 0 - for _ in range(z): - base._unflatten(*base._flatten()) # pylint: disable=protected-access - - -def _pow_pow_resource(base_class, base_params, z): # pylint: disable=unused-argument - """Resources of the power of the power of a gate.""" - base_class, base_params, base_z = ( - base_params["base_class"], - base_params["base_params"], - base_params["z"], - ) - return {pow_resource_rep(base_class, base_params, z * base_z): 1} - - -@register_resources(_pow_pow_resource) -def merge_powers(*_, base, z, **__): - """Decompose the power of the power of a gate.""" - qml.pow(base.base, z=z * base.z) - - -@functools.lru_cache(maxsize=1) -def same_type_adjoint_ops(): - """A set of operators whose adjoint is an instance of its own type.""" - return frozenset( - { - # identity - qml.Identity, - qml.GlobalPhase, - # non-parametric gates - qml.H, - qml.X, - qml.Y, - qml.Z, - qml.SWAP, - qml.ECR, - # single-qubit parametric gates - qml.Rot, - qml.U1, - qml.U2, - qml.U3, - qml.RX, - qml.RY, - qml.RZ, - qml.PhaseShift, - # multi-qubit parametric gates - qml.MultiRZ, - qml.PauliRot, - qml.PCPhase, - qml.IsingXX, - qml.IsingYY, - qml.IsingZZ, - qml.IsingXY, - qml.PSWAP, - qml.CPhaseShift00, - qml.CPhaseShift01, - qml.CPhaseShift10, - # matrix gates - qml.QubitUnitary, - qml.DiagonalQubitUnitary, - qml.BlockEncode, - qml.SpecialUnitary, - # custom controlled ops - qml.CH, - qml.CY, - qml.CZ, - qml.CNOT, - qml.CSWAP, - qml.CCZ, - qml.Toffoli, - qml.MultiControlledX, - qml.CRX, - qml.CRY, - qml.CRZ, - qml.CRot, - qml.ControlledPhaseShift, - # arithmetic ops - qml.QubitSum, - qml.IntegerComparator, - # qchem ops - qml.SingleExcitation, - qml.SingleExcitationMinus, - qml.SingleExcitationPlus, - qml.DoubleExcitation, - qml.DoubleExcitationPlus, - qml.DoubleExcitationMinus, - qml.OrbitalRotation, - qml.FermionicSWAP, - # templates - qml.CommutingEvolution, - } - ) + def f(): + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten(params, new_struct) + + qml.cond(z % 2 == 1, f)() + + +def _pow_rotation_resource(base_class, base_params, z): # pylint: disable=unused-argument + return {resource_rep(base_class, **base_params): 1} + + +# pylint: disable=protected-access +@register_resources(_pow_rotation_resource) +def pow_rotation(phi, wires, base, z, **__): + """Decompose the power of a general rotation operator by multiplying the power by the angle.""" + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten((phi * z,), new_struct) + + +def _decompose_to_base_resource(base_class, base_params, **__): + return {resource_rep(base_class, **base_params): 1} + + +@register_resources(_decompose_to_base_resource) +def decompose_to_base(*params, wires, base, **__): + """Decompose a symbolic operator to its base.""" + _, struct = base._flatten() + new_struct = (wires, *struct[1:]) + base._unflatten(params, new_struct) + + +self_adjoint: DecompositionRule = decompose_to_base diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 68388d50e09..020fc1a87bc 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -112,6 +112,10 @@ def _check_decomposition_new(op, heuristic_resources=False): for rule in qml.list_decomps(type(op)): _test_decomposition_rule(op, rule, heuristic_resources=heuristic_resources) + for rule in qml.list_decomps(f"Adjoint({type(op).__name__})"): + adj_op = qml.ops.Adjoint(op) + _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) + def _test_decomposition_rule(op, rule: DecompositionRule, heuristic_resources=False): """Tests that a decomposition rule is consistent with the operator.""" diff --git a/pennylane/ops/identity.py b/pennylane/ops/identity.py index d7bb811f573..ef96cb3ddda 100644 --- a/pennylane/ops/identity.py +++ b/pennylane/ops/identity.py @@ -21,6 +21,9 @@ from scipy import sparse import pennylane as qml +from pennylane.decomposition import add_decomps +from pennylane.decomposition.decomposition_rule import null_decomp +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import ( AllWires, AnyWires, @@ -62,6 +65,12 @@ class Identity(CVObservable, Operation): ev_order = 1 + resource_keys = set() + + @property + def resource_params(self) -> dict: + return {} + @classmethod def _primitive_bind_call( cls, wires: WiresLike = (), **kwargs @@ -235,6 +244,10 @@ def pow(self, z): simulators should always be equal to 1. """ +add_decomps(Identity, null_decomp) +add_decomps("Adjoint(Identity)", null_decomp) +add_decomps("Pow(Identity)", null_decomp) + class GlobalPhase(Operation): r"""A global phase operation that multiplies all components of the state by :math:`e^{-i \phi}`. @@ -476,3 +489,7 @@ def generator(self): # needs to return a new_opmath instance regardless of whether new_opmath is enabled, because # it otherwise can't handle Identity with no wires, see PR #5194 return qml.s_prod(-1, qml.I(self.wires)) + + +add_decomps("Adjoint(GlobalPhase)", adjoint_rotation) +add_decomps("Pow(GlobalPhase)", pow_rotation) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 0d9eeb12ba7..6143871fb81 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -33,7 +33,6 @@ from pennylane import math as qmlmath from pennylane import operation from pennylane.compiler import compiler -from pennylane.decomposition.controlled_decomposition import base_to_custom_ctrl_op from pennylane.operation import Operator from pennylane.wires import Wires, WiresLike @@ -1002,3 +1001,28 @@ def _(base, *control_wires, control_values=None, work_wires=None, id=None): # easier to just keep the same primitive for both versions # dispatch between the two types happens inside instance creation anyway ControlledOp._primitive = Controlled._primitive # pylint: disable=protected-access + + +@functools.lru_cache(maxsize=1) +def base_to_custom_ctrl_op(): + """A dictionary mapping base op types to their custom controlled versions. + + This dictionary is used under the assumption that all custom controlled operations do not + have resource params (which is why `ControlledQubitUnitary` is not included here). + + """ + + ops_with_custom_ctrl_ops = { + (qml.PauliZ, 1): qml.CZ, + (qml.PauliZ, 2): qml.CCZ, + (qml.PauliY, 1): qml.CY, + (qml.CZ, 1): qml.CCZ, + (qml.SWAP, 1): qml.CSWAP, + (qml.Hadamard, 1): qml.CH, + (qml.RX, 1): qml.CRX, + (qml.RY, 1): qml.CRY, + (qml.RZ, 1): qml.CRZ, + (qml.Rot, 1): qml.CRot, + (qml.PhaseShift, 1): qml.ControlledPhaseShift, + } + return ops_with_custom_ctrl_ops diff --git a/pennylane/ops/op_math/controlled_ops.py b/pennylane/ops/op_math/controlled_ops.py index edc2406c44c..7a19de5b175 100644 --- a/pennylane/ops/op_math/controlled_ops.py +++ b/pennylane/ops/op_math/controlled_ops.py @@ -24,6 +24,12 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import ( + adjoint_rotation, + pow_of_self_adjoint, + pow_rotation, + self_adjoint, +) from pennylane.operation import AnyWires, Wires from pennylane.ops.qubit.parametric_ops_single_qubit import stack_last from pennylane.typing import TensorLike @@ -103,7 +109,12 @@ class ControlledQubitUnitary(ControlledOp): ndim_params = (2,) """tuple[int]: Number of dimensions per trainable parameter that the operator depends on.""" - resource_keys = {"base", "num_control_wires", "num_zero_control_values", "num_work_wires"} + resource_keys = { + "num_target_wires", + "num_control_wires", + "num_zero_control_values", + "num_work_wires", + } grad_method = None """Gradient computation method.""" @@ -171,7 +182,7 @@ def __init__( @property def resource_params(self) -> dict: return { - "base": self.base, + "num_target_wires": len(self.base.wires), "num_control_wires": len(self.control_wires), "num_zero_control_values": len([val for val in self.control_values if not val]), "num_work_wires": len(self.work_wires), @@ -329,6 +340,8 @@ def _ch_to_ry_cz_ry(wires: WiresLike, **__): add_decomps(CH, _ch_to_ry_cz_ry) +add_decomps("Adjoint(CH)", self_adjoint) +add_decomps("Pow(CH)", pow_of_self_adjoint) class CY(ControlledOp): @@ -462,6 +475,8 @@ def _cy(wires: WiresLike, **__): add_decomps(CY, _cy) +add_decomps("Adjoint(CY)", self_adjoint) +add_decomps("Pow(CY)", pow_of_self_adjoint) class CZ(ControlledOp): @@ -578,6 +593,8 @@ def _cz_to_cnot(wires: WiresLike, **__): add_decomps(CZ, _cz_to_cps, _cz_to_cnot) +add_decomps("Adjoint(CZ)", self_adjoint) +add_decomps("Pow(CZ)", pow_of_self_adjoint) class CSWAP(ControlledOp): @@ -728,6 +745,8 @@ def _cswap(wires: WiresLike, **__): add_decomps(CSWAP, _cswap) +add_decomps("Adjoint(CSWAP)", self_adjoint) +add_decomps("Pow(CSWAP)", pow_of_self_adjoint) class CCZ(ControlledOp): @@ -924,6 +943,8 @@ def _ccz(wires: WiresLike, **__): add_decomps(CCZ, _ccz) +add_decomps("Adjoint(CCZ)", self_adjoint) +add_decomps("Pow(CCZ)", pow_of_self_adjoint) class CNOT(ControlledOp): @@ -1054,6 +1075,8 @@ def _cnot_to_cz_h(wires: WiresLike, **__): add_decomps(CNOT, _cnot_to_cz_h) +add_decomps("Adjoint(CNOT)", self_adjoint) +add_decomps("Pow(CNOT)", pow_of_self_adjoint) class Toffoli(ControlledOp): @@ -1266,6 +1289,8 @@ def _toffoli(wires: WiresLike, **__): add_decomps(Toffoli, _toffoli) +add_decomps("Adjoint(Toffoli)", self_adjoint) +add_decomps("Pow(Toffoli)", pow_of_self_adjoint) class MultiControlledX(ControlledOp): @@ -1547,6 +1572,10 @@ def decomposition(self): ) +add_decomps("Adjoint(MultiControlledX)", self_adjoint) +add_decomps("Pow(MultiControlledX)", pow_of_self_adjoint) + + class CRX(ControlledOp): r"""The controlled-RX operator @@ -1752,6 +1781,8 @@ def _crx_to_h_crz(phi: TensorLike, wires: WiresLike, **__): add_decomps(CRX, _crx_to_rx_cz, _crx_to_rz_ry, _crx_to_h_crz) +add_decomps("Adjoint(CRX)", adjoint_rotation) +add_decomps("Pow(CRX)", pow_rotation) class CRY(ControlledOp): @@ -1929,6 +1960,8 @@ def _cry(phi: TensorLike, wires: WiresLike, **__): add_decomps(CRY, _cry) +add_decomps("Adjoint(CRY)", adjoint_rotation) +add_decomps("Pow(CRY)", pow_rotation) class CRZ(ControlledOp): @@ -2147,6 +2180,8 @@ def _crz(phi: TensorLike, wires: WiresLike, **__): add_decomps(CRZ, _crz) +add_decomps("Adjoint(CRZ)", adjoint_rotation) +add_decomps("Pow(CRZ)", pow_rotation) class CRot(ControlledOp): @@ -2365,6 +2400,14 @@ def _crot(phi: TensorLike, theta: TensorLike, omega: TensorLike, wires: WiresLik add_decomps(CRot, _crot) +@register_resources({CRot: 1}) +def _adjoint_crot(phi, theta, omega, wires, **_): + CRot(-omega, -theta, -phi, wires=wires) + + +add_decomps("Adjoint(CRot)", _adjoint_crot) + + class ControlledPhaseShift(ControlledOp): r"""A qubit controlled phase shift. @@ -2565,5 +2608,7 @@ def _cphase_to_rz_cnot(phi: TensorLike, wires: WiresLike, **__): add_decomps(ControlledPhaseShift, _cphase_to_rz_cnot) +add_decomps("Adjoint(ControlledPhaseShift)", adjoint_rotation) +add_decomps("Pow(ControlledPhaseShift)", pow_rotation) CPhase = ControlledPhaseShift diff --git a/pennylane/ops/qubit/arithmetic_ops.py b/pennylane/ops/qubit/arithmetic_ops.py index 2a474049f59..5467cff37b4 100644 --- a/pennylane/ops/qubit/arithmetic_ops.py +++ b/pennylane/ops/qubit/arithmetic_ops.py @@ -23,6 +23,7 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint from pennylane.operation import AnyWires, FlatPytree, Operation from pennylane.ops import Identity from pennylane.typing import TensorLike @@ -357,6 +358,8 @@ def _qubitsum_to_cnots(wires: WiresLike, **__): add_decomps(QubitSum, _qubitsum_to_cnots) +add_decomps("Adjoint(QubitSum)", self_adjoint) +add_decomps("Pow(QubitSum)", pow_of_self_adjoint) class IntegerComparator(Operation): diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index 7b4c7daf0a5..5948832849c 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -26,19 +26,10 @@ from scipy.sparse import csr_matrix import pennylane as qml +from pennylane import math from pennylane import numpy as pnp -from pennylane.decomposition.decomposition_rule import add_decomps -from pennylane.math import ( - cast, - conj, - eye, - norm, - sqrt, - sqrt_matrix, - sqrt_matrix_sparse, - transpose, - zeros, -) +from pennylane.decomposition import add_decomps, register_resources, resource_rep +from pennylane.decomposition.symbolic_decomposition import is_integer from pennylane.operation import AnyWires, DecompositionUndefinedError, FlatPytree, Operation from pennylane.ops.op_math.decompositions.unitary_decompositions import ( rot_decomp_rule, @@ -365,6 +356,39 @@ def label( ) +def _qubit_unitary_resource(base_class, base_params): + return {resource_rep(base_class, **base_params): 1} + + +@register_resources(_qubit_unitary_resource) +def _adjoint_qubit_unitary(U, wires, **_): + U = ( + U.conjugate().transpose() + if sp.sparse.issparse(U) + else qml.math.moveaxis(qml.math.conj(U), -2, -1) + ) + QubitUnitary(U, wires=wires) + + +add_decomps("Adjoint(QubitUnitary)", _adjoint_qubit_unitary) + + +def _matrix_pow(U, z): + if sp.sparse.issparse(U): + return sp.sparse.linalg.matrix_power(U, z) + if is_integer(z) and qml.math.get_deep_interface(U) != "tensorflow": + return qml.math.linalg.matrix_power(U, z) + return qml.math.convert_like(fractional_matrix_power(U, z), U) + + +@register_resources(_qubit_unitary_resource) +def _pow_qubit_unitary(U, wires, z, **_): + QubitUnitary(_matrix_pow(U, z), wires=wires) + + +add_decomps("Pow(QubitUnitary)", _pow_qubit_unitary) + + class DiagonalQubitUnitary(Operation): r"""DiagonalQubitUnitary(D, wires) Apply an arbitrary diagonal unitary matrix with a dimension that is a power of two. @@ -393,6 +417,12 @@ class DiagonalQubitUnitary(Operation): grad_method = None """Gradient computation method.""" + resource_keys = {"num_wires"} + + @property + def resource_params(self) -> dict: + return {"num_wires": len(self.wires)} + @staticmethod def compute_matrix(D: TensorLike) -> TensorLike: # pylint: disable=arguments-differ r"""Representation of the operator as a canonical matrix in the computational basis (static method). @@ -552,6 +582,27 @@ def label( return super().label(decimals=decimals, base_label=base_label or "U", cache=cache) +def _diagonal_qubit_unitary_resource(base_class, base_params): + return {resource_rep(base_class, **base_params): 1} + + +@register_resources(_diagonal_qubit_unitary_resource) +def _adjoint_diagonal_unitary(U, wires, **_): + U = qml.math.conj(U) + DiagonalQubitUnitary(U, wires=wires) + + +add_decomps("Adjoint(DiagonalQubitUnitary)", _adjoint_diagonal_unitary) + + +@register_resources(_diagonal_qubit_unitary_resource) +def _pow_diagonal_unitary(U, wires, z, **_): + DiagonalQubitUnitary(qml.math.cast(U, np.complex128) ** z, wires=wires) + + +add_decomps("Pow(DiagonalQubitUnitary)", _pow_diagonal_unitary) + + class BlockEncode(Operation): r"""BlockEncode(A, wires) Construct a unitary :math:`U(A)` such that an arbitrary matrix :math:`A` @@ -651,8 +702,8 @@ def __init__(self, A: TensorLike, wires: WiresLike, id: Optional[str] = None): shape_a = qml.math.shape(A) normalization = qml.math.maximum( - norm(A @ qml.math.transpose(qml.math.conj(A)), ord=pnp.inf), - norm(qml.math.transpose(qml.math.conj(A)) @ A, ord=pnp.inf), + math.norm(A @ qml.math.transpose(qml.math.conj(A)), ord=pnp.inf), + math.norm(qml.math.transpose(qml.math.conj(A)) @ A, ord=pnp.inf), ) subspace = (*shape_a, 2 ** len(wires)) @@ -755,7 +806,7 @@ def _process_blockencode(A, subspace): n, m, k = subspace shape_a = qml.math.shape(A) - sqrtm = sqrt_matrix_sparse if sp.sparse.issparse(A) else sqrt_matrix + sqrtm = math.sqrt_matrix_sparse if sp.sparse.issparse(A) else math.sqrt_matrix def _stack(lst, h=False, like=None): if like == "tensorflow": @@ -766,19 +817,24 @@ def _stack(lst, h=False, like=None): interface = qml.math.get_interface(A) if qml.math.sum(shape_a) <= 2: - col1 = _stack([A, sqrt(1 - A * conj(A))], like=interface) - col2 = _stack([sqrt(1 - A * conj(A)), -conj(A)], like=interface) + col1 = _stack([A, math.sqrt(1 - A * math.conj(A))], like=interface) + col2 = _stack([math.sqrt(1 - A * math.conj(A)), -math.conj(A)], like=interface) u = _stack([col1, col2], h=True, like=interface) else: d1, d2 = shape_a col1 = _stack( - [A, sqrtm(cast(eye(d2, like=A), A.dtype) - qml.math.transpose(conj(A)) @ A)], + [ + A, + sqrtm( + math.cast(math.eye(d2, like=A), A.dtype) - qml.math.transpose(math.conj(A)) @ A + ), + ], like=interface, ) col2 = _stack( [ - sqrtm(cast(eye(d1, like=A), A.dtype) - A @ transpose(conj(A))), - -transpose(conj(A)), + sqrtm(math.cast(math.eye(d1, like=A), A.dtype) - A @ math.transpose(math.conj(A))), + -math.transpose(math.conj(A)), ], like=interface, ) @@ -786,8 +842,8 @@ def _stack(lst, h=False, like=None): if n + m < k: r = k - (n + m) - col1 = _stack([u, zeros((r, n + m), like=A)], like=interface) - col2 = _stack([zeros((n + m, r), like=A), eye(r, like=A)], like=interface) + col1 = _stack([u, math.zeros((r, n + m), like=A)], like=interface) + col2 = _stack([math.zeros((n + m, r), like=A), math.eye(r, like=A)], like=interface) u = _stack([col1, col2], h=True, like=interface) return u diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 9dbbe03513a..3331e08f5a2 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -25,7 +25,15 @@ from scipy import sparse import pennylane as qml -from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition import ( + DecompositionNotApplicable, + add_decomps, + adjoint_resource_rep, + pow_resource_rep, + register_resources, + resource_rep, +) +from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint from pennylane.operation import Observable, Operation from pennylane.typing import TensorLike from pennylane.wires import Wires, WiresLike @@ -230,6 +238,9 @@ def _hadamard_to_rz_ry(wires: WiresLike, **__): add_decomps(Hadamard, _hadamard_to_rz_rx, _hadamard_to_rz_ry) +add_decomps("Adjoint(Hadamard)", self_adjoint) +add_decomps("Pow(Hadamard)", pow_of_self_adjoint) + H = Hadamard r"""H(wires) @@ -449,17 +460,19 @@ def single_qubit_rot_angles(self) -> list[TensorLike]: """ -def _paulix_to_rx_gp_resources(): +def _paulix_to_rx_resources(): return {qml.GlobalPhase: 1, qml.RX: 1} -@register_resources(_paulix_to_rx_gp_resources) -def _paulix_to_rx_gp(wires: WiresLike, **__): +@register_resources(_paulix_to_rx_resources) +def _paulix_to_rx(wires: WiresLike, **__): qml.RX(np.pi, wires=wires) qml.GlobalPhase(-np.pi / 2, wires=wires) -add_decomps(PauliX, _paulix_to_rx_gp) +add_decomps(PauliX, _paulix_to_rx) +add_decomps("Adjoint(PauliX)", self_adjoint) +add_decomps("Pow(PauliX)", pow_of_self_adjoint) class PauliY(Observable, Operation): @@ -672,6 +685,8 @@ def _pauliy_to_ry_gp(wires: WiresLike, **__): add_decomps(PauliY, _pauliy_to_ry_gp) +add_decomps("Adjoint(PauliY)", self_adjoint) +add_decomps("Pow(PauliY)", pow_of_self_adjoint) class PauliZ(Observable, Operation): @@ -892,6 +907,8 @@ def _pauliz_to_ps(wires: WiresLike, **__): add_decomps(PauliZ, _pauliz_to_ps) +add_decomps("Adjoint(PauliZ)", self_adjoint) +add_decomps("Pow(PauliZ)", pow_of_self_adjoint) class S(Operation): @@ -1042,6 +1059,33 @@ def _s_phaseshift(wires, **__): add_decomps(S, _s_phaseshift) +def _pow_s_resource(base_class, base_params, z): # pylint: disable=unused_argument + z_mod4 = z % 4 + if z_mod4 == 0.5: + return {T: 1} + if z_mod4 == 2: + return {Z: 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_s_resource) +def _pow_s(wires, z, **__): + z_mod4 = z % 4 + qml.cond( + z_mod4 == 0.5, + lambda: T(wires=wires), + lambda: qml.pow(S(wires), z_mod4), + elifs=[ + (z_mod4 == 2, lambda: Z(wires=wires)), + ], + )() + + +add_decomps("Pow(S)", _pow_s) + + class T(Operation): r"""T(wires) The single-qubit T gate @@ -1190,6 +1234,33 @@ def _t_phaseshift(wires, **__): add_decomps(T, _t_phaseshift) +def _pow_t_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod8 = z % 8 + if z_mod8 == 2: + return {S: 1} + if z_mod8 == 4: + return {Z: 1} + if z != z_mod8: + return {pow_resource_rep(base_class, base_params, z=z_mod8): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_t_resource) +def _pow_t(wires, z, **__): + z_mod8 = z % 8 + qml.cond( + z_mod8 == 2, + lambda: S(wires=wires), + lambda: qml.pow(T(wires), z_mod8), + elifs=[ + (z_mod8 == 4, lambda: Z(wires=wires)), + ], + )() + + +add_decomps("Pow(T)", _pow_t) + + class SX(Operation): r"""SX(wires) The single-qubit Square-Root X operator. @@ -1327,19 +1398,39 @@ def single_qubit_rot_angles(self) -> list[TensorLike]: return [np.pi / 2, np.pi / 2, -np.pi / 2] -def _sx_to_rz_ry_rz_ps_resources(): - return {qml.PhaseShift: 1, qml.RZ: 2, qml.RY: 1} +def _sx_to_rx_resources(): + return {qml.RX: 1, qml.GlobalPhase: 1} -@register_resources(_sx_to_rz_ry_rz_ps_resources) -def _sx_to_rz_ry_rz_ps(wires: WiresLike, **__): - qml.RZ(np.pi / 2, wires=wires) - qml.RY(np.pi / 2, wires=wires) - qml.RZ(-np.pi, wires=wires) - qml.PhaseShift(np.pi / 2, wires=wires) +@register_resources(_sx_to_rx_resources) +def _sx_to_rx(wires: WiresLike, **__): + qml.RX(np.pi / 2, wires=wires) + qml.GlobalPhase(-np.pi / 4, wires=wires) + +add_decomps(SX, _sx_to_rx) -add_decomps(SX, _sx_to_rz_ry_rz_ps) + +def _pow_sx_to_x_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod4 = z % 4 + if z_mod4 == 2: + return {X: 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_sx_to_x_resource) +def _pow_sx_to_x(*params, wires, z, base, **__): # pylint: disable=unused-argument + z_mod4 = z % 4 + qml.cond( + z_mod4 == 2, + lambda: X(wires=wires), + lambda: qml.pow(SX(wires), z_mod4), + )() + + +add_decomps("Pow(SX)", _pow_sx_to_x) class SWAP(Operation): @@ -1494,6 +1585,8 @@ def _swap_to_cnot(wires, **__): add_decomps(SWAP, _swap_to_cnot) +add_decomps("Adjoint(SWAP)", self_adjoint) +add_decomps("Pow(SWAP)", pow_of_self_adjoint) class ECR(Operation): @@ -1657,6 +1750,8 @@ def _ecr_decomp(wires, **__): add_decomps(ECR, _ecr_decomp) +add_decomps("Adjoint(ECR)", self_adjoint) +add_decomps("Pow(ECR)", pow_of_self_adjoint) class ISWAP(Operation): @@ -1811,6 +1906,28 @@ def _iswap_decomp(wires, **__): add_decomps(ISWAP, _iswap_decomp) +def _pow_iswap_to_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod2 = z % 2 + if z_mod2 == 0.5: + return {SISWAP: 1} + if z != z_mod2: + return {pow_resource_rep(base_class, base_params, z=z_mod2): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_iswap_to_siswap_resource) +def _pow_iswap_to_siswap(wires, z, **__): + z_mod2 = z % 2 + qml.cond( + z_mod2 == 0.5, + lambda: SISWAP(wires=wires), + lambda: qml.pow(ISWAP(wires=wires), z_mod2), + )() + + +add_decomps("Pow(ISWAP)", _pow_iswap_to_siswap) + + class SISWAP(Operation): r"""SISWAP(wires) The square root of i-swap operator. Can also be accessed as ``qml.SQISW`` @@ -1988,4 +2105,27 @@ def _siswap_decomp(wires, **__): add_decomps(SISWAP, _siswap_decomp) + +def _pow_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument + z_mod4 = z % 4 + if qml.math.allclose(z_mod4, 2): + return {ISWAP: 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + raise DecompositionNotApplicable + + +@register_resources(_pow_siswap_resource) +def _pow_siswap(wires, z, **__): + z_mod4 = z % 4 + qml.cond( + qml.math.allclose(z_mod4, 2), + lambda: ISWAP(wires=wires), + lambda: qml.pow(SISWAP(wires=wires), z_mod4), + ) + + +add_decomps("Pow(SISWAP)", _pow_siswap) + + SQISW = SISWAP diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py index a7b06ce9a2f..b79beb88c6b 100644 --- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py @@ -25,6 +25,7 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.math import expand_matrix from pennylane.operation import AnyWires, FlatPytree, Operation from pennylane.typing import TensorLike @@ -240,6 +241,8 @@ def _post_cnot(i): add_decomps(MultiRZ, _multi_rz_decomposition) +add_decomps("Adjoint(MultiRZ)", adjoint_rotation) +add_decomps("Pow(MultiRZ)", pow_rotation) class PauliRot(Operation): @@ -569,6 +572,18 @@ def pow(self, z): return [PauliRot(self.data[0] * z, self.hyperparameters["pauli_word"], wires=self.wires)] +def _pauli_rot_resources(pauli_word): + if set(pauli_word) == {"I"}: + return {qml.GlobalPhase: 1} + num_active_wires = len(pauli_word.replace("I", "")) + return { + qml.Hadamard: 2 * pauli_word.count("X"), + qml.RX: 2 * pauli_word.count("Y"), + qml.resource_rep(qml.MultiRZ, num_wires=num_active_wires): 1, + } + + +@register_resources(_pauli_rot_resources) def _pauli_rot_decomposition(theta, pauli_word, wires, **__): if set(pauli_word) == {"I"}: qml.GlobalPhase(theta / 2) @@ -589,19 +604,9 @@ def _pauli_rot_decomposition(theta, pauli_word, wires, **__): qml.RX(-np.pi / 2, wires=[wire]) -def _pauli_rot_resources(pauli_word): - if set(pauli_word) == {"I"}: - return {qml.GlobalPhase: 1} - num_active_wires = len(pauli_word.replace("I", "")) - return { - qml.Hadamard: 2 * pauli_word.count("X"), - qml.RX: 2 * pauli_word.count("Y"), - qml.resource_rep(qml.MultiRZ, num_wires=num_active_wires): 1, - } - - -pauli_rot_decomposition = qml.register_resources(_pauli_rot_resources, _pauli_rot_decomposition) -add_decomps(PauliRot, pauli_rot_decomposition) +add_decomps(PauliRot, _pauli_rot_decomposition) +add_decomps("Adjoint(PauliRot)", adjoint_rotation) +add_decomps("Pow(PauliRot)", pow_rotation) class PCPhase(Operation): @@ -984,6 +989,8 @@ def _isingxx_to_cnot_rx_cnot(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingXX, _isingxx_to_cnot_rx_cnot) +add_decomps("Adjoint(IsingXX)", adjoint_rotation) +add_decomps("Pow(IsingXX)", pow_rotation) class IsingYY(Operation): @@ -1146,6 +1153,8 @@ def _isingyy_to_cy_ry_cy(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingYY, _isingyy_to_cy_ry_cy) +add_decomps("Adjoint(IsingYY)", adjoint_rotation) +add_decomps("Pow(IsingYY)", pow_rotation) class IsingZZ(Operation): @@ -1339,6 +1348,8 @@ def _isingzz_to_cnot_rz_cnot(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingZZ, _isingzz_to_cnot_rz_cnot) +add_decomps("Adjoint(IsingZZ)", adjoint_rotation) +add_decomps("Pow(IsingZZ)", pow_rotation) class IsingXY(Operation): @@ -1563,6 +1574,8 @@ def _isingxy_to_h_cy(phi: TensorLike, wires: WiresLike, **__): add_decomps(IsingXY, _isingxy_to_h_cy) +add_decomps("Adjoint(IsingXY)", adjoint_rotation) +add_decomps("Pow(IsingXY)", pow_rotation) class PSWAP(Operation): @@ -1739,6 +1752,8 @@ def _pswap_to_swap_cnot_phaseshift_cnot(phi: TensorLike, wires: WiresLike, **__) add_decomps(PSWAP, _pswap_to_swap_cnot_phaseshift_cnot) +add_decomps("Adjoint(PSWAP)", adjoint_rotation) +add_decomps("Pow(PSWAP)", pow_rotation) class CPhaseShift00(Operation): @@ -1957,6 +1972,8 @@ def _cphaseshift00(phi: TensorLike, wires: WiresLike, **__): add_decomps(CPhaseShift00, _cphaseshift00) +add_decomps("Adjoint(CPhaseShift00)", adjoint_rotation) +add_decomps("Pow(CPhaseShift00)", pow_rotation) class CPhaseShift01(Operation): @@ -2166,6 +2183,8 @@ def _cphaseshift01(phi: TensorLike, wires: WiresLike, **__): add_decomps(CPhaseShift01, _cphaseshift01) +add_decomps("Adjoint(CPhaseShift01)", adjoint_rotation) +add_decomps("Pow(CPhaseShift01)", pow_rotation) class CPhaseShift10(Operation): @@ -2369,3 +2388,5 @@ def _cphaseshift10(phi: TensorLike, wires: WiresLike, **__): add_decomps(CPhaseShift10, _cphaseshift10) +add_decomps("Adjoint(CPhaseShift10)", adjoint_rotation) +add_decomps("Pow(CPhaseShift10)", pow_rotation) diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py index 6f636967df8..f194fd7ca92 100644 --- a/pennylane/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py @@ -24,7 +24,13 @@ import scipy as sp import pennylane as qml -from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition import ( + DecompositionNotApplicable, + add_decomps, + register_resources, + resource_rep, +) +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import Operation from pennylane.typing import TensorLike from pennylane.wires import WiresLike @@ -175,6 +181,8 @@ def _rx_to_rz_ry(phi, wires: WiresLike, **__): add_decomps(RX, _rx_to_rot, _rx_to_rz_ry) +add_decomps("Adjoint(RX)", adjoint_rotation) +add_decomps("Pow(RX)", pow_rotation) class RY(Operation): @@ -307,6 +315,8 @@ def _ry_to_rz_rx(phi, wires: WiresLike, **__): add_decomps(RY, _ry_to_rot, _ry_to_rz_rx) +add_decomps("Adjoint(RY)", adjoint_rotation) +add_decomps("Pow(RY)", pow_rotation) class RZ(Operation): @@ -478,6 +488,8 @@ def _rz_to_ry_rx(phi, wires: WiresLike, **__): add_decomps(RZ, _rz_to_rot, _rz_to_ry_rx) +add_decomps("Adjoint(RZ)", adjoint_rotation) +add_decomps("Pow(RZ)", pow_rotation) class PhaseShift(Operation): @@ -667,6 +679,8 @@ def _phaseshift_to_rz_gp(phi, wires: WiresLike, **__): add_decomps(PhaseShift, _phaseshift_to_rz_gp) +add_decomps("Adjoint(PhaseShift)", adjoint_rotation) +add_decomps("Pow(PhaseShift)", pow_rotation) class Rot(Operation): @@ -874,6 +888,14 @@ def _rot_to_rz_ry_rz(phi, theta, omega, wires: WiresLike, **__): add_decomps(Rot, _rot_to_rz_ry_rz) +@register_resources({Rot: 1}) +def _adjoint_rot(phi, theta, omega, wires, **__): + Rot(-omega, -theta, -phi, wires=wires) + + +add_decomps("Adjoint(Rot)", _adjoint_rot) + + class U1(Operation): r""" U1 gate. @@ -1008,6 +1030,8 @@ def _u1_phaseshift(phi, wires, **__): add_decomps(U1, _u1_phaseshift) +add_decomps("Adjoint(U1)", adjoint_rotation) +add_decomps("Pow(U1)", pow_rotation) class U2(Operation): @@ -1177,6 +1201,16 @@ def _u2_phaseshift_rot(phi, delta, wires, **__): add_decomps(U2, _u2_phaseshift_rot) +@register_resources({U2: 1}) +def _adjoint_u2(phi, delta, wires, **__): + new_delta = qml.math.mod((np.pi - phi), (2 * np.pi)) + new_phi = qml.math.mod((np.pi - delta), (2 * np.pi)) + U2(new_phi, new_delta, wires=wires) + + +add_decomps("Adjoint(U2)", _adjoint_u2) + + class U3(Operation): r""" Arbitrary single qubit unitary. @@ -1377,3 +1411,13 @@ def _u3_phaseshift_rot(theta, phi, delta, wires, **__): add_decomps(U3, _u3_phaseshift_rot) + + +@register_resources({U3: 1}) +def _adjoint_u3(theta, phi, delta, wires, **__): + new_delta = qml.math.mod((np.pi - phi), (2 * np.pi)) + new_phi = qml.math.mod((np.pi - delta), (2 * np.pi)) + U3(theta, new_phi, new_delta, wires=wires) + + +add_decomps("Adjoint(U3)", _adjoint_u3) diff --git a/pennylane/ops/qubit/qchem_ops.py b/pennylane/ops/qubit/qchem_ops.py index b06703b7cd4..3789c6030b6 100644 --- a/pennylane/ops/qubit/qchem_ops.py +++ b/pennylane/ops/qubit/qchem_ops.py @@ -24,6 +24,7 @@ import pennylane as qml from pennylane.decomposition import add_decomps, register_resources +from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import Operation from pennylane.typing import TensorLike from pennylane.wires import WiresLike @@ -322,6 +323,8 @@ def _single_excit(phi, wires, **__): add_decomps(SingleExcitation, _single_excit) +add_decomps("Adjoint(SingleExcitation)", adjoint_rotation) +add_decomps("Pow(SingleExcitation)", pow_rotation) class SingleExcitationMinus(Operation): @@ -481,6 +484,8 @@ def _single_excitation_minus_decomp(phi, wires: WiresLike, **__): add_decomps(SingleExcitationMinus, _single_excitation_minus_decomp) +add_decomps("Adjoint(SingleExcitationMinus)", adjoint_rotation) +add_decomps("Pow(SingleExcitationMinus)", pow_rotation) class SingleExcitationPlus(Operation): @@ -640,6 +645,8 @@ def _single_excitation_plus_decomp(phi, wires: WiresLike, **__): add_decomps(SingleExcitationPlus, _single_excitation_plus_decomp) +add_decomps("Adjoint(SingleExcitationPlus)", adjoint_rotation) +add_decomps("Pow(SingleExcitationPlus)", pow_rotation) class DoubleExcitation(Operation): @@ -893,6 +900,8 @@ def _doublexcit(phi, wires, **__): add_decomps(DoubleExcitation, _doublexcit) +add_decomps("Adjoint(DoubleExcitation)", adjoint_rotation) +add_decomps("Pow(DoubleExcitation)", pow_rotation) class DoubleExcitationPlus(Operation): @@ -988,6 +997,10 @@ def label( return super().label(decimals=decimals, base_label=base_label or "G²₊", cache=cache) +add_decomps("Adjoint(DoubleExcitationPlus)", adjoint_rotation) +add_decomps("Pow(DoubleExcitationPlus)", pow_rotation) + + class DoubleExcitationMinus(Operation): r""" Double excitation rotation with negative phase-shift outside the rotation subspace. @@ -1079,6 +1092,10 @@ def label( return super().label(decimals=decimals, base_label=base_label or "G²₋", cache=cache) +add_decomps("Adjoint(DoubleExcitationMinus)", adjoint_rotation) +add_decomps("Pow(DoubleExcitationMinus)", pow_rotation) + + class OrbitalRotation(Operation): r""" Spin-adapted spatial orbital rotation. @@ -1291,6 +1308,8 @@ def _orbital_rotation_decomp(phi, wires: WiresLike, **__): add_decomps(OrbitalRotation, _orbital_rotation_decomp) +add_decomps("Adjoint(OrbitalRotation)", adjoint_rotation) +add_decomps("Pow(OrbitalRotation)", pow_rotation) class FermionicSWAP(Operation): @@ -1524,3 +1543,5 @@ def _fermionic_swap_decomp(phi, wires: WiresLike, **__): add_decomps(FermionicSWAP, _fermionic_swap_decomp) +add_decomps("Adjoint(FermionicSWAP)", adjoint_rotation) +add_decomps("Pow(FermionicSWAP)", pow_rotation) diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index f57a0267eaf..ad586013bdb 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -21,6 +21,12 @@ import pennylane as qml from pennylane.decomposition import Resources from pennylane.decomposition.decomposition_rule import _auto_wrap +from pennylane.decomposition.symbolic_decomposition import ( + adjoint_rotation, + pow_of_self_adjoint, + pow_rotation, + self_adjoint, +) decompositions = defaultdict(list) @@ -126,22 +132,12 @@ def _t_ps(wires, **__): decompositions["T"] = [_t_ps] +################################################ +# Custom Decompositions For Symbolic Operators # +################################################ -@qml.register_resources({qml.H: 1}) -def _adjoint_hadamard(*_, wires, **__): - qml.H(wires) - - -decompositions["Adjoint(Hadamard)"] = [_adjoint_hadamard] - - -def _pow_hadamard_resource(z, **__): - return {qml.H: z % 2} - - -@qml.register_resources(_pow_hadamard_resource) -def _pow_hadamard(*_, wires, z, **__): - qml.cond(z % 2 == 1, qml.H)(wires=wires) - - -decompositions["Pow(Hadamard)"] = [_pow_hadamard] +decompositions["Adjoint(Hadamard)"] = [self_adjoint] +decompositions["Pow(Hadamard)"] = [pow_of_self_adjoint] +decompositions["Adjoint(RX)"] = [adjoint_rotation] +decompositions["Adjoint(CNOT)"] = [self_adjoint] +decompositions["Adjoint(PhaseShift)"] = [adjoint_rotation] diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 956c17645ca..289be3a6cbe 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -449,7 +449,7 @@ def custom_controlled_decomp(wires): class TestSymbolicDecompositions: """Tests decompositions of symbolic ops.""" - def test_adjoint_adjoint(self, _): + def test_cancel_adjoint(self, _): """Tests that a nested adjoint operator is flattened properly.""" op = qml.adjoint(qml.adjoint(qml.RX(0.5, wires=[0]))) @@ -467,26 +467,6 @@ def test_adjoint_adjoint(self, _): assert q.queue == [qml.RX(0.5, wires=[0])] assert graph.resource_estimate(op) == to_resources({qml.RX: 1}) - def test_adjoint_pow(self, _): - """tests that an adjoint of a power of am operator that has adjoint is decomposed.""" - - op = qml.adjoint(qml.pow(qml.H(0), z=3)) - - graph = DecompositionGraph(operations=[op], gate_set={"H"}) - # 3 operator nodes: Adjoint(Pow(H)), Pow(H), and H, and 1 dummy starting node - # 1 decomposition nodes for Adjoint(Pow(H)) and two decomposition nodes for Pow(H) - assert len(graph._graph.nodes()) == 7 - # 3 edges from decompositions to ops and 3 edges from ops to decompositions - # and 1 edge from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 7 - - graph.solve() - with qml.queuing.AnnotatedQueue() as q: - graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) - - assert q.queue == [qml.pow(qml.H(0), z=3)] - assert graph.resource_estimate(op) == to_resources({qml.H: 1}) - def test_adjoint_custom(self, _): """Tests adjoint of an operator that defines its own adjoint.""" @@ -504,35 +484,6 @@ def test_adjoint_custom(self, _): assert q.queue == [qml.RX(-0.5, wires=[0])] assert graph.resource_estimate(op) == to_resources({qml.RX: 1}) - def test_adjoint_controlled(self, _): - """Tests that the adjoint of a controlled operator is decomposed properly.""" - - op = qml.adjoint(qml.ops.Controlled(qml.RX(0.5, wires=[0]), control_wires=1)) - op2 = qml.adjoint(qml.ctrl(qml.U1(0.5, wires=0), control=[1])) - - graph = DecompositionGraph(operations=[op, op2], gate_set={"ControlledPhaseShift", "CRX"}) - # 5 operator nodes: Adjoint(C(RX)), Adjoint(C(U1)), CRX, C(U1), ControlledPhaseShift - # 3 decomposition nodes leading into Adjoint(C(RX)), Adjoint(C(U1)), and C(U1) - # and the dummy starting node - assert len(graph._graph.nodes()) == 9 - # 3 edges from decompositions to ops and 3 edges from ops to decompositions, - # and 2 edges from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 8 - - graph.solve() - with qml.queuing.AnnotatedQueue() as q: - graph.decomposition(op)(*op.parameters, wires=op.wires, **op.hyperparameters) - graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - - assert q.queue == [qml.CRX(-0.5, wires=[1, 0]), qml.ctrl(qml.U1(-0.5, wires=0), control=1)] - - op3 = qml.ctrl(qml.U1(-0.5, wires=0), control=1) - with qml.queuing.AnnotatedQueue() as q: - graph.decomposition(op3)(*op3.parameters, wires=op3.wires, **op3.hyperparameters) - - assert q.queue == [qml.ControlledPhaseShift(-0.5, wires=[1, 0])] - assert graph.resource_estimate(op2) == to_resources({qml.ControlledPhaseShift: 1}) - def test_adjoint_general(self, _): """Tests decomposition of a generalized adjoint operation.""" @@ -560,13 +511,12 @@ def custom_decomp(phi, wires): alt_decomps={CustomOp: [custom_decomp]}, ) # 10 operator nodes: A(CustomOp), A(H), A(CNOT), A(RX), A(T), H, CNOT, RX, A(PhaseShift), PhaseShift - # 5 decomposition nodes for: A(CustomOp), A(CNOT), A(RX), A(T), A(PhaseShift) - # 2 decomposition nodes for A(H) + # 6 decomposition nodes for: A(CustomOp), A(CNOT), A(RX), A(T), A(PhaseShift), A(H) # 1 dummy starting node - assert len(graph._graph.nodes()) == 18 - # 10 edges from ops to decompositions and 7 edges from decompositions to ops. + assert len(graph._graph.nodes()) == 17 + # 9 edges from ops to decompositions and 6 edges from decompositions to ops. # and 4 edges from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 21 + assert len(graph._graph.edges()) == 19 graph.solve() with qml.queuing.AnnotatedQueue() as q: @@ -589,12 +539,13 @@ def test_nested_powers(self, _): op = qml.pow(qml.pow(qml.H(0), 3), 2) graph = DecompositionGraph(operations=[op], gate_set={"H"}) # 3 operator nodes: Pow(Pow(H)), Pow(H), and H - # 1 decomposition nodes for Pow(Pow(H)) and 2 nodes for Pow(H) + # 1 decomposition nodes for Pow(Pow(H)) and 1 nodes for Pow(H) # and the dummy starting node - assert len(graph._graph.nodes()) == 7 - # 3 edges from decompositions to ops and 3 edges from ops to decompositions - # and 1 edge from the dummy starting node to the target gate set. - assert len(graph._graph.edges()) == 7 + assert len(graph._graph.nodes()) == 5 + # 2 edges from decompositions to ops and 1 edge from ops to decompositions + # and 1 edge from the dummy starting node to the target gate set. Note that + # H**6 decomposes to nothing, so H isn't counted. + assert len(graph._graph.edges()) == 4 graph.solve() with qml.queuing.AnnotatedQueue() as q: diff --git a/tests/decomposition/test_resources.py b/tests/decomposition/test_resources.py index 7e9171a2f71..72ed3d77736 100644 --- a/tests/decomposition/test_resources.py +++ b/tests/decomposition/test_resources.py @@ -375,10 +375,10 @@ def test_nested_controlled_qubit_unitary(self): { "base_class": qml.ControlledQubitUnitary, "base_params": { + "num_target_wires": 1, "num_control_wires": 2, "num_zero_control_values": 1, "num_work_wires": 1, - "base": qml.QubitUnitary(U, wires=[0]), }, "num_control_wires": 1, "num_zero_control_values": 1, diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index b704fb0a6f1..01795492f58 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -17,23 +17,33 @@ import pytest import pennylane as qml -from pennylane.decomposition.resources import Resources, pow_resource_rep +from pennylane import queuing +from pennylane.decomposition import DecompositionNotApplicable +from pennylane.decomposition.resources import ( + Resources, + adjoint_resource_rep, + pow_resource_rep, + resource_rep, +) from pennylane.decomposition.symbolic_decomposition import ( - AdjointDecomp, - adjoint_controlled_decomp, - adjoint_pow_decomp, + adjoint_rotation, cancel_adjoint, + flip_pow_adjoint, + make_adjoint_decomp, merge_powers, + pow_of_self_adjoint, + pow_rotation, repeat_pow_base, - same_type_adjoint_decomp, + self_adjoint, ) from tests.decomposition.conftest import to_resources +@pytest.mark.unit class TestAdjointDecompositionRules: """Tests the decomposition rules defined for the adjoint of operations.""" - def test_adjoint_adjoint(self): + def test_cancel_adjoint(self): """Tests that the adjoint of an adjoint cancels out.""" op = qml.adjoint(qml.adjoint(qml.RX(0.5, wires=0))) @@ -45,7 +55,7 @@ def test_adjoint_adjoint(self): assert cancel_adjoint.compute_resources(**op.resource_params) == to_resources({qml.RX: 1}) @pytest.mark.jax - def test_adjoint_adjoint_capture(self): + def test_cancel_adjoint_capture(self): """Tests that the adjoint of an adjoint works with capture.""" from pennylane.tape.plxpr_conversion import CollectOpsandMeas @@ -66,82 +76,6 @@ def circuit(): if not capture_enabled: qml.capture.disable() - def test_adjoint_controlled(self): - """Tests that the adjoint of a controlled operation is correctly decomposed.""" - - op1 = qml.adjoint(qml.ctrl(qml.U1(0.5, wires=0), control=1)) - op2 = qml.adjoint(qml.ops.Controlled(qml.RX(0.5, wires=0), control_wires=[1])) - - with qml.queuing.AnnotatedQueue() as q: - adjoint_controlled_decomp(*op1.parameters, wires=op1.wires, **op1.hyperparameters) - adjoint_controlled_decomp(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - - assert q.queue == [qml.ctrl(qml.U1(-0.5, wires=0), control=1), qml.CRX(-0.5, wires=[1, 0])] - assert adjoint_controlled_decomp.compute_resources(**op1.resource_params) == Resources( - { - qml.decomposition.controlled_resource_rep( - qml.U1, {}, num_control_wires=1, num_zero_control_values=0, num_work_wires=0 - ): 1, - } - ) - assert adjoint_controlled_decomp.compute_resources(**op2.resource_params) == Resources( - {qml.resource_rep(qml.CRX): 1} - ) - - def test_adjoint_controlled_x(self): - """Tests the adjoint of controlled X operations are correctly decomposed.""" - - op1 = qml.adjoint(qml.ops.Controlled(qml.X(1), control_wires=[0])) - op2 = qml.adjoint(qml.ops.Controlled(qml.X(2), control_wires=[0, 1])) - op3 = qml.adjoint(qml.ops.Controlled(qml.X(3), control_wires=[0, 1, 2])) - - with qml.queuing.AnnotatedQueue() as q: - adjoint_controlled_decomp(*op1.parameters, wires=op1.wires, **op1.hyperparameters) - adjoint_controlled_decomp(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - adjoint_controlled_decomp(*op3.parameters, wires=op3.wires, **op3.hyperparameters) - - assert q.queue == [ - qml.CNOT(wires=[0, 1]), - qml.Toffoli(wires=[0, 1, 2]), - qml.MultiControlledX(wires=[0, 1, 2, 3]), - ] - - assert adjoint_controlled_decomp.compute_resources(**op1.resource_params) == Resources( - {qml.resource_rep(qml.CNOT): 1} - ) - assert adjoint_controlled_decomp.compute_resources(**op2.resource_params) == Resources( - {qml.resource_rep(qml.Toffoli): 1} - ) - assert adjoint_controlled_decomp.compute_resources(**op3.resource_params) == Resources( - { - qml.resource_rep( - qml.MultiControlledX, - num_control_wires=3, - num_zero_control_values=0, - num_work_wires=0, - ): 1 - } - ) - - def test_same_type_adjoint(self): - """Tests that the adjoint of an operation that has adjoint of same - type is correctly decomposed.""" - - op1 = qml.adjoint(qml.H(0)) - op2 = qml.adjoint(qml.RX(0.5, wires=0)) - - with qml.queuing.AnnotatedQueue() as q: - same_type_adjoint_decomp(*op1.parameters, wires=op1.wires, **op1.hyperparameters) - same_type_adjoint_decomp(*op2.parameters, wires=op2.wires, **op2.hyperparameters) - - assert q.queue == [qml.H(0), qml.RX(-0.5, wires=0)] - assert same_type_adjoint_decomp.compute_resources(**op1.resource_params) == to_resources( - {qml.H: 1} - ) - assert same_type_adjoint_decomp.compute_resources(**op2.resource_params) == to_resources( - {qml.RX: 1} - ) - def test_adjoint_general(self): """Tests the adjoint of a general operator can be correctly decomposed.""" @@ -162,7 +96,7 @@ def custom_decomp(phi, wires): qml.T(wires[2]) op = qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])) - rule = AdjointDecomp(custom_decomp) + rule = make_adjoint_decomp(custom_decomp) with qml.queuing.AnnotatedQueue() as q: rule(*op.parameters, wires=op.wires, **op.hyperparameters) @@ -177,30 +111,59 @@ def custom_decomp(phi, wires): assert rule.compute_resources(**op.resource_params) == Resources( { - qml.decomposition.adjoint_resource_rep(qml.T): 1, - qml.decomposition.adjoint_resource_rep(qml.CNOT): 2, - qml.decomposition.adjoint_resource_rep(qml.RX): 1, - qml.decomposition.adjoint_resource_rep(qml.H): 1, + adjoint_resource_rep(qml.T): 1, + adjoint_resource_rep(qml.CNOT): 2, + adjoint_resource_rep(qml.RX): 1, + adjoint_resource_rep(qml.H): 1, } ) - def test_adjoint_pow(self): - """Tests decomposing the adjoint of a Pow of an operator that has adjoint.""" + def test_adjoint_rotation(self): + """Tests the adjoint_rotation decomposition.""" - op = qml.adjoint(qml.pow(qml.H(0), 3)) - with qml.queuing.AnnotatedQueue() as q: - adjoint_pow_decomp(*op.parameters, wires=op.wires, **op.hyperparameters) + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} - assert q.queue == [qml.pow(qml.H(0), 3)] - assert adjoint_pow_decomp.compute_resources(**op.resource_params) == to_resources( - {pow_resource_rep(qml.H, {}, 3): 1} + op = qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])) + with queuing.AnnotatedQueue() as q: + adjoint_rotation(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [CustomOp(-0.5, wires=[0, 1, 2])] + assert adjoint_rotation.compute_resources(**op.resource_params) == Resources( + {resource_rep(CustomOp): 1} ) + def test_self_adjoint(self): + """Tests the self_adjoint decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + @property + def resource_params(self): + return {} + + op = qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])) + with queuing.AnnotatedQueue() as q: + self_adjoint(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [CustomOp(0.5, wires=[0, 1, 2])] + assert self_adjoint.compute_resources(**op.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) + + +@pytest.mark.unit class TestPowDecomposition: """Tests the decomposition rule defined for Pow.""" - def test_pow_pow(self): + def test_merge_powers(self): """Test the decomposition rule for nested powers.""" op = qml.pow(qml.pow(qml.H(0), 3), 2) @@ -212,7 +175,7 @@ def test_pow_pow(self): {pow_resource_rep(qml.H, {}, 6): 1} ) - def test_pow_general(self): + def test_repeat_pow_base(self): """Tests repeating the same op z number of times.""" op = qml.pow(qml.H(0), 3) @@ -223,7 +186,7 @@ def test_pow_general(self): assert repeat_pow_base.compute_resources(**op.resource_params) == to_resources({qml.H: 3}) @pytest.mark.jax - def test_pow_general_capture(self): + def test_repeat_pow_base_capture(self): """Tests that the general pow decomposition works with capture.""" from pennylane.tape.plxpr_conversion import CollectOpsandMeas @@ -244,12 +207,88 @@ def circuit(): if not capture_enabled: qml.capture.disable() - def test_non_integer_pow_not_implemented(self): - """Tests that NotImplementedError is raised when z isn't a positive integer.""" + def test_non_integer_pow_not_applicable(self): + """Tests that DecompositionNotApplicable is raised when z isn't a positive integer.""" op = qml.pow(qml.H(0), 0.5) - with pytest.raises(NotImplementedError, match="Non-integer or negative powers"): + with pytest.raises(DecompositionNotApplicable): repeat_pow_base.compute_resources(**op.resource_params) op = qml.pow(qml.H(0), -1) - with pytest.raises(NotImplementedError, match="Non-integer or negative powers"): + with pytest.raises(DecompositionNotApplicable): repeat_pow_base.compute_resources(**op.resource_params) + + def test_flip_pow_adjoint(self): + """Tests the flip_pow_adjoint decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} + + op = qml.pow(qml.adjoint(CustomOp(0.5, wires=[0, 1, 2])), 2) + with queuing.AnnotatedQueue() as q: + flip_pow_adjoint(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [qml.adjoint(qml.pow(CustomOp(0.5, wires=[0, 1, 2]), 2))] + assert flip_pow_adjoint.compute_resources(**op.resource_params) == Resources( + { + adjoint_resource_rep( + qml.ops.Pow, {"base_class": CustomOp, "base_params": {}, "z": 2} + ): 1 + } + ) + + def test_pow_of_self_adjoint(self): + """Tests the pow_of_self_adjoint decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} + + op1 = qml.pow(CustomOp(wires=[0, 1, 2]), 1) + op2 = qml.pow(CustomOp(wires=[0, 1, 2]), 2) + op3 = qml.pow(CustomOp(wires=[0, 1, 2]), 3) + op4 = qml.pow(CustomOp(wires=[0, 1, 2]), 4) + + with qml.queuing.AnnotatedQueue() as q: + pow_of_self_adjoint(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + pow_of_self_adjoint(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + pow_of_self_adjoint(*op3.parameters, wires=op3.wires, **op3.hyperparameters) + pow_of_self_adjoint(*op4.parameters, wires=op4.wires, **op4.hyperparameters) + + assert q.queue == [CustomOp(wires=[0, 1, 2]), CustomOp(wires=[0, 1, 2])] + assert pow_of_self_adjoint.compute_resources(**op1.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) + assert pow_of_self_adjoint.compute_resources(**op3.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) + assert pow_of_self_adjoint.compute_resources(**op2.resource_params) == Resources() + assert pow_of_self_adjoint.compute_resources(**op4.resource_params) == Resources() + + def test_pow_rotations(self): + """Tests the pow_rotations decomposition.""" + + class CustomOp(qml.operation.Operator): # pylint: disable=too-few-public-methods + + resource_keys = set() + + @property + def resource_params(self): + return {} + + op = qml.pow(CustomOp(0.3, wires=[0, 1, 2]), 2.5) + with queuing.AnnotatedQueue() as q: + pow_rotation(*op.parameters, wires=op.wires, **op.hyperparameters) + + assert q.queue == [CustomOp(0.3 * 2.5, wires=[0, 1, 2])] + assert pow_rotation.compute_resources(**op.resource_params) == Resources( + {resource_rep(CustomOp): 1} + ) From 9483966e6ba27c4e94d16aa66802f194c555cecd Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 14:58:16 -0400 Subject: [PATCH 02/16] changelog --- doc/releases/changelog-dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 76a096fd64f..814b7f5054f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -123,6 +123,7 @@ :func:`~.transforms.decompose` transform, allowing custom decomposition rules to be defined and registered for symbolic operators. [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347) + [(#7352)](https://github.com/PennyLaneAI/pennylane/pull/7352) ```python @register_resources({qml.RY: 1}) def my_adjoint_ry(phi, wires, **_): From 9a141f760562d4ef3bcf772ebb87d29d6c65adfd Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 15:14:45 -0400 Subject: [PATCH 03/16] pylint and black --- pennylane/decomposition/decomposition_graph.py | 4 ++-- pennylane/decomposition/symbolic_decomposition.py | 2 +- pennylane/ops/qubit/non_parametric_ops.py | 2 -- pennylane/ops/qubit/parametric_ops_single_qubit.py | 7 +------ tests/decomposition/conftest.py | 1 + tests/decomposition/test_resources.py | 2 -- 6 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pennylane/decomposition/decomposition_graph.py b/pennylane/decomposition/decomposition_graph.py index 9947d0ebb7e..d12bb98027e 100644 --- a/pennylane/decomposition/decomposition_graph.py +++ b/pennylane/decomposition/decomposition_graph.py @@ -222,10 +222,10 @@ def _add_op_node(self, op_node: CompressedResourceOp) -> int: return op_node_idx - def _add_decomp(self, rule: DecompositionRule, op: CompressedResourceOp, op_idx: int): + def _add_decomp(self, rule: DecompositionRule, op_node: CompressedResourceOp, op_idx: int): """Adds a decomposition rule to the graph.""" try: - decomp_resource = rule.compute_resources(**op.params) + decomp_resource = rule.compute_resources(**op_node.params) d_node = _DecompositionNode(rule, decomp_resource) d_node_idx = self._graph.add_node(d_node) if not decomp_resource.gate_counts: diff --git a/pennylane/decomposition/symbolic_decomposition.py b/pennylane/decomposition/symbolic_decomposition.py index 2b576e69373..bf84f33f61f 100644 --- a/pennylane/decomposition/symbolic_decomposition.py +++ b/pennylane/decomposition/symbolic_decomposition.py @@ -93,7 +93,7 @@ def _loop(i): new_struct = (wires, *struct[1:]) base._unflatten(params, new_struct) - _loop() + _loop() # pylint: disable=no-value-for-parameter def _merge_powers_resource(base_class, base_params, z): # pylint: disable=unused-argument diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 3331e08f5a2..d3aa1b20ab5 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -28,10 +28,8 @@ from pennylane.decomposition import ( DecompositionNotApplicable, add_decomps, - adjoint_resource_rep, pow_resource_rep, register_resources, - resource_rep, ) from pennylane.decomposition.symbolic_decomposition import pow_of_self_adjoint, self_adjoint from pennylane.operation import Observable, Operation diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py index f194fd7ca92..0a5b8086dfb 100644 --- a/pennylane/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py @@ -24,12 +24,7 @@ import scipy as sp import pennylane as qml -from pennylane.decomposition import ( - DecompositionNotApplicable, - add_decomps, - register_resources, - resource_rep, -) +from pennylane.decomposition import add_decomps, register_resources from pennylane.decomposition.symbolic_decomposition import adjoint_rotation, pow_rotation from pennylane.operation import Operation from pennylane.typing import TensorLike diff --git a/tests/decomposition/conftest.py b/tests/decomposition/conftest.py index ad586013bdb..1b9cd94be91 100644 --- a/tests/decomposition/conftest.py +++ b/tests/decomposition/conftest.py @@ -139,5 +139,6 @@ def _t_ps(wires, **__): decompositions["Adjoint(Hadamard)"] = [self_adjoint] decompositions["Pow(Hadamard)"] = [pow_of_self_adjoint] decompositions["Adjoint(RX)"] = [adjoint_rotation] +decompositions["Pow(RX)"] = [pow_rotation] decompositions["Adjoint(CNOT)"] = [self_adjoint] decompositions["Adjoint(PhaseShift)"] = [adjoint_rotation] diff --git a/tests/decomposition/test_resources.py b/tests/decomposition/test_resources.py index 72ed3d77736..43c5f69e077 100644 --- a/tests/decomposition/test_resources.py +++ b/tests/decomposition/test_resources.py @@ -368,8 +368,6 @@ def test_controlled_resource_op_flatten_x(self): def test_nested_controlled_qubit_unitary(self): """Tests that a nested controlled qubit unitary is flattened.""" - U = qml.math.eye(2) - rep = controlled_resource_rep( qml.ops.Controlled, { From 855111315c342f7060426c85bc50a9eb4ca65667 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 15:19:31 -0400 Subject: [PATCH 04/16] ooops --- pennylane/decomposition/resources.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py index 9c84258c7dd..ebd3995aaa5 100644 --- a/pennylane/decomposition/resources.py +++ b/pennylane/decomposition/resources.py @@ -356,11 +356,6 @@ def adjoint_resource_rep(base_class: Type[Operator], base_params: dict = None): ) -def _is_integer(x): - """Checks if x is an integer.""" - return isinstance(x, int) or np.issubdtype(getattr(x, "dtype", None), np.integer) - - def pow_resource_rep(base_class, base_params, z): """Creates a ``CompressedResourceOp`` representation of the power of an operator. @@ -370,8 +365,6 @@ def pow_resource_rep(base_class, base_params, z): z (int or float): the power """ - if (not qml.math.is_abstract(z)) and (not _is_integer(z) or z < 0): - raise NotImplementedError("Non-integer powers or negative powers are not supported yet.") base_resource_rep = resource_rep(base_class, **base_params) return CompressedResourceOp( qml.ops.Pow, From a3705e89c32373e4f61f2d82aa2ae163572d5c6c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 15:20:57 -0400 Subject: [PATCH 05/16] unused import --- pennylane/decomposition/resources.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pennylane/decomposition/resources.py b/pennylane/decomposition/resources.py index ebd3995aaa5..c1771562cd4 100644 --- a/pennylane/decomposition/resources.py +++ b/pennylane/decomposition/resources.py @@ -21,8 +21,6 @@ from functools import cached_property from typing import Optional, Type -import numpy as np - import pennylane as qml from pennylane.operation import Operator From 12a52c12d0a5cab8f5ce0fe9a0d8c362995638e7 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 16:10:25 -0400 Subject: [PATCH 06/16] remove test --- tests/decomposition/test_resources.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/decomposition/test_resources.py b/tests/decomposition/test_resources.py index 43c5f69e077..2b87302ce4d 100644 --- a/tests/decomposition/test_resources.py +++ b/tests/decomposition/test_resources.py @@ -488,12 +488,3 @@ def test_pow_resource_rep(self): op = qml.pow(qml.MultiRZ(0.5, wires=[0, 1, 2]), 3) assert op.resource_params == rep.params - - def test_non_integer_pow_not_supported(self): - """Tests that non-integer power is not supported yet.""" - - with pytest.raises(NotImplementedError, match="Non-integer powers"): - qml.decomposition.pow_resource_rep(qml.MultiRZ, {"num_wires": 3}, 3.5) - - with pytest.raises(NotImplementedError, match="Non-integer powers"): - qml.decomposition.pow_resource_rep(qml.MultiRZ, {"num_wires": 3}, -1) From 7a0d0d1544f54fc8b3571c8c8b94bcb42a5992fa Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 17:03:41 -0400 Subject: [PATCH 07/16] add missing coverage --- .../decomposition/test_decomposition_graph.py | 74 +++++++++++++++++++ .../test_symbolic_decomposition.py | 3 + 2 files changed, 77 insertions(+) diff --git a/tests/decomposition/test_decomposition_graph.py b/tests/decomposition/test_decomposition_graph.py index 289be3a6cbe..a4efd9721a8 100644 --- a/tests/decomposition/test_decomposition_graph.py +++ b/tests/decomposition/test_decomposition_graph.py @@ -596,3 +596,77 @@ def my_adjoint_rx(theta, wires, **__): assert graph.resource_estimate(op2) == to_resources({qml.H: 1}) assert graph.resource_estimate(op3) == to_resources({qml.CH: 1}) assert graph.resource_estimate(op4) == to_resources({qml.RX: 1}) + + def test_special_pow_decomps(self, _): + """Tests special cases for decomposing a power.""" + + class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-methods + """A custom operation.""" + + resource_keys = set() + + @property + def resource_params(self): + return {} + + graph = DecompositionGraph( + operations=[qml.pow(CustomOp(0), 0), qml.pow(CustomOp(1), 1)], gate_set={"CustomOp"} + ) + # 3 operator nodes: Pow(CustomOp, 0), Pow(CustomOp, 1), and CustomOp + # 1 decomposition node for Pow(CustomOp, 0) and 1 node for Pow(CustomOp, 1) + # and the dummy starting node + assert len(graph._graph.nodes()) == 6 + # 2 edges from decompositions to ops and 1 edge from ops to decompositions + # and 1 edge from the dummy starting node to the target gate set. + # and 1 edge from the dummy starting node to the empty decomposition. + assert len(graph._graph.edges()) == 5 + + op1 = qml.pow(CustomOp(0), 0) + op2 = qml.pow(CustomOp(1), 1) + + graph.solve() + with qml.queuing.AnnotatedQueue() as q: + graph.decomposition(op1)(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + + assert q.queue == [CustomOp(1)] + assert graph.resource_estimate(op1) == to_resources({}) + assert graph.resource_estimate(op2) == to_resources({CustomOp: 1}) + + def test_general_pow_decomps(self, _): + """Tests the more general power decomposition rules.""" + + class CustomOp(qml.operation.Operation): # pylint: disable=too-few-public-methods + """A custom operation.""" + + resource_keys = set() + + @property + def resource_params(self): + return {} + + graph = DecompositionGraph( + operations=[qml.pow(CustomOp(0), 2), qml.pow(qml.adjoint(CustomOp(1)), 2)], + gate_set={"CustomOp", "Adjoint(CustomOp)"}, + ) + # 5 operator nodes: Pow(CustomOp), Pow(Adjoint(CustomOp)), Adjoint(Pow(CustomOp)), + # Adjoint(CustomOp), CustomOp, and the dummy starting node + # 3 decomposition nodes for each of Pow(CustomOp), Pow(Adjoint(CustomOp)), Adjoint(Pow(CustomOp)) + assert len(graph._graph.nodes()) == 9 + # 3 edges from decompositions to ops and 3 edges from ops to decompositions + # and 2 edges from the dummy starting node to the target gate set. + assert len(graph._graph.edges()) == 8 + + op1 = qml.pow(CustomOp(0), 2) + op2 = qml.pow(qml.adjoint(CustomOp(1)), 2) + + graph.solve() + with qml.queuing.AnnotatedQueue() as q: + graph.decomposition(op1)(*op1.parameters, wires=op1.wires, **op1.hyperparameters) + graph.decomposition(op2)(*op2.parameters, wires=op2.wires, **op2.hyperparameters) + + assert q.queue == [ + CustomOp(0), + CustomOp(0), + qml.adjoint(qml.pow(CustomOp(1), 2)), + ] diff --git a/tests/decomposition/test_symbolic_decomposition.py b/tests/decomposition/test_symbolic_decomposition.py index 01795492f58..7fa87156f18 100644 --- a/tests/decomposition/test_symbolic_decomposition.py +++ b/tests/decomposition/test_symbolic_decomposition.py @@ -273,6 +273,9 @@ def resource_params(self): assert pow_of_self_adjoint.compute_resources(**op2.resource_params) == Resources() assert pow_of_self_adjoint.compute_resources(**op4.resource_params) == Resources() + with pytest.raises(DecompositionNotApplicable): + pow_of_self_adjoint.compute_resources(CustomOp, {}, z=0.5) + def test_pow_rotations(self): """Tests the pow_rotations decomposition.""" From 7188ceefe519fa42e0bafb7c011891e82e5e459b Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Tue, 29 Apr 2025 17:41:23 -0400 Subject: [PATCH 08/16] tests and missing coverage --- pennylane/ops/functions/assert_valid.py | 5 +++++ pennylane/ops/qubit/matrix_ops.py | 4 ++-- pennylane/ops/qubit/non_parametric_ops.py | 22 +++++++++---------- .../ops/qubit/parametric_ops_multi_qubit.py | 1 - 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index 020fc1a87bc..a2f10019aa8 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -116,6 +116,11 @@ def _check_decomposition_new(op, heuristic_resources=False): adj_op = qml.ops.Adjoint(op) _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) + for rule in qml.list_decomps(f"Pow({type(op).__name__})"): + for z in [2, 3, 3.5]: + pow_op = qml.ops.Pow(op, z) + _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) + def _test_decomposition_rule(op, rule: DecompositionRule, heuristic_resources=False): """Tests that a decomposition rule is consistent with the operator.""" diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index 5948832849c..4941f638c62 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -356,7 +356,7 @@ def label( ) -def _qubit_unitary_resource(base_class, base_params): +def _qubit_unitary_resource(base_class, base_params, **_): return {resource_rep(base_class, **base_params): 1} @@ -582,7 +582,7 @@ def label( return super().label(decimals=decimals, base_label=base_label or "U", cache=cache) -def _diagonal_qubit_unitary_resource(base_class, base_params): +def _diagonal_qubit_unitary_resource(base_class, base_params, **_): return {resource_rep(base_class, **base_params): 1} diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index d3aa1b20ab5..bf33b71e301 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -1881,10 +1881,10 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod2 = z % 2 - if abs(z_mod2 - 0.5) < 1e-6: + z_mod4 = z % 4 + if abs(z_mod4 - 0.5) < 1e-6: return [SISWAP(wires=self.wires)] - return super().pow(z_mod2) + return super().pow(z_mod4) def _iswap_decomp_resources(): @@ -1905,21 +1905,21 @@ def _iswap_decomp(wires, **__): def _pow_iswap_to_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument - z_mod2 = z % 2 - if z_mod2 == 0.5: + z_mod4 = z % 4 + if z_mod4 == 0.5: return {SISWAP: 1} - if z != z_mod2: - return {pow_resource_rep(base_class, base_params, z=z_mod2): 1} + if z != z_mod4: + return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} raise DecompositionNotApplicable @register_resources(_pow_iswap_to_siswap_resource) def _pow_iswap_to_siswap(wires, z, **__): - z_mod2 = z % 2 + z_mod4 = z % 4 qml.cond( - z_mod2 == 0.5, + z_mod4 == 0.5, lambda: SISWAP(wires=wires), - lambda: qml.pow(ISWAP(wires=wires), z_mod2), + lambda: qml.pow(ISWAP(wires=wires), z_mod4), )() @@ -2120,7 +2120,7 @@ def _pow_siswap(wires, z, **__): qml.math.allclose(z_mod4, 2), lambda: ISWAP(wires=wires), lambda: qml.pow(SISWAP(wires=wires), z_mod4), - ) + )() add_decomps("Pow(SISWAP)", _pow_siswap) diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py index b79beb88c6b..19560c87fdd 100644 --- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py +++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py @@ -1753,7 +1753,6 @@ def _pswap_to_swap_cnot_phaseshift_cnot(phi: TensorLike, wires: WiresLike, **__) add_decomps(PSWAP, _pswap_to_swap_cnot_phaseshift_cnot) add_decomps("Adjoint(PSWAP)", adjoint_rotation) -add_decomps("Pow(PSWAP)", pow_rotation) class CPhaseShift00(Operation): From 58a3b4dc3079889e2c32d22259072cb737c57a01 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 09:37:43 -0400 Subject: [PATCH 09/16] fix test --- tests/ops/qubit/test_non_parametric_ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 7f9d929c3b5..8841cb28aa9 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -827,7 +827,6 @@ def test_repr(self): qml.PauliZ(0), qml.Hadamard("a"), qml.SWAP(wires=(0, 1)), - qml.ISWAP(wires=(0, 1)), qml.ECR(wires=(0, 1)), # Controlled operations qml.CNOT(wires=(0, 1)), @@ -842,6 +841,7 @@ def test_repr(self): class TestPowMethod: + @pytest.mark.parametrize("op", period_two_ops) @pytest.mark.parametrize("n", (1, 5, -1, -5)) def test_period_two_pow_odd(self, op, n): @@ -919,7 +919,7 @@ def test_pauliz_general_power(self, n): assert op_pow[0].__class__ is qml.PhaseShift assert qml.math.allclose(op_pow[0].data[0], np.pi * (n % 2)) - @pytest.mark.parametrize("n", (0.5, 2.5, -1.5)) + @pytest.mark.parametrize("n", (0.5, 4.5, -3.5)) def test_ISWAP_sqaure_root(self, n): """Test that SISWAP is the square root of ISWAP.""" op = qml.ISWAP(wires=(0, 1)) From 499cf5958a242ab2945fc67b03eda75dee3fb69a Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 10:00:50 -0400 Subject: [PATCH 10/16] fix siswap and tests --- doc/releases/changelog-dev.md | 3 ++ pennylane/ops/functions/assert_valid.py | 2 +- pennylane/ops/qubit/non_parametric_ops.py | 63 +++++++++++++++------- tests/ops/qubit/test_non_parametric_ops.py | 2 +- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 814b7f5054f..548b0fc14d5 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,6 +302,9 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) +* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to `qml.SISWAP`. + [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347) +

Contributors ✍️

This release contains contributions from (in alphabetical order): diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index a2f10019aa8..fce07efb559 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -117,7 +117,7 @@ def _check_decomposition_new(op, heuristic_resources=False): _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) for rule in qml.list_decomps(f"Pow({type(op).__name__})"): - for z in [2, 3, 3.5]: + for z in [0.5, 2, 3, 4, 4.5]: pow_op = qml.ops.Pow(op, z) _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index bf33b71e301..3dc27de0033 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -1904,26 +1904,36 @@ def _iswap_decomp(wires, **__): add_decomps(ISWAP, _iswap_decomp) -def _pow_iswap_to_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument +def _pow_iswap_resource(base_class, base_params, z): # pylint: disable=unused-argument z_mod4 = z % 4 if z_mod4 == 0.5: return {SISWAP: 1} + if z_mod4 == 2: + return {Z: 2} if z != z_mod4: return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} raise DecompositionNotApplicable -@register_resources(_pow_iswap_to_siswap_resource) -def _pow_iswap_to_siswap(wires, z, **__): +@register_resources(_pow_iswap_resource) +def _pow_iswap_decomp(wires, z, **__): + z_mod4 = z % 4 - qml.cond( - z_mod4 == 0.5, - lambda: SISWAP(wires=wires), - lambda: qml.pow(ISWAP(wires=wires), z_mod4), - )() + def _siswap(): + SISWAP(wires=wires) + + def _general_case(): + qml.pow(ISWAP(wires=wires), z_mod4) -add_decomps("Pow(ISWAP)", _pow_iswap_to_siswap) + def _zz(): + qml.Z(wires[0]) + qml.Z(wires[1]) + + qml.cond(z_mod4 == 0.5, _siswap, _general_case, elifs=[(z_mod4 == 2, _zz)])() + + +add_decomps("Pow(ISWAP)", _pow_iswap_decomp) class SISWAP(Operation): @@ -2077,8 +2087,8 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod4 = z % 4 - return [ISWAP(wires=self.wires)] if z_mod4 == 2 else super().pow(z_mod4) + z_mod8 = z % 8 + return [ISWAP(wires=self.wires)] if z_mod8 == 2 else super().pow(z_mod8) def _siswap_decomp_resources(): @@ -2105,21 +2115,36 @@ def _siswap_decomp(wires, **__): def _pow_siswap_resource(base_class, base_params, z): # pylint: disable=unused-argument - z_mod4 = z % 4 - if qml.math.allclose(z_mod4, 2): + z_mod8 = z % 8 + if qml.math.allclose(z_mod8, 2): return {ISWAP: 1} - if z != z_mod4: - return {pow_resource_rep(base_class, base_params, z=z_mod4): 1} + if qml.math.allclose(z_mod8, 4): + return {Z: 2} + if z != z_mod8: + return {pow_resource_rep(base_class, base_params, z=z_mod8): 1} raise DecompositionNotApplicable @register_resources(_pow_siswap_resource) def _pow_siswap(wires, z, **__): - z_mod4 = z % 4 + + z_mod8 = z % 8 + + def _iswap(): + ISWAP(wires=wires) + + def _zz(): + qml.Z(wires[0]) + qml.Z(wires[1]) + + def _general_case(): + qml.pow(SISWAP(wires=wires), z_mod8) + qml.cond( - qml.math.allclose(z_mod4, 2), - lambda: ISWAP(wires=wires), - lambda: qml.pow(SISWAP(wires=wires), z_mod4), + qml.math.allclose(z_mod8, 2), + _iswap, + _general_case, + elifs=[(qml.math.allclose(z_mod8, 4), _zz)], )() diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 8841cb28aa9..8435dea13f7 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -972,7 +972,7 @@ def test_SX_pow(self, offset): with pytest.raises(qml.operation.PowUndefinedError): op.pow(2.43 + offset) - @pytest.mark.parametrize("offset", (0, 4, -4)) + @pytest.mark.parametrize("offset", (0, 8, -8)) def test_SISWAP_pow(self, offset): """Test powers of the SISWAP operator""" op = qml.SISWAP(wires=("b", "c")) From 82c87808d3c9bb241d54f13bcb6b3948e6f50fef Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 10:01:38 -0400 Subject: [PATCH 11/16] ooops --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 548b0fc14d5..5a287fce0c7 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,7 +302,7 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) -* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to `qml.SISWAP`. +* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to nothing. [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347)

Contributors ✍️

From 85f04856da34a0f76c127bf63dc41e96a36db99d Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 10:49:56 -0400 Subject: [PATCH 12/16] add missing coverage --- pennylane/ops/functions/assert_valid.py | 2 +- .../decompositions/unitary_decompositions.py | 2 ++ tests/ops/qubit/test_matrix_ops.py | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index fce07efb559..fb4bad395a3 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -117,7 +117,7 @@ def _check_decomposition_new(op, heuristic_resources=False): _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) for rule in qml.list_decomps(f"Pow({type(op).__name__})"): - for z in [0.5, 2, 3, 4, 4.5]: + for z in [0.5, 2, 4, 8, 9, 10]: pow_op = qml.ops.Pow(op, z) _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) diff --git a/pennylane/ops/op_math/decompositions/unitary_decompositions.py b/pennylane/ops/op_math/decompositions/unitary_decompositions.py index 7167e17e248..6676a44273f 100644 --- a/pennylane/ops/op_math/decompositions/unitary_decompositions.py +++ b/pennylane/ops/op_math/decompositions/unitary_decompositions.py @@ -258,6 +258,8 @@ def _get_impl(self): """The implementation of the decomposition rule.""" def _impl(U, wires, **__): + if sp.issparse(U): + U = U.todense() U, global_phase = math.convert_to_su2(U, return_global_phase=True) self._naive_rule(U, wires=wires) ops.GlobalPhase(-global_phase) diff --git a/tests/ops/qubit/test_matrix_ops.py b/tests/ops/qubit/test_matrix_ops.py index c8379033b7f..ec2ab3f53cd 100644 --- a/tests/ops/qubit/test_matrix_ops.py +++ b/tests/ops/qubit/test_matrix_ops.py @@ -191,6 +191,32 @@ def test_csr_matrix_decomposition(self): mat2 = qml.matrix(op.decomposition, wire_order=[0])() assert qml.math.allclose(mat2, mat.todense()) + def test_csr_matrix_decomposition_new(self): + """Tests that the QubitUnitary's decomposition works with csr_matrix.""" + + U = csr_matrix(unitary_group.rvs(2)) + op = qml.QubitUnitary(U, wires=[0]) + rule = qml.list_decomps(qml.QubitUnitary)[0] + with qml.queuing.AnnotatedQueue() as q: + rule(*op.parameters, wires=op.wires, **op.hyperparameters) + + tape = qml.tape.QuantumScript.from_queue(q) + actual_mat = qml.matrix(tape) + assert qml.math.allclose(actual_mat, U.todense()) + + def test_csr_matrix_pow_new(self): + """Tests the pow decomposition of a QubitUnitary works with csr_matrix.""" + + U = csr_matrix(unitary_group.rvs(2)) + op = qml.pow(qml.QubitUnitary(U, wires=[0]), 2) + rule = qml.list_decomps("Pow(QubitUnitary)")[0] + with qml.queuing.AnnotatedQueue() as q: + rule(*op.parameters, wires=op.wires, **op.hyperparameters) + + tape = qml.tape.QuantumScript.from_queue(q) + actual_mat = qml.matrix(tape) + assert qml.math.allclose(actual_mat, (U @ U).todense()) + class TestQubitUnitary: """Tests for the QubitUnitary class.""" From 46a18a04a83109ee1c268de6f82b0892e57e606c Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 11:11:26 -0400 Subject: [PATCH 13/16] missing coverage --- pennylane/ops/functions/assert_valid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py index fb4bad395a3..9f1655bf879 100644 --- a/pennylane/ops/functions/assert_valid.py +++ b/pennylane/ops/functions/assert_valid.py @@ -117,7 +117,7 @@ def _check_decomposition_new(op, heuristic_resources=False): _test_decomposition_rule(adj_op, rule, heuristic_resources=heuristic_resources) for rule in qml.list_decomps(f"Pow({type(op).__name__})"): - for z in [0.5, 2, 4, 8, 9, 10]: + for z in [0.5, 2, 3, 4, 8, 9]: pow_op = qml.ops.Pow(op, z) _test_decomposition_rule(pow_op, rule, heuristic_resources=heuristic_resources) From 92f0b8415ad9d6236ac75cd8bfe673120f3e02ec Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 11:22:24 -0400 Subject: [PATCH 14/16] changelog --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 5a287fce0c7..ee01a54253a 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,7 +302,7 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) -* Fixes a bug where the `qml.ISWAP` raised to a power of 2 incorrectly decomposes to nothing. +* Fixes a bug where the `qml.ISWAP` raised to a power of 2 and `qml.SISWAP` raised to a power of 4 incorrectly decompose to nothing. [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347)

Contributors ✍️

From 2060ca1004d6f60d95e528b8309ca8d45bdde900 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Wed, 30 Apr 2025 14:01:13 -0400 Subject: [PATCH 15/16] revert some changes --- doc/releases/changelog-dev.md | 3 --- pennylane/ops/qubit/non_parametric_ops.py | 10 +++++----- tests/ops/qubit/test_non_parametric_ops.py | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index ee01a54253a..814b7f5054f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -302,9 +302,6 @@ * Fixed a bug where the phase is used as the wire label for a `qml.GlobalPhase` when capture is enabled. [(#7211)](https://github.com/PennyLaneAI/pennylane/pull/7211) -* Fixes a bug where the `qml.ISWAP` raised to a power of 2 and `qml.SISWAP` raised to a power of 4 incorrectly decompose to nothing. - [(#7347)](https://github.com/PennyLaneAI/pennylane/pull/7347) -

Contributors ✍️

This release contains contributions from (in alphabetical order): diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py index 3dc27de0033..9f649245e7c 100644 --- a/pennylane/ops/qubit/non_parametric_ops.py +++ b/pennylane/ops/qubit/non_parametric_ops.py @@ -1881,10 +1881,10 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod4 = z % 4 - if abs(z_mod4 - 0.5) < 1e-6: + z_mod2 = z % 2 + if abs(z_mod2 - 0.5) < 1e-6: return [SISWAP(wires=self.wires)] - return super().pow(z_mod4) + return super().pow(z_mod2) def _iswap_decomp_resources(): @@ -2087,8 +2087,8 @@ def compute_decomposition(wires: WiresLike) -> list[qml.operation.Operator]: ] def pow(self, z: Union[int, float]) -> list[qml.operation.Operator]: - z_mod8 = z % 8 - return [ISWAP(wires=self.wires)] if z_mod8 == 2 else super().pow(z_mod8) + z_mod4 = z % 4 + return [ISWAP(wires=self.wires)] if z_mod4 == 2 else super().pow(z_mod4) def _siswap_decomp_resources(): diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py index 8435dea13f7..37dc16439e2 100644 --- a/tests/ops/qubit/test_non_parametric_ops.py +++ b/tests/ops/qubit/test_non_parametric_ops.py @@ -919,7 +919,7 @@ def test_pauliz_general_power(self, n): assert op_pow[0].__class__ is qml.PhaseShift assert qml.math.allclose(op_pow[0].data[0], np.pi * (n % 2)) - @pytest.mark.parametrize("n", (0.5, 4.5, -3.5)) + @pytest.mark.parametrize("n", (0.5, 2.5, -1.5)) def test_ISWAP_sqaure_root(self, n): """Test that SISWAP is the square root of ISWAP.""" op = qml.ISWAP(wires=(0, 1)) @@ -972,7 +972,7 @@ def test_SX_pow(self, offset): with pytest.raises(qml.operation.PowUndefinedError): op.pow(2.43 + offset) - @pytest.mark.parametrize("offset", (0, 8, -8)) + @pytest.mark.parametrize("offset", (0, 4, -4)) def test_SISWAP_pow(self, offset): """Test powers of the SISWAP operator""" op = qml.SISWAP(wires=("b", "c")) From 30c0c3c13903a89266a877b3fb99242582c09825 Mon Sep 17 00:00:00 2001 From: Astral Cai Date: Fri, 2 May 2025 10:00:11 -0400 Subject: [PATCH 16/16] ooops --- pennylane/ops/op_math/controlled.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py index 6143871fb81..0d9eeb12ba7 100644 --- a/pennylane/ops/op_math/controlled.py +++ b/pennylane/ops/op_math/controlled.py @@ -33,6 +33,7 @@ from pennylane import math as qmlmath from pennylane import operation from pennylane.compiler import compiler +from pennylane.decomposition.controlled_decomposition import base_to_custom_ctrl_op from pennylane.operation import Operator from pennylane.wires import Wires, WiresLike @@ -1001,28 +1002,3 @@ def _(base, *control_wires, control_values=None, work_wires=None, id=None): # easier to just keep the same primitive for both versions # dispatch between the two types happens inside instance creation anyway ControlledOp._primitive = Controlled._primitive # pylint: disable=protected-access - - -@functools.lru_cache(maxsize=1) -def base_to_custom_ctrl_op(): - """A dictionary mapping base op types to their custom controlled versions. - - This dictionary is used under the assumption that all custom controlled operations do not - have resource params (which is why `ControlledQubitUnitary` is not included here). - - """ - - ops_with_custom_ctrl_ops = { - (qml.PauliZ, 1): qml.CZ, - (qml.PauliZ, 2): qml.CCZ, - (qml.PauliY, 1): qml.CY, - (qml.CZ, 1): qml.CCZ, - (qml.SWAP, 1): qml.CSWAP, - (qml.Hadamard, 1): qml.CH, - (qml.RX, 1): qml.CRX, - (qml.RY, 1): qml.CRY, - (qml.RZ, 1): qml.CRZ, - (qml.Rot, 1): qml.CRot, - (qml.PhaseShift, 1): qml.ControlledPhaseShift, - } - return ops_with_custom_ctrl_ops