diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py new file mode 100644 index 000000000000..2a389b141ba6 --- /dev/null +++ b/qiskit/circuit/annotated_operation.py @@ -0,0 +1,188 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Annotated Operations.""" + +import dataclasses +from typing import Union, List + +from qiskit.circuit.operation import Operation +from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int +from qiskit.circuit.exceptions import CircuitError + + +class Modifier: + """The base class that all modifiers of :class:`~.AnnotatedOperation` should + inherit from.""" + + pass + + +@dataclasses.dataclass +class InverseModifier(Modifier): + """Inverse modifier: specifies that the operation is inverted.""" + + pass + + +@dataclasses.dataclass +class ControlModifier(Modifier): + """Control modifier: specifies that the operation is controlled by ``num_ctrl_qubits`` + and has control state ``ctrl_state``.""" + + num_ctrl_qubits: int = 0 + ctrl_state: Union[int, str, None] = None + + def __init__(self, num_ctrl_qubits: int = 0, ctrl_state: Union[int, str, None] = None): + self.num_ctrl_qubits = num_ctrl_qubits + self.ctrl_state = _ctrl_state_to_int(ctrl_state, num_ctrl_qubits) + + +@dataclasses.dataclass +class PowerModifier(Modifier): + """Power modifier: specifies that the operation is raised to the power ``power``.""" + + power: float + + +class AnnotatedOperation(Operation): + """Annotated operation.""" + + def __init__(self, base_op: Operation, modifiers: Union[Modifier, List[Modifier]]): + """ + Create a new AnnotatedOperation. + + An "annotated operation" allows to add a list of modifiers to the + "base" operation. For now, the only supported modifiers are of + types :class:`~.InverseModifier`, :class:`~.ControlModifier` and + :class:`~.PowerModifier`. + + An annotated operation can be viewed as an extension of + :class:`~.ControlledGate` (which also allows adding control to the + base operation). However, an important difference is that the + circuit definition of an annotated operation is not constructed when + the operation is declared, and instead happens during transpilation, + specifically during the :class:`~.HighLevelSynthesis` transpiler pass. + + An annotated operation can be also viewed as a "higher-level" + or "more abstract" object that can be added to a quantum circuit. + This enables writing transpiler optimization passes that make use of + this higher-level representation, for instance removing a gate + that is immediately followed by its inverse. + + Args: + base_op: base operation being modified + modifiers: ordered list of modifiers. Supported modifiers include + ``InverseModifier``, ``ControlModifier`` and ``PowerModifier``. + + Examples:: + + op1 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2)]) + + op2_inner = AnnotatedGate(SGate(), InverseModifier()) + op2 = AnnotatedGate(op2_inner, ControlModifier(2)) + + Both op1 and op2 are semantically equivalent to an ``SGate()`` which is first + inverted and then controlled by 2 qubits. + """ + self.base_op = base_op + self.modifiers = modifiers if isinstance(modifiers, List) else [modifiers] + + @property + def name(self): + """Unique string identifier for operation type.""" + return "annotated" + + @property + def num_qubits(self): + """Number of qubits.""" + num_ctrl_qubits = 0 + for modifier in self.modifiers: + if isinstance(modifier, ControlModifier): + num_ctrl_qubits += modifier.num_ctrl_qubits + + return num_ctrl_qubits + self.base_op.num_qubits + + @property + def num_clbits(self): + """Number of classical bits.""" + return self.base_op.num_clbits + + def __eq__(self, other) -> bool: + """Checks if two AnnotatedOperations are equal.""" + return ( + isinstance(other, AnnotatedOperation) + and self.modifiers == other.modifiers + and self.base_op == other.base_op + ) + + def copy(self) -> "AnnotatedOperation": + """Return a copy of the :class:`~.AnnotatedOperation`.""" + return AnnotatedOperation(base_op=self.base_op, modifiers=self.modifiers.copy()) + + def to_matrix(self): + """Return a matrix representation (allowing to construct Operator).""" + from qiskit.quantum_info.operators import Operator # pylint: disable=cyclic-import + + operator = Operator(self.base_op) + + for modifier in self.modifiers: + if isinstance(modifier, InverseModifier): + operator = operator.power(-1) + elif isinstance(modifier, ControlModifier): + operator = Operator( + _compute_control_matrix( + operator.data, modifier.num_ctrl_qubits, modifier.ctrl_state + ) + ) + elif isinstance(modifier, PowerModifier): + operator = operator.power(modifier.power) + else: + raise CircuitError(f"Unknown modifier {modifier}.") + return operator + + +def _canonicalize_modifiers(modifiers): + """ + Returns the canonical representative of the modifier list. This is possible + since all the modifiers commute; also note that InverseModifier is a special + case of PowerModifier. The current solution is to compute the total number + of control qubits / control state and the total power. The InverseModifier + will be present if total power is negative, whereas the power modifier will + be present only with positive powers different from 1. + """ + power = 1 + num_ctrl_qubits = 0 + ctrl_state = 0 + + for modifier in modifiers: + if isinstance(modifier, InverseModifier): + power *= -1 + elif isinstance(modifier, ControlModifier): + num_ctrl_qubits += modifier.num_ctrl_qubits + ctrl_state = (ctrl_state << modifier.num_ctrl_qubits) | modifier.ctrl_state + elif isinstance(modifier, PowerModifier): + power *= modifier.power + else: + raise CircuitError(f"Unknown modifier {modifier}.") + + canonical_modifiers = [] + if power < 0: + canonical_modifiers.append(InverseModifier()) + power *= -1 + + if power != 1: + canonical_modifiers.append(PowerModifier(power)) + if num_ctrl_qubits > 0: + canonical_modifiers.append(ControlModifier(num_ctrl_qubits, ctrl_state)) + + return canonical_modifiers diff --git a/qiskit/converters/circuit_to_gate.py b/qiskit/converters/circuit_to_gate.py index 5441d0207fd6..283dd87dbd71 100644 --- a/qiskit/converters/circuit_to_gate.py +++ b/qiskit/converters/circuit_to_gate.py @@ -12,11 +12,21 @@ """Helper function for converting a circuit to a gate""" +from qiskit.circuit.annotated_operation import AnnotatedOperation from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.exceptions import QiskitError +def _check_is_gate(op): + """Checks whether op can be converted to Gate.""" + if isinstance(op, Gate): + return True + elif isinstance(op, AnnotatedOperation): + return _check_is_gate(op.base_op) + return False + + def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label=None): """Build a :class:`.Gate` object from a :class:`.QuantumCircuit`. @@ -50,7 +60,7 @@ def circuit_to_gate(circuit, parameter_map=None, equivalence_library=None, label raise QiskitError("Circuit with classical bits cannot be converted to gate.") for instruction in circuit.data: - if not isinstance(instruction.operation, Gate): + if not _check_is_gate(instruction.operation): raise QiskitError( ( "One or more instructions cannot be converted to" diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 5de9efc380e5..9f24920b2b5a 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -706,9 +706,10 @@ def _instruction_to_matrix(cls, obj): # pylint: disable=cyclic-import from qiskit.quantum_info import Clifford + from qiskit.circuit.annotated_operation import AnnotatedOperation - if not isinstance(obj, (Instruction, Clifford)): - raise QiskitError("Input is neither an Instruction nor Clifford.") + if not isinstance(obj, (Instruction, Clifford, AnnotatedOperation)): + raise QiskitError("Input is neither Instruction, Clifford or AnnotatedOperation.") mat = None if hasattr(obj, "to_matrix"): # If instruction is a gate first we see if it has a diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index a503536b37de..8081377be460 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -13,15 +13,24 @@ """Synthesize higher-level objects.""" -from typing import Optional +from typing import Optional, Union, List, Tuple +from qiskit.circuit.operation import Operation from qiskit.converters import circuit_to_dag + from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.transpiler.target import Target from qiskit.transpiler.coupling import CouplingMap from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + InverseModifier, + ControlModifier, + PowerModifier, +) from qiskit.synthesis.clifford import ( synth_clifford_full, synth_clifford_layers, @@ -103,10 +112,16 @@ def set_methods(self, hls_name, hls_methods): class HighLevelSynthesis(TransformationPass): - """Synthesize higher-level objects using synthesis plugins. + """Synthesize higher-level objects. + + The input to this pass is a DAG that may contain higher-level objects, + including abstract mathematical objects (e.g., objects of type :class:`.LinearFunction`) + and annotated operations (objects of type :class:`.AnnotatedOperation`). + By default, all higher-level objects are synthesized, so the output is a DAG without such objects. - Synthesis plugins apply synthesis methods specified in the high-level-synthesis - config (refer to the documentation for :class:`~.HLSConfig`). + The abstract mathematical objects are synthesized using synthesis plugins, applying + synthesis methods specified in the high-level-synthesis config (refer to the documentation + for :class:`~.HLSConfig`). As an example, let us assume that ``op_a`` and ``op_b`` are names of two higher-level objects, that ``op_a``-objects have two synthesis methods ``default`` which does require any additional @@ -120,6 +135,10 @@ class HighLevelSynthesis(TransformationPass): shows how to run the alternative synthesis method ``other`` for ``op_b``-objects, while using the ``default`` methods for all other high-level objects, including ``op_a``-objects. + + The annotated operations (consisting of a base operation and a list of inverse, control and power + modifiers) are synthesizing recursively, first synthesizing the base operation, and then applying + synthesis methods for creating inverted, controlled, or powered versions of that). """ def __init__( @@ -151,6 +170,7 @@ def __init__( # When the config file is not provided, we will use the "default" method # to synthesize Operations (when available). self.hls_config = HLSConfig(True) + self.hls_plugin_manager = HighLevelSynthesisPluginManager() self._coupling_map = coupling_map self._target = target @@ -160,84 +180,203 @@ def __init__( def run(self, dag: DAGCircuit) -> DAGCircuit: """Run the HighLevelSynthesis pass on `dag`. + Args: dag: input dag. + Returns: - Output dag with certain Operations synthesized (as specified by - the hls_config). + Output dag with higher-level operations synthesized. Raises: - TranspilerError: when the specified synthesis method is not available. + TranspilerError: when the transpiler is unable to synthesize the given DAG + (for instance, when the specified synthesis method is not available). """ - # If there aren't any high level operations to synthesize return fast + + # In the future this pass will be recursive as we may have annotated gates + # whose definitions consist of other annotated gates, whose definitions include + # LinearFunctions. Note that in order to synthesize a controlled linear + # function, we must first fully synthesize the linear function, and then + # synthesize the circuit obtained by adding control logic. + # Additionally, see https://github.com/Qiskit/qiskit/pull/9846#pullrequestreview-1626991425. + + # If there are no high level operations / annotated gates to synthesize, return fast hls_names = set(self.hls_plugin_manager.plugins_by_op) - if not hls_names.intersection(dag.count_ops()): + node_names = dag.count_ops() + if "annotated" not in node_names and not hls_names.intersection(node_names): return dag - for node in dag.op_nodes(): - if node.name in self.hls_config.methods.keys(): - # the operation's name appears in the user-provided config, - # we use the list of methods provided by the user - methods = self.hls_config.methods[node.name] - elif ( - self.hls_config.use_default_on_unspecified - and "default" in self.hls_plugin_manager.method_names(node.name) - ): - # the operation's name does not appear in the user-specified config, - # we use the "default" method when instructed to do so and the "default" - # method is available - methods = ["default"] - else: - methods = [] - - for method in methods: - # There are two ways to specify a synthesis method. The more explicit - # way is to specify it as a tuple consisting of a synthesis algorithm and a - # list of additional arguments, e.g., - # ("kms", {"all_mats": 1, "max_paths": 100, "orig_circuit": 0}), or - # ("pmh", {}). - # When the list of additional arguments is empty, one can also specify - # just the synthesis algorithm, e.g., - # "pmh". - if isinstance(method, tuple): - plugin_specifier, plugin_args = method - else: - plugin_specifier = method - plugin_args = {} - - # There are two ways to specify a synthesis algorithm being run, - # either by name, e.g. "kms" (which then should be specified in entry_points), - # or directly as a class inherited from HighLevelSynthesisPlugin (which then - # does not need to be specified in entry_points). - if isinstance(plugin_specifier, str): - if plugin_specifier not in self.hls_plugin_manager.method_names(node.name): - raise TranspilerError( - "Specified method: %s not found in available plugins for %s" - % (plugin_specifier, node.name) - ) - plugin_method = self.hls_plugin_manager.method(node.name, plugin_specifier) - else: - plugin_method = plugin_specifier - qubits = ( - [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None - ) + # copy dag_op_nodes because we are modifying the DAG below + dag_op_nodes = dag.op_nodes() - decomposition = plugin_method.run( - node.op, - coupling_map=self._coupling_map, - target=self._target, - qubits=qubits, - **plugin_args, - ) + for node in dag_op_nodes: + qubits = ( + [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None + ) + decomposition, modified = self._recursively_handle_op(node.op, qubits) + + if not modified: + continue - # The synthesis methods that are not suited for the given higher-level-object - # will return None, in which case the next method in the list will be used. - if decomposition is not None: - dag.substitute_node_with_dag(node, circuit_to_dag(decomposition)) - break + if isinstance(decomposition, QuantumCircuit): + dag.substitute_node_with_dag( + node, circuit_to_dag(decomposition, copy_operations=False) + ) + elif isinstance(decomposition, Operation): + dag.substitute_node(node, decomposition) return dag + def _recursively_handle_op( + self, op: Operation, qubits: Optional[List] = None + ) -> Tuple[Union[Operation, QuantumCircuit], bool]: + """Recursively synthesizes a single operation. + + There are several possible results: + + - The given operation is unchanged + - The result is a quantum circuit: e.g., synthesizing Clifford using plugin + - The result is an Operation: e.g., adding control to CXGate results in CCXGate + + The function returns the result of the synthesis (either a quantum circuit or + an Operation), and, as an optimization, a boolean indicating whether + synthesis did anything. + + The function is recursive as synthesizing an annotated operation + involves synthesizing its "base operation" which might also be + an annotated operation. + """ + + # First, try to apply plugin mechanism + decomposition = self._synthesize_op_using_plugins(op, qubits) + if decomposition: + return decomposition, True + + # Second, handle annotated operations + # For now ignore the qubits over which the annotated operation is defined. + decomposition = self._synthesize_annotated_op(op) + if decomposition: + return decomposition, True + + # In the future, we will support recursion. + return op, False + + def _synthesize_op_using_plugins( + self, op: Operation, qubits: List + ) -> Union[QuantumCircuit, None]: + """ + Attempts to synthesize op using plugin mechanism. + Returns either the synthesized circuit or None (which occurs when no + synthesis methods are available or specified). + """ + hls_plugin_manager = self.hls_plugin_manager + + if op.name in self.hls_config.methods.keys(): + # the operation's name appears in the user-provided config, + # we use the list of methods provided by the user + methods = self.hls_config.methods[op.name] + elif ( + self.hls_config.use_default_on_unspecified + and "default" in hls_plugin_manager.method_names(op.name) + ): + # the operation's name does not appear in the user-specified config, + # we use the "default" method when instructed to do so and the "default" + # method is available + methods = ["default"] + else: + methods = [] + + for method in methods: + # There are two ways to specify a synthesis method. The more explicit + # way is to specify it as a tuple consisting of a synthesis algorithm and a + # list of additional arguments, e.g., + # ("kms", {"all_mats": 1, "max_paths": 100, "orig_circuit": 0}), or + # ("pmh", {}). + # When the list of additional arguments is empty, one can also specify + # just the synthesis algorithm, e.g., + # "pmh". + if isinstance(method, tuple): + plugin_specifier, plugin_args = method + else: + plugin_specifier = method + plugin_args = {} + + # There are two ways to specify a synthesis algorithm being run, + # either by name, e.g. "kms" (which then should be specified in entry_points), + # or directly as a class inherited from HighLevelSynthesisPlugin (which then + # does not need to be specified in entry_points). + if isinstance(plugin_specifier, str): + if plugin_specifier not in hls_plugin_manager.method_names(op.name): + raise TranspilerError( + "Specified method: %s not found in available plugins for %s" + % (plugin_specifier, op.name) + ) + plugin_method = hls_plugin_manager.method(op.name, plugin_specifier) + else: + plugin_method = plugin_specifier + + decomposition = plugin_method.run( + op, + coupling_map=self._coupling_map, + target=self._target, + qubits=qubits, + **plugin_args, + ) + + # The synthesis methods that are not suited for the given higher-level-object + # will return None, in which case the next method in the list will be used. + if decomposition is not None: + return decomposition + + return None + + def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: + """ + Recursively synthesizes annotated operations. + Returns either the synthesized operation or None (which occurs when the operation + is not an annotated operation). + """ + if isinstance(op, AnnotatedOperation): + # Currently, we ignore the qubits when recursively synthesizing the base operation. + synthesized_op, _ = self._recursively_handle_op(op.base_op, qubits=None) + + # Currently, we depend on recursive synthesis producing either a QuantumCircuit or a Gate. + # If in the future we will want to allow HighLevelSynthesis to synthesize, say, + # a LinearFunction to a Clifford, we will need to rethink this. + if not synthesized_op or not isinstance(synthesized_op, (QuantumCircuit, Gate)): + raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {op.base_op}.") + + for modifier in op.modifiers: + if isinstance(modifier, InverseModifier): + # Both QuantumCircuit and Gate have inverse method + synthesized_op = synthesized_op.inverse() + elif isinstance(modifier, ControlModifier): + # Both QuantumCircuit and Gate have inverse method + synthesized_op = synthesized_op.control( + num_ctrl_qubits=modifier.num_ctrl_qubits, + label=None, + ctrl_state=modifier.ctrl_state, + ) + elif isinstance(modifier, PowerModifier): + # QuantumCircuit has power method, and Gate needs to be converted + # to a quantum circuit. + if isinstance(synthesized_op, QuantumCircuit): + qc = synthesized_op + else: + qc = QuantumCircuit(synthesized_op.num_qubits, synthesized_op.num_clbits) + qc.append( + synthesized_op, + range(synthesized_op.num_qubits), + range(synthesized_op.num_clbits), + ) + + qc = qc.power(modifier.power) + synthesized_op = qc.to_gate() + else: + raise TranspilerError(f"Unknown modifier {modifier}.") + + return synthesized_op + return None + class DefaultSynthesisClifford(HighLevelSynthesisPlugin): """The default clifford synthesis plugin. diff --git a/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml new file mode 100644 index 000000000000..ca9a0abca850 --- /dev/null +++ b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml @@ -0,0 +1,60 @@ +--- +features: + - | + Added a new class :class:`~.AnnotatedOperation` that is a subclass of + :class:`~.Operation` and represents some "base operation" modified by a + list of "modifiers". The base operation is of type :class:`~.Operation` + and the currently supported modifiers are of types :class:`~.InverseModifier`, + :class:`~.ControlModifier` and :class:`~.PowerModifier`. + The modifiers are applied in the order they appear in the list. + + As an example:: + + gate = AnnotatedOperation( + base_op=SGate(), + modifiers=[ + InverseModifier(), + ControlModifier(1), + InverseModifier(), + PowerModifier(2), + ], + ) + + is logically equivalent to ``gate = SGate().inverse().control(1).inverse().power(2)``, + or to:: + + gate = AnnotatedOperation( + AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(1)]), + [InverseModifier(), PowerModifier(2)], + ) + + However, this equivalence is only logical, the internal representations are very + different. + + For convenience, a single modifier can be also passed directly, thus + ``AnnotatedGate(SGate(), [ControlModifier(1)])`` is equivalent to + ``AnnotatedGate(SGate(), ControlModifier(1))``. + + A distinguishing feature of an annotated operation is that circuit definition + is not constructed when the operation is declared, and instead happens only during + transpilation, specifically during the :class:`~.HighLevelSynthesis` transpiler pass. + + An annotated operation can be also viewed as a "higher-level" or a "more abstract" + object that can be added onto a quantum circuit. This enables writing transpiler + optimization passes that make use of this higher-level representation, for instance + removing a gate that is immediately followed by its inverse (note that this reduction + might not be possible if both the gate and its inverse are first synthesized into simpler + gates). + + In a sense, an annotated operation can be viewed as an extension of + :class:`~.ControlledGate`, which also allows adding control to the base operation. + In the future we are planning to replace :class:`~.ControlledGate` by + :class:`~.AnnotatedOperation`. Similar to controlled gates, the transpiler synthesizes + annotated operations before layout/routing takes place. + + As of now, the annotated operations can appear only in the top-level of a quantum + circuit, that is they cannot appear inside of the recursively-defined ``definition`` + circuit. We are planning to remove this limitation later. + - | + The :class:`.HighLevelSynthesis` is extended to synthesize circuits with objects + of type :class:`~.AnnotatedOperation`. diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py new file mode 100644 index 000000000000..46fc32b021c6 --- /dev/null +++ b/test/python/circuit/test_annotated_operation.py @@ -0,0 +1,162 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test Qiskit's AnnotatedOperation class.""" + +import unittest + +from qiskit.circuit._utils import _compute_control_matrix +from qiskit.test import QiskitTestCase +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + ControlModifier, + InverseModifier, + PowerModifier, + _canonicalize_modifiers, +) +from qiskit.circuit.library import SGate, SdgGate +from qiskit.quantum_info import Operator + + +class TestAnnotatedOperationlass(QiskitTestCase): + """Testing qiskit.circuit.AnnotatedOperation""" + + def test_create_gate_with_modifier(self): + """Test creating a gate with a single modifier.""" + op = AnnotatedOperation(SGate(), InverseModifier()) + self.assertIsInstance(op, AnnotatedOperation) + self.assertIsInstance(op.base_op, SGate) + + def test_create_gate_with_modifier_list(self): + """Test creating a gate with a list of modifiers.""" + op = AnnotatedOperation( + SGate(), [InverseModifier(), ControlModifier(2), PowerModifier(3), InverseModifier()] + ) + self.assertIsInstance(op, AnnotatedOperation) + self.assertIsInstance(op.base_op, SGate) + self.assertEqual( + op.modifiers, + [InverseModifier(), ControlModifier(2), PowerModifier(3), InverseModifier()], + ) + self.assertNotEqual( + op.modifiers, + [InverseModifier(), PowerModifier(3), ControlModifier(2), InverseModifier()], + ) + + def test_create_gate_with_empty_modifier_list(self): + """Test creating a gate with an empty list of modifiers.""" + op = AnnotatedOperation(SGate(), []) + self.assertIsInstance(op, AnnotatedOperation) + self.assertIsInstance(op.base_op, SGate) + self.assertEqual(op.modifiers, []) + + def test_create_nested_annotated_gates(self): + """Test creating an annotated gate whose base operation is also an annotated gate.""" + op_inner = AnnotatedOperation(SGate(), ControlModifier(3)) + op = AnnotatedOperation(op_inner, InverseModifier()) + self.assertIsInstance(op, AnnotatedOperation) + self.assertIsInstance(op.base_op, AnnotatedOperation) + self.assertIsInstance(op.base_op.base_op, SGate) + + def test_equality(self): + """Test equality/non-equality of annotated operations + (note that the lists of modifiers are ordered). + """ + op1 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2)]) + op2 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2)]) + self.assertEqual(op1, op2) + op3 = AnnotatedOperation(SGate(), [ControlModifier(2), InverseModifier()]) + self.assertNotEqual(op1, op3) + op4 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2, ctrl_state=2)]) + op5 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2, ctrl_state=3)]) + op6 = AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(2, ctrl_state=None)]) + self.assertNotEqual(op1, op4) + self.assertEqual(op1, op5) + self.assertEqual(op1, op6) + op7 = AnnotatedOperation(SdgGate(), [InverseModifier(), ControlModifier(2)]) + self.assertNotEqual(op1, op7) + + def test_num_qubits(self): + """Tests that number of qubits is computed correctly.""" + op_inner = AnnotatedOperation( + SGate(), + [ + ControlModifier(4, ctrl_state=1), + InverseModifier(), + ControlModifier(2), + PowerModifier(3), + InverseModifier(), + ], + ) + op = AnnotatedOperation(op_inner, ControlModifier(3)) + self.assertEqual(op.num_qubits, 10) + + def test_num_clbits(self): + """Tests that number of clbits is computed correctly.""" + op_inner = AnnotatedOperation( + SGate(), + [ + ControlModifier(4, ctrl_state=1), + InverseModifier(), + ControlModifier(2), + PowerModifier(3), + InverseModifier(), + ], + ) + op = AnnotatedOperation(op_inner, ControlModifier(3)) + self.assertEqual(op.num_clbits, 0) + + def test_to_matrix_with_control_modifier(self): + """Test that ``to_matrix`` works correctly for control modifiers.""" + num_ctrl_qubits = 3 + for ctrl_state in [5, None, 0, 7, "110"]: + op = AnnotatedOperation( + SGate(), ControlModifier(num_ctrl_qubits=num_ctrl_qubits, ctrl_state=ctrl_state) + ) + target_mat = _compute_control_matrix(SGate().to_matrix(), num_ctrl_qubits, ctrl_state) + self.assertEqual(Operator(op), Operator(target_mat)) + + def test_to_matrix_with_inverse_modifier(self): + """Test that ``to_matrix`` works correctly for inverse modifiers.""" + op = AnnotatedOperation(SGate(), InverseModifier()) + self.assertEqual(Operator(op), Operator(SGate()).power(-1)) + + def test_to_matrix_with_power_modifier(self): + """Test that ``to_matrix`` works correctly for power modifiers with integer powers.""" + for power in [0, 1, -1, 2, -2]: + op = AnnotatedOperation(SGate(), PowerModifier(power)) + self.assertEqual(Operator(op), Operator(SGate()).power(power)) + + def test_canonicalize_modifiers(self): + """Test that ``canonicalize_modifiers`` works correctly.""" + original_list = [ + InverseModifier(), + ControlModifier(2), + PowerModifier(2), + ControlModifier(1), + InverseModifier(), + PowerModifier(-3), + ] + canonical_list = _canonicalize_modifiers(original_list) + expected_list = [InverseModifier(), PowerModifier(6), ControlModifier(3)] + self.assertEqual(canonical_list, expected_list) + + def test_canonicalize_inverse(self): + """Tests that canonicalization cancels pairs of inverse modifiers.""" + original_list = _canonicalize_modifiers([InverseModifier(), InverseModifier()]) + canonical_list = _canonicalize_modifiers(original_list) + expected_list = [] + self.assertEqual(canonical_list, expected_list) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 6d9555c15a49..f41f0a3dd16f 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -19,6 +19,7 @@ import sys import unittest from logging import StreamHandler, getLogger + from test import combine # pylint: disable=wrong-import-order from unittest.mock import patch @@ -39,6 +40,12 @@ SwitchCaseOp, WhileLoopOp, ) +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + InverseModifier, + ControlModifier, + PowerModifier, +) from qiskit.circuit.classical import expr from qiskit.circuit.delay import Delay from qiskit.circuit.library import ( @@ -53,6 +60,7 @@ U2Gate, UGate, XGate, + SGate, ) from qiskit.circuit.measure import Measure from qiskit.compiler import transpile @@ -1662,6 +1670,35 @@ def test_paulis_to_constrained_1q_basis(self, opt_level, basis): self.assertGreaterEqual(set(basis) | {"barrier"}, transpiled.count_ops().keys()) self.assertEqual(Operator(qc), Operator(transpiled)) + @combine(opt_level=[0, 1, 2, 3]) + def test_transpile_annotated_ops(self, opt_level): + """Test transpilation of circuits with annotated operations.""" + qc = QuantumCircuit(3) + qc.append(AnnotatedOperation(SGate(), InverseModifier()), [0]) + qc.append(AnnotatedOperation(XGate(), ControlModifier(1)), [1, 2]) + qc.append(AnnotatedOperation(HGate(), PowerModifier(3)), [2]) + expected = QuantumCircuit(3) + expected.sdg(0) + expected.cx(1, 2) + expected.h(2) + transpiled = transpile(qc, optimization_level=opt_level, seed_transpiler=42) + self.assertNotIn("annotated", transpiled.count_ops().keys()) + self.assertEqual(Operator(qc), Operator(transpiled)) + self.assertEqual(Operator(qc), Operator(expected)) + + @combine(opt_level=[0, 1, 2, 3]) + def test_transpile_annotated_ops_with_backend(self, opt_level): + """Test transpilation of circuits with annotated operations given a backend.""" + qc = QuantumCircuit(3) + qc.append(AnnotatedOperation(SGate(), InverseModifier()), [0]) + qc.append(AnnotatedOperation(XGate(), ControlModifier(1)), [1, 2]) + qc.append(AnnotatedOperation(HGate(), PowerModifier(3)), [2]) + backend = FakeMelbourne() + transpiled = transpile( + qc, optimization_level=opt_level, backend=backend, seed_transpiler=42 + ) + self.assertLessEqual(set(transpiled.count_ops().keys()), {"u1", "u2", "u3", "cx"}) + @ddt class TestPostTranspileIntegration(QiskitTestCase): diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index a387d7b6a0b9..1515a6c3a67d 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -16,12 +16,21 @@ import unittest.mock - +import numpy as np from qiskit.circuit import QuantumCircuit, Operation +from qiskit.circuit.library import SwapGate, CXGate, RZGate, PermutationGate +from qiskit.quantum_info import Clifford from qiskit.test import QiskitTestCase from qiskit.transpiler import PassManager, TranspilerError, CouplingMap from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPlugin from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + ControlModifier, + InverseModifier, + PowerModifier, +) +from qiskit.quantum_info import Operator from qiskit.providers.fake_provider.fake_backend_v2 import FakeBackend5QV2 @@ -174,7 +183,7 @@ def method(self, op_name, method_name): return self.plugins[plugin_name]() -class TestHighLeverSynthesisInterface(QiskitTestCase): +class TestHighLevelSynthesisInterface(QiskitTestCase): """Tests for the synthesis plugin interface.""" def create_circ(self): @@ -462,5 +471,320 @@ def test_qubits_get_passed_to_plugins(self): pm_use_qubits_true.run(qc) +class TestHighLevelSynthesisModifiers(QiskitTestCase): + """Tests for high-level-synthesis pass.""" + + def test_control_basic_gates(self): + """Test lazy control synthesis of basic gates (each has its class ``control`` method).""" + lazy_gate1 = AnnotatedOperation(SwapGate(), ControlModifier(2)) + lazy_gate2 = AnnotatedOperation(CXGate(), ControlModifier(1)) + lazy_gate3 = AnnotatedOperation(RZGate(np.pi / 4), ControlModifier(1)) + circuit = QuantumCircuit(4) + circuit.append(lazy_gate1, [0, 1, 2, 3]) + circuit.append(lazy_gate2, [0, 1, 2]) + circuit.append(lazy_gate3, [2, 3]) + transpiled_circuit = HighLevelSynthesis()(circuit) + controlled_gate1 = SwapGate().control(2) + controlled_gate2 = CXGate().control(1) + controlled_gate3 = RZGate(np.pi / 4).control(1) + expected_circuit = QuantumCircuit(4) + expected_circuit.append(controlled_gate1, [0, 1, 2, 3]) + expected_circuit.append(controlled_gate2, [0, 1, 2]) + expected_circuit.append(controlled_gate3, [2, 3]) + + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_control_custom_gates(self): + """Test lazy control synthesis of custom gates (which inherits ``control`` method from + ``Gate``). + """ + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.h(1) + gate = qc.to_gate() + circuit = QuantumCircuit(4) + circuit.append(AnnotatedOperation(gate, ControlModifier(2)), [0, 1, 2, 3]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(4) + expected_circuit.append(gate.control(2), [0, 1, 2, 3]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_control_clifford(self): + """Test lazy control synthesis of Clifford objects (no ``control`` method defined).""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.h(1) + cliff = Clifford(qc) + circuit = QuantumCircuit(4) + circuit.append(AnnotatedOperation(cliff, ControlModifier(2)), [0, 1, 2, 3]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(4) + expected_circuit.append(cliff.to_instruction().control(2), [0, 1, 2, 3]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_multiple_controls(self): + """Test lazy controlled synthesis with multiple control modifiers.""" + lazy_gate1 = AnnotatedOperation(SwapGate(), [ControlModifier(2), ControlModifier(1)]) + circuit = QuantumCircuit(5) + circuit.append(lazy_gate1, [0, 1, 2, 3, 4]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(5) + expected_circuit.append(SwapGate().control(2).control(1), [0, 1, 2, 3, 4]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_nested_controls(self): + """Test lazy controlled synthesis of nested lazy gates.""" + lazy_gate1 = AnnotatedOperation(SwapGate(), ControlModifier(2)) + lazy_gate2 = AnnotatedOperation(lazy_gate1, ControlModifier(1)) + circuit = QuantumCircuit(5) + circuit.append(lazy_gate2, [0, 1, 2, 3, 4]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(5) + expected_circuit.append(SwapGate().control(2).control(1), [0, 1, 2, 3, 4]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_nested_controls_permutation(self): + """Test lazy controlled synthesis of ``PermutationGate`` with nested lazy gates. + Note that ``PermutationGate`` currently does not have definition.""" + lazy_gate1 = AnnotatedOperation(PermutationGate([3, 1, 0, 2]), ControlModifier(2)) + lazy_gate2 = AnnotatedOperation(lazy_gate1, ControlModifier(1)) + circuit = QuantumCircuit(7) + circuit.append(lazy_gate2, [0, 1, 2, 3, 4, 5, 6]) + transpiled_circuit = HighLevelSynthesis()(circuit) + self.assertEqual(Operator(circuit), Operator(transpiled_circuit)) + + def test_inverse_basic_gates(self): + """Test lazy inverse synthesis of basic gates (each has its class ``control`` method).""" + lazy_gate1 = AnnotatedOperation(SwapGate(), InverseModifier()) + lazy_gate2 = AnnotatedOperation(CXGate(), InverseModifier()) + lazy_gate3 = AnnotatedOperation(RZGate(np.pi / 4), InverseModifier()) + circuit = QuantumCircuit(4) + circuit.append(lazy_gate1, [0, 2]) + circuit.append(lazy_gate2, [0, 1]) + circuit.append(lazy_gate3, [2]) + transpiled_circuit = HighLevelSynthesis()(circuit) + inverse_gate1 = SwapGate().inverse() + inverse_gate2 = CXGate().inverse() + inverse_gate3 = RZGate(np.pi / 4).inverse() + expected_circuit = QuantumCircuit(4) + expected_circuit.append(inverse_gate1, [0, 2]) + expected_circuit.append(inverse_gate2, [0, 1]) + expected_circuit.append(inverse_gate3, [2]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_inverse_custom_gates(self): + """Test lazy control synthesis of custom gates (which inherits ``inverse`` method from + ``Gate``). + """ + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.h(1) + gate = qc.to_gate() + circuit = QuantumCircuit(2) + circuit.append(AnnotatedOperation(gate, InverseModifier()), [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(gate.inverse(), [0, 1]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_inverse_clifford(self): + """Test lazy inverse synthesis of Clifford objects (no ``inverse`` method defined).""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.h(1) + cliff = Clifford(qc) + circuit = QuantumCircuit(2) + circuit.append(AnnotatedOperation(cliff, InverseModifier()), [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(cliff.to_instruction().inverse(), [0, 1]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_two_inverses(self): + """Test lazy controlled synthesis with multiple inverse modifiers (even).""" + lazy_gate1 = AnnotatedOperation(SwapGate(), [InverseModifier(), InverseModifier()]) + circuit = QuantumCircuit(2) + circuit.append(lazy_gate1, [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(SwapGate().inverse().inverse(), [0, 1]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_three_inverses(self): + """Test lazy controlled synthesis with multiple inverse modifiers (odd).""" + lazy_gate1 = AnnotatedOperation( + RZGate(np.pi / 4), [InverseModifier(), InverseModifier(), InverseModifier()] + ) + circuit = QuantumCircuit(1) + circuit.append(lazy_gate1, [0]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(1) + expected_circuit.append(RZGate(np.pi / 4).inverse().inverse().inverse(), [0]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_nested_inverses(self): + """Test lazy synthesis with nested lazy gates.""" + lazy_gate1 = AnnotatedOperation(SwapGate(), InverseModifier()) + lazy_gate2 = AnnotatedOperation(lazy_gate1, InverseModifier()) + circuit = QuantumCircuit(2) + circuit.append(lazy_gate2, [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(SwapGate(), [0, 1]) + self.assertEqual(transpiled_circuit, expected_circuit) + + def test_nested_inverses_permutation(self): + """Test lazy controlled synthesis of ``PermutationGate`` with nested lazy gates. + Note that ``PermutationGate`` currently does not have definition.""" + lazy_gate1 = AnnotatedOperation(PermutationGate([3, 1, 0, 2]), InverseModifier()) + lazy_gate2 = AnnotatedOperation(lazy_gate1, InverseModifier()) + circuit = QuantumCircuit(4) + circuit.append(lazy_gate2, [0, 1, 2, 3]) + transpiled_circuit = HighLevelSynthesis()(circuit) + self.assertEqual(Operator(circuit), Operator(transpiled_circuit)) + + def test_power_posint_basic_gates(self): + """Test lazy power synthesis of basic gates with positive and zero integer powers.""" + lazy_gate1 = AnnotatedOperation(SwapGate(), PowerModifier(2)) + lazy_gate2 = AnnotatedOperation(CXGate(), PowerModifier(1)) + lazy_gate3 = AnnotatedOperation(RZGate(np.pi / 4), PowerModifier(3)) + lazy_gate4 = AnnotatedOperation(CXGate(), PowerModifier(0)) + circuit = QuantumCircuit(4) + circuit.append(lazy_gate1, [0, 1]) + circuit.append(lazy_gate2, [1, 2]) + circuit.append(lazy_gate3, [3]) + circuit.append(lazy_gate4, [2, 3]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(4) + expected_circuit.append(SwapGate(), [0, 1]) + expected_circuit.append(SwapGate(), [0, 1]) + expected_circuit.append(CXGate(), [1, 2]) + expected_circuit.append(RZGate(np.pi / 4), [3]) + expected_circuit.append(RZGate(np.pi / 4), [3]) + expected_circuit.append(RZGate(np.pi / 4), [3]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_power_negint_basic_gates(self): + """Test lazy power synthesis of basic gates with negative integer powers.""" + lazy_gate1 = AnnotatedOperation(CXGate(), PowerModifier(-1)) + lazy_gate2 = AnnotatedOperation(RZGate(np.pi / 4), PowerModifier(-3)) + circuit = QuantumCircuit(4) + circuit.append(lazy_gate1, [0, 1]) + circuit.append(lazy_gate2, [2]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(4) + expected_circuit.append(CXGate(), [0, 1]) + expected_circuit.append(RZGate(-np.pi / 4), [2]) + expected_circuit.append(RZGate(-np.pi / 4), [2]) + expected_circuit.append(RZGate(-np.pi / 4), [2]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_power_float_basic_gates(self): + """Test lazy power synthesis of basic gates with floating-point powers.""" + lazy_gate1 = AnnotatedOperation(SwapGate(), PowerModifier(0.5)) + lazy_gate2 = AnnotatedOperation(CXGate(), PowerModifier(0.2)) + lazy_gate3 = AnnotatedOperation(RZGate(np.pi / 4), PowerModifier(-0.25)) + circuit = QuantumCircuit(4) + circuit.append(lazy_gate1, [0, 1]) + circuit.append(lazy_gate2, [1, 2]) + circuit.append(lazy_gate3, [3]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(4) + expected_circuit.append(SwapGate().power(0.5), [0, 1]) + expected_circuit.append(CXGate().power(0.2), [1, 2]) + expected_circuit.append(RZGate(np.pi / 4).power(-0.25), [3]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_power_custom_gates(self): + """Test lazy power synthesis of custom gates with positive integer powers.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.h(1) + gate = qc.to_gate() + circuit = QuantumCircuit(2) + circuit.append(AnnotatedOperation(gate, PowerModifier(3)), [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(gate, [0, 1]) + expected_circuit.append(gate, [0, 1]) + expected_circuit.append(gate, [0, 1]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_power_posint_clifford(self): + """Test lazy power synthesis of Clifford objects with positive integer power.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.h(1) + cliff = Clifford(qc) + circuit = QuantumCircuit(2) + circuit.append(AnnotatedOperation(cliff, PowerModifier(3)), [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(cliff.to_instruction().power(3), [0, 1]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_power_float_clifford(self): + """Test lazy power synthesis of Clifford objects with floating-point power.""" + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.h(1) + cliff = Clifford(qc) + circuit = QuantumCircuit(2) + circuit.append(AnnotatedOperation(cliff, PowerModifier(-0.5)), [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(cliff.to_instruction().power(-0.5), [0, 1]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_multiple_powers(self): + """Test lazy controlled synthesis with multiple power modifiers.""" + lazy_gate1 = AnnotatedOperation(SwapGate(), [PowerModifier(2), PowerModifier(-1)]) + circuit = QuantumCircuit(2) + circuit.append(lazy_gate1, [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(SwapGate().power(-2), [0, 1]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_nested_powers(self): + """Test lazy synthesis with nested lazy gates.""" + lazy_gate1 = AnnotatedOperation(SwapGate(), PowerModifier(2)) + lazy_gate2 = AnnotatedOperation(lazy_gate1, PowerModifier(-1)) + circuit = QuantumCircuit(2) + circuit.append(lazy_gate2, [0, 1]) + transpiled_circuit = HighLevelSynthesis()(circuit) + expected_circuit = QuantumCircuit(2) + expected_circuit.append(SwapGate().power(-2), [0, 1]) + self.assertEqual(Operator(transpiled_circuit), Operator(expected_circuit)) + + def test_nested_powers_permutation(self): + """Test lazy controlled synthesis of ``PermutationGate`` with nested lazy gates. + Note that ``PermutationGate`` currently does not have definition.""" + lazy_gate1 = AnnotatedOperation(PermutationGate([3, 1, 0, 2]), PowerModifier(2)) + lazy_gate2 = AnnotatedOperation(lazy_gate1, PowerModifier(-1)) + circuit = QuantumCircuit(4) + circuit.append(lazy_gate2, [0, 1, 2, 3]) + transpiled_circuit = HighLevelSynthesis()(circuit) + self.assertEqual(Operator(circuit), Operator(transpiled_circuit)) + + def test_reordered_modifiers(self): + """Test involving gates with different modifiers.""" + lazy_gate1 = AnnotatedOperation( + PermutationGate([3, 1, 0, 2]), [InverseModifier(), ControlModifier(2), PowerModifier(3)] + ) + lazy_gate2 = AnnotatedOperation( + PermutationGate([3, 1, 0, 2]), [PowerModifier(3), ControlModifier(2), InverseModifier()] + ) + qc1 = QuantumCircuit(6) + qc1.append(lazy_gate1, [0, 1, 2, 3, 4, 5]) + qc2 = QuantumCircuit(6) + qc2.append(lazy_gate2, [0, 1, 2, 3, 4, 5]) + self.assertEqual(Operator(qc1), Operator(qc2)) + transpiled1 = HighLevelSynthesis()(qc1) + transpiled2 = HighLevelSynthesis()(qc2) + self.assertEqual(Operator(transpiled1), Operator(transpiled2)) + self.assertEqual(Operator(qc1), Operator(transpiled1)) + + if __name__ == "__main__": unittest.main()