From 796e2e73c845edd533b4d9137545bc097e52f7ca Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sat, 15 Oct 2022 13:41:42 -0400 Subject: [PATCH 01/19] make Optimize1qGatesDecomposition target aware --- .../optimization/optimize_1q_decomposition.py | 27 ++++++++++--------- ...ize-1q-decomposition-cb9bb4651607b639.yaml | 8 ++++++ .../test_optimize_1q_decomposition.py | 20 +++++++------- 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 7b8fbf9a60ef..9666fb04a0a7 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -29,17 +29,20 @@ class Optimize1qGatesDecomposition(TransformationPass): """Optimize chains of single-qubit gates by combining them into a single gate.""" - def __init__(self, basis=None): + def __init__(self, basis=None, target=None): """Optimize1qGatesDecomposition initializer. Args: basis (list[str]): Basis gates to consider, e.g. `['u3', 'cx']`. For the effects of this pass, the basis is the set intersection between the `basis` parameter - and the Euler basis. + and the Euler basis. Ignored if ``target`` is also specified. + target (Optional[Target]): The :class:`~.Target` object corresponding to the compilation + target. When specified, any argument specified for ``basis_gates`` is ignored. """ super().__init__() - self._target_basis = basis + self._basis_gates = basis + self._target = target self._decomposers = None if basis: @@ -63,6 +66,7 @@ def __init__(self, basis=None): self._decomposers[ tuple(gates) ] = one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name) + print(self._decomposers) def _resynthesize_run(self, run): """ @@ -103,7 +107,7 @@ def _substitution_checks(self, dag, old_run, new_circ, new_basis): uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in old_run) # does this run have gates not in the image of ._decomposers _and_ uncalibrated? uncalibrated_and_not_basis_p = any( - g.name not in self._target_basis and (not has_cals_p or not dag.has_calibration_for(g)) + g.name not in self._basis_gates and (not has_cals_p or not dag.has_calibration_for(g)) for g in old_run ) @@ -115,7 +119,7 @@ def _substitution_checks(self, dag, old_run, new_circ, new_basis): + "\n".join([str(node.op) for node in old_run]) + "\n\nand got\n\n" + "\n".join([str(node[0]) for node in new_circ]) - + f"\n\nbut the original was native (for {self._target_basis}) and the new value " + + f"\n\nbut the original was native (for {self._basis_gates}) and the new value " "is longer. This indicates an efficiency bug in synthesis. Please report it by " "opening an issue here: " "https://github.com/Qiskit/qiskit-terra/issues/new/choose", @@ -151,21 +155,20 @@ def run(self, dag): DAGCircuit: the optimized DAG. """ if self._decomposers is None: - logger.info("Skipping pass because no basis is set") + logger.info("Skipping pass because no basis or target is set") return dag runs = dag.collect_1q_runs() for run in runs: # SPECIAL CASE: Don't bother to optimize single U3 gates which are in the basis set. # The U3 decomposer is only going to emit a sequence of length 1 anyhow. - if "u3" in self._target_basis and len(run) == 1 and isinstance(run[0].op, U3Gate): - # Toss U3 gates equivalent to the identity; there we get off easy. - if np.allclose(run[0].op.to_matrix(), np.eye(2), 1e-15, 0): - dag.remove_op_node(run[0]) - continue + if "u3" in self._basis_gates and len(run) == 1 and isinstance(run[0].op, U3Gate): # We might rewrite into lower `u`s if they're available. - if "u2" not in self._target_basis and "u1" not in self._target_basis: + if "u2" not in self._basis_gates and "u1" not in self._basis_gates: continue + # Same deal for U gates + if "u" in self._basis_gates and len(run) == 1 and isinstance(run[0].op, UGate): + continue new_basis, new_circ = self._resynthesize_run(run) diff --git a/releasenotes/notes/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml b/releasenotes/notes/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml new file mode 100644 index 000000000000..969b2aa2a24b --- /dev/null +++ b/releasenotes/notes/target-aware-optimize-1q-decomposition-cb9bb4651607b639.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The :class:`~.Optimize1qGatesDecomposition` transpiler pass has a new keyword + argument, ``target``, on its constructor. This argument can be used to + specify a :class:`~.Target` object that represnts the compilation target. + If used it superscedes the ``basis`` argument to determine if an + instruction in the circuit is present on the target backend. diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index b32a596f9da1..a3127504d232 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -43,7 +43,7 @@ class TestOptimize1qGatesDecomposition(QiskitTestCase): ["cz", "rx", "rz"], ["rxx", "rx", "ry"], ["iswap", "rx", "rz"], - ["u1", "rx"], + ["rz", "rx"], ["rz", "sx"], ["p", "sx"], ["r"], @@ -74,7 +74,7 @@ def test_optimize_h_gates_pass_manager(self, basis): ["cz", "rx", "rz"], ["rxx", "rx", "ry"], ["iswap", "rx", "rz"], - ["u1", "rx"], + ["rz", "rx"], ["rz", "sx"], ["p", "sx"], ["r"], @@ -104,7 +104,7 @@ def test_ignores_conditional_rotations(self, basis): ["cz", "rx", "rz"], ["rxx", "rx", "ry"], ["iswap", "rx", "rz"], - ["u1", "rx"], + ["rz", "rx"], ["rz", "sx"], ["p", "sx"], ["r"], @@ -141,7 +141,7 @@ def test_in_the_back(self, basis): ["rxx", "rx", "ry"], ["iswap", "rx", "rz"], ["rz", "sx"], - ["u1", "rx"], + ["rz", "rx"], ["p", "sx"], ) def test_single_parameterized_circuit(self, basis): @@ -175,7 +175,7 @@ def test_single_parameterized_circuit(self, basis): ["cz", "rx", "rz"], ["rxx", "rx", "ry"], ["iswap", "rx", "rz"], - ["u1", "rx"], + ["rz", "rx"], ["rz", "sx"], ["p", "sx"], ) @@ -213,7 +213,7 @@ def test_parameterized_circuits(self, basis): ["cz", "rx", "rz"], ["rxx", "rx", "ry"], ["iswap", "rx", "rz"], - ["u1", "rx"], + ["rz", "rx"], ["rz", "sx"], ["p", "sx"], ) @@ -314,12 +314,12 @@ def test_identity_r(self): result = passmanager.run(circuit) self.assertEqual([], result.data) - def test_identity_u1x(self): - """Test lone identity gates in u1 rx basis are removed.""" + def test_identity_rzx(self): + """Test lone identity gates in rz rx basis are removed.""" circuit = QuantumCircuit(2) - circuit.append(U1Gate(0), [0]) + circuit.rz(0, 0) circuit.rx(0, 1) - basis = ["cx", "u1", "rx"] + basis = ["cx", "rz", "rx"] passmanager = PassManager() passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) From 9f43e7f9d633b1eae976d11e17238be04f1fa003 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 01:38:46 -0400 Subject: [PATCH 02/19] choose decomopsition based on fidelity --- .../optimization/optimize_1q_decomposition.py | 160 ++++++++---------- 1 file changed, 73 insertions(+), 87 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 9666fb04a0a7..d4b93c5970e5 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -14,10 +14,9 @@ import copy import logging - +from functools import partial import numpy as np -from qiskit.circuit.library.standard_gates import U3Gate from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.passes.utils import control_flow from qiskit.quantum_info.synthesis import one_qubit_decompose @@ -43,105 +42,66 @@ def __init__(self, basis=None, target=None): self._basis_gates = basis self._target = target - self._decomposers = None + self._global_decomposers = None + self._local_decomposers_cache = {} if basis: - self._decomposers = {} - basis_set = set(basis) - euler_basis_gates = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES - for euler_basis_name, gates in euler_basis_gates.items(): - if set(gates).issubset(basis_set): - basis_copy = copy.copy(self._decomposers) - for base in basis_copy.keys(): - # check if gates are a superset of another basis - if set(base).issubset(set(gates)): - # if so, remove that basis - del self._decomposers[base] - # check if the gates are a subset of another basis - elif set(gates).issubset(set(base)): - # if so, don't bother - break - # if not a subset, add it to the list - else: - self._decomposers[ - tuple(gates) - ] = one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name) - print(self._decomposers) - - def _resynthesize_run(self, run): + self._global_decomposers = _possible_decomposers(set(basis)) + + def _resynthesize_run(self, run, qubit): """ Resynthesizes one `run`, typically extracted via `dag.collect_1q_runs`. - Returns (basis, circuit) containing the newly synthesized circuit in the indicated basis, or - (None, None) if no synthesis routine applied. + Returns the newly synthesized circuit in the indicated basis, or None + if no synthesis routine applied. """ - operator = run[0].op.to_matrix() for gate in run[1:]: operator = gate.op.to_matrix().dot(operator) - new_circs = {k: v._decompose(operator) for k, v in self._decomposers.items()} - - new_basis, new_circ = None, None - if len(new_circs) > 0: - new_basis, new_circ = min(new_circs.items(), key=lambda x: len(x[1])) - - return new_basis, new_circ - - def _substitution_checks(self, dag, old_run, new_circ, new_basis): + if self._target: + qubits_tuple = (qubit,) + if qubits_tuple in self._local_decomposers_cache: + decomposers = self._local_decomposers_cache[qubits_tuple] + else: + available_1q_basis = set(self._target.operation_names_for_qargs(qubits_tuple)) + decomposers = _possible_decomposers(available_1q_basis) + self._local_decomposers_cache[qubits_tuple] = decomposers + else: + decomposers = self._global_decomposers + + new_circs = [decomposer._decompose(operator) for decomposer in decomposers] + + if len(new_circs) == 0: + return None + else: + return min(new_circs, key=partial(_error, target=self._target, qubit=qubit)) + + def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): """ - Returns `True` when it is recommended to replace `old_run` with `new_circ`. + Returns `True` when it is recommended to replace `old_run` with `new_circ` over `basis`. """ - if new_circ is None: return False # do we even have calibrations? has_cals_p = dag.calibrations is not None and len(dag.calibrations) > 0 - # is this run in the target set of this particular decomposer and also uncalibrated? - rewriteable_and_in_basis_p = all( - g.name in new_basis and (not has_cals_p or not dag.has_calibration_for(g)) - for g in old_run - ) # does this run have uncalibrated gates? uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in old_run) # does this run have gates not in the image of ._decomposers _and_ uncalibrated? uncalibrated_and_not_basis_p = any( - g.name not in self._basis_gates and (not has_cals_p or not dag.has_calibration_for(g)) + g.name not in basis and (not has_cals_p or not dag.has_calibration_for(g)) for g in old_run ) - if rewriteable_and_in_basis_p and len(old_run) < len(new_circ): - # NOTE: This is short-circuited on calibrated gates, which we're timid about - # reducing. - logger.debug( - "Resynthesized \n\n" - + "\n".join([str(node.op) for node in old_run]) - + "\n\nand got\n\n" - + "\n".join([str(node[0]) for node in new_circ]) - + f"\n\nbut the original was native (for {self._basis_gates}) and the new value " - "is longer. This indicates an efficiency bug in synthesis. Please report it by " - "opening an issue here: " - "https://github.com/Qiskit/qiskit-terra/issues/new/choose", - stacklevel=2, - ) - # if we're outside of the basis set, we're obligated to logically decompose. # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. - # NOTE: Here we use circuit length as a weak proxy for "improvement"; in reality, - # we care about something more like fidelity at runtime, which would mean, - # e.g., a preference for `RZGate`s over `RXGate`s. In fact, users sometimes - # express a preference for a "canonical form" of a circuit, which may come in - # the form of some parameter values, also not visible at the level of circuit - # length. Since we don't have a framework for the caller to programmatically - # express what they want here, we include some special casing for particular - # gates which we've promised to normalize --- but this is fragile and should - # ultimately be done away with. return ( - uncalibrated_and_not_basis_p - or (uncalibrated_p and len(old_run) > len(new_circ)) - or isinstance(old_run[0].op, U3Gate) + uncalibrated_and_not_basis_p or + (uncalibrated_p and + _error(new_circ, self._target, qubit) < _error(old_run, self._target, qubit) + ) ) @control_flow.trivial_recurse @@ -154,25 +114,21 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - if self._decomposers is None: + if self._basis_gates is None and self._target is None: logger.info("Skipping pass because no basis or target is set") return dag runs = dag.collect_1q_runs() for run in runs: - # SPECIAL CASE: Don't bother to optimize single U3 gates which are in the basis set. - # The U3 decomposer is only going to emit a sequence of length 1 anyhow. - if "u3" in self._basis_gates and len(run) == 1 and isinstance(run[0].op, U3Gate): - # We might rewrite into lower `u`s if they're available. - if "u2" not in self._basis_gates and "u1" not in self._basis_gates: - continue - # Same deal for U gates - if "u" in self._basis_gates and len(run) == 1 and isinstance(run[0].op, UGate): - continue - - new_basis, new_circ = self._resynthesize_run(run) - - if new_circ is not None and self._substitution_checks(dag, run, new_circ, new_basis): + qubit = dag.qubits.index(run[0].qargs[0]) + new_circ = self._resynthesize_run(run, qubit) + + if self._target is None: + basis = self._basis_gates + else: + basis = self._target.operation_names_for_qargs((qubit,)) + + if new_circ is not None and self._substitution_checks(dag, run, new_circ, basis, qubit): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) # Delete the other nodes in the run @@ -180,3 +136,33 @@ def run(self, dag): dag.remove_op_node(current_node) return dag + + +def _possible_decomposers(basis_set): + decomposers = [] + euler_basis_gates = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES + for euler_basis_name, gates in euler_basis_gates.items(): + if set(gates).issubset(basis_set): + decomposer = one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name) + decomposers.append(decomposer) + return decomposers + + +def _error(circuit, target, qubit): + """ + Calculate a rough error for a `circuit` that runs on a specific + `qubit` of `target` (circuit could also be a list of DAGNodes) + + Use basis errors from target if available, otherwise use length + of circuit as a weak proxy for error. + """ + if target is None: + return len(circuit) + else: + if isinstance(circuit, list): + gate_errors = [1 - getattr(target[node.name][(qubit,)], 'error', 0.0) + for node in circuit] + else: + gate_errors = [1 - getattr(target[inst.operation.name][(qubit,)], 'error', 0.0) + for inst in circuit] + return 1 - np.product(gate_errors) From bef910c6e0f16f7563067c4f6ad00dcbe9ccd17b Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 02:11:21 -0400 Subject: [PATCH 03/19] fix test that assumed cost of u3 vs. u1 to now rely on target --- .../test_optimize_1q_decomposition.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index a3127504d232..86f2771447e6 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -17,12 +17,12 @@ import ddt import numpy as np -from qiskit.circuit import QuantumRegister, QuantumCircuit, ClassicalRegister +from qiskit.circuit import QuantumRegister, QuantumCircuit, ClassicalRegister, Parameter from qiskit.circuit.library.standard_gates import UGate, SXGate, PhaseGate from qiskit.circuit.library.standard_gates import U3Gate, U2Gate, U1Gate from qiskit.circuit.random import random_circuit from qiskit.compiler import transpile -from qiskit.transpiler import PassManager +from qiskit.transpiler import PassManager, Target, InstructionProperties from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import BasisTranslator from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel @@ -442,10 +442,18 @@ def test_optimize_u3_to_u1(self): expected = QuantumCircuit(qr) expected.append(U1Gate(np.pi / 4), [qr[0]]) - basis = ["u1", "u2", "u3"] + θ = Parameter('θ') + ϕ = Parameter('ϕ') + λ = Parameter('λ') + u1_props = {(0,) : InstructionProperties(error=0)} + u2_props = {(0,): InstructionProperties(error=1e-4)} + u3_props = {(0,): InstructionProperties(error=2e-4)} + target = Target() + target.add_instruction(U1Gate(θ), u1_props, name='u1') + target.add_instruction(U2Gate(θ, ϕ), u2_props, name='u2') + target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name='u3') passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) - passmanager.append(Optimize1qGatesDecomposition(basis)) + passmanager.append(Optimize1qGatesDecomposition(target=target)) result = passmanager.run(circuit) msg = f"expected:\n{expected}\nresult:\n{result}" @@ -460,10 +468,19 @@ def test_optimize_u3_to_u2(self): expected = QuantumCircuit(qr) expected.append(U2Gate(0, np.pi / 4), [qr[0]]) - basis = ["u1", "u2", "u3"] + θ = Parameter('θ') + ϕ = Parameter('ϕ') + λ = Parameter('λ') + u1_props = {(0,) : InstructionProperties(error=0)} + u2_props = {(0,): InstructionProperties(error=1e-4)} + u3_props = {(0,): InstructionProperties(error=2e-4)} + target = Target() + target.add_instruction(U1Gate(θ), u1_props, name='u1') + target.add_instruction(U2Gate(θ, ϕ), u2_props, name='u2') + target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name='u3') + passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) - passmanager.append(Optimize1qGatesDecomposition(basis)) + passmanager.append(Optimize1qGatesDecomposition(target=target)) result = passmanager.run(circuit) self.assertEqual(expected, result) msg = f"expected:\n{expected}\nresult:\n{result}" From d8c712df1f39d846b241d25b8d3daf32c0211257 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 09:48:11 -0400 Subject: [PATCH 04/19] always replace identity with empty --- .../passes/optimization/optimize_1q_decomposition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index d4b93c5970e5..5b920b12c15c 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -48,7 +48,7 @@ def __init__(self, basis=None, target=None): if basis: self._global_decomposers = _possible_decomposers(set(basis)) - def _resynthesize_run(self, run, qubit): + def _resynthesize_run(self, run, qubit=None): """ Resynthesizes one `run`, typically extracted via `dag.collect_1q_runs`. @@ -102,6 +102,7 @@ def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): (uncalibrated_p and _error(new_circ, self._target, qubit) < _error(old_run, self._target, qubit) ) + or np.isclose(_error(new_circ, self._target, qubit), 0) ) @control_flow.trivial_recurse From 79637cf080111db8812249a3b434c59dfdf0fa98 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 09:56:59 -0400 Subject: [PATCH 05/19] black --- .../optimization/optimize_1q_decomposition.py | 23 ++++++++------- .../test_optimize_1q_decomposition.py | 28 +++++++++---------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 5b920b12c15c..e57cfe31bbd2 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -71,7 +71,7 @@ def _resynthesize_run(self, run, qubit=None): decomposers = self._global_decomposers new_circs = [decomposer._decompose(operator) for decomposer in decomposers] - + if len(new_circs) == 0: return None else: @@ -98,10 +98,11 @@ def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. return ( - uncalibrated_and_not_basis_p or - (uncalibrated_p and - _error(new_circ, self._target, qubit) < _error(old_run, self._target, qubit) - ) + uncalibrated_and_not_basis_p + or ( + uncalibrated_p + and _error(new_circ, self._target, qubit) < _error(old_run, self._target, qubit) + ) or np.isclose(_error(new_circ, self._target, qubit), 0) ) @@ -151,7 +152,7 @@ def _possible_decomposers(basis_set): def _error(circuit, target, qubit): """ - Calculate a rough error for a `circuit` that runs on a specific + Calculate a rough error for a `circuit` that runs on a specific `qubit` of `target` (circuit could also be a list of DAGNodes) Use basis errors from target if available, otherwise use length @@ -161,9 +162,11 @@ def _error(circuit, target, qubit): return len(circuit) else: if isinstance(circuit, list): - gate_errors = [1 - getattr(target[node.name][(qubit,)], 'error', 0.0) - for node in circuit] + gate_errors = [ + 1 - getattr(target[node.name][(qubit,)], "error", 0.0) for node in circuit + ] else: - gate_errors = [1 - getattr(target[inst.operation.name][(qubit,)], 'error', 0.0) - for inst in circuit] + gate_errors = [ + 1 - getattr(target[inst.operation.name][(qubit,)], "error", 0.0) for inst in circuit + ] return 1 - np.product(gate_errors) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 86f2771447e6..6ab48f8c51f5 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -442,16 +442,16 @@ def test_optimize_u3_to_u1(self): expected = QuantumCircuit(qr) expected.append(U1Gate(np.pi / 4), [qr[0]]) - θ = Parameter('θ') - ϕ = Parameter('ϕ') - λ = Parameter('λ') - u1_props = {(0,) : InstructionProperties(error=0)} + θ = Parameter("θ") + ϕ = Parameter("ϕ") + λ = Parameter("λ") + u1_props = {(0,): InstructionProperties(error=0)} u2_props = {(0,): InstructionProperties(error=1e-4)} u3_props = {(0,): InstructionProperties(error=2e-4)} target = Target() - target.add_instruction(U1Gate(θ), u1_props, name='u1') - target.add_instruction(U2Gate(θ, ϕ), u2_props, name='u2') - target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name='u3') + target.add_instruction(U1Gate(θ), u1_props, name="u1") + target.add_instruction(U2Gate(θ, ϕ), u2_props, name="u2") + target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name="u3") passmanager = PassManager() passmanager.append(Optimize1qGatesDecomposition(target=target)) result = passmanager.run(circuit) @@ -468,16 +468,16 @@ def test_optimize_u3_to_u2(self): expected = QuantumCircuit(qr) expected.append(U2Gate(0, np.pi / 4), [qr[0]]) - θ = Parameter('θ') - ϕ = Parameter('ϕ') - λ = Parameter('λ') - u1_props = {(0,) : InstructionProperties(error=0)} + θ = Parameter("θ") + ϕ = Parameter("ϕ") + λ = Parameter("λ") + u1_props = {(0,): InstructionProperties(error=0)} u2_props = {(0,): InstructionProperties(error=1e-4)} u3_props = {(0,): InstructionProperties(error=2e-4)} target = Target() - target.add_instruction(U1Gate(θ), u1_props, name='u1') - target.add_instruction(U2Gate(θ, ϕ), u2_props, name='u2') - target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name='u3') + target.add_instruction(U1Gate(θ), u1_props, name="u1") + target.add_instruction(U2Gate(θ, ϕ), u2_props, name="u2") + target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name="u3") passmanager = PassManager() passmanager.append(Optimize1qGatesDecomposition(target=target)) From 24b05348feb36a3b2fc3112173d2acafdea7ef37 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 10:19:59 -0400 Subject: [PATCH 06/19] remove a bunch of unnecessary basis translations in tests --- .../test_optimize_1q_decomposition.py | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 6ab48f8c51f5..6d1d3b6eead1 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -60,7 +60,6 @@ def test_optimize_h_gates_pass_manager(self, basis): expected.u(np.pi / 2, 0, np.pi, qr) # U2(0, pi) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) @@ -90,7 +89,6 @@ def test_ignores_conditional_rotations(self, basis): circuit.p(0.4, qr) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) @@ -126,7 +124,6 @@ def test_in_the_back(self, basis): expected.h(qr) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) @@ -157,7 +154,6 @@ def test_single_parameterized_circuit(self, basis): qc.p(0.2, qr) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) @@ -195,7 +191,6 @@ def test_parameterized_circuits(self, basis): qc.p(0.2, qr) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) @@ -236,7 +231,6 @@ def test_parameterized_expressions_in_circuits(self, basis): qc.p(0.2, qr) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) @@ -253,7 +247,6 @@ def test_identity_xyx(self): circuit.ry(0, 0) basis = ["rxx", "rx", "ry"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual([], result.data) @@ -265,7 +258,6 @@ def test_identity_zxz(self): circuit.rz(0, 0) basis = ["cz", "rx", "rz"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual([], result.data) @@ -276,7 +268,6 @@ def test_identity_psx(self): circuit.p(0, 0) basis = ["cx", "p", "sx"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual([], result.data) @@ -287,7 +278,6 @@ def test_identity_u(self): circuit.u(0, 0, 0, 0) basis = ["cx", "u"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual([], result.data) @@ -298,7 +288,6 @@ def test_identity_u3(self): circuit.append(U3Gate(0, 0, 0), [0]) basis = ["cx", "u3"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual([], result.data) @@ -309,7 +298,6 @@ def test_identity_r(self): circuit.r(0, 0, 0) basis = ["r"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual([], result.data) @@ -321,7 +309,6 @@ def test_identity_rzx(self): circuit.rx(0, 1) basis = ["cx", "rz", "rx"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual([], result.data) @@ -346,7 +333,6 @@ def test_euler_decomposition_worse(self): circuit.rz(-np.pi / 2, 0) basis = ["rx", "rz"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) # decomposition of circuit will result in 3 gates instead of 2 @@ -360,7 +346,6 @@ def test_euler_decomposition_worse_2(self): circuit.ry(-0.14, 0) basis = ["ry", "rz"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") @@ -375,7 +360,6 @@ def test_euler_decomposition_zsx(self): basis = ["sx", "rz"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") @@ -390,7 +374,6 @@ def test_euler_decomposition_zsx_2(self): basis = ["sx", "rz"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") @@ -406,7 +389,6 @@ def test_optimize_u_to_phase_gate(self): basis = ["p", "sx"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) @@ -426,7 +408,6 @@ def test_optimize_u_to_p_sx_p(self): basis = ["p", "sx"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) @@ -492,7 +473,6 @@ def test_y_simplification_rz_sx_x(self): qc.y(0) basis = ["id", "rz", "sx", "x", "cx"] passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) expected = QuantumCircuit(1) @@ -577,14 +557,13 @@ def test_if_else(self): test.if_else((0, True), test_true.copy(), None, range(num_qubits), [0]) expected = QuantumCircuit(qr, cr) - expected.u(np.pi / 2, 0, np.pi, 0) + expected.u(np.pi / 2, 0, -np.pi, 0) expected.measure(0, 0) expected_true = QuantumCircuit(qr) expected_true.u(np.pi / 2, 0, -np.pi, qr[0]) expected.if_else((0, True), expected_true, None, range(num_qubits), [0]) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(test) self.assertEqual(result, expected) @@ -610,7 +589,7 @@ def test_nested_control_flow(self): test.if_else((0, True), test_true.copy(), None, range(num_qubits), [0]) expected = QuantumCircuit(qr, cr) - expected.u(np.pi / 2, 0, np.pi, 0) + expected.u(np.pi / 2, 0, -np.pi, 0) expected.measure(0, 0) expected_true = QuantumCircuit(qr, cr) expected_for = QuantumCircuit(qr, cr) @@ -619,7 +598,6 @@ def test_nested_control_flow(self): expected.if_else((0, True), expected_true, None, range(num_qubits), [0]) passmanager = PassManager() - passmanager.append(BasisTranslator(sel, basis)) passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(test) self.assertEqual(result, expected) From b6bceefd66307a6139df76ae1e13176a5f966e90 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 11:32:37 -0400 Subject: [PATCH 07/19] tests for the target path --- .../test_optimize_1q_decomposition.py | 137 ++++++++++++++++-- 1 file changed, 121 insertions(+), 16 deletions(-) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 6d1d3b6eead1..5a1db9ca44a7 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -18,23 +18,140 @@ import numpy as np from qiskit.circuit import QuantumRegister, QuantumCircuit, ClassicalRegister, Parameter -from qiskit.circuit.library.standard_gates import UGate, SXGate, PhaseGate -from qiskit.circuit.library.standard_gates import U3Gate, U2Gate, U1Gate +from qiskit.circuit.library.standard_gates import ( + UGate, + SXGate, + PhaseGate, + U3Gate, + U2Gate, + U1Gate, + RZGate, + RXGate, + RYGate, + HGate, +) from qiskit.circuit.random import random_circuit from qiskit.compiler import transpile from qiskit.transpiler import PassManager, Target, InstructionProperties from qiskit.transpiler.passes import Optimize1qGatesDecomposition from qiskit.transpiler.passes import BasisTranslator +from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import _error from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel from qiskit.quantum_info import Operator from qiskit.test import QiskitTestCase from qiskit.circuit import Parameter +θ = Parameter("θ") +ϕ = Parameter("ϕ") +λ = Parameter("λ") + +# a typical target where u1 is cheaper than u2 is cheaper than u3 +u1_props = {(0,): InstructionProperties(error=0)} +u2_props = {(0,): InstructionProperties(error=1e-4)} +u3_props = {(0,): InstructionProperties(error=2e-4)} +target_u1_u2_u3 = Target() +target_u1_u2_u3.add_instruction(U1Gate(θ), u1_props, name="u1") +target_u1_u2_u3.add_instruction(U2Gate(θ, ϕ), u2_props, name="u2") +target_u1_u2_u3.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name="u3") + +# a typical target where continuous rz and rx are available; rz is cheaper +rz_props = {(0,): InstructionProperties(duration=0, error=0)} +rx_props = {(0,): InstructionProperties(duration=0.5e-8, error=0.00025)} +target_rz_rx = Target() +target_rz_rx.add_instruction(RZGate(θ), rz_props, name="rz") +target_rz_rx.add_instruction(RXGate(θ), rx_props, name="rx") + +# a typical target where continuous rz, and discrete sx are available; rz is cheaper +rz_props = {(0,): InstructionProperties(duration=0, error=0)} +sx_props = {(0,): InstructionProperties(duration=0.5e-8, error=0.00025)} +target_rz_sx = Target() +target_rz_sx.add_instruction(RZGate(θ), rz_props, name="rz") +target_rz_sx.add_instruction(SXGate(), sx_props, name="sx") + +# a target with overcomplete basis, rz is cheaper than ry is cheaper than u +rz_props = {(0,): InstructionProperties(duration=0.1e-8, error=0.0001)} +ry_props = {(0,): InstructionProperties(duration=0.5e-8, error=0.0002)} +u_props = {(0,): InstructionProperties(duration=0.9e-8, error=0.0005)} +target_rz_ry_u = Target() +target_rz_ry_u.add_instruction(RZGate(θ), rz_props, name="rz") +target_rz_ry_u.add_instruction(RYGate(θ), ry_props, name="ry") +target_rz_ry_u.add_instruction(UGate(θ, ϕ, λ), u_props, name="u") + +# a target with hadamard and phase, we don't yet have an explicit decomposer +# but we can at least recognize circuits that are native for it +h_props = {(0,): InstructionProperties(duration=0.3e-8, error=0.0003)} +p_props = {(0,): InstructionProperties(duration=0, error=0)} +target_h_p = Target() +target_h_p.add_instruction(HGate(), h_props, name="h") +target_h_p.add_instruction(PhaseGate(θ), p_props, name="p") + + @ddt.ddt class TestOptimize1qGatesDecomposition(QiskitTestCase): """Test for 1q gate optimizations.""" + @ddt.data(target_u1_u2_u3, target_rz_rx, target_rz_sx, target_rz_ry_u, target_h_p) + def test_optimize_h_gates_target(self, target): + """Transpile: qr:--[H]-[H]-[H]--""" + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.h(qr[0]) + circuit.h(qr[0]) + circuit.h(qr[0]) + + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(target=target)) + result = passmanager.run(circuit) + + self.assertTrue(Operator(circuit).equiv(Operator(result))) + + @ddt.data( + target_u1_u2_u3, + target_rz_rx, + target_rz_sx, + target_rz_ry_u, + ) + def test_optimize_identity_target(self, target): + """Transpile: qr:--[RY(θ), RY(-θ)]-- to null.""" + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.ry(np.pi / 7, qr[0]) + circuit.ry(-np.pi / 7, qr[0]) + + expected = QuantumCircuit(qr) + + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(target=target)) + result = passmanager.run(circuit) + self.assertEqual(expected, result) + + def test_optimize_error_over_target_1(self): + """XZX is re-written as ZXZ, which is cheaper according to target.""" + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.rx(np.pi / 7, qr[0]) + circuit.rz(np.pi / 4, qr[0]) + circuit.rx(np.pi / 3, qr[0]) + + target = target_rz_rx + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(target=target)) + result = passmanager.run(circuit) + self.assertLess(_error(result, target, 0), _error(circuit, target, 0)) + + def test_optimize_error_over_target_2(self): + """U is re-written as ZYZ, which is cheaper according to target.""" + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.u(np.pi / 7, np.pi / 4, np.pi / 3, qr[0]) + + target = target_rz_ry_u + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(target=target)) + result = passmanager.run(circuit) + self.assertLess(_error(result, target, 0), _error(circuit, target, 0)) + @ddt.data( ["cx", "u3"], ["cz", "u3"], @@ -56,14 +173,12 @@ def test_optimize_h_gates_pass_manager(self, basis): circuit.h(qr[0]) circuit.h(qr[0]) - expected = QuantumCircuit(qr) - expected.u(np.pi / 2, 0, np.pi, qr) # U2(0, pi) - passmanager = PassManager() passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(circuit) self.assertTrue(Operator(circuit).equiv(Operator(result))) + self.assertLessEqual(result.depth(), circuit.depth()) @ddt.data( ["cx", "u3"], @@ -423,18 +538,8 @@ def test_optimize_u3_to_u1(self): expected = QuantumCircuit(qr) expected.append(U1Gate(np.pi / 4), [qr[0]]) - θ = Parameter("θ") - ϕ = Parameter("ϕ") - λ = Parameter("λ") - u1_props = {(0,): InstructionProperties(error=0)} - u2_props = {(0,): InstructionProperties(error=1e-4)} - u3_props = {(0,): InstructionProperties(error=2e-4)} - target = Target() - target.add_instruction(U1Gate(θ), u1_props, name="u1") - target.add_instruction(U2Gate(θ, ϕ), u2_props, name="u2") - target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name="u3") passmanager = PassManager() - passmanager.append(Optimize1qGatesDecomposition(target=target)) + passmanager.append(Optimize1qGatesDecomposition(target=target_u1_u2_u3)) result = passmanager.run(circuit) msg = f"expected:\n{expected}\nresult:\n{result}" From 148c9646f3dd9298660e77f0034d4e5e9ca14124 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 11:57:14 -0400 Subject: [PATCH 08/19] add to preset passmanagers --- qiskit/transpiler/preset_passmanagers/level1.py | 2 +- qiskit/transpiler/preset_passmanagers/level2.py | 2 +- qiskit/transpiler/preset_passmanagers/level3.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index b3a58fe703d8..2c280c2fa6cd 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -178,7 +178,7 @@ def _vf2_match_not_found(property_set): def _opt_control(property_set): return (not property_set["depth_fixed_point"]) or (not property_set["size_fixed_point"]) - _opt = [Optimize1qGatesDecomposition(basis_gates), CXCancellation()] + _opt = [Optimize1qGatesDecomposition(basis=basis_gates, target=target), CXCancellation()] unroll_3q = None # Build full pass manager diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 1edd917c1160..3326d3dae1a3 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -145,7 +145,7 @@ def _opt_control(property_set): return (not property_set["depth_fixed_point"]) or (not property_set["size_fixed_point"]) _opt = [ - Optimize1qGatesDecomposition(basis_gates), + Optimize1qGatesDecomposition(basis=basis_gates, target=target), CommutativeCancellation(basis_gates=basis_gates), ] diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 46cc03e58917..fc59ee355b95 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -162,7 +162,7 @@ def _opt_control(property_set): plugin_config=unitary_synthesis_plugin_config, target=target, ), - Optimize1qGatesDecomposition(basis_gates), + Optimize1qGatesDecomposition(basis=basis_gates, target=target), CommutativeCancellation(), ] From 6c7395d1349aebad031bdf01c8a4422c8e042b2f Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 13:50:33 -0400 Subject: [PATCH 09/19] fixup Optimize1qGatesSimpleCommutation to work with new Optimize1qGatesDecomposition --- .../passes/optimization/optimize_1q_commutation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py index 20a82fafa7ae..cecab30e2a32 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py @@ -157,7 +157,7 @@ def _resynthesize(self, new_run): NOTE: Returns None when resynthesis is not possible. """ if len(new_run) == 0: - return (), QuantumCircuit(1) + return QuantumCircuit(1) return self._optimize1q._resynthesize_run(new_run) @@ -210,13 +210,13 @@ def _step(self, dag): ) # re-synthesize - new_preceding_basis, new_preceding_run = self._resynthesize( + new_preceding_run = self._resynthesize( preceding_run + commuted_preceding ) - new_succeeding_basis, new_succeeding_run = self._resynthesize( + new_succeeding_run = self._resynthesize( commuted_succeeding + succeeding_run ) - new_basis, new_run = self._resynthesize(run_clone) + new_run = self._resynthesize(run_clone) # perform the replacement if it was indeed a good idea if self._optimize1q._substitution_checks( @@ -227,7 +227,8 @@ def _step(self, dag): + (new_run or QuantumCircuit(1)).data + (new_succeeding_run or QuantumCircuit(1)).data ), - new_basis + new_preceding_basis + new_succeeding_basis, + self._optimize1q._basis_gates, + dag.qubits.index(run[0].qargs[0]) ): if preceding_run and new_preceding_run is not None: self._replace_subdag(dag, preceding_run, new_preceding_run) From e6fa77a0c2af057a71fbc9c280c6daf8e8037136 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 14:15:44 -0400 Subject: [PATCH 10/19] dont access duration and error unconditionally --- qiskit/transpiler/target.py | 41 ++++++++++++------------- test/python/compiler/test_transpiler.py | 8 ++--- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index ee4f889912b4..ea1f2b33297e 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -1025,7 +1025,7 @@ def target_to_backend_properties(target: Target): for qargs, props in qargs_list.items(): property_list = [] if props is not None: - if props.duration is not None: + if getattr(props, "duration", None) is not None: property_list.append( { "date": datetime.datetime.utcnow(), @@ -1034,7 +1034,7 @@ def target_to_backend_properties(target: Target): "value": props.duration, } ) - if props.error is not None: + if getattr(props, "error", None) is not None: property_list.append( { "date": datetime.datetime.utcnow(), @@ -1059,25 +1059,24 @@ def target_to_backend_properties(target: Target): continue qubit = qargs[0] props_list = [] - if props is not None: - if props.error is not None: - props_list.append( - { - "date": datetime.datetime.utcnow(), - "name": "readout_error", - "unit": "", - "value": props.error, - } - ) - if props.duration is not None: - props_list.append( - { - "date": datetime.datetime.utcnow(), - "name": "readout_length", - "unit": "s", - "value": props.duration, - } - ) + if getattr(props, "error", None) is not None: + props_list.append( + { + "date": datetime.datetime.utcnow(), + "name": "readout_error", + "unit": "", + "value": props.error, + } + ) + if getattr(props, "duration", None) is not None: + props_list.append( + { + "date": datetime.datetime.utcnow(), + "name": "readout_length", + "unit": "s", + "value": props.duration, + } + ) if not props_list: qubit_props = {} break diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 8a9cf6816ae6..264682196745 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1465,10 +1465,10 @@ def test_target_ideal_gates(self, opt_level): theta = Parameter("θ") phi = Parameter("ϕ") lam = Parameter("λ") - target = Target(num_qubits=2) - target.add_instruction(UGate(theta, phi, lam)) - target.add_instruction(CXGate()) - target.add_instruction(Measure()) + target = Target(2) + target.add_instruction(UGate(theta, phi, lam), {(0,): None, (1,): None}) + target.add_instruction(CXGate(), {(0,1): None}) + target.add_instruction(Measure(), {(0,): None, (1,): None}) qubit_reg = QuantumRegister(2, name="q") clbit_reg = ClassicalRegister(2, name="c") qc = QuantumCircuit(qubit_reg, clbit_reg, name="bell") From 061c9d12cc56c2617049a36d030176fd446fe0b1 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 14:24:38 -0400 Subject: [PATCH 11/19] black --- .../passes/optimization/optimize_1q_commutation.py | 10 +++------- test/python/compiler/test_transpiler.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py index cecab30e2a32..91e6fd105148 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py @@ -210,12 +210,8 @@ def _step(self, dag): ) # re-synthesize - new_preceding_run = self._resynthesize( - preceding_run + commuted_preceding - ) - new_succeeding_run = self._resynthesize( - commuted_succeeding + succeeding_run - ) + new_preceding_run = self._resynthesize(preceding_run + commuted_preceding) + new_succeeding_run = self._resynthesize(commuted_succeeding + succeeding_run) new_run = self._resynthesize(run_clone) # perform the replacement if it was indeed a good idea @@ -228,7 +224,7 @@ def _step(self, dag): + (new_succeeding_run or QuantumCircuit(1)).data ), self._optimize1q._basis_gates, - dag.qubits.index(run[0].qargs[0]) + dag.qubits.index(run[0].qargs[0]), ): if preceding_run and new_preceding_run is not None: self._replace_subdag(dag, preceding_run, new_preceding_run) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 264682196745..7024c85c63d8 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1467,7 +1467,7 @@ def test_target_ideal_gates(self, opt_level): lam = Parameter("λ") target = Target(2) target.add_instruction(UGate(theta, phi, lam), {(0,): None, (1,): None}) - target.add_instruction(CXGate(), {(0,1): None}) + target.add_instruction(CXGate(), {(0, 1): None}) target.add_instruction(Measure(), {(0,): None, (1,): None}) qubit_reg = QuantumRegister(2, name="q") clbit_reg = ClassicalRegister(2, name="c") From 9014de0dd3d154f6a2ac9f4d2bd64cfae675fa0f Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 15:55:20 -0400 Subject: [PATCH 12/19] more docs --- .../passes/optimization/optimize_1q_decomposition.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index e57cfe31bbd2..434482e9458c 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -26,7 +26,16 @@ class Optimize1qGatesDecomposition(TransformationPass): - """Optimize chains of single-qubit gates by combining them into a single gate.""" + """Optimize chains of single-qubit gates by combining them into a single gate. + + The decision to replace the original chain with a new resynthesis depends on: + - whether the original chain was out of basis: replace + - whether the original chain was in basis but resynthesis is lower error: replace + - whether the original chain contains a pulse gate: do not replace + - whether the original chain amounts to identity: replace with null + + Error is computed as a multiplication of the errors of individual gates on that qubit. + """ def __init__(self, basis=None, target=None): """Optimize1qGatesDecomposition initializer. From e154aba81f0a247abd841f45f3546d80fbff2175 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 16:36:14 -0400 Subject: [PATCH 13/19] lint --- .../optimization/optimize_1q_decomposition.py | 1 - .../transpiler/test_optimize_1q_decomposition.py | 14 +------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 434482e9458c..5cfd2d296101 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -12,7 +12,6 @@ """Optimize chains of single-qubit gates using Euler 1q decomposer""" -import copy import logging from functools import partial import numpy as np diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 5a1db9ca44a7..65857c2a567f 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -39,7 +39,6 @@ from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel from qiskit.quantum_info import Operator from qiskit.test import QiskitTestCase -from qiskit.circuit import Parameter θ = Parameter("θ") @@ -554,19 +553,8 @@ def test_optimize_u3_to_u2(self): expected = QuantumCircuit(qr) expected.append(U2Gate(0, np.pi / 4), [qr[0]]) - θ = Parameter("θ") - ϕ = Parameter("ϕ") - λ = Parameter("λ") - u1_props = {(0,): InstructionProperties(error=0)} - u2_props = {(0,): InstructionProperties(error=1e-4)} - u3_props = {(0,): InstructionProperties(error=2e-4)} - target = Target() - target.add_instruction(U1Gate(θ), u1_props, name="u1") - target.add_instruction(U2Gate(θ, ϕ), u2_props, name="u2") - target.add_instruction(U3Gate(θ, ϕ, λ), u3_props, name="u3") - passmanager = PassManager() - passmanager.append(Optimize1qGatesDecomposition(target=target)) + passmanager.append(Optimize1qGatesDecomposition(target=target_u1_u2_u3)) result = passmanager.run(circuit) self.assertEqual(expected, result) msg = f"expected:\n{expected}\nresult:\n{result}" From 750142fc6b84aa84cf52b6d3615a504a8380ee33 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Sun, 16 Oct 2022 20:09:46 -0400 Subject: [PATCH 14/19] simplify test --- test/python/compiler/test_transpiler.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 7024c85c63d8..1dea499f8bb8 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1474,23 +1474,10 @@ def test_target_ideal_gates(self, opt_level): qc = QuantumCircuit(qubit_reg, clbit_reg, name="bell") qc.h(qubit_reg[0]) qc.cx(qubit_reg[0], qubit_reg[1]) - if opt_level != 3: - qc.measure(qubit_reg, clbit_reg) + result = transpile(qc, target=target, optimization_level=opt_level) - # The Unitary synthesis optimization pass results for optimization level 3 - # results in a different output than the other optimization levels - # and can differ based on fp precision. To avoid relying on a hard match - # do a unitary equiv - - if opt_level == 3: - result_op = Operator.from_circuit(result) - self.assertTrue(result_op.equiv(qc)) - else: - expected = QuantumCircuit(qubit_reg, clbit_reg) - expected.u(np.pi / 2, 0, np.pi, qubit_reg[0]) - expected.cx(qubit_reg[0], qubit_reg[1]) - expected.measure(qubit_reg, clbit_reg) - self.assertEqual(result, expected) + + self.assertEqual(Operator.from_circuit(result), Operator.from_circuit(qc)) # TODO: Add optimization level 2 and 3 after they support control flow # compilation From 7897507ed260e95a3425329714fbf7bd9f94c822 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Tue, 18 Oct 2022 00:25:43 -0400 Subject: [PATCH 15/19] work around not being able to filter global gates by qubit --- .../passes/optimization/optimize_1q_decomposition.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 5cfd2d296101..dfd0a975f963 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -171,10 +171,11 @@ def _error(circuit, target, qubit): else: if isinstance(circuit, list): gate_errors = [ - 1 - getattr(target[node.name][(qubit,)], "error", 0.0) for node in circuit + 1 - getattr(target[node.name].get((qubit,)), "error", 0.0) for node in circuit ] else: gate_errors = [ - 1 - getattr(target[inst.operation.name][(qubit,)], "error", 0.0) for inst in circuit + 1 - getattr(target[inst.operation.name].get((qubit,)), "error", 0.0) + for inst in circuit ] return 1 - np.product(gate_errors) From 91eb94a45fe25e612388a49d3ed7435eeeb9f031 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Wed, 16 Nov 2022 23:02:06 -0500 Subject: [PATCH 16/19] review comments --- qiskit/transpiler/target.py | 37 ++++++++++++------------- test/python/compiler/test_transpiler.py | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index ea1f2b33297e..b7cb21ac8a16 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -1024,25 +1024,24 @@ def target_to_backend_properties(target: Target): if gate != "measure": for qargs, props in qargs_list.items(): property_list = [] - if props is not None: - if getattr(props, "duration", None) is not None: - property_list.append( - { - "date": datetime.datetime.utcnow(), - "name": "gate_length", - "unit": "s", - "value": props.duration, - } - ) - if getattr(props, "error", None) is not None: - property_list.append( - { - "date": datetime.datetime.utcnow(), - "name": "gate_error", - "unit": "", - "value": props.error, - } - ) + if getattr(props, "duration", None) is not None: + property_list.append( + { + "date": datetime.datetime.utcnow(), + "name": "gate_length", + "unit": "s", + "value": props.duration, + } + ) + if getattr(props, "error", None) is not None: + property_list.append( + { + "date": datetime.datetime.utcnow(), + "name": "gate_error", + "unit": "", + "value": props.error, + } + ) if property_list: gates.append( { diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 1dea499f8bb8..4964c9c723c6 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1465,7 +1465,7 @@ def test_target_ideal_gates(self, opt_level): theta = Parameter("θ") phi = Parameter("ϕ") lam = Parameter("λ") - target = Target(2) + target = Target(num_qubits=2) target.add_instruction(UGate(theta, phi, lam), {(0,): None, (1,): None}) target.add_instruction(CXGate(), {(0, 1): None}) target.add_instruction(Measure(), {(0,): None, (1,): None}) From 649e6001289d8392b1f5fb35ff6bbe3083d29b74 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Thu, 17 Nov 2022 09:43:46 -0500 Subject: [PATCH 17/19] add a test for the case of no errors specified in target: pick shortest decomp. --- .../optimization/optimize_1q_decomposition.py | 10 +++++--- .../test_optimize_1q_decomposition.py | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index dfd0a975f963..bfd1a3711d40 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -170,12 +170,16 @@ def _error(circuit, target, qubit): return len(circuit) else: if isinstance(circuit, list): - gate_errors = [ + gate_fidelities = [ 1 - getattr(target[node.name].get((qubit,)), "error", 0.0) for node in circuit ] else: - gate_errors = [ + gate_fidelities = [ 1 - getattr(target[inst.operation.name].get((qubit,)), "error", 0.0) for inst in circuit ] - return 1 - np.product(gate_errors) + gate_error = 1 - np.product(gate_fidelities) + if gate_error == 0.0: + return -100 + len(circuit) # prefer shorter circuits among those with zero error + else: + return gate_error diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 65857c2a567f..3e8b0fc0909c 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -85,6 +85,16 @@ target_h_p.add_instruction(HGate(), h_props, name="h") target_h_p.add_instruction(PhaseGate(θ), p_props, name="p") +# a target with rz, ry, and u. Error are not specified so we should prefer +# shorter decompositions. +rz_props = {(0,): None} +ry_props = {(0,): None} +u_props = {(0,): None} +target_rz_ry_u_noerror = Target() +target_rz_ry_u_noerror.add_instruction(RZGate(θ), rz_props, name="rz") +target_rz_ry_u_noerror.add_instruction(RYGate(θ), ry_props, name="ry") +target_rz_ry_u_noerror.add_instruction(UGate(θ, ϕ, λ), u_props, name="u") + @ddt.ddt class TestOptimize1qGatesDecomposition(QiskitTestCase): @@ -151,6 +161,19 @@ def test_optimize_error_over_target_2(self): result = passmanager.run(circuit) self.assertLess(_error(result, target, 0), _error(circuit, target, 0)) + def test_optimize_error_over_target_3(self): + """U is shorter than RZ-RY-RZ or RY-RZ-RY so use it when no error given.""" + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.u(np.pi / 7, np.pi / 4, np.pi / 3, qr[0]) + + target = target_rz_ry_u_noerror + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(target=target)) + result = passmanager.run(circuit) + self.assertEqual(len(result), 1) + self.assertIsInstance(result[0].operation, UGate) + @ddt.data( ["cx", "u3"], ["cz", "u3"], From f26ccdfd6bbd42fde50678ee629facb4b92b5b68 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Thu, 17 Nov 2022 12:01:51 -0500 Subject: [PATCH 18/19] performance for qubit index lookup --- .../transpiler/passes/optimization/optimize_1q_commutation.py | 3 ++- .../passes/optimization/optimize_1q_decomposition.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py index 91e6fd105148..9cc5edcd8b6a 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py @@ -187,6 +187,7 @@ def _step(self, dag): runs = dag.collect_1q_runs() did_work = False + qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} for run in runs: # identify the preceding blocking gates run_clone = copy(run) @@ -224,7 +225,7 @@ def _step(self, dag): + (new_succeeding_run or QuantumCircuit(1)).data ), self._optimize1q._basis_gates, - dag.qubits.index(run[0].qargs[0]), + qubit_indices[run[0].qargs[0]] ): if preceding_run and new_preceding_run is not None: self._replace_subdag(dag, preceding_run, new_preceding_run) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index bfd1a3711d40..662f4e6d1a72 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -129,8 +129,9 @@ def run(self, dag): return dag runs = dag.collect_1q_runs() + qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} for run in runs: - qubit = dag.qubits.index(run[0].qargs[0]) + qubit = qubit_indices[run[0].qargs[0]] new_circ = self._resynthesize_run(run, qubit) if self._target is None: From 02057e5770582c67258db0961d2b97f5f958a3b4 Mon Sep 17 00:00:00 2001 From: Ali Javadi Date: Thu, 17 Nov 2022 14:51:24 -0500 Subject: [PATCH 19/19] black --- .../transpiler/passes/optimization/optimize_1q_commutation.py | 2 +- .../transpiler/passes/optimization/optimize_1q_decomposition.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py index 9cc5edcd8b6a..ec3ed38ec03c 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_commutation.py @@ -225,7 +225,7 @@ def _step(self, dag): + (new_succeeding_run or QuantumCircuit(1)).data ), self._optimize1q._basis_gates, - qubit_indices[run[0].qargs[0]] + qubit_indices[run[0].qargs[0]], ): if preceding_run and new_preceding_run is not None: self._replace_subdag(dag, preceding_run, new_preceding_run) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 662f4e6d1a72..2a529540bb73 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -181,6 +181,6 @@ def _error(circuit, target, qubit): ] gate_error = 1 - np.product(gate_fidelities) if gate_error == 0.0: - return -100 + len(circuit) # prefer shorter circuits among those with zero error + return -100 + len(circuit) # prefer shorter circuits among those with zero error else: return gate_error