From b32a53174cbb14acaf885d5ad3d81459b8ce95f1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 7 Jul 2021 18:01:25 +1200 Subject: [PATCH] Fix `Optimize1qGatesDecomposition` length heuristic (#6553) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix 1q optimization heuristic * drop unnecessary brackets Co-authored-by: Ali Javadi-Abhari * actually fill out reno template :P * maker linter happier * make lev happier * add a GH link to the sloppy synth warning Co-authored-by: Matthew Treinish * improve source linking in changelog Co-authored-by: Matthew Treinish * remember target basis name * update circuit definitions * improve linter cheerfulness * increase reluctance to decompose calibrated gates * change .basis slot to ._decomposers * my local linter thinks everything is fine :/ * add some U3 special cases * make black happy * fix a claimed circular import * add a couple more rewrite tests * satisfy black * avoid lambda = ± pi in 1Q KAK * optimize on strict length * more thorough gate elision during 1Q synthesis * respond to most of Lev's feedback * add an 'allow_non_canonical' parameters * normalize K rolls * Update qiskit/quantum_info/synthesis/one_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> * Update qiskit/quantum_info/synthesis/one_qubit_decompose.py Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> * add some Euler special case tests for pushing a K(pi) through an A(alpha) * add some Euler special case tests for pushing a K(pi) through an A(alpha) * Update one_qubit_decompose.py a by-hand attempt at reformatting the docstring * ok linter Co-authored-by: Ali Javadi-Abhari Co-authored-by: Matthew Treinish Co-authored-by: Lev Bishop <18673315+levbishop@users.noreply.github.com> --- .../synthesis/one_qubit_decompose.py | 186 +++++++++++------- .../optimization/optimize_1q_decomposition.py | 89 ++++++--- ...ping-short-sequences-044a64740bf414a7.yaml | 5 + test/python/compiler/test_transpiler.py | 6 +- test/python/opflow/test_pauli_sum_op.py | 3 +- test/python/pulse/test_builder.py | 12 +- test/python/quantum_info/test_synthesis.py | 35 +++- .../transpiler/test_basis_translator.py | 1 + .../test_optimize_1q_decomposition.py | 86 +++++++- 9 files changed, 309 insertions(+), 114 deletions(-) create mode 100644 releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 73e94cbbc1b8..dfa60b75c17f 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -281,92 +281,121 @@ def _params_u1x(mat): return theta, phi, lam, phase - 0.5 * (theta + phi + lam) @staticmethod - def _circuit_zyz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - gphase = phase - (phi + lam) / 2 - qr = QuantumRegister(1, "qr") - circuit = QuantumCircuit(qr) - if not simplify: - atol = -1.0 - if abs(theta) < atol: - tot = _mod_2pi(phi + lam, atol) - if abs(tot) > atol: - circuit._append(RZGate(tot), [qr[0]], []) - gphase += tot / 2 - circuit.global_phase = gphase - return circuit - if abs(theta - np.pi) < atol: - gphase += phi - lam, phi = lam - phi, 0 - lam = _mod_2pi(lam, atol) - if abs(lam) > atol: - gphase += lam / 2 - circuit._append(RZGate(lam), [qr[0]], []) - circuit._append(RYGate(theta), [qr[0]], []) - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - gphase += phi / 2 - circuit._append(RZGate(phi), [qr[0]], []) - circuit.global_phase = gphase - return circuit + def _circuit_kak( + theta, + phi, + lam, + phase, + simplify=True, + atol=DEFAULT_ATOL, + allow_non_canonical=True, + k_gate=RZGate, + a_gate=RYGate, + ): + """ + Installs the angles phi, theta, and lam into a KAK-type decomposition of the form + K(phi) . A(theta) . K(lam) , where K and A are an orthogonal pair drawn from RZGate, RYGate, + and RXGate. - @staticmethod - def _circuit_zxz(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): - gphase = phase - (phi + lam) / 2 - qr = QuantumRegister(1, "qr") - circuit = QuantumCircuit(qr) - if not simplify: - atol = -1.0 - if abs(theta) < atol: - tot = _mod_2pi(phi + lam) - if abs(tot) > atol: - circuit._append(RZGate(tot), [qr[0]], []) - gphase += tot / 2 - circuit.global_phase = gphase - return circuit - if abs(theta - np.pi) < atol: - gphase += phi - lam, phi = lam - phi, 0 - lam = _mod_2pi(lam, atol) - if abs(lam) > atol: - gphase += lam / 2 - circuit._append(RZGate(lam), [qr[0]], []) - circuit._append(RXGate(theta), [qr[0]], []) - phi = _mod_2pi(phi, atol) - if abs(phi) > atol: - gphase += phi / 2 - circuit._append(RZGate(phi), [qr[0]], []) - circuit.global_phase = gphase - return circuit + Args: + theta (float): The middle KAK parameter. Expected to lie in [0, pi). + phi (float): The first KAK parameter. + lam (float): The final KAK parameter. + phase (float): The input global phase. + k_gate (Callable): The constructor for the K gate Instruction. + a_gate (Callable): The constructor for the A gate Instruction. + simplify (bool): Indicates whether gates should be elided / coalesced where possible. + allow_non_canonical (bool): Indicates whether we are permitted to reverse the sign of + the middle parameter, theta, in the output. When this and `simplify` are both + enabled, we take the opportunity to commute half-rotations in the outer gates past + the middle gate, which permits us to coalesce them at the cost of reversing the sign + of theta. - @staticmethod - def _circuit_xyx(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): + Returns: + QuantumCircuit: The assembled circuit. + """ gphase = phase - (phi + lam) / 2 qr = QuantumRegister(1, "qr") circuit = QuantumCircuit(qr) if not simplify: atol = -1.0 + # Early return for the middle-gate-free case if abs(theta) < atol: - tot = _mod_2pi(phi + lam, atol) - if abs(tot) > atol: - circuit._append(RXGate(tot), [qr[0]], []) - gphase += tot / 2 + lam, phi = lam + phi, 0 + # NOTE: The following normalization is safe, because the gphase correction below + # fixes a particular diagonal entry to 1, which prevents any potential phase + # slippage coming from _mod_2pi injecting multiples of 2pi. + lam = _mod_2pi(lam, atol) + if abs(lam) > atol: + + circuit._append(k_gate(lam), [qr[0]], []) + gphase += lam / 2 circuit.global_phase = gphase return circuit if abs(theta - np.pi) < atol: gphase += phi lam, phi = lam - phi, 0 + if allow_non_canonical and ( + abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi + np.pi)) < atol + ): + lam, theta, phi = lam + np.pi, -theta, phi + np.pi lam = _mod_2pi(lam, atol) if abs(lam) > atol: gphase += lam / 2 - circuit._append(RXGate(lam), [qr[0]], []) - circuit._append(RYGate(theta), [qr[0]], []) + circuit._append(k_gate(lam), [qr[0]], []) + circuit._append(a_gate(theta), [qr[0]], []) phi = _mod_2pi(phi, atol) if abs(phi) > atol: gphase += phi / 2 - circuit._append(RXGate(phi), [qr[0]], []) + circuit._append(k_gate(phi), [qr[0]], []) circuit.global_phase = gphase return circuit + def _circuit_zyz( + self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True + ): + return self._circuit_kak( + theta, + phi, + lam, + phase, + simplify=simplify, + atol=atol, + allow_non_canonical=allow_non_canonical, + k_gate=RZGate, + a_gate=RYGate, + ) + + def _circuit_zxz( + self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True + ): + return self._circuit_kak( + theta, + phi, + lam, + phase, + simplify=simplify, + atol=atol, + allow_non_canonical=allow_non_canonical, + k_gate=RZGate, + a_gate=RXGate, + ) + + def _circuit_xyx( + self, theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL, allow_non_canonical=True + ): + return self._circuit_kak( + theta, + phi, + lam, + phase, + simplify=simplify, + atol=atol, + allow_non_canonical=allow_non_canonical, + k_gate=RXGate, + a_gate=RYGate, + ) + @staticmethod def _circuit_u3(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): qr = QuantumRegister(1, "qr") @@ -407,35 +436,44 @@ def _circuit_u(theta, phi, lam, phase, simplify=True, atol=DEFAULT_ATOL): @staticmethod def _circuit_psx_gen(theta, phi, lam, phase, atol, pfun, xfun, xpifun=None): - """Generic X90, phase decomposition""" + """ + Generic X90, phase decomposition + + NOTE: `pfun` is responsible for eliding gates where appropriate (e.g., at angle value 0). + """ qr = QuantumRegister(1, "qr") circuit = QuantumCircuit(qr, global_phase=phase) - # Check for decomposition into minimimal number required SX pulses + # Early return for zero SX decomposition if np.abs(theta) < atol: - # Zero SX gate decomposition pfun(circuit, qr, lam + phi) return circuit + # Early return for single SX decomposition if abs(theta - np.pi / 2) < atol: - # Single SX gate decomposition pfun(circuit, qr, lam - np.pi / 2) xfun(circuit, qr) pfun(circuit, qr, phi + np.pi / 2) return circuit - # General two-SX gate decomposition - # Shift theta and phi so decomposition is - # P(phi).SX.P(theta).SX.P(lam) + # General double SX decomposition if abs(theta - np.pi) < atol: circuit.global_phase += lam phi, lam = phi - lam, 0 + if abs(_mod_2pi(lam + np.pi)) < atol or abs(_mod_2pi(phi)) < atol: + lam, theta, phi = lam + np.pi, -theta, phi + np.pi + circuit.global_phase -= theta + # Shift theta and phi to turn the decomposition from + # RZ(phi).RY(theta).RZ(lam) = RZ(phi).RX(-pi/2).RZ(theta).RX(pi/2).RZ(lam) + # into RZ(phi+pi).SX.RZ(theta+pi).SX.RZ(lam) . + theta, phi = theta + np.pi, phi + np.pi circuit.global_phase -= np.pi / 2 + # Emit circuit pfun(circuit, qr, lam) - if xpifun and abs(_mod_2pi(theta + np.pi)) < atol: + if xpifun and abs(_mod_2pi(theta)) < atol: xpifun(circuit, qr) else: xfun(circuit, qr) - pfun(circuit, qr, theta + np.pi) + pfun(circuit, qr, theta) xfun(circuit, qr) - pfun(circuit, qr, phi + np.pi) + pfun(circuit, qr, phi) return circuit diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index fd3ea68ec358..44959d48ba40 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -14,13 +14,13 @@ import copy import logging -import math +import warnings import numpy as np +from qiskit.circuit.library.standard_gates import U3Gate from qiskit.transpiler.basepasses import TransformationPass from qiskit.quantum_info.synthesis import one_qubit_decompose -from qiskit.circuit.library.standard_gates import U3Gate from qiskit.converters import circuit_to_dag logger = logging.getLogger(__name__) @@ -38,25 +38,26 @@ def __init__(self, basis=None): and the Euler basis. """ super().__init__() - self.basis = None + self._target_basis = basis + self._decomposers = None if basis: - self.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.basis) + basis_copy = copy.copy(self._decomposers) for base in basis_copy: # check if gates are a superset of another basis # and if so, remove that basis if set(euler_basis_gates[base.basis]).issubset(set(gates)): - self.basis.remove(base) + self._decomposers.remove(base) # check if the gates are a subset of another basis elif set(gates).issubset(set(euler_basis_gates[base.basis])): break # if not a subset, add it to the list else: - self.basis.append( + self._decomposers.append( one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name) ) @@ -69,40 +70,76 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - if not self.basis: + if not self._decomposers: logger.info("Skipping pass because no basis is set") return dag runs = dag.collect_1q_runs() - identity_matrix = np.eye(2) for run in runs: - single_u3 = False - # Don't try to optimize a single 1q gate, except for U3 - if len(run) <= 1: - params = run[0].op.params - # Remove single identity gates - if len(params) > 0 and np.array_equal(run[0].op.to_matrix(), identity_matrix): + # 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.array_equal(run[0].op.to_matrix(), np.eye(2)): dag.remove_op_node(run[0]) continue - if isinstance(run[0].op, U3Gate): - param = float(params[0]) - if math.isclose(param, 0, rel_tol=0, abs_tol=1e-12) or math.isclose( - param, np.pi / 2, abs_tol=1e-12, rel_tol=0 - ): - single_u3 = True - else: - continue - else: + # 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: continue new_circs = [] operator = run[0].op.to_matrix() for gate in run[1:]: operator = gate.op.to_matrix().dot(operator) - for decomposer in self.basis: + for decomposer in self._decomposers: new_circs.append(decomposer._decompose(operator)) if new_circs: new_circ = min(new_circs, key=len) - if len(run) > len(new_circ) or (single_u3 and new_circ.data[0][0].name != "u3"): + + # do we even have calibrations? + has_cals_p = dag.calibrations is not None and len(dag.calibrations) > 0 + # is this run all in the target set and also uncalibrated? + rewriteable_and_in_basis_p = all( + g.name in self._target_basis + and (not has_cals_p or not dag.has_calibration_for(g)) + for g in run + ) + # does this run have uncalibrated gates? + uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in 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)) + for g in run + ) + + if rewriteable_and_in_basis_p and len(run) < len(new_circ): + # NOTE: This is short-circuited on calibrated gates, which we're timid about + # reducing. + warnings.warn( + f"Resynthesized {run} and got {new_circ}, " + f"but the original was native and the new value is longer. This " + f"indicates an efficiency bug in synthesis. Please report it by " + f"opening an issue here: " + f"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. + if ( + uncalibrated_and_not_basis_p + or (uncalibrated_p and len(run) > len(new_circ)) + or isinstance(run[0].op, U3Gate) + ): new_dag = circuit_to_dag(new_circ) dag.substitute_node_with_dag(run[0], new_dag) # Delete the other nodes in the run diff --git a/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml new file mode 100644 index 000000000000..336895951a90 --- /dev/null +++ b/releasenotes/notes/fixed-bug-in-Optimize1qGatesDecomposition-skipping-short-sequences-044a64740bf414a7.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixes a bug in :func:`~qiskit.transpiler.passes.Optimize1qGatesDecomposition` previously causing certain + short sequences of gates to erroneously not be rewritten. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 7be923e812c5..e6f93a830892 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1212,12 +1212,12 @@ def test_no_infinite_loop(self, optimization_level): # a -0.5 * theta phase for RZ to P twice, once at theta, and once at 3 pi # for the second and third RZ gates in the U3 decomposition. expected = QuantumCircuit( - 1, global_phase=-np.pi / 2 - 0.5 * (0.2 + np.pi) - 0.5 * 3 * np.pi + 1, global_phase=-np.pi / 2 - 0.5 * (-0.2 + np.pi) - 0.5 * 3 * np.pi ) + expected.p(-np.pi, 0) expected.sx(0) - expected.p(-np.pi + 0.2, 0) + expected.p(np.pi - 0.2, 0) expected.sx(0) - expected.p(-np.pi, 0) error_message = ( f"\nOutput circuit:\n{out!s}\n{Operator(out).data}\n" diff --git a/test/python/opflow/test_pauli_sum_op.py b/test/python/opflow/test_pauli_sum_op.py index 8214fb9c1e15..e0a25901e05e 100644 --- a/test/python/opflow/test_pauli_sum_op.py +++ b/test/python/opflow/test_pauli_sum_op.py @@ -212,7 +212,8 @@ def test_to_instruction(self): target = ((X + Z) / np.sqrt(2)).to_instruction() qc = QuantumCircuit(1) qc.u(np.pi / 2, 0, np.pi, 0) - self.assertEqual(transpile(target.definition, basis_gates=["u"]), qc) + qc_out = transpile(target.definition, basis_gates=["u"]) + self.assertEqual(qc_out, qc) def test_to_pauli_op(self): """test to_pauli_op method""" diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 86969e64329c..712289fb0d8d 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -817,10 +817,10 @@ def test_cx(self): def test_u1(self): """Test u1 gate.""" with pulse.build(self.backend) as schedule: - pulse.u1(np.pi, 0) + pulse.u1(np.pi / 2, 0) reference_qc = circuit.QuantumCircuit(1) - reference_qc.append(circuit.library.U1Gate(np.pi), [0]) + reference_qc.append(circuit.library.U1Gate(np.pi / 2), [0]) reference = compiler.schedule(reference_qc, self.backend) self.assertScheduleEqual(schedule, reference) @@ -828,10 +828,10 @@ def test_u1(self): def test_u2(self): """Test u2 gate.""" with pulse.build(self.backend) as schedule: - pulse.u2(np.pi, 0, 0) + pulse.u2(np.pi / 2, 0, 0) reference_qc = circuit.QuantumCircuit(1) - reference_qc.append(circuit.library.U2Gate(np.pi, 0), [0]) + reference_qc.append(circuit.library.U2Gate(np.pi / 2, 0), [0]) reference = compiler.schedule(reference_qc, self.backend) self.assertScheduleEqual(schedule, reference) @@ -839,10 +839,10 @@ def test_u2(self): def test_u3(self): """Test u3 gate.""" with pulse.build(self.backend) as schedule: - pulse.u3(np.pi, 0, np.pi / 2, 0) + pulse.u3(np.pi / 8, np.pi / 16, np.pi / 4, 0) reference_qc = circuit.QuantumCircuit(1) - reference_qc.append(circuit.library.U3Gate(np.pi, 0, np.pi / 2), [0]) + reference_qc.append(circuit.library.U3Gate(np.pi / 8, np.pi / 16, np.pi / 4), [0]) reference = compiler.schedule(reference_qc, self.backend) self.assertScheduleEqual(schedule, reference) diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 52c7d9c3fd2e..b6b282752946 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -262,7 +262,7 @@ def test_euler_angles_1q_random(self, seed): self.check_one_qubit_euler_angles(unitary) -ANGEXP_ZYZ = [ # Special cases for ZYZ type expansions +ANGEXP_ZYZ = [ [(1.0e-13, 0.1, -0.1, 0), (0, 0)], [(1.0e-13, 0.2, -0.1, 0), (1, 0)], [(1.0e-13, np.pi, np.pi, 0), (0, 0)], @@ -276,8 +276,24 @@ def test_euler_angles_1q_random(self, seed): [(0.1, 0.0, 0.0, 0), (0, 1)], [(0.1, 1.0e-13, 0.2, 0), (1, 1)], [(0.1, 0.2, 0.3, 0), (2, 1)], + [(0.1, 0.2, np.pi, 0), (1, 1)], + [(0.1, np.pi, 0.1, 0), (1, 1)], + [(0.1, np.pi, np.pi, 0), (0, 1)], ] -ANGEXP_PSX = [ # Special cases for Z.X90.Z.X90.Z type expansions +""" +Special cases for ZYZ type expansions. Each list entry is of the format + + (alpha, beta, gamma, delta), (r, s), + +and encodes the assertion that + + (K(b) @ A(a) @ K(c), global_phase=d) + +re-synthesizes to have r applications of the K gate and s of the A gate. +""" + + +ANGEXP_PSX = [ [(0.0, 0.1, -0.1), (0, 0)], [(0.0, 0.1, 0.2), (1, 0)], [(-np.pi / 2, 0.2, 0.0), (2, 1)], @@ -288,7 +304,22 @@ def test_euler_angles_1q_random(self, seed): [(np.pi, np.pi + 0.1, 0.1), (0, 2)], [(np.pi, np.pi + 0.2, -0.1), (1, 2)], [(0.1, 0.2, 0.3), (3, 2)], + [(0.1, np.pi, 0.2), (2, 2)], + [(0.1, 0.2, 0.0), (2, 2)], + [(0.1, 0.2, np.pi), (2, 2)], + [(0.1, np.pi, 0), (1, 2)], ] +""" +Special cases for Z.X90.Z.X90.Z type expansions. Each list entry is of the format + + (alpha, beta, gamma), (r, s), + +and encodes the assertion that + + U3(alpha, beta, gamma) + +re-synthesizes to have r applications of the P gate and s of the SX gate. +""" @ddt diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 4fc0b0fdb2a3..540df7d20ae9 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -815,6 +815,7 @@ def test_condition_set_substitute_node(self): expected.cx(0, 1) expected.measure(1, 1) expected.u2(0, pi, 0).c_if(cr, 1) + self.assertEqual(circ_transpiled, expected) def test_skip_target_basis_equivalences_1(self): diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index 32e5d191db5b..86e9d379aaa1 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -354,6 +354,48 @@ def test_euler_decomposition_worse(self): # assert optimization pass doesn't use it. self.assertEqual(circuit, result, f"Circuit:\n{circuit}\nResult:\n{result}") + def test_euler_decomposition_worse_2(self): + """Ensure we don't decompose to a deeper circuit in an edge case.""" + circuit = QuantumCircuit(1) + circuit.rz(0.13, 0) + 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}") + + def test_euler_decomposition_zsx(self): + """Ensure we don't decompose to a deeper circuit in the ZSX basis.""" + circuit = QuantumCircuit(1) + circuit.rz(0.3, 0) + circuit.sx(0) + circuit.rz(0.2, 0) + circuit.sx(0) + + 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}") + + def test_euler_decomposition_zsx_2(self): + """Ensure we don't decompose to a deeper circuit in the ZSX basis.""" + circuit = QuantumCircuit(1) + circuit.sx(0) + circuit.rz(0.2, 0) + circuit.sx(0) + circuit.rz(0.3, 0) + + 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}") + def test_optimize_u_to_phase_gate(self): """U(0, 0, pi/4) -> p(pi/4). Basis [p, sx].""" qr = QuantumRegister(2, "qr") @@ -438,9 +480,49 @@ def test_y_simplification_rz_sx_x(self): passmanager.append(Optimize1qGatesDecomposition(basis)) result = passmanager.run(qc) expected = QuantumCircuit(1) - expected.x(0) expected.rz(-np.pi, 0) - expected.global_phase += np.pi + expected.x(0) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) + + def test_short_string(self): + """Test that a shorter-than-universal string is still rewritten.""" + qc = QuantumCircuit(1) + qc.h(0) + qc.ry(np.pi / 2, 0) + basis = ["sx", "rz"] + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(qc) + expected = QuantumCircuit(1) + expected.sx(0) + expected.sx(0) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) + + def test_u_rewrites_to_rz(self): + """Test that a phase-like U-gate gets rewritten into an RZ gate.""" + qc = QuantumCircuit(1) + qc.u(0, 0, np.pi / 6, 0) + basis = ["sx", "rz"] + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(qc) + expected = QuantumCircuit(1, global_phase=np.pi / 12) + expected.rz(np.pi / 6, 0) + msg = f"expected:\n{expected}\nresult:\n{result}" + self.assertEqual(expected, result, msg=msg) + + def test_u_rewrites_to_phase(self): + """Test that a phase-like U-gate gets rewritten into an RZ gate.""" + qc = QuantumCircuit(1) + qc.u(0, 0, np.pi / 6, 0) + basis = ["sx", "p"] + passmanager = PassManager() + passmanager.append(Optimize1qGatesDecomposition(basis)) + result = passmanager.run(qc) + expected = QuantumCircuit(1) + expected.p(np.pi / 6, 0) msg = f"expected:\n{expected}\nresult:\n{result}" self.assertEqual(expected, result, msg=msg)