From 774005aeda1ff0e3cbca9df2cadeb81b92959e84 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 23 Mar 2023 10:50:58 +0200 Subject: [PATCH 01/24] Defining and synthesizing AnnotatedOperation --- qiskit/circuit/annotated_operation.py | 179 ++++++++++ qiskit/converters/circuit_to_gate.py | 12 +- qiskit/quantum_info/operators/operator.py | 5 +- .../passes/synthesis/high_level_synthesis.py | 245 +++++++++---- .../circuit/test_annotated_operation.py | 86 +++++ .../transpiler/test_high_level_synthesis.py | 330 +++++++++++++++++- 6 files changed, 790 insertions(+), 67 deletions(-) create mode 100644 qiskit/circuit/annotated_operation.py create mode 100644 test/python/circuit/test_annotated_operation.py diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py new file mode 100644 index 000000000000..f68d37e570aa --- /dev/null +++ b/qiskit/circuit/annotated_operation.py @@ -0,0 +1,179 @@ +import dataclasses +from typing import Optional, 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: + pass + + +@dataclasses.dataclass +class InverseModifier(Modifier): + pass + + +@dataclasses.dataclass +class ControlModifier(Modifier): + num_ctrl_qubits: int + 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: float + + +class AnnotatedOperation(Operation): + """Gate and modifiers inside.""" + + def __init__( + self, + base_op: Operation, + modifiers: Union[Modifier, List[Modifier]] + ): + 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 "lazy" + + @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 lazy_inverse(self): + """Returns lazy inverse + """ + + # ToDo: Should we copy base_op? modifiers? + modifiers = self.modifiers.copy() + modifiers.append(InverseModifier()) + return AnnotatedOperation(self.base_op, modifiers) + + def inverse(self): + return self.lazy_inverse() + + def lazy_control( + self, + num_ctrl_qubits: int = 1, + label: Optional[str] = None, + ctrl_state: Optional[Union[int, str]] = None, + ): + """Maybe does not belong here""" + modifiers = self.modifiers.copy() + modifiers.append(ControlModifier(num_ctrl_qubits, ctrl_state)) + return AnnotatedOperation(self.base_op, modifiers) + + def control( + self, + num_ctrl_qubits: int = 1, + label: Optional[str] = None, + ctrl_state: Optional[Union[int, str]] = None, + ): + return self.lazy_control(num_ctrl_qubits, label, ctrl_state) + + def lazy_power(self, power: float) -> "AnnotatedOperation": + modifiers = self.modifiers.copy() + modifiers.append(PowerModifier(power)) + return AnnotatedOperation(self.base_op, modifiers) + + def power(self, power: float): + return self.lazy_power(power) + + 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 print_rec(self, offset=0, depth=100, header=""): + """Temporary debug function.""" + line = " " * offset + header + " LazyGate " + self.name + for modifier in self.modifiers: + if isinstance(modifier, InverseModifier): + line += "[inv] " + elif isinstance(modifier, ControlModifier): + line += "[ctrl=" + str(modifier.num_ctrl_qubits) + ", state=" + str(modifier.ctrl_state) + "] " + elif isinstance(modifier, PowerModifier): + line += "[power=" + str(modifier.power) + "] " + else: + raise CircuitError(f"Unknown modifier {modifier}.") + + print(line) + if depth >= 0: + self.base_op.print_rec(offset + 2, depth - 1, header="base gate") + + 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 import Operator + + 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): + 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 1b04e7e9f85d..96a99828025e 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -551,9 +551,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 fd57a74b2cf2..0c4fcff2c786 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -14,14 +14,20 @@ """Synthesize higher-level objects.""" -from qiskit.converters import circuit_to_dag +from typing import Union + +from qiskit.circuit.operation import Operation +from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.synthesis import synth_permutation_basic, synth_permutation_acg from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError -from qiskit.synthesis import synth_clifford_full +from qiskit.synthesis.clifford import synth_clifford_full from qiskit.synthesis.linear import synth_cnot_count_full_pmh from qiskit.synthesis.permutation import synth_permutation_depth_lnn_kms +from qiskit.circuit.annotated_operation import AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier + from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin @@ -88,10 +94,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 @@ -105,6 +117,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__(self, hls_config=None): @@ -117,77 +133,182 @@ def __init__(self, hls_config=None): # to synthesize Operations (when available). self.hls_config = HLSConfig(True) + self.hls_plugin_manager = HighLevelSynthesisPluginManager() + 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). """ - hls_plugin_manager = HighLevelSynthesisPluginManager() - - 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 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 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 = hls_plugin_manager.method(node.name, plugin_specifier) - else: - plugin_method = plugin_specifier + # The pass is recursive, as we may have annotated gates whose definitions + # consist of other annotated gates, whose definitions include for instance + # 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. + + # copy dag_op_nodes because we are modifying the DAG below + dag_op_nodes = dag.op_nodes() + + for node in dag_op_nodes: + decomposition = self._recursively_handle_op(node.op) - # ToDo: similarly to UnitarySynthesis, we should pass additional parameters - # e.g. coupling_map to the synthesis algorithm. - decomposition = plugin_method.run(node.op, **plugin_args) + if not isinstance(decomposition, (QuantumCircuit, Operation)): + raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {node.op}.") - # 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)) + elif isinstance(decomposition, Operation): + dag.substitute_node(node, decomposition) return dag + def _recursively_handle_op(self, op: Operation) -> Union[Operation, QuantumCircuit]: + """Recursively synthesizes a single operation. + + The result can be either another operation or a quantum circuit. + + Some examples when the result can be another operation: + Adding control to CX-gate results in CCX-gate, + Adding inverse to SGate results in SdgGate. + + Some examples when the result can be a quantum circuit: + Synthesizing a LinearFunction produces a quantum circuit consisting of + CX-gates. + + The function recursively handles operation's definition, if it exists. + """ + + # First, try to apply plugin mechanism + decomposition = self._synthesize_op_using_plugins(op) + if decomposition: + return decomposition + + # Second, handle annotated operations + decomposition = self._synthesize_annotated_op(op) + if decomposition: + return decomposition + + # Third, recursively descend into op's definition if exists + if getattr(op, "definition", None) is not None: + dag = circuit_to_dag(op.definition) + dag = self.run(dag) + op.definition = dag_to_circuit(dag) + + return op + + def _synthesize_op_using_plugins(self, op: Operation) -> 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 + + # ToDo: similarly to UnitarySynthesis, we should pass additional parameters + # e.g. coupling_map to the synthesis algorithm. + # print(f"{plugin_method = }, {op = }, {plugin_args = }") + decomposition = plugin_method.run(op, **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): + synthesized_op = self._recursively_handle_op(op.base_op) + + if not synthesized_op: + raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {op.base_op}.") + if 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): + # ToDo: what do we do for clifford or Operation without inverse method? + synthesized_op = synthesized_op.inverse() + elif isinstance(modifier, ControlModifier): + # Above we checked that we either have a gate or a quantum circuit + synthesized_op = synthesized_op.control( + num_ctrl_qubits=modifier.num_ctrl_qubits, + label=None, + ctrl_state=modifier.ctrl_state, + ) + elif isinstance(modifier, PowerModifier): + 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/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py new file mode 100644 index 000000000000..244035d5cf49 --- /dev/null +++ b/test/python/circuit/test_annotated_operation.py @@ -0,0 +1,86 @@ +# 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 + +import numpy as np + +from qiskit.circuit._utils import _compute_control_matrix +from qiskit.test import QiskitTestCase +from qiskit.circuit import QuantumCircuit, Barrier, Measure, Reset, Gate, Operation +from qiskit.circuit.annotated_operation import AnnotatedOperation, ControlModifier, InverseModifier, _canonicalize_modifiers +from qiskit.circuit.library import XGate, CXGate, SGate +from qiskit.quantum_info.operators import Clifford, CNOTDihedral, Pauli +from qiskit.extensions.quantum_initializer import Initialize, Isometry +from qiskit.quantum_info import Operator + + +class TestAnnotatedOperationlass(QiskitTestCase): + """Testing qiskit.circuit.AnnotatedOperation""" + + def test_lazy_inverse(self): + """Test that lazy inverse results in AnnotatedOperation.""" + gate = SGate() + lazy_gate = gate.lazy_inverse() + self.assertIsInstance(lazy_gate, AnnotatedOperation) + self.assertIsInstance(lazy_gate.base_op, SGate) + + def test_lazy_control(self): + """Test that lazy control results in AnnotatedOperation.""" + gate = CXGate() + lazy_gate = gate.lazy_control(2) + self.assertIsInstance(lazy_gate, AnnotatedOperation) + self.assertIsInstance(lazy_gate.base_op, CXGate) + + def test_lazy_iterative(self): + """Test that iteratively applying lazy inverse and control + combines lazy modifiers.""" + lazy_gate = CXGate().lazy_inverse().lazy_control(2).lazy_inverse().lazy_control(1) + self.assertIsInstance(lazy_gate, AnnotatedOperation) + self.assertIsInstance(lazy_gate.base_op, CXGate) + self.assertEqual(len(lazy_gate.modifiers), 4) + + def test_eq(self): + lazy1 = CXGate().lazy_inverse().lazy_control(2) + + lazy2 = CXGate().lazy_inverse().lazy_control(2, ctrl_state=None) + self.assertEqual(lazy1, lazy2) + + lazy3 = CXGate().lazy_inverse().lazy_control(2, ctrl_state=2) + self.assertNotEqual(lazy1, lazy3) + + lazy4 = CXGate().lazy_inverse().lazy_control(2, ctrl_state=3) + self.assertEqual(lazy1, lazy4) + + lazy5 = CXGate().lazy_control(2).lazy_inverse() + self.assertNotEqual(lazy1, lazy5) + + def test_lazy_open_control(self): + base_gate = XGate() + base_mat = base_gate.to_matrix() + num_ctrl_qubits = 3 + + for ctrl_state in [5, None, 0, 7, "110"]: + lazy_gate = AnnotatedOperation(base_gate, ControlModifier(num_ctrl_qubits=num_ctrl_qubits, ctrl_state=ctrl_state)) + target_mat = _compute_control_matrix(base_mat, num_ctrl_qubits, ctrl_state) + self.assertEqual(Operator(lazy_gate), Operator(target_mat)) + + def test_canonize(self): + modifiers = [ControlModifier(num_ctrl_qubits=2, ctrl_state=None)] + canonical_modifiers = _canonicalize_modifiers(modifiers) + self.assertEqual(modifiers, canonical_modifiers) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 19da830955f0..7d4e8e48711f 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -16,12 +16,16 @@ 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 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 # In what follows, we create two simple operations OpA and OpB, that potentially mimic @@ -146,7 +150,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): @@ -365,5 +369,327 @@ def test_synthesis_using_alternate_short_form(self): self.assertEqual(ops["swap"], 6) +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_multiple_modifiers(self): + """Test involving gates with different modifiers.""" + qc = QuantumCircuit(4) + lazy_gate1 = AnnotatedOperation(PermutationGate([3, 1, 0, 2]), InverseModifier()) + lazy_gate2 = AnnotatedOperation(SwapGate(), ControlModifier(2)) + qc.append(lazy_gate1, [0, 1, 2, 3]) + qc.append(lazy_gate2, [0, 1, 2, 3]) + custom_gate = qc.to_gate() + lazy_gate3 = AnnotatedOperation(custom_gate, ControlModifier(2)) + circuit = QuantumCircuit(6) + circuit.append(lazy_gate3, [0, 1, 2, 3, 4, 5]) + 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() From ecdd73c88ef2a896ee076d595686d59065d83ac1 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 23 Mar 2023 11:42:49 +0200 Subject: [PATCH 02/24] reworking annotated operation tests --- .../circuit/test_annotated_operation.py | 140 +++++++++++------- 1 file changed, 84 insertions(+), 56 deletions(-) diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py index 244035d5cf49..c2aa4c65150a 100644 --- a/test/python/circuit/test_annotated_operation.py +++ b/test/python/circuit/test_annotated_operation.py @@ -14,72 +14,100 @@ import unittest -import numpy as np - from qiskit.circuit._utils import _compute_control_matrix from qiskit.test import QiskitTestCase -from qiskit.circuit import QuantumCircuit, Barrier, Measure, Reset, Gate, Operation -from qiskit.circuit.annotated_operation import AnnotatedOperation, ControlModifier, InverseModifier, _canonicalize_modifiers -from qiskit.circuit.library import XGate, CXGate, SGate -from qiskit.quantum_info.operators import Clifford, CNOTDihedral, Pauli -from qiskit.extensions.quantum_initializer import Initialize, Isometry +from qiskit.circuit.annotated_operation import AnnotatedOperation, ControlModifier, InverseModifier, PowerModifier, _canonicalize_modifiers +from qiskit.circuit.library import XGate, CXGate, SGate, SdgGate from qiskit.quantum_info import Operator class TestAnnotatedOperationlass(QiskitTestCase): """Testing qiskit.circuit.AnnotatedOperation""" - def test_lazy_inverse(self): - """Test that lazy inverse results in AnnotatedOperation.""" - gate = SGate() - lazy_gate = gate.lazy_inverse() - self.assertIsInstance(lazy_gate, AnnotatedOperation) - self.assertIsInstance(lazy_gate.base_op, SGate) - - def test_lazy_control(self): - """Test that lazy control results in AnnotatedOperation.""" - gate = CXGate() - lazy_gate = gate.lazy_control(2) - self.assertIsInstance(lazy_gate, AnnotatedOperation) - self.assertIsInstance(lazy_gate.base_op, CXGate) - - def test_lazy_iterative(self): - """Test that iteratively applying lazy inverse and control - combines lazy modifiers.""" - lazy_gate = CXGate().lazy_inverse().lazy_control(2).lazy_inverse().lazy_control(1) - self.assertIsInstance(lazy_gate, AnnotatedOperation) - self.assertIsInstance(lazy_gate.base_op, CXGate) - self.assertEqual(len(lazy_gate.modifiers), 4) - - def test_eq(self): - lazy1 = CXGate().lazy_inverse().lazy_control(2) - - lazy2 = CXGate().lazy_inverse().lazy_control(2, ctrl_state=None) - self.assertEqual(lazy1, lazy2) - - lazy3 = CXGate().lazy_inverse().lazy_control(2, ctrl_state=2) - self.assertNotEqual(lazy1, lazy3) - - lazy4 = CXGate().lazy_inverse().lazy_control(2, ctrl_state=3) - self.assertEqual(lazy1, lazy4) - - lazy5 = CXGate().lazy_control(2).lazy_inverse() - self.assertNotEqual(lazy1, lazy5) - - def test_lazy_open_control(self): - base_gate = XGate() - base_mat = base_gate.to_matrix() + 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"]: - lazy_gate = AnnotatedOperation(base_gate, ControlModifier(num_ctrl_qubits=num_ctrl_qubits, ctrl_state=ctrl_state)) - target_mat = _compute_control_matrix(base_mat, num_ctrl_qubits, ctrl_state) - self.assertEqual(Operator(lazy_gate), Operator(target_mat)) - - def test_canonize(self): - modifiers = [ControlModifier(num_ctrl_qubits=2, ctrl_state=None)] - canonical_modifiers = _canonicalize_modifiers(modifiers) - self.assertEqual(modifiers, canonical_modifiers) + 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.""" + op = AnnotatedOperation(SGate(), _canonicalize_modifiers([InverseModifier(), ControlModifier(2), PowerModifier(2), ControlModifier(1), InverseModifier(), PowerModifier(-3)])) + expected_op = AnnotatedOperation(SGate(), [InverseModifier(), PowerModifier(6), ControlModifier(3)]) + self.assertEqual(op, expected_op) if __name__ == "__main__": From 24b3a9d48bccf8cc2f369201bdf0020e0310ff2f Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 23 Mar 2023 11:57:23 +0200 Subject: [PATCH 03/24] cleaning up annotated operation --- qiskit/circuit/annotated_operation.py | 93 +++++++------------ .../circuit/test_annotated_operation.py | 14 ++- 2 files changed, 45 insertions(+), 62 deletions(-) diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index f68d37e570aa..1e704edfc7cc 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Optional, Union, List +from typing import Union, List from qiskit.circuit.operation import Operation from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int @@ -7,16 +7,20 @@ class Modifier: + """Modifier class. """ 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 ctrl_state: Union[int, str, None] = None @@ -27,24 +31,43 @@ def __init__(self, num_ctrl_qubits: int = 0, ctrl_state: Union[int, str, None] = @dataclasses.dataclass class PowerModifier(Modifier): + """Power modifier: specifies that the operation is raised to the power ``power``.""" power: float class AnnotatedOperation(Operation): - """Gate and modifiers inside.""" + """Annotated operation.""" def __init__( self, base_op: Operation, modifiers: Union[Modifier, List[Modifier]] ): + """ + Create a new AnnotatedOperation. + + 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 "lazy" + return "annotated" @property def num_qubits(self): @@ -61,45 +84,6 @@ def num_clbits(self): """Number of classical bits.""" return self.base_op.num_clbits - def lazy_inverse(self): - """Returns lazy inverse - """ - - # ToDo: Should we copy base_op? modifiers? - modifiers = self.modifiers.copy() - modifiers.append(InverseModifier()) - return AnnotatedOperation(self.base_op, modifiers) - - def inverse(self): - return self.lazy_inverse() - - def lazy_control( - self, - num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[int, str]] = None, - ): - """Maybe does not belong here""" - modifiers = self.modifiers.copy() - modifiers.append(ControlModifier(num_ctrl_qubits, ctrl_state)) - return AnnotatedOperation(self.base_op, modifiers) - - def control( - self, - num_ctrl_qubits: int = 1, - label: Optional[str] = None, - ctrl_state: Optional[Union[int, str]] = None, - ): - return self.lazy_control(num_ctrl_qubits, label, ctrl_state) - - def lazy_power(self, power: float) -> "AnnotatedOperation": - modifiers = self.modifiers.copy() - modifiers.append(PowerModifier(power)) - return AnnotatedOperation(self.base_op, modifiers) - - def power(self, power: float): - return self.lazy_power(power) - def __eq__(self, other) -> bool: """Checks if two AnnotatedOperations are equal.""" return ( @@ -108,23 +92,6 @@ def __eq__(self, other) -> bool: and self.base_op == other.base_op ) - def print_rec(self, offset=0, depth=100, header=""): - """Temporary debug function.""" - line = " " * offset + header + " LazyGate " + self.name - for modifier in self.modifiers: - if isinstance(modifier, InverseModifier): - line += "[inv] " - elif isinstance(modifier, ControlModifier): - line += "[ctrl=" + str(modifier.num_ctrl_qubits) + ", state=" + str(modifier.ctrl_state) + "] " - elif isinstance(modifier, PowerModifier): - line += "[power=" + str(modifier.power) + "] " - else: - raise CircuitError(f"Unknown modifier {modifier}.") - - print(line) - if depth >= 0: - self.base_op.print_rec(offset + 2, depth - 1, header="base gate") - def copy(self) -> "AnnotatedOperation": """Return a copy of the :class:`AnnotatedOperation`.""" return AnnotatedOperation( @@ -151,6 +118,14 @@ def to_matrix(self): 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 diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py index c2aa4c65150a..2949e0ee7683 100644 --- a/test/python/circuit/test_annotated_operation.py +++ b/test/python/circuit/test_annotated_operation.py @@ -105,9 +105,17 @@ def test_to_matrix_with_power_modifier(self): def test_canonicalize_modifiers(self): """Test that ``canonicalize_modifiers`` works correctly.""" - op = AnnotatedOperation(SGate(), _canonicalize_modifiers([InverseModifier(), ControlModifier(2), PowerModifier(2), ControlModifier(1), InverseModifier(), PowerModifier(-3)])) - expected_op = AnnotatedOperation(SGate(), [InverseModifier(), PowerModifier(6), ControlModifier(3)]) - self.assertEqual(op, expected_op) + 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__": From bc2fa673b4ad1c4fdd23ac00b4299d67001b652e Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 23 Mar 2023 11:57:49 +0200 Subject: [PATCH 04/24] running black --- qiskit/circuit/annotated_operation.py | 23 ++++---- .../passes/synthesis/high_level_synthesis.py | 13 ++++- .../circuit/test_annotated_operation.py | 58 ++++++++++++++++--- .../transpiler/test_high_level_synthesis.py | 19 ++++-- 4 files changed, 87 insertions(+), 26 deletions(-) diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index 1e704edfc7cc..94702ae63830 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -7,13 +7,15 @@ class Modifier: - """Modifier class. """ + """Modifier class.""" + pass @dataclasses.dataclass class InverseModifier(Modifier): """Inverse modifier: specifies that the operation is inverted.""" + pass @@ -21,6 +23,7 @@ class InverseModifier(Modifier): 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 ctrl_state: Union[int, str, None] = None @@ -32,17 +35,14 @@ def __init__(self, num_ctrl_qubits: int = 0, ctrl_state: Union[int, str, None] = @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]] - ): + def __init__(self, base_op: Operation, modifiers: Union[Modifier, List[Modifier]]): """ Create a new AnnotatedOperation. @@ -94,10 +94,7 @@ def __eq__(self, other) -> bool: def copy(self) -> "AnnotatedOperation": """Return a copy of the :class:`AnnotatedOperation`.""" - return AnnotatedOperation( - base_op=self.base_op, - modifiers=self.modifiers.copy() - ) + return AnnotatedOperation(base_op=self.base_op, modifiers=self.modifiers.copy()) def to_matrix(self): """Return a matrix representation (allowing to construct Operator).""" @@ -109,7 +106,11 @@ def to_matrix(self): 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)) + operator = Operator( + _compute_control_matrix( + operator.data, modifier.num_ctrl_qubits, modifier.ctrl_state + ) + ) elif isinstance(modifier, PowerModifier): operator = operator.power(modifier.power) else: diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 0c4fcff2c786..b91a1c4a6977 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -26,7 +26,12 @@ from qiskit.synthesis.clifford import synth_clifford_full from qiskit.synthesis.linear import synth_cnot_count_full_pmh from qiskit.synthesis.permutation import synth_permutation_depth_lnn_kms -from qiskit.circuit.annotated_operation import AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + InverseModifier, + ControlModifier, + PowerModifier, +) from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin @@ -299,7 +304,11 @@ def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: 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.append( + synthesized_op, + range(synthesized_op.num_qubits), + range(synthesized_op.num_clbits), + ) qc = qc.power(modifier.power) synthesized_op = qc.to_gate() diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py index 2949e0ee7683..edd1da1b8abf 100644 --- a/test/python/circuit/test_annotated_operation.py +++ b/test/python/circuit/test_annotated_operation.py @@ -16,7 +16,13 @@ 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.annotated_operation import ( + AnnotatedOperation, + ControlModifier, + InverseModifier, + PowerModifier, + _canonicalize_modifiers, +) from qiskit.circuit.library import XGate, CXGate, SGate, SdgGate from qiskit.quantum_info import Operator @@ -32,11 +38,19 @@ def test_create_gate_with_modifier(self): 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()]) + 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()]) + 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.""" @@ -73,14 +87,31 @@ def test_equality(self): 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_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_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) @@ -88,7 +119,9 @@ 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)) + 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)) @@ -105,7 +138,14 @@ def test_to_matrix_with_power_modifier(self): def test_canonicalize_modifiers(self): """Test that ``canonicalize_modifiers`` works correctly.""" - original_list = [InverseModifier(), ControlModifier(2), PowerModifier(2), ControlModifier(1), InverseModifier(), PowerModifier(-3)] + 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) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 7d4e8e48711f..d233367f1b81 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -24,7 +24,12 @@ from qiskit.transpiler import PassManager 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.circuit.annotated_operation import ( + AnnotatedOperation, + ControlModifier, + InverseModifier, + PowerModifier, +) from qiskit.quantum_info import Operator @@ -509,7 +514,9 @@ def test_two_inverses(self): def test_three_inverses(self): """Test lazy controlled synthesis with multiple inverse modifiers (odd).""" - lazy_gate1 = AnnotatedOperation(RZGate(np.pi / 4), [InverseModifier(), InverseModifier(), InverseModifier()]) + lazy_gate1 = AnnotatedOperation( + RZGate(np.pi / 4), [InverseModifier(), InverseModifier(), InverseModifier()] + ) circuit = QuantumCircuit(1) circuit.append(lazy_gate1, [0]) transpiled_circuit = HighLevelSynthesis()(circuit) @@ -678,8 +685,12 @@ def test_multiple_modifiers(self): 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()]) + 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) From b2d432ae53992c7769e011c74ade73099763d7ab Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 23 Mar 2023 14:40:08 +0200 Subject: [PATCH 05/24] lint --- qiskit/circuit/annotated_operation.py | 16 +++++++++++++++- test/python/circuit/test_annotated_operation.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index 94702ae63830..b074d4405c98 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -1,3 +1,17 @@ +# 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 @@ -98,7 +112,7 @@ def copy(self) -> "AnnotatedOperation": def to_matrix(self): """Return a matrix representation (allowing to construct Operator).""" - from qiskit.quantum_info import Operator + from qiskit.quantum_info.operators import Operator # pylint: disable=cyclic-import operator = Operator(self.base_op) diff --git a/test/python/circuit/test_annotated_operation.py b/test/python/circuit/test_annotated_operation.py index edd1da1b8abf..46fc32b021c6 100644 --- a/test/python/circuit/test_annotated_operation.py +++ b/test/python/circuit/test_annotated_operation.py @@ -23,7 +23,7 @@ PowerModifier, _canonicalize_modifiers, ) -from qiskit.circuit.library import XGate, CXGate, SGate, SdgGate +from qiskit.circuit.library import SGate, SdgGate from qiskit.quantum_info import Operator From 75d4dbe8f96ed062a2fd3c60fe292620a51a3c4c Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Wed, 29 Mar 2023 16:32:15 +0300 Subject: [PATCH 06/24] adding recurse argument and fixing the way definition is handled --- qiskit/circuit/quantumcircuit.py | 4 +-- .../passes/synthesis/high_level_synthesis.py | 28 +++++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 2d883758e3d3..422c8acf1ceb 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1558,12 +1558,12 @@ def decompose( """ # pylint: disable=cyclic-import from qiskit.transpiler.passes.basis.decompose import Decompose - from qiskit.transpiler.passes.synthesis import HighLevelSynthesis + from qiskit.transpiler.passes.synthesis import HighLevelSynthesis, HLSConfig from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.converters.dag_to_circuit import dag_to_circuit dag = circuit_to_dag(self) - dag = HighLevelSynthesis().run(dag) + dag = HighLevelSynthesis(HLSConfig(recurse=False)).run(dag) pass_ = Decompose(gates_to_decompose) for _ in range(reps): dag = pass_.run(dag) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index b91a1c4a6977..eaba27d01825 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -74,16 +74,19 @@ class HLSConfig: documentation for :class:`~.HighLevelSynthesis`. """ - def __init__(self, use_default_on_unspecified=True, **kwargs): + def __init__(self, use_default_on_unspecified=True, recurse=True, **kwargs): """Creates a high-level-synthesis config. Args: use_default_on_unspecified (bool): if True, every higher-level-object without an explicitly specified list of methods will be synthesized using the "default" algorithm if it exists. + recurse (bool): if True, also recursively processes objects in gates' ``definition`` + field, if such exists. kwargs: a dictionary mapping higher-level-objects to lists of synthesis methods. """ self.use_default_on_unspecified = use_default_on_unspecified + self.recurse = recurse self.methods = {} for key, value in kwargs.items(): @@ -202,11 +205,20 @@ def _recursively_handle_op(self, op: Operation) -> Union[Operation, QuantumCircu if decomposition: return decomposition - # Third, recursively descend into op's definition if exists - if getattr(op, "definition", None) is not None: - dag = circuit_to_dag(op.definition) - dag = self.run(dag) - op.definition = dag_to_circuit(dag) + # Third, optionally recursively descend into op's definition if exists + if self.hls_config.recurse: + try: + # extract definition + definition = op.definition + except TypeError as err: + raise TranspilerError( + f"HighLevelSynthesis was unable to extract definition for {op.name}: {err}" + ) + + if definition is not None: + dag = circuit_to_dag(definition) + dag = self.run(dag) + op.definition = dag_to_circuit(dag) return op @@ -283,9 +295,7 @@ def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: if isinstance(op, AnnotatedOperation): synthesized_op = self._recursively_handle_op(op.base_op) - if not synthesized_op: - raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {op.base_op}.") - if not isinstance(synthesized_op, (QuantumCircuit, Gate)): + 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: From 874e863282fce89ec2989ca46457bef124837da6 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Wed, 29 Mar 2023 17:44:28 +0300 Subject: [PATCH 07/24] treating the case when there is no definition --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index eaba27d01825..f6c3b7c0249c 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -214,6 +214,9 @@ def _recursively_handle_op(self, op: Operation) -> Union[Operation, QuantumCircu raise TranspilerError( f"HighLevelSynthesis was unable to extract definition for {op.name}: {err}" ) + except AttributeError: + # definition is None + definition = None if definition is not None: dag = circuit_to_dag(definition) From ce6cd796059cf41c77d34218ccbf6bc9dc5593d1 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 30 Mar 2023 10:36:28 +0300 Subject: [PATCH 08/24] lint --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index f6c3b7c0249c..2a5806d3829e 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -213,7 +213,7 @@ def _recursively_handle_op(self, op: Operation) -> Union[Operation, QuantumCircu except TypeError as err: raise TranspilerError( f"HighLevelSynthesis was unable to extract definition for {op.name}: {err}" - ) + ) from err except AttributeError: # definition is None definition = None From a5aedd14d767c765781b9930d10377ac78748a67 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 30 Mar 2023 12:53:31 +0300 Subject: [PATCH 09/24] black --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index d3f004e036d0..0b0c2f4c0d4c 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -167,7 +167,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # copy dag_op_nodes because we are modifying the DAG below dag_op_nodes = dag.op_nodes() - for node in dag_op_nodes: decomposition = self._recursively_handle_op(node.op) From 5d25348d57aa1419d4a1c245b0f3e91ce292cf7b Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Fri, 31 Mar 2023 09:01:40 +0300 Subject: [PATCH 10/24] removing duplicated line resulting from merge --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 0b0c2f4c0d4c..2a5806d3829e 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -140,7 +140,6 @@ def __init__(self, hls_config=None): # 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.hls_plugin_manager = HighLevelSynthesisPluginManager() From f9152c8f0c141832213c7b2e485d16f07d8b759e Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Fri, 31 Mar 2023 09:19:40 +0300 Subject: [PATCH 11/24] improving comments --- .../transpiler/passes/synthesis/high_level_synthesis.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 2a5806d3829e..509808f8995d 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -298,21 +298,26 @@ def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: if isinstance(op, AnnotatedOperation): synthesized_op = self._recursively_handle_op(op.base_op) + # 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): - # ToDo: what do we do for clifford or Operation without inverse method? + # Both QuantumCircuit and Gate have inverse method synthesized_op = synthesized_op.inverse() elif isinstance(modifier, ControlModifier): - # Above we checked that we either have a gate or a quantum circuit + # 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: From 58cf9d3b1d74d94531b6609e07155ebcb28adc97 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Tue, 11 Jul 2023 14:32:45 +0300 Subject: [PATCH 12/24] fix to merge conflicts --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 3cf3ed95a8f2..af7cc2c61382 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -29,7 +29,7 @@ InverseModifier, ControlModifier, PowerModifier, - +) from qiskit.synthesis.clifford import ( synth_clifford_full, synth_clifford_layers, @@ -38,10 +38,7 @@ synth_clifford_ag, synth_clifford_bm, ) -from qiskit.synthesis.linear import ( - synth_cnot_count_full_pmh, - synth_cnot_depth_line_kms -) +from qiskit.synthesis.linear import synth_cnot_count_full_pmh, synth_cnot_depth_line_kms from qiskit.synthesis.permutation import ( synth_permutation_basic, synth_permutation_acg, From 623dfbcdd4ac1563d32434851eb3eb27ecfd241a Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Sun, 13 Aug 2023 14:47:39 +0300 Subject: [PATCH 13/24] black; additional fixes for prelim support of coupling map with annotated operations --- .../passes/synthesis/high_level_synthesis.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index e2354bb31a1e..d37f9150cb3f 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -13,9 +13,7 @@ """Synthesize higher-level objects.""" -from typing import Optional - -from typing import Union +from typing import Optional, Union, List from qiskit.circuit.operation import Operation from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -207,7 +205,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: dag_op_nodes = dag.op_nodes() for node in dag_op_nodes: - decomposition = self._recursively_handle_op(node.op) + qubits = ( + [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None + ) + decomposition = self._recursively_handle_op(node.op, qubits) if not isinstance(decomposition, (QuantumCircuit, Operation)): raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {node.op}.") @@ -219,7 +220,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: return dag - def _recursively_handle_op(self, op: Operation) -> Union[Operation, QuantumCircuit]: + def _recursively_handle_op( + self, op: Operation, qubits: Optional[List] = None + ) -> Union[Operation, QuantumCircuit]: """Recursively synthesizes a single operation. The result can be either another operation or a quantum circuit. @@ -236,11 +239,12 @@ def _recursively_handle_op(self, op: Operation) -> Union[Operation, QuantumCircu """ # First, try to apply plugin mechanism - decomposition = self._synthesize_op_using_plugins(op) + decomposition = self._synthesize_op_using_plugins(op, qubits) if decomposition: return decomposition # 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 @@ -265,7 +269,9 @@ def _recursively_handle_op(self, op: Operation) -> Union[Operation, QuantumCircu return op - def _synthesize_op_using_plugins(self, op: Operation) -> Union[QuantumCircuit, None]: + 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 @@ -316,13 +322,9 @@ def _synthesize_op_using_plugins(self, op: Operation) -> Union[QuantumCircuit, N plugin_method = hls_plugin_manager.method(op.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 - ) decomposition = plugin_method.run( - node.op, + op, coupling_map=self._coupling_map, target=self._target, qubits=qubits, @@ -343,7 +345,8 @@ def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: is not an annotated operation). """ if isinstance(op, AnnotatedOperation): - synthesized_op = self._recursively_handle_op(op.base_op) + # 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, From d9d59deb7e2c6e5a56c169168027b1ca553a4854 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Sun, 13 Aug 2023 19:28:17 +0300 Subject: [PATCH 14/24] calling substitude_node with propagate_conditions=False --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index d37f9150cb3f..c819eae9c755 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -214,9 +214,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {node.op}.") if isinstance(decomposition, QuantumCircuit): - dag.substitute_node_with_dag(node, circuit_to_dag(decomposition)) + dag.substitute_node_with_dag(node, circuit_to_dag(decomposition), propagate_condition=False) elif isinstance(decomposition, Operation): - dag.substitute_node(node, decomposition) + dag.substitute_node(node, decomposition, propagate_condition=False) return dag From e4585844a8a9c09655d29c190443aefbf2c5989d Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Sun, 13 Aug 2023 20:56:02 +0300 Subject: [PATCH 15/24] black --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index c819eae9c755..56a34cd0ffc5 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -214,7 +214,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {node.op}.") if isinstance(decomposition, QuantumCircuit): - dag.substitute_node_with_dag(node, circuit_to_dag(decomposition), propagate_condition=False) + dag.substitute_node_with_dag( + node, circuit_to_dag(decomposition), propagate_condition=False + ) elif isinstance(decomposition, Operation): dag.substitute_node(node, decomposition, propagate_condition=False) From 1c71712fec9a0b670e186b5d6dbb3f55594b2491 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 28 Sep 2023 09:38:36 +0300 Subject: [PATCH 16/24] removing unused imports --- qiskit/circuit/quantumcircuit.py | 4 +- .../passes/synthesis/high_level_synthesis.py | 41 +++++-------------- .../transpiler/test_high_level_synthesis.py | 15 +------ 3 files changed, 14 insertions(+), 46 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 33d0e58c77eb..6e01f3f32572 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1580,12 +1580,12 @@ def decompose( """ # pylint: disable=cyclic-import from qiskit.transpiler.passes.basis.decompose import Decompose - from qiskit.transpiler.passes.synthesis import HighLevelSynthesis, HLSConfig + from qiskit.transpiler.passes.synthesis import HighLevelSynthesis from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.converters.dag_to_circuit import dag_to_circuit dag = circuit_to_dag(self) - dag = HighLevelSynthesis(HLSConfig(recurse=False)).run(dag) + dag = HighLevelSynthesis().run(dag) pass_ = Decompose(gates_to_decompose) for _ in range(reps): dag = pass_.run(dag) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index c264c16412f8..31830065a87a 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -16,7 +16,7 @@ from typing import Optional, Union, List from qiskit.circuit.operation import Operation -from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import TransformationPass from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate @@ -87,19 +87,16 @@ class HLSConfig: documentation for :class:`~.HighLevelSynthesis`. """ - def __init__(self, use_default_on_unspecified=True, recurse=True, **kwargs): + def __init__(self, use_default_on_unspecified=True, **kwargs): """Creates a high-level-synthesis config. Args: use_default_on_unspecified (bool): if True, every higher-level-object without an explicitly specified list of methods will be synthesized using the "default" algorithm if it exists. - recurse (bool): if True, also recursively processes objects in gates' ``definition`` - field, if such exists. kwargs: a dictionary mapping higher-level-objects to lists of synthesis methods. """ self.use_default_on_unspecified = use_default_on_unspecified - self.recurse = recurse self.methods = {} for key, value in kwargs.items(): @@ -195,18 +192,19 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: (for instance, when the specified synthesis method is not available). """ + # 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) node_names = dag.count_ops() - if 'annotated' not in node_names and not hls_names.intersection(node_names): + if "annotated" not in node_names and not hls_names.intersection(node_names): return dag - # The pass is recursive, as we may have annotated gates whose definitions - # consist of other annotated gates, whose definitions include for instance - # 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. - # copy dag_op_nodes because we are modifying the DAG below dag_op_nodes = dag.op_nodes() @@ -257,24 +255,7 @@ def _recursively_handle_op( if decomposition: return decomposition - # Third, optionally recursively descend into op's definition if exists - if self.hls_config.recurse: - try: - # extract definition - definition = op.definition - except TypeError as err: - raise TranspilerError( - f"HighLevelSynthesis was unable to extract definition for {op.name}: {err}" - ) from err - except AttributeError: - # definition is None - definition = None - - if definition is not None: - dag = circuit_to_dag(definition) - dag = self.run(dag) - op.definition = dag_to_circuit(dag) - + # In the future, we will support recursion. return op def _synthesize_op_using_plugins( diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 0785b3954c7a..1515a6c3a67d 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -491,6 +491,7 @@ def test_control_basic_gates(self): 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): @@ -766,20 +767,6 @@ def test_nested_powers_permutation(self): transpiled_circuit = HighLevelSynthesis()(circuit) self.assertEqual(Operator(circuit), Operator(transpiled_circuit)) - def test_multiple_modifiers(self): - """Test involving gates with different modifiers.""" - qc = QuantumCircuit(4) - lazy_gate1 = AnnotatedOperation(PermutationGate([3, 1, 0, 2]), InverseModifier()) - lazy_gate2 = AnnotatedOperation(SwapGate(), ControlModifier(2)) - qc.append(lazy_gate1, [0, 1, 2, 3]) - qc.append(lazy_gate2, [0, 1, 2, 3]) - custom_gate = qc.to_gate() - lazy_gate3 = AnnotatedOperation(custom_gate, ControlModifier(2)) - circuit = QuantumCircuit(6) - circuit.append(lazy_gate3, [0, 1, 2, 3, 4, 5]) - 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( From 77127e400b949681ff181d80bb99be8b10782e7e Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 28 Sep 2023 14:18:03 +0300 Subject: [PATCH 17/24] improving documentation of AnnotatedOperation --- qiskit/circuit/annotated_operation.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index b074d4405c98..812ccd1d7bcb 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -21,7 +21,8 @@ class Modifier: - """Modifier class.""" + """The base class that all modifiers of :class:`~.AnnotatedOperation` should + inherit from.""" pass @@ -38,7 +39,7 @@ 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 + 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): @@ -60,6 +61,26 @@ 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`. In the future, we are planning to make + the modifier interface extendable, accommodating for user-supplied + modifiers. + + 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 :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. + we are planning to add 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 From ebfd0f2257b9d81ce21f9f753e2b78ca2ffd9763 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Fri, 29 Sep 2023 10:05:47 +0300 Subject: [PATCH 18/24] minor --- qiskit/circuit/annotated_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index 812ccd1d7bcb..0c99159cff11 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -128,7 +128,7 @@ def __eq__(self, other) -> bool: ) def copy(self) -> "AnnotatedOperation": - """Return a copy of the :class:`AnnotatedOperation`.""" + """Return a copy of the :class:`~.AnnotatedOperation`.""" return AnnotatedOperation(base_op=self.base_op, modifiers=self.modifiers.copy()) def to_matrix(self): From f23d3843b92f5d2c9168cf36f6b4ce91b576c176 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Fri, 29 Sep 2023 10:06:04 +0300 Subject: [PATCH 19/24] release notes --- ...annotated-operations-de35a0d8d98eec23.yaml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml diff --git a/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml new file mode 100644 index 000000000000..f349d643e036 --- /dev/null +++ b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml @@ -0,0 +1,62 @@ +--- +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`. Applying + 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 :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. Later we are planning to add 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. + + In the future, we are also planning to make the modifier interface extendable, + accommodating for user-supplied modifiers. + - | + The :class:`.HighLevelSynthesis` is extended to synthesize circuits with objects + of type :class:`~.AnnotatedOperation`. From 4fcd575e22aa5b4e1d6f8b7bc4272c2abac16051 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Fri, 29 Sep 2023 10:19:35 +0300 Subject: [PATCH 20/24] release notes typos --- .../notes/add-annotated-operations-de35a0d8d98eec23.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml index f349d643e036..0cf5b6df929a 100644 --- a/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml +++ b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml @@ -5,7 +5,7 @@ features: :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`. Applying + :class:`~.ControlModifier` and :class:`~.PowerModifier`. The modifiers are applied in the order they appear in the list. As an example:: @@ -20,7 +20,8 @@ features: ], ) - is logically equivalent to ``gate = SGate().inverse().control(1).inverse().power(2)``, or to:: + is logically equivalent to ``gate = SGate().inverse().control(1).inverse().power(2)``, + or to:: gate = AnnotatedOperation( AnnotatedOperation(SGate(), [InverseModifier(), ControlModifier(1)]), From 02d53ef1670fead411963955bba85e9f2a8a4219 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Wed, 4 Oct 2023 12:59:39 +0300 Subject: [PATCH 21/24] removing propagate_conditions --- qiskit/transpiler/passes/synthesis/high_level_synthesis.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 31830065a87a..039e60c8b2a9 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -218,11 +218,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {node.op}.") if isinstance(decomposition, QuantumCircuit): - dag.substitute_node_with_dag( - node, circuit_to_dag(decomposition), propagate_condition=False - ) + dag.substitute_node_with_dag(node, circuit_to_dag(decomposition)) elif isinstance(decomposition, Operation): - dag.substitute_node(node, decomposition, propagate_condition=False) + dag.substitute_node(node, decomposition) return dag From c77b81063739f8adf67204955f2ed516858dcb90 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 5 Oct 2023 13:13:37 +0300 Subject: [PATCH 22/24] applying suggestions from review --- qiskit/circuit/annotated_operation.py | 8 +++----- .../transpiler/passes/synthesis/high_level_synthesis.py | 8 ++++++-- .../notes/add-annotated-operations-de35a0d8d98eec23.yaml | 7 ++----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/qiskit/circuit/annotated_operation.py b/qiskit/circuit/annotated_operation.py index 0c99159cff11..2a389b141ba6 100644 --- a/qiskit/circuit/annotated_operation.py +++ b/qiskit/circuit/annotated_operation.py @@ -64,20 +64,18 @@ def __init__(self, base_op: Operation, modifiers: Union[Modifier, List[Modifier] 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`. In the future, we are planning to make - the modifier interface extendable, accommodating for user-supplied - modifiers. + :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 :class:`~.HighLevelSynthesis` transpiler pass. + 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. - we are planning to add transpiler optimization passes that make use of + 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. diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 039e60c8b2a9..661974433c89 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -218,7 +218,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {node.op}.") if isinstance(decomposition, QuantumCircuit): - dag.substitute_node_with_dag(node, circuit_to_dag(decomposition)) + dag.substitute_node_with_dag( + node, circuit_to_dag(decomposition, copy_operations=False) + ) elif isinstance(decomposition, Operation): dag.substitute_node(node, decomposition) @@ -239,7 +241,9 @@ def _recursively_handle_op( Synthesizing a LinearFunction produces a quantum circuit consisting of CX-gates. - The function recursively handles operation's definition, if it exists. + 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 diff --git a/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml index 0cf5b6df929a..ca9a0abca850 100644 --- a/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml +++ b/releasenotes/notes/add-annotated-operations-de35a0d8d98eec23.yaml @@ -37,10 +37,10 @@ features: 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 :class:`~.HighLevelSynthesis` transpiler pass. + 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. Later we are planning to add transpiler + 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 @@ -55,9 +55,6 @@ features: 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. - - In the future, we are also planning to make the modifier interface extendable, - accommodating for user-supplied modifiers. - | The :class:`.HighLevelSynthesis` is extended to synthesize circuits with objects of type :class:`~.AnnotatedOperation`. From fd0d7be65012ade8b18d582fa72563f6bccd0b57 Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 5 Oct 2023 14:52:28 +0300 Subject: [PATCH 23/24] tests for full transpiler flow, with and without backend --- test/python/compiler/test_transpiler.py | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) 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): From ba42605f617e164e1f8beae19ce66b259bb465eb Mon Sep 17 00:00:00 2001 From: AlexanderIvrii Date: Thu, 12 Oct 2023 14:57:46 +0300 Subject: [PATCH 24/24] not replace dag.op when op did not change --- .../passes/synthesis/high_level_synthesis.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 661974433c89..8081377be460 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -13,7 +13,7 @@ """Synthesize higher-level objects.""" -from typing import Optional, Union, List +from typing import Optional, Union, List, Tuple from qiskit.circuit.operation import Operation from qiskit.converters import circuit_to_dag @@ -212,10 +212,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: qubits = ( [dag.find_bit(x).index for x in node.qargs] if self._use_qubit_indices else None ) - decomposition = self._recursively_handle_op(node.op, qubits) + decomposition, modified = self._recursively_handle_op(node.op, qubits) - if not isinstance(decomposition, (QuantumCircuit, Operation)): - raise TranspilerError(f"HighLevelSynthesis was unable to synthesize {node.op}.") + if not modified: + continue if isinstance(decomposition, QuantumCircuit): dag.substitute_node_with_dag( @@ -228,18 +228,18 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: def _recursively_handle_op( self, op: Operation, qubits: Optional[List] = None - ) -> Union[Operation, QuantumCircuit]: + ) -> Tuple[Union[Operation, QuantumCircuit], bool]: """Recursively synthesizes a single operation. - The result can be either another operation or a quantum circuit. + There are several possible results: - Some examples when the result can be another operation: - Adding control to CX-gate results in CCX-gate, - Adding inverse to SGate results in SdgGate. + - 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 - Some examples when the result can be a quantum circuit: - Synthesizing a LinearFunction produces a quantum circuit consisting of - CX-gates. + 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 @@ -249,16 +249,16 @@ def _recursively_handle_op( # First, try to apply plugin mechanism decomposition = self._synthesize_op_using_plugins(op, qubits) if decomposition: - return 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 + return decomposition, True # In the future, we will support recursion. - return op + return op, False def _synthesize_op_using_plugins( self, op: Operation, qubits: List @@ -337,7 +337,7 @@ def _synthesize_annotated_op(self, op: Operation) -> Union[Operation, None]: """ 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) + 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,