From f2234fcfa12149daea06d0979d38ab5944c14f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 7 Feb 2025 14:43:37 +0100 Subject: [PATCH 1/9] Remove use of `BackendProperties` (BackendV1) in `UnitarySynthesis` pass (#13706) * Remove BackendV1 from unitary synthesis unit tests * Remove backend_properties input * Remove backend_properties argument from generate_translation_passmanager * Say use target instead of backend in docs * Fix conflict --- .../passes/synthesis/unitary_synthesis.py | 85 ++----- .../preset_passmanagers/builtin_plugins.py | 4 - .../transpiler/preset_passmanagers/common.py | 7 - .../transpiler/test_unitary_synthesis.py | 223 ++++++++---------- 4 files changed, 118 insertions(+), 201 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index a2bd044c734..ab66b38bd99 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -55,7 +55,6 @@ from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.exceptions import QiskitError -from qiskit.providers.models.backendproperties import BackendProperties from qiskit.quantum_info import Operator from qiskit.synthesis.one_qubit import one_qubit_decompose from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments @@ -129,7 +128,7 @@ def _find_matching_euler_bases(target, qubit): def _choose_bases(basis_gates, basis_dict=None): - """Find the matching basis string keys from the list of basis gates from the backend.""" + """Find the matching basis string keys from the list of basis gates from the target.""" if basis_gates is None: basis_set = set() else: @@ -320,7 +319,6 @@ def __init__( basis_gates: list[str] = None, approximation_degree: float | None = 1.0, coupling_map: CouplingMap = None, - backend_props: BackendProperties = None, pulse_optimize: bool | None = None, natural_direction: bool | None = None, synth_gates: list[str] | None = None, @@ -332,7 +330,7 @@ def __init__( """Synthesize unitaries over some basis gates. This pass can approximate 2-qubit unitaries given some - gate fidelities (either via ``backend_props`` or ``target``). + gate fidelities (via ``target``). More approximation can be forced by setting a heuristic dial ``approximation_degree``. @@ -345,13 +343,11 @@ def __init__( (1.0=no approximation, 0.0=maximal approximation). Approximation can make the synthesized circuit cheaper at the cost of straying from the original unitary. If None, approximation is done based on gate fidelities. - coupling_map (CouplingMap): the coupling map of the backend + coupling_map (CouplingMap): the coupling map of the target in case synthesis is done on a physical circuit. The directionality of the coupling_map will be taken into account if ``pulse_optimize`` is ``True``/``None`` and ``natural_direction`` is ``True``/``None``. - backend_props (BackendProperties): Properties of a backend to - synthesize for (e.g. gate fidelities). pulse_optimize (bool): Whether to optimize pulses during synthesis. A value of ``None`` will attempt it but fall back if it does not succeed. A value of ``True`` will raise @@ -363,7 +359,7 @@ def __init__( coupling map is unidirectional. If there is no coupling map or the coupling map is bidirectional, the gate direction with the shorter - duration from the backend properties will be used. If + duration from the target properties will be used. If set to True, and a natural direction can not be determined, raises :class:`.TranspilerError`. If set to None, no exception will be raised if a natural direction can @@ -383,7 +379,7 @@ def __init__( your unitary synthesis plugin on how to use this. target: The optional :class:`~.Target` for the target device the pass is compiling for. If specified this will supersede the values - set for ``basis_gates``, ``coupling_map``, and ``backend_props``. + set for ``basis_gates`` and ``coupling_map``. Raises: TranspilerError: if ``method`` was specified but is not found in the @@ -399,7 +395,6 @@ def __init__( if method != "default": self.plugins = plugin.UnitarySynthesisPluginManager() self._coupling_map = coupling_map - self._backend_props = backend_props self._pulse_optimize = pulse_optimize self._natural_direction = natural_direction self._plugin_config = plugin_config @@ -496,23 +491,19 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if method.supports_pulse_optimize: kwargs["pulse_optimize"] = self._pulse_optimize if method.supports_gate_lengths: - _gate_lengths = _gate_lengths or _build_gate_lengths( - self._backend_props, self._target - ) + _gate_lengths = _gate_lengths or _build_gate_lengths(self._target) kwargs["gate_lengths"] = _gate_lengths if method.supports_gate_errors: - _gate_errors = _gate_errors or _build_gate_errors( - self._backend_props, self._target - ) + _gate_errors = _gate_errors or _build_gate_errors(self._target) kwargs["gate_errors"] = _gate_errors if method.supports_gate_lengths_by_qubit: _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( - self._backend_props, self._target + self._target ) kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit if method.supports_gate_errors_by_qubit: _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( - self._backend_props, self._target + self._target ) kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit supported_bases = method.supported_bases @@ -610,9 +601,8 @@ def _run_main_loop( return out_dag -def _build_gate_lengths(props=None, target=None): - """Builds a ``gate_lengths`` dictionary from either ``props`` (BackendV1) - or ``target`` (BackendV2). +def _build_gate_lengths(target=None): + """Builds a ``gate_lengths`` dictionary from ``target`` (BackendV2). The dictionary has the form: {gate_name: {(qubits,): duration}} @@ -624,21 +614,11 @@ def _build_gate_lengths(props=None, target=None): for qubit, gate_props in prop_dict.items(): if gate_props is not None and gate_props.duration is not None: gate_lengths[gate][qubit] = gate_props.duration - elif props is not None: - for gate in props._gates: - gate_lengths[gate] = {} - for k, v in props._gates[gate].items(): - length = v.get("gate_length") - if length: - gate_lengths[gate][k] = length[0] - if not gate_lengths[gate]: - del gate_lengths[gate] return gate_lengths -def _build_gate_errors(props=None, target=None): - """Builds a ``gate_error`` dictionary from either ``props`` (BackendV1) - or ``target`` (BackendV2). +def _build_gate_errors(target=None): + """Builds a ``gate_error`` dictionary from ``target`` (BackendV2). The dictionary has the form: {gate_name: {(qubits,): error_rate}} @@ -650,22 +630,12 @@ def _build_gate_errors(props=None, target=None): for qubit, gate_props in prop_dict.items(): if gate_props is not None and gate_props.error is not None: gate_errors[gate][qubit] = gate_props.error - if props is not None: - for gate in props._gates: - gate_errors[gate] = {} - for k, v in props._gates[gate].items(): - error = v.get("gate_error") - if error: - gate_errors[gate][k] = error[0] - if not gate_errors[gate]: - del gate_errors[gate] return gate_errors -def _build_gate_lengths_by_qubit(props=None, target=None): +def _build_gate_lengths_by_qubit(target=None): """ - Builds a `gate_lengths` dictionary from either `props` (BackendV1) - or `target (BackendV2)`. + Builds a `gate_lengths` dictionary from `target (BackendV2)`. The dictionary has the form: {(qubits): [Gate, duration]} @@ -682,23 +652,12 @@ def _build_gate_lengths_by_qubit(props=None, target=None): operation_and_durations.append((operation, duration)) if operation_and_durations: gate_lengths[qubits] = operation_and_durations - elif props is not None: - for gate_name, gate_props in props._gates.items(): - gate = GateNameToGate[gate_name] - for qubits, properties in gate_props.items(): - duration = properties.get("gate_length", [0.0])[0] - operation_and_durations = (gate, duration) - if qubits in gate_lengths: - gate_lengths[qubits].append(operation_and_durations) - else: - gate_lengths[qubits] = [operation_and_durations] return gate_lengths -def _build_gate_errors_by_qubit(props=None, target=None): +def _build_gate_errors_by_qubit(target=None): """ - Builds a `gate_error` dictionary from either `props` (BackendV1) - or `target (BackendV2)`. + Builds a `gate_error` dictionary from `target (BackendV2)`. The dictionary has the form: {(qubits): [Gate, error]} @@ -715,16 +674,6 @@ def _build_gate_errors_by_qubit(props=None, target=None): operation_and_errors.append((operation, error)) if operation_and_errors: gate_errors[qubits] = operation_and_errors - elif props is not None: - for gate_name, gate_props in props._gates.items(): - gate = GateNameToGate[gate_name] - for qubits, properties in gate_props.items(): - error = properties.get("gate_error", [0.0])[0] - operation_and_errors = (gate, error) - if qubits in gate_errors: - gate_errors[qubits].append(operation_and_errors) - else: - gate_errors[qubits] = [operation_and_errors] return gate_errors diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 4742de6fda4..d1ffa8d3d1b 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -218,7 +218,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana method="translator", approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, hls_config=pass_manager_config.hls_config, @@ -236,7 +235,6 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana method="synthesis", approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, hls_config=pass_manager_config.hls_config, @@ -617,7 +615,6 @@ def _opt_control(property_set): pass_manager_config.basis_gates, approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, method=pass_manager_config.unitary_synthesis_method, plugin_config=pass_manager_config.unitary_synthesis_plugin_config, target=pass_manager_config.target, @@ -664,7 +661,6 @@ def _unroll_condition(property_set): pass_manager_config.basis_gates, approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, method=pass_manager_config.unitary_synthesis_method, plugin_config=pass_manager_config.unitary_synthesis_plugin_config, target=pass_manager_config.target, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index e3cd76bc9a4..8422ab20945 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -429,7 +429,6 @@ def generate_translation_passmanager( method="translator", approximation_degree=None, coupling_map=None, - backend_props=None, unitary_synthesis_method="default", unitary_synthesis_plugin_config=None, hls_config=None, @@ -452,8 +451,6 @@ def generate_translation_passmanager( unitary_synthesis_plugin_config (dict): The optional dictionary plugin configuration, this is plugin specific refer to the specified plugin's documentation for how to use. - backend_props (BackendProperties): Properties of a backend to - synthesize for (e.g. gate fidelities). unitary_synthesis_method (str): The unitary synthesis method to use. You can see a list of installed plugins with :func:`.unitary_synthesis_plugin_names`. hls_config (HLSConfig): An optional configuration class to use for @@ -477,7 +474,6 @@ def generate_translation_passmanager( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, @@ -502,7 +498,6 @@ def generate_translation_passmanager( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, min_qubits=3, @@ -527,7 +522,6 @@ def generate_translation_passmanager( basis_gates=basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, @@ -550,7 +544,6 @@ def generate_translation_passmanager( basis_gates=basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 767b126bbb3..bc9e218e79c 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -22,7 +22,7 @@ from ddt import ddt, data from qiskit import transpile, generate_preset_pass_manager -from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.library import quantum_volume from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -70,6 +70,7 @@ from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order FakeMumbaiFractionalCX, ) +from ..legacy_cmaps import YORKTOWN_CMAP class FakeBackend2QV2(GenericBackendV2): @@ -147,78 +148,18 @@ def test_two_qubit_synthesis_to_basis(self, basis_gates): out = UnitarySynthesis(basis_gates).run(dag) self.assertTrue(set(out.count_ops()).issubset(basis_gates)) - @combine(gate=["unitary", "swap"], natural_direction=[True, False]) - def test_two_qubit_synthesis_to_directional_cx(self, gate, natural_direction): - """Verify two qubit unitaries are synthesized to match basis gates.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) - - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - if gate == "unitary": - qc.unitary(random_unitary(4, seed=12), [0, 1]) - elif gate == "swap": - qc.swap(qr[0], qr[1]) - - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=natural_direction, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - - @data(True, False) - def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self, natural_direction): - """Verify two qubit unitaries are synthesized to match basis gates - across multiple registers.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr0 = QuantumRegister(1) - qr1 = QuantumRegister(1) - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr0, qr1) - qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=natural_direction, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - @data(True, False, None) def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_direction): """Verify natural cx direction is used when specified in coupling map.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=natural_direction, ) @@ -239,9 +180,7 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_d def test_two_qubit_synthesis_not_pulse_optimal(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) @@ -250,9 +189,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=False, natural_direction=True, ), @@ -262,9 +200,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=True, ), @@ -275,21 +212,18 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): self.assertGreater(qc_nonoptimal.count_ops()["sx"], qc_optimal.count_ops()["sx"]) def test_two_qubit_pulse_optimal_true_raises(self): - """Verify raises if pulse optimal==True but cx is not in the backend basis.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + """Verify raises if pulse optimal==True but cx is not in the basis.""" + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] # this assumes iswap pulse optimal decomposition doesn't exist - conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] + basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=basis_gates, coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=True, ) @@ -297,47 +231,17 @@ def test_two_qubit_pulse_optimal_true_raises(self): with self.assertRaises(QiskitError): pm.run(qc) - def test_two_qubit_natural_direction_true_duration_fallback(self): - """Verify fallback path when pulse_optimize==True.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr = QuantumRegister(2) - coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertTrue( - all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - def test_two_qubit_natural_direction_true_gate_length_raises(self): """Verify that error is raised if preferred direction cannot be inferred from gate lenghts/errors. """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - for _, nduv in backend.properties()._gates["cx"].items(): - nduv["gate_length"] = (4e-7, nduv["gate_length"][1]) - nduv["gate_error"] = (7e-3, nduv["gate_error"][1]) qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - backend_props=backend.properties(), + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], pulse_optimize=True, natural_direction=True, ) @@ -347,18 +251,14 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): def test_two_qubit_pulse_optimal_none_optimal(self): """Verify pulse optimal decomposition when pulse_optimize==None.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=None, natural_direction=True, ) @@ -374,20 +274,17 @@ def test_two_qubit_pulse_optimal_none_optimal(self): def test_two_qubit_pulse_optimal_none_no_raise(self): """Verify pulse optimal decomposition when pulse_optimize==None doesn't raise when pulse optimal decomposition unknown.""" - # this assumes iswawp pulse optimal decomposition doesn't exist - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] + # this assumes iswap pulse optimal decomposition doesn't exist + basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=basis_gates, coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=None, natural_direction=True, ) @@ -884,7 +781,7 @@ def test_3q_with_measure(self): def test_3q_series(self): """Test a series of 3-qubit blocks.""" - backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1) x = QuantumCircuit(3) x.x(2) @@ -911,7 +808,7 @@ def test_3q_measure_all(self): qc.append(op.power(8), [0, 1, 2]) qc.measure_all() - backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1) tqc = transpile(qc, backend) ops = tqc.count_ops() @@ -952,6 +849,88 @@ def test_determinism(self): for basis_gate in basis_gates: self.assertLessEqual(out.count_ops()[basis_gate], gate_counts[basis_gate]) + @combine(gate=["unitary", "swap"], natural_direction=[True, False]) + def test_two_qubit_synthesis_to_directional_cx_target(self, gate, natural_direction): + """Verify two qubit unitaries are synthesized to match basis gates.""" + # TODO: should make check more explicit e.g. explicitly set gate + # direction in test instead of using specific fake backend + backend = GenericBackendV2( + num_qubits=5, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=YORKTOWN_CMAP, + seed=1, + ) + coupling_map = CouplingMap(backend.coupling_map) + triv_layout_pass = TrivialLayout(coupling_map) + + qr = QuantumRegister(2) + qc = QuantumCircuit(qr) + if gate == "unitary": + qc.unitary(random_unitary(4, seed=12), [0, 1]) + elif gate == "swap": + qc.swap(qr[0], qr[1]) + + unisynth_pass = UnitarySynthesis( + target=backend.target, + pulse_optimize=True, + natural_direction=natural_direction, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertEqual(Operator(qc), Operator(qc_out)) + + @data(True, False) + def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target( + self, natural_direction + ): + """Verify two qubit unitaries are synthesized to match basis gates + across multiple registers.""" + # TODO: should make check more explicit e.g. explicitly set gate + # direction in test instead of using specific fake backend + backend = GenericBackendV2( + num_qubits=5, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=YORKTOWN_CMAP, + seed=1, + ) + qr0 = QuantumRegister(1) + qr1 = QuantumRegister(1) + coupling_map = CouplingMap(backend.coupling_map) + triv_layout_pass = TrivialLayout(coupling_map) + qc = QuantumCircuit(qr0, qr1) + qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]]) + unisynth_pass = UnitarySynthesis( + target=backend.target, + pulse_optimize=True, + natural_direction=natural_direction, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertEqual(Operator(qc), Operator(qc_out)) + + def test_two_qubit_natural_direction_true_duration_fallback_target(self): + """Verify fallback path when pulse_optimize==True.""" + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] + qr = QuantumRegister(2) + coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) + backend = GenericBackendV2( + num_qubits=5, basis_gates=basis_gates, coupling_map=coupling_map, seed=1 + ) + + triv_layout_pass = TrivialLayout(coupling_map) + qc = QuantumCircuit(qr) + qc.unitary(random_unitary(4, seed=12), [0, 1]) + unisynth_pass = UnitarySynthesis( + target=backend.target, + pulse_optimize=True, + natural_direction=True, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertTrue( + all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) + ) + if __name__ == "__main__": unittest.main() From 4ae023a32e2dc74da10eae279ff499e0db99610f Mon Sep 17 00:00:00 2001 From: Eli Arbel <46826214+eliarbel@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:15:57 +0200 Subject: [PATCH 2/9] Remove pulse-related passes (#13798) * Remove pulse-related passes This commit removes the calibration builder and pulse-gate passes which were deprecated in Qiskit 1.3, together with the pulse package itself. Related unit test have also been removed. * Fix lint issues --- qiskit/transpiler/passes/__init__.py | 13 - .../transpiler/passes/calibration/__init__.py | 17 - .../transpiler/passes/calibration/builders.py | 20 - .../passes/calibration/pulse_gate.py | 100 --- .../passes/calibration/rx_builder.py | 166 ----- .../passes/calibration/rzx_builder.py | 411 ------------ .../passes/optimization/__init__.py | 1 - .../echo_rzx_weyl_decomposition.py | 162 ----- .../transpiler/passes/scheduling/__init__.py | 2 +- .../passes/scheduling/alignments/__init__.py | 1 - .../alignments/pulse_gate_validation.py | 107 --- .../transpiler/preset_passmanagers/common.py | 16 +- .../remove-pulse-passes-3128f27ed7e42bf6.yaml | 7 + test/python/compiler/test_transpiler.py | 52 +- test/python/qpy/test_circuit_load_from_qpy.py | 41 +- .../test_instruction_alignments.py | 102 +-- .../transpiler/test_calibrationbuilder.py | 619 ------------------ .../test_echo_rzx_weyl_decomposition.py | 260 -------- .../transpiler/test_instruction_alignments.py | 120 ---- test/python/transpiler/test_passmanager.py | 43 +- .../python/transpiler/test_pulse_gate_pass.py | 577 ---------------- 21 files changed, 38 insertions(+), 2799 deletions(-) delete mode 100644 qiskit/transpiler/passes/calibration/__init__.py delete mode 100644 qiskit/transpiler/passes/calibration/builders.py delete mode 100644 qiskit/transpiler/passes/calibration/pulse_gate.py delete mode 100644 qiskit/transpiler/passes/calibration/rx_builder.py delete mode 100644 qiskit/transpiler/passes/calibration/rzx_builder.py delete mode 100644 qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py delete mode 100644 qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py create mode 100644 releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml delete mode 100644 test/python/transpiler/test_calibrationbuilder.py delete mode 100644 test/python/transpiler/test_echo_rzx_weyl_decomposition.py delete mode 100644 test/python/transpiler/test_instruction_alignments.py delete mode 100644 test/python/transpiler/test_pulse_gate_pass.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 8823e1ce723..1fd8454159a 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -85,7 +85,6 @@ RemoveFinalReset HoareOptimizer TemplateOptimization - EchoRZXWeylDecomposition ResetAfterMeasureSimplification OptimizeCliffords ElidePermutations @@ -100,11 +99,6 @@ .. autosummary:: :toctree: ../stubs/ - PulseGates - RZXCalibrationBuilder - RZXCalibrationBuilderNoEcho - RXCalibrationBuilder - .. autofunction:: rzx_templates Scheduling @@ -119,7 +113,6 @@ PadDynamicalDecoupling PadDelay ConstrainedReschedule - ValidatePulseGates InstructionDurationCheck SetIOLatency ALAPSchedule @@ -237,7 +230,6 @@ from .optimization import HoareOptimizer from .optimization import TemplateOptimization from .optimization import InverseCancellation -from .optimization import EchoRZXWeylDecomposition from .optimization import CollectAndCollapse from .optimization import CollectLinearFunctions from .optimization import CollectCliffords @@ -270,10 +262,6 @@ from .synthesis import AQCSynthesisPlugin # calibration -from .calibration import PulseGates -from .calibration import RZXCalibrationBuilder -from .calibration import RZXCalibrationBuilderNoEcho -from .calibration import RXCalibrationBuilder from .calibration.rzx_templates import rzx_templates # circuit scheduling @@ -281,7 +269,6 @@ from .scheduling import ALAPScheduleAnalysis from .scheduling import ASAPScheduleAnalysis from .scheduling import PadDynamicalDecoupling -from .scheduling import ValidatePulseGates from .scheduling import PadDelay from .scheduling import ConstrainedReschedule from .scheduling import InstructionDurationCheck diff --git a/qiskit/transpiler/passes/calibration/__init__.py b/qiskit/transpiler/passes/calibration/__init__.py deleted file mode 100644 index 990249373f3..00000000000 --- a/qiskit/transpiler/passes/calibration/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021 -# -# 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. - -"""Module containing transpiler calibration passes.""" - -from .pulse_gate import PulseGates -from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho -from .rx_builder import RXCalibrationBuilder diff --git a/qiskit/transpiler/passes/calibration/builders.py b/qiskit/transpiler/passes/calibration/builders.py deleted file mode 100644 index 49c2c642731..00000000000 --- a/qiskit/transpiler/passes/calibration/builders.py +++ /dev/null @@ -1,20 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Calibration creators.""" - -# TODO This import path will be deprecated. - -# pylint: disable=unused-import -from .pulse_gate import PulseGates -from .rzx_builder import RZXCalibrationBuilder, RZXCalibrationBuilderNoEcho -from .rx_builder import RXCalibrationBuilder diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py deleted file mode 100644 index f5b56ad0f35..00000000000 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ /dev/null @@ -1,100 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -"""Instruction schedule map reference pass.""" - -from typing import List, Union - -from qiskit.circuit import Instruction as CircuitInst -from qiskit.pulse import Schedule, ScheduleBlock -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.transpiler.target import Target -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -from .base_builder import CalibrationBuilder - - -class PulseGates(CalibrationBuilder): - """Pulse gate adding pass. - - This pass adds gate calibrations from the supplied ``InstructionScheduleMap`` - to a quantum circuit. - - This pass checks each DAG circuit node and acquires a corresponding schedule from - the instruction schedule map object that may be provided by the target backend. - Because this map is a mutable object, the end-user can provide a configured backend to - execute the circuit with customized gate implementations. - - This mapping object returns a schedule with "publisher" metadata which is an integer Enum - value representing who created the gate schedule. - If the gate schedule is provided by end-users, this pass attaches the schedule to - the DAG circuit as a calibration. - - This pass allows users to easily override quantum circuit with custom gate definitions - without directly dealing with those schedules. - - References - * [1] OpenQASM 3: A broader and deeper quantum assembly language - https://arxiv.org/abs/2104.14722 - """ - - @deprecate_pulse_dependency - def __init__( - self, - inst_map: InstructionScheduleMap = None, - target: Target = None, - ): - """Create new pass. - - Args: - inst_map: Instruction schedule map that user may override. - target: The :class:`~.Target` representing the target backend, if both - ``inst_map`` and ``target`` are specified then it updates instructions - in the ``target`` with ``inst_map``. - """ - super().__init__() - - if inst_map is None and target is None: - raise TranspilerError("inst_map and target cannot be None simulataneously.") - - if target is None: - target = Target() - target.update_from_instruction_schedule_map(inst_map) - self.target = target - - def supported(self, node_op: CircuitInst, qubits: List) -> bool: - """Determine if a given node supports the calibration. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return ``True`` is calibration can be provided. - """ - return self.target._has_calibration(node_op.name, tuple(qubits)) - - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: - """Gets the calibrated schedule for the given instruction and qubits. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return Schedule of target gate instruction. - - Raises: - TranspilerError: When node is parameterized and calibration is raw schedule object. - """ - return self.target._get_calibration(node_op.name, tuple(qubits), *node_op.params) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py deleted file mode 100644 index 7dfeb56c68b..00000000000 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ /dev/null @@ -1,166 +0,0 @@ -# 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. - -"""Add single-pulse RX calibrations that are bootstrapped from the SX calibration.""" - -from typing import Union -from functools import lru_cache -import numpy as np - -from qiskit.circuit import Instruction -from qiskit.circuit.library.standard_gates import RXGate -from qiskit.exceptions import QiskitError -from qiskit.pulse import Schedule, ScheduleBlock, builder, ScalableSymbolicPulse -from qiskit.pulse.channels import Channel -from qiskit.pulse.library.symbolic_pulses import Drag -from qiskit.transpiler.passes.calibration.base_builder import CalibrationBuilder -from qiskit.transpiler.target import Target -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -class RXCalibrationBuilder(CalibrationBuilder): - """Add single-pulse RX calibrations that are bootstrapped from the SX calibration. - - .. note:: - - Requirement: NormalizeRXAngles pass (one of the optimization passes). - - It is recommended to place this pass in the post-optimization stage of a passmanager. - A simple demo: - - .. plot:: - :include-source: - :nofigs: - - from qiskit.providers.fake_provider import GenericBackendV2 - from qiskit.transpiler import PassManager, PassManagerConfig - from qiskit.transpiler.preset_passmanagers import level_1_pass_manager - from qiskit.circuit import Parameter - from qiskit.circuit.library import QuantumVolume - from qiskit.circuit.library.standard_gates import RXGate - - from qiskit.transpiler.passes import RXCalibrationBuilder - - qv = QuantumVolume(4, 4, seed=1004) - - # Transpiling with single pulse RX gates enabled - backend_with_single_pulse_rx = GenericBackendV2(5) - rx_inst_props = {} - for i in range(backend_with_single_pulse_rx.num_qubits): - rx_inst_props[(i,)] = None - backend_with_single_pulse_rx.target.add_instruction(RXGate(Parameter("theta")), rx_inst_props) - config_with_rx = PassManagerConfig.from_backend(backend=backend_with_single_pulse_rx) - pm_with_rx = level_1_pass_manager(pass_manager_config=config_with_rx) - rx_builder = RXCalibrationBuilder(target=backend_with_single_pulse_rx.target) - pm_with_rx.post_optimization = PassManager([rx_builder]) - transpiled_circ_with_single_pulse_rx = pm_with_rx.run(qv) - transpiled_circ_with_single_pulse_rx.count_ops() - - # Conventional transpilation: each RX gate is decomposed into a sequence with two SX gates - original_backend = GenericBackendV2(5) - original_config = PassManagerConfig.from_backend(backend=original_backend) - original_pm = level_1_pass_manager(pass_manager_config=original_config) - original_transpiled_circ = original_pm.run(qv) - original_transpiled_circ.count_ops() - - References - * [1]: Gokhale et al. (2020), Optimized Quantum Compilation for - Near-Term Algorithms with OpenPulse. - `arXiv:2004.11205 ` - """ - - @deprecate_pulse_dependency - def __init__( - self, - target: Target = None, - ): - """Bootstrap single-pulse RX gate calibrations from the - (hardware-calibrated) SX gate calibration. - - Args: - target (Target): Should contain a SX calibration that will be - used for bootstrapping RX calibrations. - """ - from qiskit.transpiler.passes.optimization import NormalizeRXAngle - - super().__init__() - self.target = target - self.already_generated = {} - self.requires = [NormalizeRXAngle(self.target)] - - def supported(self, node_op: Instruction, qubits: list) -> bool: - """ - Check if the calibration for SX gate exists and it's a single DRAG pulse. - """ - return ( - isinstance(node_op, RXGate) - and self.target._has_calibration("sx", tuple(qubits)) - and (len(self.target._get_calibration("sx", tuple(qubits)).instructions) == 1) - and isinstance( - self.target._get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, - ScalableSymbolicPulse, - ) - and self.target._get_calibration("sx", tuple(qubits)) - .instructions[0][1] - .pulse.pulse_type - == "Drag" - ) - - def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, ScheduleBlock]: - """ - Generate RX calibration for the rotation angle specified in node_op. - """ - # already within [0, pi] by NormalizeRXAngles pass - angle = node_op.params[0] - - try: - angle = float(angle) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - params = ( - self.target._get_calibration("sx", tuple(qubits)) - .instructions[0][1] - .pulse.parameters.copy() - ) - new_rx_sched = _create_rx_sched( - rx_angle=angle, - channel=self.target._get_calibration("sx", tuple(qubits)).channels[0], - duration=params["duration"], - amp=params["amp"], - sigma=params["sigma"], - beta=params["beta"], - ) - - return new_rx_sched - - -@lru_cache -def _create_rx_sched( - rx_angle: float, - duration: int, - amp: float, - sigma: float, - beta: float, - channel: Channel, -): - """Generates (and caches) pulse calibrations for RX gates. - Assumes that the rotation angle is in [0, pi]. - """ - new_amp = rx_angle / (np.pi / 2) * amp - with builder.build() as new_rx_sched: - builder.play( - Drag(duration=duration, amp=new_amp, sigma=sigma, beta=beta, angle=0), - channel=channel, - ) - - return new_rx_sched diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py deleted file mode 100644 index 72cf347db9b..00000000000 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ /dev/null @@ -1,411 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -"""RZX calibration builders.""" -from __future__ import annotations - -import enum -import math -import warnings -from collections.abc import Sequence -from math import pi, erf - -import numpy as np -from qiskit.circuit import Instruction as CircuitInst -from qiskit.circuit.library.standard_gates import RZXGate -from qiskit.exceptions import QiskitError -from qiskit.pulse import ( - Play, - Schedule, - ScheduleBlock, - ControlChannel, - DriveChannel, - GaussianSquare, - Waveform, -) -from qiskit.pulse import builder -from qiskit.pulse.filters import filter_instructions -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.transpiler.target import Target -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -from .base_builder import CalibrationBuilder -from .exceptions import CalibrationNotAvailable - - -class CRCalType(enum.Enum): - """Estimated calibration type of backend cross resonance operations.""" - - ECR_FORWARD = "Echoed Cross Resonance corresponding to native operation" - ECR_REVERSE = "Echoed Cross Resonance reverse of native operation" - ECR_CX_FORWARD = "Echoed Cross Resonance CX corresponding to native operation" - ECR_CX_REVERSE = "Echoed Cross Resonance CX reverse of native operation" - DIRECT_CX_FORWARD = "Direct CX corresponding to native operation" - DIRECT_CX_REVERSE = "Direct CX reverse of native operation" - - -class RZXCalibrationBuilder(CalibrationBuilder): - """ - Creates calibrations for RZXGate(theta) by stretching and compressing - Gaussian square pulses in the CX gate. This is done by retrieving (for a given pair of - qubits) the CX schedule in the instruction schedule map of the backend defaults. - The CX schedule must be an echoed cross-resonance gate optionally with rotary tones. - The cross-resonance drive tones and rotary pulses must be Gaussian square pulses. - The width of the Gaussian square pulse is adjusted so as to match the desired rotation angle. - If the rotation angle is small such that the width disappears then the amplitude of the - zero width Gaussian square pulse (i.e. a Gaussian) is reduced to reach the target rotation - angle. Additional details can be found in https://arxiv.org/abs/2012.11660. - """ - - @deprecate_pulse_dependency - def __init__( - self, - instruction_schedule_map: InstructionScheduleMap = None, - verbose: bool = True, - target: Target = None, - ): - """ - Initializes a RZXGate calibration builder. - - Args: - instruction_schedule_map: The :obj:`InstructionScheduleMap` object representing the - default pulse calibrations for the target backend - verbose: Set True to raise a user warning when RZX schedule cannot be built. - target: The :class:`~.Target` representing the target backend, if both - ``instruction_schedule_map`` and this are specified then this argument will take - precedence and ``instruction_schedule_map`` will be ignored. - - Raises: - QiskitError: Instruction schedule map is not provided. - """ - super().__init__() - self._inst_map = instruction_schedule_map - self._verbose = verbose - if target: - self._inst_map = target._instruction_schedule_map() - if self._inst_map is None: - raise QiskitError("Calibrations can only be added to Pulse-enabled backends") - - def supported(self, node_op: CircuitInst, qubits: list) -> bool: - """Determine if a given node supports the calibration. - - Args: - node_op: Target instruction object. - qubits: Integer qubit indices to check. - - Returns: - Return ``True`` is calibration can be provided. - """ - return isinstance(node_op, RZXGate) and ( - "cx" in self._inst_map.instructions or "ecr" in self._inst_map.instructions - ) - - @staticmethod - @builder.macro - def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> int: - """A builder macro to play stretched pulse. - - Args: - instruction: The instruction from which to create a new shortened or lengthened pulse. - theta: desired angle, pi/2 is assumed to be the angle that the pulse in the given - play instruction implements. - sample_mult: All pulses must be a multiple of sample_mult. - - Returns: - Duration of stretched pulse. - - Raises: - QiskitError: if rotation angle is not assigned. - """ - try: - theta = float(theta) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - # This method is called for instructions which are guaranteed to play GaussianSquare pulse - params = instruction.pulse.parameters.copy() - risefall_sigma_ratio = (params["duration"] - params["width"]) / params["sigma"] - - # The error function is used because the Gaussian may have chopped tails. - # Area is normalized by amplitude. - # This makes widths robust to the rounding error. - risefall_area = params["sigma"] * math.sqrt(2 * pi) * erf(risefall_sigma_ratio) - full_area = params["width"] + risefall_area - - # Get estimate of target area. Assume this is pi/2 controlled rotation. - cal_angle = pi / 2 - target_area = abs(theta) / cal_angle * full_area - new_width = target_area - risefall_area - - if new_width >= 0: - width = new_width - params["amp"] *= np.sign(theta) - else: - width = 0 - params["amp"] *= np.sign(theta) * target_area / risefall_area - - round_duration = ( - round((width + risefall_sigma_ratio * params["sigma"]) / sample_mult) * sample_mult - ) - params["duration"] = round_duration - params["width"] = width - - stretched_pulse = GaussianSquare(**params) - builder.play(stretched_pulse, instruction.channel) - - return round_duration - - def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | ScheduleBlock: - """Builds the calibration schedule for the RZXGate(theta) with echos. - - Args: - node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. - qubits: List of qubits for which to get the schedules. The first qubit is - the control and the second is the target. - - Returns: - schedule: The calibration schedule for the RZXGate(theta). - - Raises: - QiskitError: if rotation angle is not assigned. - QiskitError: If the control and target qubits cannot be identified. - CalibrationNotAvailable: RZX schedule cannot be built for input node. - """ - theta = node_op.params[0] - - try: - theta = float(theta) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - if np.isclose(theta, 0.0): - return ScheduleBlock(name="rzx(0.000)") - - cal_type, cr_tones, comp_tones = _check_calibration_type(self._inst_map, qubits) - - if cal_type in [CRCalType.DIRECT_CX_FORWARD, CRCalType.DIRECT_CX_REVERSE]: - if self._verbose: - warnings.warn( - f"CR instruction for qubits {qubits} is likely {cal_type.value} sequence. " - "Pulse stretch for this calibration is not currently implemented. " - "RZX schedule is not generated for this qubit pair.", - UserWarning, - ) - raise CalibrationNotAvailable - - # The CR instruction is in the forward (native) direction - if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # `InstructionScheduleMap.get` and the pulse builder emit deprecation warnings - # as they use classes and methods which are deprecated in Qiskit 1.3 as part of the - # Qiskit Pulse deprecation - xgate = self._inst_map.get("x", qubits[0]) - with builder.build( - default_alignment="sequential", name=f"rzx({theta:.3f})" - ) as rzx_theta_native: - for cr_tone, comp_tone in zip(cr_tones, comp_tones): - with builder.align_left(): - self.rescale_cr_inst(cr_tone, theta) - self.rescale_cr_inst(comp_tone, theta) - builder.call(xgate) - return rzx_theta_native - - # The direction is not native. Add Hadamard gates to flip the direction. - xgate = self._inst_map.get("x", qubits[1]) - szc = self._inst_map.get("rz", qubits[1], pi / 2) - sxc = self._inst_map.get("sx", qubits[1]) - szt = self._inst_map.get("rz", qubits[0], pi / 2) - sxt = self._inst_map.get("sx", qubits[0]) - with builder.build(name="hadamard") as hadamard: - # Control qubit - builder.call(szc, name="szc") - builder.call(sxc, name="sxc") - builder.call(szc, name="szc") - # Target qubit - builder.call(szt, name="szt") - builder.call(sxt, name="sxt") - builder.call(szt, name="szt") - - with builder.build( - default_alignment="sequential", name=f"rzx({theta:.3f})" - ) as rzx_theta_flip: - builder.call(hadamard, name="hadamard") - for cr_tone, comp_tone in zip(cr_tones, comp_tones): - with builder.align_left(): - self.rescale_cr_inst(cr_tone, theta) - self.rescale_cr_inst(comp_tone, theta) - builder.call(xgate) - builder.call(hadamard, name="hadamard") - return rzx_theta_flip - - -class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): - """ - Creates calibrations for RZXGate(theta) by stretching and compressing - Gaussian square pulses in the CX gate. - - The ``RZXCalibrationBuilderNoEcho`` is a variation of the - :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` pass - that creates calibrations for the cross-resonance pulses without inserting - the echo pulses in the pulse schedule. This enables exposing the echo in - the cross-resonance sequence as gates so that the transpiler can simplify them. - The ``RZXCalibrationBuilderNoEcho`` only supports the hardware-native direction - of the CX gate. - """ - - def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | ScheduleBlock: - """Builds the calibration schedule for the RZXGate(theta) without echos. - - Args: - node_op: Instruction of the RZXGate(theta). I.e. params[0] is theta. - qubits: List of qubits for which to get the schedules. The first qubit is - the control and the second is the target. - - Returns: - schedule: The calibration schedule for the RZXGate(theta). - - Raises: - QiskitError: if rotation angle is not assigned. - QiskitError: If the control and target qubits cannot be identified, - or the backend does not natively support the specified direction of the cx. - CalibrationNotAvailable: RZX schedule cannot be built for input node. - """ - theta = node_op.params[0] - - try: - theta = float(theta) - except TypeError as ex: - raise QiskitError("Target rotation angle is not assigned.") from ex - - if np.isclose(theta, 0.0): - return ScheduleBlock(name="rzx(0.000)") - - cal_type, cr_tones, comp_tones = _check_calibration_type(self._inst_map, qubits) - - if cal_type in [CRCalType.DIRECT_CX_FORWARD, CRCalType.DIRECT_CX_REVERSE]: - if self._verbose: - warnings.warn( - f"CR instruction for qubits {qubits} is likely {cal_type.value} sequence. " - "Pulse stretch for this calibration is not currently implemented. " - "RZX schedule is not generated for this qubit pair.", - UserWarning, - ) - raise CalibrationNotAvailable - - # RZXCalibrationNoEcho only good for forward CR direction - if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # Pulse builder emits deprecation warnings as part of the - # Qiskit Pulse deprecation - with builder.build(default_alignment="left", name=f"rzx({theta:.3f})") as rzx_theta: - stretched_dur = self.rescale_cr_inst(cr_tones[0], 2 * theta) - self.rescale_cr_inst(comp_tones[0], 2 * theta) - # Placeholder to make pulse gate work - builder.delay(stretched_dur, DriveChannel(qubits[0])) - return rzx_theta - - raise QiskitError("RZXCalibrationBuilderNoEcho only supports hardware-native RZX gates.") - - -def _filter_cr_tone(time_inst_tup): - """A helper function to filter pulses on control channels.""" - valid_types = ["GaussianSquare"] - - _, inst = time_inst_tup - if isinstance(inst, Play) and isinstance(inst.channel, ControlChannel): - pulse = inst.pulse - if isinstance(pulse, Waveform) or pulse.pulse_type in valid_types: - return True - return False - - -def _filter_comp_tone(time_inst_tup): - """A helper function to filter pulses on drive channels.""" - valid_types = ["GaussianSquare"] - - _, inst = time_inst_tup - if isinstance(inst, Play) and isinstance(inst.channel, DriveChannel): - pulse = inst.pulse - if isinstance(pulse, Waveform) or pulse.pulse_type in valid_types: - return True - return False - - -def _check_calibration_type( - inst_sched_map: InstructionScheduleMap, qubits: Sequence[int] -) -> tuple[CRCalType, list[Play], list[Play]]: - """A helper function to check type of CR calibration. - - Args: - inst_sched_map: instruction schedule map of the backends - qubits: ordered tuple of qubits for cross resonance (q_control, q_target) - - Returns: - Filtered instructions and most-likely type of calibration. - - Raises: - QiskitError: Unknown calibration type is detected. - """ - cal_type = None - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # `InstructionScheduleMap.get` and `filter_instructions` emit deprecation warnings - # as they use classes and methods which are deprecated in Qiskit 1.3 as part of the - # Qiskit Pulse deprecation - if inst_sched_map.has("cx", qubits): - cr_sched = inst_sched_map.get("cx", qubits=qubits) - elif inst_sched_map.has("ecr", qubits): - cr_sched = inst_sched_map.get("ecr", qubits=qubits) - cal_type = CRCalType.ECR_FORWARD - elif inst_sched_map.has("ecr", tuple(reversed(qubits))): - cr_sched = inst_sched_map.get("ecr", tuple(reversed(qubits))) - cal_type = CRCalType.ECR_REVERSE - else: - raise QiskitError( - f"Native direction cannot be determined: operation on qubits {qubits} " - f"for the following instruction schedule map:\n{inst_sched_map}" - ) - - cr_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_cr_tone]).instructions] - comp_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_comp_tone]).instructions] - - if cal_type is None: - if len(comp_tones) == 0: - raise QiskitError( - f"{repr(cr_sched)} has no target compensation tones. " - "Native ECR direction cannot be determined." - ) - # Determine native direction, assuming only single drive channel per qubit. - # This guarantees channel and qubit index equality. - if comp_tones[0].channel.index == qubits[1]: - cal_type = CRCalType.ECR_CX_FORWARD - else: - cal_type = CRCalType.ECR_CX_REVERSE - - if len(cr_tones) == 2 and len(comp_tones) in (0, 2): - # ECR can be implemented without compensation tone at price of lower fidelity. - # Remarkable noisy terms are usually eliminated by echo. - return cal_type, cr_tones, comp_tones - - if len(cr_tones) == 1 and len(comp_tones) == 1: - # Direct CX must have compensation tone on target qubit. - # Otherwise, it cannot eliminate IX interaction. - if comp_tones[0].channel.index == qubits[1]: - return CRCalType.DIRECT_CX_FORWARD, cr_tones, comp_tones - else: - return CRCalType.DIRECT_CX_REVERSE, cr_tones, comp_tones - raise QiskitError( - f"{repr(cr_sched)} is undefined pulse sequence. " - "Check if this is a calibration for cross resonance operation." - ) diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 0e5108f44d2..683b5d8e9e1 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -29,7 +29,6 @@ from .template_optimization import TemplateOptimization from .inverse_cancellation import InverseCancellation from .collect_1q_runs import Collect1qRuns -from .echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition from .collect_linear_functions import CollectLinearFunctions from .reset_after_measure_simplification import ResetAfterMeasureSimplification from .optimize_cliffords import OptimizeCliffords diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py deleted file mode 100644 index c926e15800a..00000000000 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ /dev/null @@ -1,162 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2021. -# -# 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. - -"""Weyl decomposition of two-qubit gates in terms of echoed cross-resonance gates.""" - -from typing import Tuple - -from qiskit.circuit import QuantumRegister -from qiskit.circuit.library.standard_gates import RZXGate, HGate, XGate - -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.layout import Layout -from qiskit.transpiler.passes.calibration.rzx_builder import _check_calibration_type, CRCalType -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -from qiskit.dagcircuit import DAGCircuit -from qiskit.converters import circuit_to_dag - - -class EchoRZXWeylDecomposition(TransformationPass): - """Rewrite two-qubit gates using the Weyl decomposition. - - This transpiler pass rewrites two-qubit gates in terms of echoed cross-resonance gates according - to the Weyl decomposition. A two-qubit gate will be replaced with at most six non-echoed RZXGates. - Each pair of RZXGates forms an echoed RZXGate. - """ - - @deprecate_pulse_dependency - def __init__(self, instruction_schedule_map=None, target=None): - """EchoRZXWeylDecomposition pass. - - Args: - instruction_schedule_map (InstructionScheduleMap): the mapping from circuit - :class:`~.circuit.Instruction` names and arguments to :class:`.Schedule`\\ s. - target (Target): The :class:`~.Target` representing the target backend, if both - ``instruction_schedule_map`` and ``target`` are specified then this argument will take - precedence and ``instruction_schedule_map`` will be ignored. - """ - super().__init__() - self._inst_map = instruction_schedule_map - if target is not None: - self._inst_map = target.instruction_schedule_map() - - def _is_native(self, qubit_pair: Tuple) -> bool: - """Return the direction of the qubit pair that is native.""" - cal_type, _, _ = _check_calibration_type(self._inst_map, qubit_pair) - return cal_type in [ - CRCalType.ECR_CX_FORWARD, - CRCalType.ECR_FORWARD, - CRCalType.DIRECT_CX_FORWARD, - ] - - @staticmethod - def _echo_rzx_dag(theta): - """Return the following circuit - - .. code-block:: text - - ┌───────────────┐┌───┐┌────────────────┐┌───┐ - q_0: ┤0 ├┤ X ├┤0 ├┤ X ├ - │ Rzx(theta/2) │└───┘│ Rzx(-theta/2) │└───┘ - q_1: ┤1 ├─────┤1 ├───── - └───────────────┘ └────────────────┘ - """ - rzx_dag = DAGCircuit() - qr = QuantumRegister(2) - rzx_dag.add_qreg(qr) - rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[0], qr[1]], []) - rzx_dag.apply_operation_back(XGate(), [qr[0]], []) - rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[0], qr[1]], []) - rzx_dag.apply_operation_back(XGate(), [qr[0]], []) - return rzx_dag - - @staticmethod - def _reverse_echo_rzx_dag(theta): - """Return the following circuit - - .. code-block:: text - - ┌───┐┌───────────────┐ ┌────────────────┐┌───┐ - q_0: ┤ H ├┤1 ├─────┤1 ├┤ H ├───── - ├───┤│ Rzx(theta/2) │┌───┐│ Rzx(-theta/2) │├───┤┌───┐ - q_1: ┤ H ├┤0 ├┤ X ├┤0 ├┤ X ├┤ H ├ - └───┘└───────────────┘└───┘└────────────────┘└───┘└───┘ - """ - reverse_rzx_dag = DAGCircuit() - qr = QuantumRegister(2) - reverse_rzx_dag.add_qreg(qr) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) - reverse_rzx_dag.apply_operation_back(RZXGate(theta / 2), [qr[1], qr[0]], []) - reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) - reverse_rzx_dag.apply_operation_back(RZXGate(-theta / 2), [qr[1], qr[0]], []) - reverse_rzx_dag.apply_operation_back(XGate(), [qr[1]], []) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[0]], []) - reverse_rzx_dag.apply_operation_back(HGate(), [qr[1]], []) - return reverse_rzx_dag - - def run(self, dag: DAGCircuit): - """Run the EchoRZXWeylDecomposition pass on `dag`. - - Rewrites two-qubit gates in an arbitrary circuit in terms of echoed cross-resonance - gates by computing the Weyl decomposition of the corresponding unitary. Modifies the - input dag. - - Args: - dag (DAGCircuit): DAG to rewrite. - - Returns: - DAGCircuit: The modified dag. - - Raises: - TranspilerError: If the circuit cannot be rewritten. - """ - - # pylint: disable=cyclic-import - from qiskit.quantum_info import Operator - from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitControlledUDecomposer - - if len(dag.qregs) > 1: - raise TranspilerError( - "EchoRZXWeylDecomposition expects a single qreg input DAG," - f"but input DAG had qregs: {dag.qregs}." - ) - - trivial_layout = Layout.generate_trivial_layout(*dag.qregs.values()) - - decomposer = TwoQubitControlledUDecomposer(RZXGate) - - for node in dag.two_qubit_ops(): - - unitary = Operator(node.op).data - dag_weyl = circuit_to_dag(decomposer(unitary)) - dag.substitute_node_with_dag(node, dag_weyl) - - for node in dag.two_qubit_ops(): - if node.name == "rzx": - control = node.qargs[0] - target = node.qargs[1] - - physical_q0 = trivial_layout[control] - physical_q1 = trivial_layout[target] - - is_native = self._is_native((physical_q0, physical_q1)) - - theta = node.op.params[0] - if is_native: - dag.substitute_node_with_dag(node, self._echo_rzx_dag(theta)) - else: - dag.substitute_node_with_dag(node, self._reverse_echo_rzx_dag(theta)) - - return dag diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index 0d120911b06..2eeb29661d5 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -18,7 +18,7 @@ from .scheduling import ALAPScheduleAnalysis, ASAPScheduleAnalysis, SetIOLatency from .time_unit_conversion import TimeUnitConversion from .padding import PadDelay, PadDynamicalDecoupling -from .alignments import InstructionDurationCheck, ValidatePulseGates, ConstrainedReschedule +from .alignments import InstructionDurationCheck, ConstrainedReschedule # For backward compatibility from . import alignments as instruction_alignments diff --git a/qiskit/transpiler/passes/scheduling/alignments/__init__.py b/qiskit/transpiler/passes/scheduling/alignments/__init__.py index 8478f241c26..8ecd68eacdb 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/__init__.py +++ b/qiskit/transpiler/passes/scheduling/alignments/__init__.py @@ -76,6 +76,5 @@ """ from .check_durations import InstructionDurationCheck -from .pulse_gate_validation import ValidatePulseGates from .reschedule import ConstrainedReschedule from .align_measures import AlignMeasures diff --git a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py deleted file mode 100644 index 885730aac48..00000000000 --- a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py +++ /dev/null @@ -1,107 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Analysis passes for hardware alignment constraints.""" - -from qiskit.dagcircuit import DAGCircuit -from qiskit.pulse import Play -from qiskit.transpiler.basepasses import AnalysisPass -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.target import Target -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -class ValidatePulseGates(AnalysisPass): - """Check custom gate length. - - This is a control electronics aware analysis pass. - - Quantum gates (instructions) are often implemented with shaped analog stimulus signals. - These signals may be digitally stored in the waveform memory of the control electronics - and converted into analog voltage signals by electronic components known as - digital to analog converters (DAC). - - In Qiskit SDK, we can define the pulse-level implementation of custom quantum gate - instructions, as a `pulse gate - `__, - thus user gates should satisfy all waveform memory constraints imposed by the backend. - - This pass validates all attached calibration entries and raises ``TranspilerError`` to - kill the transpilation process if any invalid calibration entry is found. - This pass saves users from waiting until job execution time to get an invalid pulse error from - the backend control electronics. - """ - - @deprecate_pulse_dependency - def __init__( - self, - granularity: int = 1, - min_length: int = 1, - target: Target = None, - ): - """Create new pass. - - Args: - granularity: Integer number representing the minimum time resolution to - define the pulse gate length in units of ``dt``. This value depends on - the control electronics of your quantum processor. - min_length: Integer number representing the minimum data point length to - define the pulse gate in units of ``dt``. This value depends on - the control electronics of your quantum processor. - target: The :class:`~.Target` representing the target backend, if - ``target`` is specified then this argument will take - precedence and ``granularity`` and ``min_length`` will be ignored. - """ - super().__init__() - self.granularity = granularity - self.min_length = min_length - if target is not None: - self.granularity = target.granularity - self.min_length = target.min_length - - def run(self, dag: DAGCircuit): - """Run the pulse gate validation attached to ``dag``. - - Args: - dag: DAG to be validated. - - Returns: - DAGCircuit: DAG with consistent timing and op nodes annotated with duration. - - Raises: - TranspilerError: When pulse gate violate pulse controller constraints. - """ - if self.granularity == 1 and self.min_length == 1: - # we can define arbitrary length pulse with dt resolution - return - - for gate, insts in dag.calibrations.items(): - for qubit_param_pair, schedule in insts.items(): - for _, inst in schedule.instructions: - if isinstance(inst, Play): - pulse = inst.pulse - if pulse.duration % self.granularity != 0: - raise TranspilerError( - f"Pulse duration is not multiple of {self.granularity}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - f"qubit {qubit_param_pair[0]}." - ) - if pulse.duration < self.min_length: - raise TranspilerError( - f"Pulse gate duration is less than {self.min_length}. " - "This pulse cannot be played on the specified backend. " - f"Please modify the duration of the custom gate pulse {pulse.name} " - f"which is associated with the gate {gate} of " - "qubit {qubit_param_pair[0]}." - ) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 8422ab20945..0f0a6b7ea0a 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -41,11 +41,9 @@ from qiskit.transpiler.passes import ApplyLayout from qiskit.transpiler.passes import RemoveResetInZeroState from qiskit.transpiler.passes import FilterOpNodes -from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import PadDelay from qiskit.transpiler.passes import InstructionDurationCheck from qiskit.transpiler.passes import ConstrainedReschedule -from qiskit.transpiler.passes import PulseGates from qiskit.transpiler.passes import ContainsInstruction from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason @@ -53,7 +51,6 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.utils import deprecate_func -from qiskit.utils.deprecate_pulse import deprecate_pulse_arg _ControlFlowState = collections.namedtuple("_ControlFlowState", ("working", "not_working")) @@ -571,9 +568,8 @@ def _direction_condition(property_set): return PassManager(unroll) -@deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map, target=None + instruction_durations, scheduling_method, timing_constraints, _, target=None ): """Generate a post optimization scheduling :class:`~qiskit.transpiler.PassManager` @@ -583,7 +579,6 @@ def generate_scheduling( ``'asap'``/``'as_soon_as_possible'`` or ``'alap'``/``'as_late_as_possible'`` timing_constraints (TimingConstraints): Hardware time alignment restrictions. - inst_map (InstructionScheduleMap): DEPRECATED. Mapping object that maps gate to schedule. target (Target): The :class:`~.Target` object representing the backend Returns: @@ -593,8 +588,6 @@ def generate_scheduling( TranspilerError: If the ``scheduling_method`` kwarg is not a valid value """ scheduling = PassManager() - if inst_map and inst_map.has_custom_gate(): - scheduling.append(PulseGates(inst_map=inst_map, target=target)) if scheduling_method: # Do scheduling after unit conversion. scheduler = { @@ -647,13 +640,6 @@ def _require_alignment(property_set): condition=_require_alignment, ) ) - scheduling.append( - ValidatePulseGates( - granularity=timing_constraints.granularity, - min_length=timing_constraints.min_length, - target=target, - ) - ) if scheduling_method: # Call padding pass if circuit is scheduled scheduling.append(PadDelay(target=target)) diff --git a/releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml b/releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml new file mode 100644 index 00000000000..49e176ce93b --- /dev/null +++ b/releasenotes/notes/remove-pulse-passes-3128f27ed7e42bf6.yaml @@ -0,0 +1,7 @@ +--- +upgrade_transpiler: + - | + The ``PulseGates``, ``ValidatePulseGates``, ``RXCalibrationBuilder``, ``RZXCalibrationBuilder``, + ``RZXCalibrationBuilderNoEcho`` and ``EchoRZXWeylDecomposition`` passes have been removed, + following their deprecation in Qiskit 1.3. These passes depend on and relate to the Pulse + package which is also being removed in Qiskit 2.0. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 851c6817d82..4bf2b437d05 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -79,7 +79,7 @@ from qiskit.providers.fake_provider import Fake20QV1, Fake27QPulseV1, GenericBackendV2 from qiskit.providers.basic_provider import BasicSimulator from qiskit.providers.options import Options -from qiskit.pulse import InstructionScheduleMap, Schedule, Play, Gaussian, DriveChannel +from qiskit.pulse import InstructionScheduleMap from qiskit.quantum_info import Operator, random_unitary from qiskit.utils import parallel from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass @@ -91,14 +91,13 @@ from qiskit.transpiler.target import ( InstructionProperties, Target, - TimingConstraints, InstructionDurations, target_to_backend_properties, ) from test import QiskitTestCase, combine, slow_test # pylint: disable=wrong-import-order -from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP, MUMBAI_CMAP, TOKYO_CMAP +from ..legacy_cmaps import MELBOURNE_CMAP, RUESCHLIKON_CMAP, TOKYO_CMAP class CustomCX(Gate): @@ -1567,53 +1566,6 @@ def test_scheduling_backend_v2(self): self.assertIn("delay", out[0].count_ops()) self.assertIn("delay", out[1].count_ops()) - def test_scheduling_timing_constraints(self): - """Test that scheduling-related loose transpile constraints - work with both BackendV1 and BackendV2.""" - - with self.assertWarns(DeprecationWarning): - backend_v1 = Fake27QPulseV1() - backend_v2 = GenericBackendV2( - num_qubits=27, - calibrate_instructions=True, - control_flow=True, - coupling_map=MUMBAI_CMAP, - seed=42, - ) - # the original timing constraints are granularity = min_length = 16 - timing_constraints = TimingConstraints(granularity=32, min_length=64) - error_msgs = { - 65: "Pulse duration is not multiple of 32", - 32: "Pulse gate duration is less than 64", - } - - for backend, duration in zip([backend_v1, backend_v2], [65, 32]): - with self.subTest(backend=backend, duration=duration): - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - with self.assertWarns(DeprecationWarning): - qc.add_calibration( - "h", - [0], - Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(0))), - [0, 0], - ) - qc.add_calibration( - "cx", - [0, 1], - Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(1))), - [0, 0], - ) - with self.assertRaisesRegex(TranspilerError, error_msgs[duration]): - with self.assertWarns(DeprecationWarning): - _ = transpile( - qc, - backend=backend, - timing_constraints=timing_constraints, - ) - def test_scheduling_instruction_constraints_backend(self): """Test that scheduling-related loose transpile constraints work with both BackendV1 and BackendV2.""" diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index 8890a45ffe9..c95e5485775 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -18,12 +18,11 @@ from ddt import ddt, data from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit, Parameter, Gate -from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.exceptions import QiskitError from qiskit.qpy import dump, load, formats, QPY_COMPATIBILITY_VERSION from qiskit.qpy.common import QPY_VERSION -from qiskit.transpiler import PassManager, TranspileLayout -from qiskit.transpiler import passes +from qiskit.transpiler import TranspileLayout from qiskit.compiler import transpile from qiskit.utils import optionals from qiskit.qpy.formats import FILE_HEADER_V10_PACK, FILE_HEADER_V10, FILE_HEADER_V10_SIZE @@ -55,42 +54,6 @@ def assert_roundtrip_equal(self, circuit, version=None, use_symengine=None): ) -@ddt -class TestCalibrationPasses(QpyCircuitTestCase): - """QPY round-trip test case of transpiled circuits with pulse level optimization.""" - - def setUp(self): - super().setUp() - # TODO remove context once https://github.com/Qiskit/qiskit/issues/12759 is fixed - with self.assertWarns(DeprecationWarning): - # This backend provides CX(0,1) with native ECR direction. - self.inst_map = Fake27QPulseV1().defaults().instruction_schedule_map - - @data(0.1, 0.7, 1.5) - def test_rzx_calibration(self, angle): - """RZX builder calibration pass with echo.""" - with self.assertWarns(DeprecationWarning): - pass_ = passes.RZXCalibrationBuilder(self.inst_map) - pass_manager = PassManager(pass_) - test_qc = QuantumCircuit(2) - test_qc.rzx(angle, 0, 1) - rzx_qc = pass_manager.run(test_qc) - with self.assertWarns(DeprecationWarning): - self.assert_roundtrip_equal(rzx_qc) - - @data(0.1, 0.7, 1.5) - def test_rzx_calibration_echo(self, angle): - """RZX builder calibration pass without echo.""" - with self.assertWarns(DeprecationWarning): - pass_ = passes.RZXCalibrationBuilderNoEcho(self.inst_map) - pass_manager = PassManager(pass_) - test_qc = QuantumCircuit(2) - test_qc.rzx(angle, 0, 1) - rzx_qc = pass_manager.run(test_qc) - with self.assertWarns(DeprecationWarning): - self.assert_roundtrip_equal(rzx_qc) - - class TestVersions(QpyCircuitTestCase): """Test version handling in qpy.""" diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py index 38f84492ee8..aee567444cf 100644 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py @@ -12,12 +12,10 @@ """Testing legacy instruction alignment pass.""" -from qiskit import QuantumCircuit, pulse +from qiskit import QuantumCircuit from qiskit.transpiler import InstructionDurations -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import ( AlignMeasures, - ValidatePulseGates, ALAPSchedule, TimeUnitConversion, ) @@ -327,101 +325,3 @@ def test_circuit_using_clbit(self): ref_circuit.measure(2, 0) self.assertEqual(aligned_circuit, ref_circuit) - - -class TestPulseGateValidation(QiskitTestCase): - """A test for pulse gate validation pass.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.pulse_gate_validation_pass = ValidatePulseGates(granularity=16, min_length=64) - - def test_invalid_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) - - def test_short_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) - - def test_short_pulse_duration_multiple_pulse(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # however total gate schedule length is 64, which accidentally satisfies the constraints - # this should fail in the validation - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - custom_gate.insert( - 32, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertRaises(TranspilerError): - self.pulse_gate_validation_pass(circuit) - - def test_valid_pulse_duration(self): - """No error raises if valid calibration is provided.""" - - # this is valid duration pulse - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(160, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - # just not raise an error - self.pulse_gate_validation_pass(circuit) - - def test_no_calibration(self): - """No error raises if no calibration is added.""" - - circuit = QuantumCircuit(1) - circuit.x(0) - - # just not raise an error - self.pulse_gate_validation_pass(circuit) diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py deleted file mode 100644 index 04bc8aef00d..00000000000 --- a/test/python/transpiler/test_calibrationbuilder.py +++ /dev/null @@ -1,619 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2024. -# -# 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 the CalibrationBuilder subclasses.""" - -from math import pi, erf - -import numpy as np -from ddt import data, ddt - -from qiskit.converters import circuit_to_dag -from qiskit import circuit, schedule, QiskitError, QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.circuit.library.standard_gates import SXGate, RXGate -from qiskit.providers.fake_provider import Fake7QPulseV1, Fake27QPulseV1, GenericBackendV2 -from qiskit.pulse import ( - ScheduleBlock, - ControlChannel, - DriveChannel, - GaussianSquare, - Waveform, - Play, - InstructionScheduleMap, - Schedule, - Drag, - Square, -) - -from qiskit.pulse import builder -from qiskit.pulse.transforms import target_qobj_transform -from qiskit.dagcircuit import DAGOpNode -from qiskit.transpiler import PassManager, Target, InstructionProperties -from qiskit.transpiler.passes.calibration.builders import ( - RZXCalibrationBuilder, - RZXCalibrationBuilderNoEcho, - RXCalibrationBuilder, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestCalibrationBuilder(QiskitTestCase): - """Test the Calibration Builder.""" - - # CR parameters - __risefall = 4 - __angle = np.pi / 2 - __granularity = 16 - - @staticmethod - def get_cr_play(cr_schedule, name): - """A helper function to filter CR pulses.""" - - def _filter_func(time_inst): - return isinstance(time_inst[1], Play) and time_inst[1].pulse.name.startswith(name) - - return cr_schedule.filter(_filter_func).instructions[0][1] - - def compute_stretch_duration(self, play_gaussian_square_pulse, theta): - """Compute duration of stretched Gaussian Square pulse.""" - pulse = play_gaussian_square_pulse.pulse - sigma = pulse.sigma - width = self.compute_stretch_width(play_gaussian_square_pulse, theta) - - duration = width + sigma * self.__risefall - return round(duration / self.__granularity) * self.__granularity - - def compute_stretch_width(self, play_gaussian_square_pulse, theta): - """Compute width of stretched Gaussian Square pulse.""" - pulse = play_gaussian_square_pulse.pulse - sigma = pulse.sigma - width = pulse.width - - risefall_area = sigma * np.sqrt(2 * np.pi) * erf(self.__risefall) - full_area = risefall_area + width - - target_area = abs(theta) / self.__angle * full_area - return max(0, target_area - risefall_area) - - def u0p_play(self, cr_schedule): - """Returns the positive CR pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90p_u") - - def u0m_play(self, cr_schedule): - """Returns the negative CR pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90m_u") - - def d1p_play(self, cr_schedule): - """Returns the positive rotary echo pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90p_d") - - def d1m_play(self, cr_schedule): - """Returns the negative rotary echo pulse from cr_schedule.""" - return self.get_cr_play(cr_schedule, "CR90m_d") - - -@ddt -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRZXCalibrationBuilder(TestCalibrationBuilder): - """Test RZXCalibrationBuilder.""" - - def build_forward( - self, - backend, - theta, - u0p_play, - d1p_play, - u0m_play, - d1m_play, - ): - """A helper function to generate reference pulse schedule for forward direction.""" - duration = self.compute_stretch_duration(u0p_play, theta) - width = self.compute_stretch_width(u0p_play, theta) - inst_sched_map = backend.defaults().instruction_schedule_map - - with builder.build( - backend, - default_alignment="sequential", - ) as ref_sched: - - with builder.align_left(): - # Positive CRs - u0p_params = u0p_play.pulse.parameters - u0p_params["duration"] = duration - u0p_params["width"] = width - builder.play( - GaussianSquare(**u0p_params), - ControlChannel(0), - ) - d1p_params = d1p_play.pulse.parameters - d1p_params["duration"] = duration - d1p_params["width"] = width - builder.play( - GaussianSquare(**d1p_params), - DriveChannel(1), - ) - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - with builder.align_left(): - # Negative CRs - u0m_params = u0m_play.pulse.parameters - u0m_params["duration"] = duration - u0m_params["width"] = width - builder.play( - GaussianSquare(**u0m_params), - ControlChannel(0), - ) - d1m_params = d1m_play.pulse.parameters - d1m_params["duration"] = duration - d1m_params["width"] = width - builder.play( - GaussianSquare(**d1m_params), - DriveChannel(1), - ) - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - return ref_sched - - def build_reverse( - self, - backend, - theta, - u0p_play, - d1p_play, - u0m_play, - d1m_play, - ): - """A helper function to generate reference pulse schedule for backward direction.""" - duration = self.compute_stretch_duration(u0p_play, theta) - width = self.compute_stretch_width(u0p_play, theta) - inst_sched_map = backend.defaults().instruction_schedule_map - - rz_qc_q0 = QuantumCircuit(1) - rz_qc_q0.rz(pi / 2, 0) - - rz_qc_q1 = QuantumCircuit(2) - rz_qc_q1.rz(pi / 2, 1) - - with self.assertWarns(DeprecationWarning): - rz_sched_q0 = schedule(rz_qc_q0, backend) - rz_sched_q1 = schedule(rz_qc_q1, backend) - - with builder.build( - backend, - default_alignment="sequential", - ) as ref_sched: - - # Get Schedule from the backend for Gates equivalent to Hadamard gates. - with builder.align_left(): - builder.call(rz_sched_q0) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(0,)).get_schedule() - ) - builder.call(rz_sched_q0) - - builder.call(rz_sched_q1) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(1,)).get_schedule() - ) - builder.call(rz_sched_q1) - - with builder.align_left(): - # Positive CRs - u0p_params = u0p_play.pulse.parameters - u0p_params["duration"] = duration - u0p_params["width"] = width - builder.play( - GaussianSquare(**u0p_params), - ControlChannel(0), - ) - d1p_params = d1p_play.pulse.parameters - d1p_params["duration"] = duration - d1p_params["width"] = width - builder.play( - GaussianSquare(**d1p_params), - DriveChannel(1), - ) - - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - with builder.align_left(): - # Negative CRs - u0m_params = u0m_play.pulse.parameters - u0m_params["duration"] = duration - u0m_params["width"] = width - builder.play( - GaussianSquare(**u0m_params), - ControlChannel(0), - ) - d1m_params = d1m_play.pulse.parameters - d1m_params["duration"] = duration - d1m_params["width"] = width - builder.play( - GaussianSquare(**d1m_params), - DriveChannel(1), - ) - - # Get Schedule for 'x' gate from the backend. - builder.call(inst_sched_map._get_calibration_entry("x", (0,)).get_schedule()) - - # Get Schedule from the backend for Gates equivalent to Hadamard gates. - with builder.align_left(): - builder.call(rz_sched_q0) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(0,)).get_schedule() - ) - builder.call(rz_sched_q0) - - builder.call(rz_sched_q1) - builder.call( - inst_sched_map._get_calibration_entry(SXGate(), qubits=(1,)).get_schedule() - ) - builder.call(rz_sched_q1) - - return ref_sched - - @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) - def test_rzx_calibration_cr_pulse_stretch(self, theta: float): - """Test that cross resonance pulse durations are computed correctly.""" - with self.assertWarns(DeprecationWarning): - # TODO this tests does not work with BackendV2/GenericBackendV2 - # https://github.com/Qiskit/qiskit/issues/12834 - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - cr_schedule = inst_map.get("cx", (0, 1)) - with builder.build() as test_sched: - RZXCalibrationBuilder.rescale_cr_inst(self.u0p_play(cr_schedule), theta) - - self.assertEqual( - test_sched.duration, self.compute_stretch_duration(self.u0p_play(cr_schedule), theta) - ) - - @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) - def test_rzx_calibration_rotary_pulse_stretch(self, theta: float): - """Test that rotary pulse durations are computed correctly.""" - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - cr_schedule = inst_map.get("cx", (0, 1)) - with builder.build() as test_sched: - RZXCalibrationBuilder.rescale_cr_inst(self.d1p_play(cr_schedule), theta) - - self.assertEqual( - test_sched.duration, self.compute_stretch_duration(self.d1p_play(cr_schedule), theta) - ) - - def test_raise(self): - """Test that the correct error is raised.""" - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 0, 1) - dag = circuit_to_dag(qc) - with self.assertWarns(DeprecationWarning): - backend = Fake7QPulseV1() - # The error is raised when calibrations in multi-qubit - # gates are not detected. - # We force this by removing the 'cx' entries from the - # instruction schedule map. - inst_map = backend.defaults().instruction_schedule_map - for qubits in inst_map.qubits_with_instruction("cx"): - inst_map.remove("cx", qubits) - inst_map = backend.defaults().instruction_schedule_map - with self.assertWarns(DeprecationWarning): - _pass = RZXCalibrationBuilder(inst_map) - - qubit_map = {qubit: i for i, qubit in enumerate(dag.qubits)} - with self.assertRaises(QiskitError): - for node in dag.gate_nodes(): - qubits = [qubit_map[q] for q in node.qargs] - _pass.get_calibration(node.op, qubits) - - def test_ecr_cx_forward(self): - """Test that correct pulse sequence is generated for native CR pair.""" - # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 0, 1) - - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilder(inst_map) - test_qc = PassManager(_pass).run(qc) - - cr_schedule = inst_map.get("cx", (0, 1)) - ref_sched = self.build_forward( - backend, - theta, - self.u0p_play(cr_schedule), - self.d1p_play(cr_schedule), - self.u0m_play(cr_schedule), - self.d1m_play(cr_schedule), - ) - - with self.assertWarns(DeprecationWarning): - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) - - def test_ecr_cx_reverse(self): - """Test that correct pulse sequence is generated for non-native CR pair.""" - # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 1, 0) - - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilder(inst_map) - test_qc = PassManager(_pass).run(qc) - - cr_schedule = inst_map.get("cx", (0, 1)) - ref_sched = self.build_reverse( - backend, - theta, - self.u0p_play(cr_schedule), - self.d1p_play(cr_schedule), - self.u0m_play(cr_schedule), - self.d1m_play(cr_schedule), - ) - - with self.assertWarns(DeprecationWarning): - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) - - def test_pass_alive_with_dcx_ish(self): - """Test if the pass is not terminated by error with direct CX input.""" - cx_sched = Schedule() - # Fake direct cr - cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) - # Fake direct compensation tone - # Compensation tone doesn't have dedicated pulse class. - # So it's reported as a waveform now. - compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) - cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - inst_map.add("cx", (1, 0), schedule=cx_sched) - - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta, 1, 0) - - with self.assertWarns(DeprecationWarning): - pass_ = RZXCalibrationBuilder(instruction_schedule_map=inst_map) - with self.assertWarns(UserWarning): - # User warning that says q0 q1 is invalid - cal_qc = PassManager(pass_).run(rzx_qc) - self.assertEqual(cal_qc, rzx_qc) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder): - """Test RZXCalibrationBuilderNoEcho.""" - - def build_forward( - self, - theta, - u0p_play, - d1p_play, - ): - """A helper function to generate reference pulse schedule for forward direction.""" - duration = self.compute_stretch_duration(u0p_play, 2.0 * theta) - width = self.compute_stretch_width(u0p_play, 2.0 * theta) - - with builder.build() as ref_sched: - # Positive CRs - u0p_params = u0p_play.pulse.parameters - u0p_params["duration"] = duration - u0p_params["width"] = width - builder.play( - GaussianSquare(**u0p_params), - ControlChannel(0), - ) - d1p_params = d1p_play.pulse.parameters - d1p_params["duration"] = duration - d1p_params["width"] = width - builder.play( - GaussianSquare(**d1p_params), - DriveChannel(1), - ) - builder.delay(duration, DriveChannel(0)) - - return ref_sched - - def test_ecr_cx_forward(self): - """Test that correct pulse sequence is generated for native CR pair. - - .. notes:: - No echo builder only supports native direction. - """ - # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling - theta = np.pi / 4 - - qc = circuit.QuantumCircuit(2) - qc.rzx(theta, 0, 1) - - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - - _pass = RZXCalibrationBuilderNoEcho(inst_map) - test_qc = PassManager(_pass).run(qc) - - cr_schedule = inst_map.get("cx", (0, 1)) - ref_sched = self.build_forward( - theta, - self.u0p_play(cr_schedule), - self.d1p_play(cr_schedule), - ) - - with self.assertWarns(DeprecationWarning): - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) - - # # TODO - write test for forward ECR native pulse - # def test_ecr_forward(self): - - def test_pass_alive_with_dcx_ish(self): - """Test if the pass is not terminated by error with direct CX input.""" - cx_sched = Schedule() - # Fake direct cr - cx_sched.insert(0, Play(GaussianSquare(800, 0.2, 64, 544), ControlChannel(1)), inplace=True) - # Fake direct compensation tone - # Compensation tone doesn't have dedicated pulse class. - # So it's reported as a waveform now. - compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) - cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - - with self.assertWarns(DeprecationWarning): - inst_map = InstructionScheduleMap() - inst_map.add("cx", (1, 0), schedule=cx_sched) - - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta, 1, 0) - - with self.assertWarns(DeprecationWarning): - pass_ = RZXCalibrationBuilderNoEcho(instruction_schedule_map=inst_map) - with self.assertWarns(UserWarning): - # User warning that says q0 q1 is invalid - cal_qc = PassManager(pass_).run(rzx_qc) - self.assertEqual(cal_qc, rzx_qc) - - -@ddt -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRXCalibrationBuilder(QiskitTestCase): - """Test RXCalibrationBuilder.""" - - def compute_correct_rx_amplitude(self, rx_theta: float, sx_amp: float): - """A helper function to compute the amplitude of the bootstrapped RX pulse.""" - return sx_amp * (np.abs(rx_theta) / (0.5 * np.pi)) - - def test_not_supported_if_no_sx_schedule(self): - """Test that supported() returns False when the target does not have SX calibration.""" - empty_target = Target() - with self.assertWarns(DeprecationWarning): - tp = RXCalibrationBuilder(empty_target) - qubits = (0,) - node_op = DAGOpNode(RXGate(0.5), qubits, []) - self.assertFalse(tp.supported(node_op, qubits)) - - def test_not_supported_if_sx_not_drag(self): - """Test that supported() returns False when the default SX calibration is not a DRAG.""" - target = Target() - with builder.build() as square_sx_cal: - builder.play(Square(amp=0.1, duration=160, phase=0), DriveChannel(0)) - with self.assertWarns(DeprecationWarning): - target.add_instruction( - SXGate(), {(0,): InstructionProperties(calibration=square_sx_cal)} - ) - tp = RXCalibrationBuilder(target) - qubits = (0,) - node_op = DAGOpNode(RXGate(0.5), qubits, []) - self.assertFalse(tp.supported(node_op, qubits)) - - def test_raises_error_when_rotation_angle_not_assigned(self): - """Test that get_calibration() fails when the RX gate's rotation angle is - an unassigned Parameter, not a number. - The QiskitError occurs while trying to typecast the Parameter into a float. - """ - backend = GenericBackendV2(num_qubits=5, seed=42) - with self.assertWarns(DeprecationWarning): - tp = RXCalibrationBuilder(backend.target) - qubits = (0,) - rx = RXGate(Parameter("theta")) - with self.assertRaises(QiskitError): - tp.get_calibration(rx, qubits) - - # Note: These input data values should be within [0, pi] because - # the required NormalizeRXAngles pass ensures that. - @data(0, np.pi / 3, (2 / 3) * np.pi) - def test_pulse_schedule(self, theta: float): - """Test that get_calibration() returns a schedule with correct amplitude.""" - backend = GenericBackendV2(num_qubits=5, seed=42) - dummy_target = Target() - sx_amp, sx_beta, sx_sigma, sx_duration, sx_angle = 0.6, 2, 40, 160, 0.5 - with builder.build(backend=backend) as dummy_sx_cal: - builder.play( - Drag( - amp=sx_amp, beta=sx_beta, sigma=sx_sigma, duration=sx_duration, angle=sx_angle - ), - DriveChannel(0), - ) - - with self.assertWarns(DeprecationWarning): - dummy_target.add_instruction( - SXGate(), {(0,): InstructionProperties(calibration=dummy_sx_cal)} - ) - tp = RXCalibrationBuilder(dummy_target) - test = tp.get_calibration(RXGate(theta), qubits=(0,)) - - with builder.build(backend=backend) as correct_rx_schedule: - builder.play( - Drag( - amp=self.compute_correct_rx_amplitude(rx_theta=theta, sx_amp=sx_amp), - beta=sx_beta, - sigma=sx_sigma, - duration=sx_duration, - angle=0, - ), - channel=DriveChannel(0), - ) - - self.assertEqual(test, correct_rx_schedule) - - def test_with_normalizerxangles(self): - """Checks that this pass works well with the NormalizeRXAngles pass.""" - # add Drag pulse to 'sx' calibrations - sched = ScheduleBlock() - sched.append( - Play( - Drag( - duration=160, - sigma=40, - beta=-2.4030014266125312, - amp=0.11622814090041741, - angle=0.04477749999041481, - name="drag_2276", - ), - DriveChannel(0), - ), - inplace=True, - ) - with self.assertWarns(DeprecationWarning): - ism = InstructionScheduleMap() - ism.add("sx", (0,), sched) - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=ism, seed=42) - - # NormalizeRXAngle pass should also be included because it's a required pass. - with self.assertWarns(DeprecationWarning): - pm = PassManager(RXCalibrationBuilder(backend.target)) - - qc = QuantumCircuit(1) - qc.rx(np.pi / 3, 0) - qc.rx(np.pi / 2, 0) - qc.rx(np.pi, 0) - - # Only RX(pi/3) should get a rx calibration. - # The others should be converted to SX and X - tc = pm.run(qc) - with self.assertWarns(DeprecationWarning): - self.assertEqual(len(tc.calibrations["rx"]), 1) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py deleted file mode 100644 index 0f279e4bb8c..00000000000 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ /dev/null @@ -1,260 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2024. -# -# 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 the EchoRZXWeylDecomposition pass""" - -import unittest -from math import pi -import numpy as np - -from qiskit import QuantumRegister, QuantumCircuit -from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import ( - EchoRZXWeylDecomposition, -) -from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.providers.fake_provider import Fake27QPulseV1 -import qiskit.quantum_info as qi -from qiskit.synthesis.two_qubit.two_qubit_decompose import TwoQubitWeylDecomposition -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestEchoRZXWeylDecomposition(QiskitTestCase): - """Tests the EchoRZXWeylDecomposition pass.""" - - def setUp(self): - super().setUp() - # TODO once https://github.com/Qiskit/qiskit/issues/12759 is fixed, replace with - # backend = GenericBackendV2(num_qubits=27, calibrate_instructions=True, - # control_flow=True, seed=42) - # self.inst_map = backend.instruction_schedule_map - with self.assertWarns(DeprecationWarning): - self.backend = Fake27QPulseV1() - self.inst_map = self.backend.defaults().instruction_schedule_map - - def assertRZXgates(self, unitary_circuit, after): - """Check the number of rzx gates""" - alpha = TwoQubitWeylDecomposition(unitary_circuit).a - beta = TwoQubitWeylDecomposition(unitary_circuit).b - gamma = TwoQubitWeylDecomposition(unitary_circuit).c - - expected_rzx_number = 0 - if not alpha == 0: - expected_rzx_number += 2 - if not beta == 0: - expected_rzx_number += 2 - if not gamma == 0: - expected_rzx_number += 2 - - circuit_rzx_number = QuantumCircuit.count_ops(after)["rzx"] - - self.assertEqual(expected_rzx_number, circuit_rzx_number) - - @staticmethod - def count_gate_number(gate, circuit): - """Count the number of a specific gate type in a circuit""" - if gate not in QuantumCircuit.count_ops(circuit): - gate_number = 0 - else: - gate_number = QuantumCircuit.count_ops(circuit)[gate] - return gate_number - - def test_rzx_number_native_weyl_decomposition(self): - """Check the number of RZX gates for a hardware-native cx""" - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - circuit.cx(qr[0], qr[1]) - - unitary_circuit = qi.Operator(circuit).data - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - after = EchoRZXWeylDecomposition(self.inst_map)(circuit) - - unitary_after = qi.Operator(after).data - - self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - - # check whether the after circuit has the correct number of rzx gates. - self.assertRZXgates(unitary_circuit, after) - - def test_h_number_non_native_weyl_decomposition_1(self): - """Check the number of added Hadamard gates for a native and non-native rzz gate""" - theta = pi / 11 - qr = QuantumRegister(2, "qr") - # rzz gate in native direction - circuit = QuantumCircuit(qr) - circuit.rzz(theta, qr[0], qr[1]) - - # rzz gate in non-native direction - circuit_non_native = QuantumCircuit(qr) - circuit_non_native.rzz(theta, qr[1], qr[0]) - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - - dag_non_native = circuit_to_dag(circuit_non_native) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after_non_native = dag_to_circuit(pass_.run(dag_non_native)) - - circuit_rzx_number = self.count_gate_number("rzx", after) - - circuit_h_number = self.count_gate_number("h", after) - circuit_non_native_h_number = self.count_gate_number("h", after_non_native) - - # for each pair of rzx gates four hadamard gates have to be added in - # the case of a non-hardware-native directed gate. - self.assertEqual( - (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number - ) - - def test_h_number_non_native_weyl_decomposition_2(self): - """Check the number of added Hadamard gates for a swap gate""" - qr = QuantumRegister(2, "qr") - # swap gate in native direction. - circuit = QuantumCircuit(qr) - circuit.swap(qr[0], qr[1]) - - # swap gate in non-native direction. - circuit_non_native = QuantumCircuit(qr) - circuit_non_native.swap(qr[1], qr[0]) - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - - dag_non_native = circuit_to_dag(circuit_non_native) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after_non_native = dag_to_circuit(pass_.run(dag_non_native)) - - circuit_rzx_number = self.count_gate_number("rzx", after) - - circuit_h_number = self.count_gate_number("h", after) - circuit_non_native_h_number = self.count_gate_number("h", after_non_native) - - # for each pair of rzx gates four hadamard gates have to be added in - # the case of a non-hardware-native directed gate. - self.assertEqual( - (circuit_rzx_number / 2) * 4, circuit_non_native_h_number - circuit_h_number - ) - - def test_weyl_decomposition_gate_angles(self): - """Check the number and angles of the RZX gates for different gates""" - thetas = [pi / 9, 2.1, -0.2] - - qr = QuantumRegister(2, "qr") - circuit_rxx = QuantumCircuit(qr) - circuit_rxx.rxx(thetas[0], qr[1], qr[0]) - - circuit_ryy = QuantumCircuit(qr) - circuit_ryy.ryy(thetas[1], qr[0], qr[1]) - - circuit_rzz = QuantumCircuit(qr) - circuit_rzz.rzz(thetas[2], qr[1], qr[0]) - - circuits = [circuit_rxx, circuit_ryy, circuit_rzz] - - for circuit in circuits: - - unitary_circuit = qi.Operator(circuit).data - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - dag_after = circuit_to_dag(after) - - unitary_after = qi.Operator(after).data - - # check whether the unitaries are equivalent. - self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - - # check whether the after circuit has the correct number of rzx gates. - self.assertRZXgates(unitary_circuit, after) - - alpha = TwoQubitWeylDecomposition(unitary_circuit).a - - rzx_angles = [] - for node in dag_after.two_qubit_ops(): - if node.name == "rzx": - rzx_angle = node.op.params[0] - # check whether the absolute values of the RZX gate angles - # are equivalent to the corresponding Weyl parameter. - self.assertAlmostEqual(np.abs(rzx_angle), alpha) - rzx_angles.append(rzx_angle) - - # check whether the angles of every RZX gate pair of an echoed RZX gate - # have opposite signs. - for idx in range(1, len(rzx_angles), 2): - self.assertAlmostEqual(rzx_angles[idx - 1], -rzx_angles[idx]) - - def test_weyl_unitaries_random_circuit(self): - """Weyl decomposition for a random two-qubit circuit.""" - theta = pi / 9 - epsilon = 5 - delta = -1 - eta = 0.2 - qr = QuantumRegister(2, "qr") - circuit = QuantumCircuit(qr) - - # random two-qubit circuit. - circuit.rzx(theta, 0, 1) - circuit.rzz(epsilon, 0, 1) - circuit.rz(eta, 0) - circuit.swap(1, 0) - circuit.h(0) - circuit.rzz(delta, 1, 0) - circuit.swap(0, 1) - circuit.cx(1, 0) - circuit.swap(0, 1) - circuit.h(1) - circuit.rxx(theta, 0, 1) - circuit.ryy(theta, 1, 0) - circuit.ecr(0, 1) - - unitary_circuit = qi.Operator(circuit).data - - dag = circuit_to_dag(circuit) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The entire Qiskit Pulse package", - ): - pass_ = EchoRZXWeylDecomposition(self.inst_map) - after = dag_to_circuit(pass_.run(dag)) - - unitary_after = qi.Operator(after).data - - self.assertTrue(np.allclose(unitary_circuit, unitary_after)) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py deleted file mode 100644 index 74d5cc5e825..00000000000 --- a/test/python/transpiler/test_instruction_alignments.py +++ /dev/null @@ -1,120 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Testing instruction alignment pass.""" - -from qiskit import QuantumCircuit, pulse -from qiskit.transpiler import PassManager -from qiskit.transpiler.exceptions import TranspilerError -from qiskit.transpiler.passes import ValidatePulseGates -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestPulseGateValidation(QiskitTestCase): - """A test for pulse gate validation pass.""" - - def test_invalid_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - with self.assertWarns(DeprecationWarning): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - with self.assertRaises(TranspilerError): - pm.run(circuit) - - def test_short_pulse_duration(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # this will cause backend error since this doesn't fit with waveform memory chunk. - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - with self.assertRaises(TranspilerError): - pm.run(circuit) - - def test_short_pulse_duration_multiple_pulse(self): - """Kill pass manager if invalid pulse gate is found.""" - - # this is invalid duration pulse - # however total gate schedule length is 64, which accidentally satisfies the constraints - # this should fail in the validation - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - custom_gate.insert( - 32, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - with self.assertRaises(TranspilerError): - pm.run(circuit) - - def test_valid_pulse_duration(self): - """No error raises if valid calibration is provided.""" - - # this is valid duration pulse - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(160, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - circuit = QuantumCircuit(1) - circuit.x(0) - with self.assertWarns(DeprecationWarning): - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - - # just not raise an error - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - pm.run(circuit) - - def test_no_calibration(self): - """No error raises if no calibration is added.""" - - circuit = QuantumCircuit(1) - circuit.x(0) - - # just not raise an error - with self.assertWarns(DeprecationWarning): - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) - pm.run(circuit) diff --git a/test/python/transpiler/test_passmanager.py b/test/python/transpiler/test_passmanager.py index 6764745facd..6a72ffb16a8 100644 --- a/test/python/transpiler/test_passmanager.py +++ b/test/python/transpiler/test_passmanager.py @@ -27,8 +27,7 @@ DoWhileController, ) from qiskit.transpiler import PassManager, PropertySet, TransformationPass -from qiskit.transpiler.passes import RXCalibrationBuilder -from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator +from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator, ResourceEstimation from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, ) @@ -83,7 +82,7 @@ def callback(**kwargs): self.assertEqual("MyCircuit", calls[1]["dag"].name) def test_callback_with_pass_requires(self): - """Test the callback with a pass with another pass requirement.""" + """Test the callback with a pass with pass requirements.""" qr = QuantumRegister(3, "qr") circuit = QuantumCircuit(qr, name="MyCircuit") circuit.z(qr[0]) @@ -106,23 +105,29 @@ def callback(**kwargs): calls.append(out_dict) passmanager = PassManager() - with self.assertWarns(DeprecationWarning): - passmanager.append(RXCalibrationBuilder()) + passmanager.append(ResourceEstimation()) passmanager.run(circuit, callback=callback) - self.assertEqual(len(calls), 2) - self.assertEqual(len(calls[0]), 5) - self.assertEqual(calls[0]["count"], 0) - self.assertEqual(calls[0]["pass_"].name(), "NormalizeRXAngle") - self.assertEqual(expected_start_dag, calls[0]["dag"]) - self.assertIsInstance(calls[0]["time"], float) - self.assertIsInstance(calls[0]["property_set"], PropertySet) - self.assertEqual("MyCircuit", calls[0]["dag"].name) - self.assertEqual(len(calls[1]), 5) - self.assertEqual(calls[1]["count"], 1) - self.assertEqual(calls[1]["pass_"].name(), "RXCalibrationBuilder") - self.assertIsInstance(calls[0]["time"], float) - self.assertIsInstance(calls[0]["property_set"], PropertySet) - self.assertEqual("MyCircuit", calls[1]["dag"].name) + + self.assertEqual(len(calls), 7) + + required_passes = [ + "Depth", + "Width", + "Size", + "CountOps", + "NumTensorFactors", + "NumQubits", + "ResourceEstimation", + ] + + for call_entry in range(7): + self.assertEqual(len(calls[call_entry]), 5) + self.assertEqual(calls[call_entry]["count"], call_entry) + self.assertEqual(calls[call_entry]["pass_"].name(), required_passes[call_entry]) + self.assertEqual(expected_start_dag, calls[call_entry]["dag"]) + self.assertIsInstance(calls[call_entry]["time"], float) + self.assertIsInstance(calls[call_entry]["property_set"], PropertySet) + self.assertEqual("MyCircuit", calls[call_entry]["dag"].name) def test_to_flow_controller(self): """Test that conversion to a `FlowController` works, and the result can be added to a diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py deleted file mode 100644 index e617f64d885..00000000000 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ /dev/null @@ -1,577 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021, 2024. -# -# 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. - -"""Transpiler pulse gate pass testing.""" - -import ddt - -from qiskit import pulse, circuit, transpile -from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 -from qiskit.providers.models.backendconfiguration import GateConfig -from qiskit.quantum_info.random import random_unitary -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings -from ..legacy_cmaps import BOGOTA_CMAP - - -@ddt.ddt -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestPulseGate(QiskitTestCase): - """Integration test of pulse gate pass with custom backend.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - - self.sched_param = circuit.Parameter("P0") - - with pulse.build(name="sx_q0") as custom_sx_q0: - pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) - self.custom_sx_q0 = custom_sx_q0 - - with pulse.build(name="sx_q1") as custom_sx_q1: - pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) - self.custom_sx_q1 = custom_sx_q1 - - with pulse.build(name="cx_q01") as custom_cx_q01: - pulse.play(pulse.Constant(100, 0.4), pulse.ControlChannel(0)) - self.custom_cx_q01 = custom_cx_q01 - - with pulse.build(name="my_gate_q0") as my_gate_q0: - pulse.shift_phase(self.sched_param, pulse.DriveChannel(0)) - pulse.play(pulse.Constant(120, 0.1), pulse.DriveChannel(0)) - self.my_gate_q0 = my_gate_q0 - - with pulse.build(name="my_gate_q1") as my_gate_q1: - pulse.shift_phase(self.sched_param, pulse.DriveChannel(1)) - pulse.play(pulse.Constant(120, 0.2), pulse.DriveChannel(1)) - self.my_gate_q1 = my_gate_q1 - - def test_transpile_with_bare_backend(self): - """Test transpile without custom calibrations.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) - - ref_calibration = {} - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_backend_target(self): - """Test transpile without custom calibrations from target.""" - - with self.assertWarns(DeprecationWarning): - target = GenericBackendV2( - num_qubits=5, coupling_map=BOGOTA_CMAP, calibrate_instructions=True, seed=42 - ).target - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - transpiled_qc = transpile(qc, initial_layout=[0, 1], target=target) - - ref_calibration = {} - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_custom_basis_gate(self): - """Test transpile with custom calibrations.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) - backend.defaults().instruction_schedule_map.add("sx", (1,), self.custom_sx_q1) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) - - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_custom_basis_gate_in_target(self): - """Test transpile with custom calibrations.""" - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - target = GenericBackendV2( - num_qubits=5, - coupling_map=BOGOTA_CMAP, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ).target - - target["sx"][(0,)].calibration = self.custom_sx_q0 - target["sx"][(1,)].calibration = self.custom_sx_q1 - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - transpiled_qc = transpile(qc, initial_layout=[0, 1], target=target) - - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_instmap(self): - """Test providing instruction schedule map.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - instmap.add("sx", (1,), self.custom_sx_q1) - - # Inst map is renewed - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.sx(0) - qc.x(0) - qc.rz(0, 0) - qc.sx(1) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, inst_map=instmap, initial_layout=[0, 1]) - - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_custom_gate(self): - """Test providing non-basis gate.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - backend.defaults().instruction_schedule_map.add( - "my_gate", (1,), self.my_gate_q1, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,), (1,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(2) - qc.append(circuit.Gate("my_gate", 1, [1.0]), [0]) - qc.append(circuit.Gate("my_gate", 1, [2.0]), [1]) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0, 1]) - - my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) - my_gate_q1_2_0 = self.my_gate_q1.assign_parameters({self.sched_param: 2.0}, inplace=False) - - ref_calibration = { - "my_gate": { - ((0,), (1.0,)): my_gate_q0_1_0, - ((1,), (2.0,)): my_gate_q1_2_0, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_parameterized_custom_gate(self): - """Test providing non-basis gate, which is kept parameterized throughout transpile.""" - with self.assertWarns(DeprecationWarning): - # TODO convert this to BackendV2/Target - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - param = circuit.Parameter("new_P0") - qc = circuit.QuantumCircuit(1) - qc.append(circuit.Gate("my_gate", 1, [param]), [0]) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) - - my_gate_q0_p = self.my_gate_q0.assign_parameters({self.sched_param: param}, inplace=False) - - ref_calibration = { - "my_gate": { - ((0,), (param,)): my_gate_q0_p, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_multiple_circuits(self): - """Test transpile with multiple circuits with custom gate.""" - with self.assertWarns(DeprecationWarning): - # TODO move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - params = [0.0, 1.0, 2.0, 3.0] - circs = [] - for param in params: - qc = circuit.QuantumCircuit(1) - qc.append(circuit.Gate("my_gate", 1, [param]), [0]) - circs.append(qc) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qcs = transpile(circs, backend, basis_gates=["my_gate"], initial_layout=[0]) - - for param, transpiled_qc in zip(params, transpiled_qcs): - my_gate_q0_x = self.my_gate_q0.assign_parameters( - {self.sched_param: param}, inplace=False - ) - ref_calibration = {"my_gate": {((0,), (param,)): my_gate_q0_x}} - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_multiple_instructions_with_different_parameters(self): - """Test adding many instruction with different parameter binding.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - # Add gate to backend configuration - backend.configuration().basis_gates.append("my_gate") - with self.assertWarns(DeprecationWarning): - dummy_config = GateConfig( - name="my_gate", parameters=[], qasm_def="", coupling_map=[(0,)] - ) - backend.configuration().gates.append(dummy_config) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(1) - qc.append(circuit.Gate("my_gate", 1, [1.0]), [0]) - qc.append(circuit.Gate("my_gate", 1, [2.0]), [0]) - qc.append(circuit.Gate("my_gate", 1, [3.0]), [0]) - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, basis_gates=["my_gate"], initial_layout=[0]) - - my_gate_q0_1_0 = self.my_gate_q0.assign_parameters({self.sched_param: 1.0}, inplace=False) - my_gate_q0_2_0 = self.my_gate_q0.assign_parameters({self.sched_param: 2.0}, inplace=False) - my_gate_q0_3_0 = self.my_gate_q0.assign_parameters({self.sched_param: 3.0}, inplace=False) - - ref_calibration = { - "my_gate": { - ((0,), (1.0,)): my_gate_q0_1_0, - ((0,), (2.0,)): my_gate_q0_2_0, - ((0,), (3.0,)): my_gate_q0_3_0, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_different_qubit(self): - """Test transpile with qubit without custom gate.""" - with self.assertWarns(DeprecationWarning): - # TODO Move this test to backendV2 - backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) - # Remove timing constraints to avoid triggering - # scheduling passes. - backend.configuration().timing_constraints = {} - - qc = circuit.QuantumCircuit(1) - qc.sx(0) - qc.measure_all() - - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled_qc = transpile(qc, backend, initial_layout=[3]) - - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, {}) - - @ddt.data(0, 1, 2, 3) - def test_transpile_with_both_instmap_and_empty_target(self, opt_level): - """Test when instmap and target are both provided - and only instmap contains custom schedules. - - Test case from Qiskit/qiskit-terra/#9489 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - instmap.add("sx", (1,), self.custom_sx_q1) - instmap.add("cx", (0, 1), self.custom_cx_q01) - - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - # This doesn't have custom schedule definition - target = GenericBackendV2( - num_qubits=5, - coupling_map=BOGOTA_CMAP, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ).target - - qc = circuit.QuantumCircuit(2) - qc.append(random_unitary(4, seed=123), [0, 1]) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - transpiled_qc = transpile( - qc, - optimization_level=opt_level, - basis_gates=["sx", "rz", "x", "cx"], - inst_map=instmap, - target=target, - initial_layout=[0, 1], - ) - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - }, - "cx": { - ((0, 1), ()): self.custom_cx_q01, - }, - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - @ddt.data(0, 1, 2, 3) - def test_transpile_with_instmap_with_v2backend(self, opt_level): - """Test when instmap is provided with V2 backend. - - Test case from Qiskit/qiskit-terra/#9489 - """ - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - - instmap = backend.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - instmap.add("sx", (1,), self.custom_sx_q1) - instmap.add("cx", (0, 1), self.custom_cx_q01) - - qc = circuit.QuantumCircuit(2) - qc.append(random_unitary(4, seed=123), [0, 1]) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - - backend = GenericBackendV2( - num_qubits=5, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ) - - with self.assertWarns(DeprecationWarning): - transpiled_qc = transpile( - qc, - backend, - optimization_level=opt_level, - inst_map=instmap, - initial_layout=[0, 1], - ) - ref_calibration = { - "sx": { - ((0,), ()): self.custom_sx_q0, - ((1,), ()): self.custom_sx_q1, - }, - "cx": { - ((0, 1), ()): self.custom_cx_q01, - }, - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - @ddt.data(0, 1, 2, 3) - def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level): - """Test when instmap is provided with V2 backend. - - In this test case, instmap contains a custom gate which doesn't belong to - Qiskit standard gate. Target must define a custom gete on the fly - to reflect user-provided instmap. - - Test case from Qiskit/qiskit-terra/#9489 - """ - with pulse.build(name="custom") as rabi12: - pulse.play(pulse.Constant(100, 0.4), pulse.DriveChannel(0)) - with self.assertWarns(DeprecationWarning): - backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map - instmap.add("rabi12", (0,), rabi12) - - gate = circuit.Gate("rabi12", 1, []) - qc = circuit.QuantumCircuit(1) - qc.append(gate, [0]) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) - transpiled_qc = transpile( - qc, - backend, - optimization_level=opt_level, - inst_map=instmap, - initial_layout=[0], - ) - ref_calibration = { - "rabi12": { - ((0,), ()): rabi12, - } - } - with self.assertWarns(DeprecationWarning): - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) - - def test_transpile_with_instmap_not_mutate_backend(self): - """Do not override default backend target when transpile with inst map. - - Providing an instmap for the transpile arguments may override target, - which might be pulled from the provided backend instance. - This should not override the source object since the same backend may - be used for future transpile without intention of instruction overriding. - """ - with self.assertWarns(DeprecationWarning): - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) - original_sx0 = backend.target["sx"][(0,)].calibration - - with self.assertWarns(DeprecationWarning): - backend_pulse = Fake27QPulseV1() - - instmap = backend_pulse.defaults().instruction_schedule_map - instmap.add("sx", (0,), self.custom_sx_q0) - - qc = circuit.QuantumCircuit(1) - qc.sx(0) - qc.measure_all() - - with self.assertWarns(DeprecationWarning): - transpiled_qc = transpile( - qc, - backend, - inst_map=instmap, - initial_layout=[0], - ) - self.assertTrue(transpiled_qc.has_calibration_for(transpiled_qc.data[0])) - - self.assertEqual( - backend.target["sx"][(0,)].calibration, - original_sx0, - ) From 28a33d7658f0eda94bbecf93bd093c393b544fcc Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Fri, 7 Feb 2025 16:21:31 +0100 Subject: [PATCH 3/9] Deprecation of MCMT in favor of MCMTGate (#13584) * remove pending deprecations in mcmt * reno * testing a regression --- .../circuit/library/generalized_gates/mcmt.py | 11 ++++---- .../followup_13150-5bd0c77248601e1a.yaml | 8 ++++++ test/python/circuit/library/test_mcmt.py | 28 ++++++++++++------- 3 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml diff --git a/qiskit/circuit/library/generalized_gates/mcmt.py b/qiskit/circuit/library/generalized_gates/mcmt.py index 8e649b6a36c..da580be9bf7 100644 --- a/qiskit/circuit/library/generalized_gates/mcmt.py +++ b/qiskit/circuit/library/generalized_gates/mcmt.py @@ -49,7 +49,7 @@ class MCMT(QuantumCircuit): :class:`~qiskit.circuit.library.MCMTVChain`. """ - @deprecate_func(since="1.3", additional_msg="Use MCMTGate instead.", pending=True) + @deprecate_func(since="1.4", additional_msg="Use MCMTGate instead.") def __init__( self, gate: Gate | Callable[[QuantumCircuit, circuit.Qubit, circuit.Qubit], circuit.Instruction], @@ -76,7 +76,7 @@ def __init__( warnings.warn( "Passing a callable to MCMT is pending deprecation since Qiskit 1.3. Pass a " "gate instance or the gate name instead, e.g. pass 'h' instead of QuantumCircuit.h.", - category=PendingDeprecationWarning, + category=DeprecationWarning, stacklevel=2, ) gate = gate.__name__ @@ -84,7 +84,7 @@ def __init__( warnings.warn( "Passing a QuantumCircuit is pending deprecation since Qiskit 1.3. Pass a gate " "or turn the circuit into a gate using the ``to_gate`` method, instead.", - category=PendingDeprecationWarning, + category=DeprecationWarning, stacklevel=2, ) gate = gate.to_gate() @@ -158,9 +158,8 @@ class MCMTVChain(MCMT): """ @deprecate_func( - since="1.3", + since="1.4", additional_msg="Use MCMTGate with the V-chain synthesis plugin instead.", - pending=True, ) def __init__( self, @@ -277,7 +276,7 @@ def _identify_base_gate(gate): warnings.warn( "Passing a controlled gate to MCMT is pending deprecation since Qiskit 1.3. Pass a " "single-qubit gate instance or the gate name instead, e.g. pass 'h' instead of 'ch'.", - category=PendingDeprecationWarning, + category=DeprecationWarning, stacklevel=2, ) base_gate = gate.base_gate diff --git a/releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml b/releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml new file mode 100644 index 00000000000..6eaa0e7a0d4 --- /dev/null +++ b/releasenotes/notes/followup_13150-5bd0c77248601e1a.yaml @@ -0,0 +1,8 @@ +--- +deprecations_circuits: + - | + The Multiple-Control-Multiple-Target in :class:`~qiskit.circuit.library.generalized_gates.MCMT` is now deprecated + and replaced by :class:`.MCMTGate`, which is a proper :class:`.Gate` subclass. Using + a gate instead of a circuit allows the compiler to reason about the object at a higher + level of abstraction and allows for multiple synthesis plugins. + diff --git a/test/python/circuit/library/test_mcmt.py b/test/python/circuit/library/test_mcmt.py index 435ee062959..cbc45710734 100644 --- a/test/python/circuit/library/test_mcmt.py +++ b/test/python/circuit/library/test_mcmt.py @@ -51,7 +51,8 @@ class TestMCMT(QiskitTestCase): def test_mcmt_as_normal_control(self, mcmt_class): """Test that the MCMT can act as normal control gate.""" qc = QuantumCircuit(2) - mcmt = mcmt_class(gate=CHGate(), num_ctrl_qubits=1, num_target_qubits=1) + with self.assertWarns(DeprecationWarning): + mcmt = mcmt_class(gate=CHGate(), num_ctrl_qubits=1, num_target_qubits=1) qc = qc.compose(mcmt, [0, 1]) ref = QuantumCircuit(2) @@ -65,12 +66,14 @@ def test_mcmt_as_normal_control(self, mcmt_class): def test_missing_qubits(self): """Test that an error is raised if qubits are missing.""" with self.subTest(msg="no control qubits"): - with self.assertRaises(AttributeError): - _ = MCMT(XGate(), num_ctrl_qubits=0, num_target_qubits=1) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(AttributeError): + _ = MCMT(XGate(), num_ctrl_qubits=0, num_target_qubits=1) with self.subTest(msg="no target qubits"): - with self.assertRaises(AttributeError): - _ = MCMT(ZGate(), num_ctrl_qubits=4, num_target_qubits=0) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(AttributeError): + _ = MCMT(ZGate(), num_ctrl_qubits=4, num_target_qubits=0) def test_different_gate_types(self): """Test the different supported input types for the target gate.""" @@ -78,7 +81,8 @@ def test_different_gate_types(self): x_circ.x(0) for input_gate in [x_circ, QuantumCircuit.cx, QuantumCircuit.x, "cx", "x", CXGate()]: with self.subTest(input_gate=input_gate): - mcmt = MCMT(input_gate, 2, 2) + with self.assertWarns(DeprecationWarning): + mcmt = MCMT(input_gate, 2, 2) if isinstance(input_gate, QuantumCircuit): self.assertEqual(mcmt.gate.definition[0].operation, XGate()) self.assertEqual(len(mcmt.gate.definition), 1) @@ -89,13 +93,15 @@ def test_mcmt_v_chain_ancilla_test(self): """Test too few and too many ancillas for the MCMT V-chain mode.""" with self.subTest(msg="insufficient number of auxiliary qubits on gate"): qc = QuantumCircuit(5) - mcmt = MCMTVChain(ZGate(), 3, 1) + with self.assertWarns(DeprecationWarning): + mcmt = MCMTVChain(ZGate(), 3, 1) with self.assertRaises(QiskitError): qc.append(mcmt, range(5)) with self.subTest(msg="too many auxiliary qubits on gate"): qc = QuantumCircuit(9) - mcmt = MCMTVChain(ZGate(), 3, 1) + with self.assertWarns(DeprecationWarning): + mcmt = MCMTVChain(ZGate(), 3, 1) with self.assertRaises(QiskitError): qc.append(mcmt, range(9)) @@ -135,7 +141,8 @@ def test_mcmt_v_chain_simulation(self, cgate, num_controls, num_targets): for i in subset: qc.x(controls[i]) - mcmt = MCMTVChain(cgate, num_controls, num_targets) + with self.assertWarns(DeprecationWarning): + mcmt = MCMTVChain(cgate, num_controls, num_targets) qc.compose(mcmt, qubits, inplace=True) for i in subset: @@ -292,7 +299,8 @@ def test_mcmt_circuit_as_gate(self): """ circuit = QuantumCircuit(2) gate = RYGate(0.1) - mcmt = MCMT(gate=gate, num_ctrl_qubits=1, num_target_qubits=1) + with self.assertWarns(DeprecationWarning): + mcmt = MCMT(gate=gate, num_ctrl_qubits=1, num_target_qubits=1) circuit.append(mcmt, circuit.qubits) # append the MCMT circuit as gate called "MCMT" transpiled = transpile(circuit, basis_gates=["u", "cx"]) From 9ae0a808b45249d6f185baf54f078cb1ed00648a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:47:56 +0100 Subject: [PATCH 4/9] Remove `schedule`, `sequence` and related unit tests (#13809) * Remove schedule, sequence and related unit tests * Forgot the reno * Fix tests, lint, update reno * Attempt to fix docs (removing examples, but the functionality is deprecated and the whole docstring will go soon) --- qiskit/__init__.py | 4 +- qiskit/compiler/__init__.py | 6 +- qiskit/compiler/scheduler.py | 109 -- qiskit/compiler/sequencer.py | 71 - qiskit/pulse/builder.py | 149 +- qiskit/visualization/timeline/interface.py | 6 +- ...ve-schedule-sequence-a6249577da8d1c86.yaml | 10 + test/python/compiler/test_scheduler.py | 103 -- test/python/compiler/test_sequencer.py | 109 -- test/python/pulse/test_builder.py | 99 +- test/python/scheduler/__init__.py | 15 - test/python/scheduler/test_basic_scheduler.py | 1218 ----------------- 12 files changed, 17 insertions(+), 1882 deletions(-) delete mode 100644 qiskit/compiler/scheduler.py delete mode 100644 qiskit/compiler/sequencer.py create mode 100644 releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml delete mode 100644 test/python/compiler/test_scheduler.py delete mode 100644 test/python/compiler/test_sequencer.py delete mode 100644 test/python/scheduler/__init__.py delete mode 100644 test/python/scheduler/test_basic_scheduler.py diff --git a/qiskit/__init__.py b/qiskit/__init__.py index c121c99112f..376881a5679 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -127,7 +127,7 @@ _config = _user_config.get_config() -from qiskit.compiler import transpile, schedule, sequence +from qiskit.compiler import transpile from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from .version import __version__ @@ -138,8 +138,6 @@ "QiskitError", "QuantumCircuit", "QuantumRegister", - "schedule", - "sequence", "transpile", "generate_preset_pass_manager", ] diff --git a/qiskit/compiler/__init__.py b/qiskit/compiler/__init__.py index cd3ed166a4d..3d412611cf0 100644 --- a/qiskit/compiler/__init__.py +++ b/qiskit/compiler/__init__.py @@ -17,15 +17,11 @@ .. currentmodule:: qiskit.compiler -Circuit and Pulse Compilation Functions +Circuit Compilation Functions ======================================= -.. autofunction:: schedule .. autofunction:: transpile -.. autofunction:: sequence """ from .transpiler import transpile -from .scheduler import schedule -from .sequencer import sequence diff --git a/qiskit/compiler/scheduler.py b/qiskit/compiler/scheduler.py deleted file mode 100644 index 9004dc24fd1..00000000000 --- a/qiskit/compiler/scheduler.py +++ /dev/null @@ -1,109 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# 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. - -""" -Convenience entry point into pulse scheduling, requiring only a circuit and a backend. For more -control over pulse scheduling, look at `qiskit.scheduler.schedule_circuit`. -""" -import logging - -from time import time -from typing import List, Optional, Union - -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.pulse import InstructionScheduleMap, Schedule -from qiskit.providers.backend import Backend -from qiskit.scheduler.config import ScheduleConfig -from qiskit.scheduler.schedule_circuit import schedule_circuit -from qiskit.utils.parallel import parallel_map -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - -logger = logging.getLogger(__name__) - - -def _log_schedule_time(start_time, end_time): - log_msg = f"Total Scheduling Time - {((end_time - start_time) * 1000):.5f} (ms)" - logger.info(log_msg) - - -@deprecate_pulse_dependency(moving_to_dynamics=True) -def schedule( - circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend: Optional[Backend] = None, - inst_map: Optional[InstructionScheduleMap] = None, - meas_map: Optional[List[List[int]]] = None, - dt: Optional[float] = None, - method: Optional[Union[str, List[str]]] = None, -) -> Union[Schedule, List[Schedule]]: - """ - Schedule a circuit to a pulse ``Schedule``, using the backend, according to any specified - methods. Supported methods are documented in :py:mod:`qiskit.scheduler.schedule_circuit`. - - Args: - circuits: The quantum circuit or circuits to translate - backend: A backend instance, which contains hardware-specific data required for scheduling - inst_map: Mapping of circuit operations to pulse schedules. If ``None``, defaults to the - ``backend``\'s ``instruction_schedule_map`` - meas_map: List of sets of qubits that must be measured together. If ``None``, defaults to - the ``backend``\'s ``meas_map`` - dt: The output sample rate of backend control electronics. For scheduled circuits - which contain time information, dt is required. If not provided, it will be - obtained from the backend configuration - method: Optionally specify a particular scheduling method - - Returns: - A pulse ``Schedule`` that implements the input circuit - - Raises: - QiskitError: If ``inst_map`` and ``meas_map`` are not passed and ``backend`` is not passed - """ - arg_circuits_list = isinstance(circuits, list) - start_time = time() - if backend and getattr(backend, "version", 0) > 1: - if inst_map is None: - inst_map = backend.instruction_schedule_map - if meas_map is None: - meas_map = backend.meas_map - if dt is None: - dt = backend.dt - else: - if inst_map is None: - if backend is None: - raise QiskitError( - "Must supply either a backend or InstructionScheduleMap for scheduling passes." - ) - defaults = backend.defaults() - if defaults is None: - raise QiskitError( - "The backend defaults are unavailable. The backend may not support pulse." - ) - inst_map = defaults.instruction_schedule_map - if meas_map is None: - if backend is None: - raise QiskitError( - "Must supply either a backend or a meas_map for scheduling passes." - ) - meas_map = backend.configuration().meas_map - if dt is None: - if backend is not None: - dt = backend.configuration().dt - - schedule_config = ScheduleConfig(inst_map=inst_map, meas_map=meas_map, dt=dt) - circuits = circuits if isinstance(circuits, list) else [circuits] - schedules = parallel_map(schedule_circuit, circuits, (schedule_config, method, backend)) - end_time = time() - _log_schedule_time(start_time, end_time) - if arg_circuits_list: - return schedules - else: - return schedules[0] diff --git a/qiskit/compiler/sequencer.py b/qiskit/compiler/sequencer.py deleted file mode 100644 index 5a381918417..00000000000 --- a/qiskit/compiler/sequencer.py +++ /dev/null @@ -1,71 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Mapping a scheduled ``QuantumCircuit`` to a pulse ``Schedule``. -""" -from typing import List, Optional, Union - -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.providers.backend import Backend -from qiskit.pulse import InstructionScheduleMap, Schedule -from qiskit.scheduler import ScheduleConfig -from qiskit.scheduler.sequence import sequence as _sequence -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -@deprecate_pulse_dependency(moving_to_dynamics=True) -def sequence( - scheduled_circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend: Optional[Backend] = None, - inst_map: Optional[InstructionScheduleMap] = None, - meas_map: Optional[List[List[int]]] = None, - dt: Optional[float] = None, -) -> Union[Schedule, List[Schedule]]: - """ - Schedule a scheduled circuit to a pulse ``Schedule``, using the backend. - - Args: - scheduled_circuits: Scheduled circuit(s) to be translated - backend: A backend instance, which contains hardware-specific data required for scheduling - inst_map: Mapping of circuit operations to pulse schedules. If ``None``, defaults to the - ``backend``\'s ``instruction_schedule_map`` - meas_map: List of sets of qubits that must be measured together. If ``None``, defaults to - the ``backend``\'s ``meas_map`` - dt: The output sample rate of backend control electronics. For scheduled circuits - which contain time information, dt is required. If not provided, it will be - obtained from the backend configuration - - Returns: - A pulse ``Schedule`` that implements the input circuit - - Raises: - QiskitError: If ``inst_map`` and ``meas_map`` are not passed and ``backend`` is not passed - """ - if inst_map is None: - if backend is None: - raise QiskitError("Must supply either a backend or inst_map for sequencing.") - inst_map = backend.defaults().instruction_schedule_map - if meas_map is None: - if backend is None: - raise QiskitError("Must supply either a backend or a meas_map for sequencing.") - meas_map = backend.configuration().meas_map - if dt is None: - if backend is None: - raise QiskitError("Must supply either a backend or a dt for sequencing.") - dt = backend.configuration().dt - - schedule_config = ScheduleConfig(inst_map=inst_map, meas_map=meas_map, dt=dt) - circuits = scheduled_circuits if isinstance(scheduled_circuits, list) else [scheduled_circuits] - schedules = [_sequence(circuit, schedule_config) for circuit in circuits] - return schedules[0] if len(schedules) == 1 else schedules diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index bc057980cf0..70ecc9d11df 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -89,160 +89,13 @@ representations, while simultaneously applying a long decoupling pulse to a neighboring qubit. We terminate the experiment with a measurement to observe the state we prepared. This program which mixes circuits and pulses will be -automatically lowered to be run as a pulse program: - -.. plot:: - :alt: Output from the previous code. - :include-source: - - from math import pi - from qiskit.compiler import schedule - from qiskit.circuit import QuantumCircuit - - from qiskit import pulse - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True) - - d2 = pulse.DriveChannel(2) - - qc = QuantumCircuit(2) - # Hadamard - qc.rz(pi/2, 0) - qc.sx(0) - qc.rz(pi/2, 0) - - qc.cx(0, 1) - - bell_sched = schedule(qc, backend) - - with pulse.build(backend) as decoupled_bell_prep_and_measure: - # We call our bell state preparation schedule constructed above. - with pulse.align_right(): - pulse.call(bell_sched) - pulse.play(pulse.Constant(bell_sched.duration, 0.02), d2) - pulse.barrier(0, 1, 2) - registers = pulse.measure_all() - - decoupled_bell_prep_and_measure.draw() - +automatically lowered to be run as a pulse program. With the pulse builder we are able to blend programming on qubits and channels. While the pulse schedule is based on instructions that operate on channels, the pulse builder automatically handles the mapping from qubits to channels for you. -In the example below we demonstrate some more features of the pulse builder: - -.. plot:: - :include-source: - :nofigs: - - import math - from qiskit.compiler import schedule - - from qiskit import pulse, QuantumCircuit - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - qc = QuantumCircuit(2, 2) - qc.cx(0, 1) - - with pulse.build(backend) as pulse_prog: - # Create a pulse. - gaussian_pulse = pulse.Gaussian(10, 1.0, 2) - # Get the qubit's corresponding drive channel from the backend. - d0 = pulse.drive_channel(0) - d1 = pulse.drive_channel(1) - # Play a pulse at t=0. - pulse.play(gaussian_pulse, d0) - # Play another pulse directly after the previous pulse at t=10. - pulse.play(gaussian_pulse, d0) - # The default scheduling behavior is to schedule pulses in parallel - # across channels. For example, the statement below - # plays the same pulse on a different channel at t=0. - pulse.play(gaussian_pulse, d1) - - # We also provide pulse scheduling alignment contexts. - # The default alignment context is align_left. - - # The sequential context schedules pulse instructions sequentially in time. - # This context starts at t=10 due to earlier pulses above. - with pulse.align_sequential(): - pulse.play(gaussian_pulse, d0) - # Play another pulse after at t=20. - pulse.play(gaussian_pulse, d1) - - # We can also nest contexts as each instruction is - # contained in its local scheduling context. - # The output of a child context is a context-schedule - # with the internal instructions timing fixed relative to - # one another. This is schedule is then called in the parent context. - - # Context starts at t=30. - with pulse.align_left(): - # Start at t=30. - pulse.play(gaussian_pulse, d0) - # Start at t=30. - pulse.play(gaussian_pulse, d1) - # Context ends at t=40. - - # Alignment context where all pulse instructions are - # aligned to the right, ie., as late as possible. - with pulse.align_right(): - # Shift the phase of a pulse channel. - pulse.shift_phase(math.pi, d1) - # Starts at t=40. - pulse.delay(100, d0) - # Ends at t=140. - - # Starts at t=130. - pulse.play(gaussian_pulse, d1) - # Ends at t=140. - - # Acquire data for a qubit and store in a memory slot. - pulse.acquire(100, 0, pulse.MemorySlot(0)) - - # We also support a variety of macros for common operations. - - # Measure all qubits. - pulse.measure_all() - - # Delay on some qubits. - # This requires knowledge of which channels belong to which qubits. - # delay for 100 cycles on qubits 0 and 1. - pulse.delay_qubits(100, 0, 1) - - # Call a schedule for a quantum circuit thereby inserting into - # the pulse schedule. - qc = QuantumCircuit(2, 2) - qc.cx(0, 1) - qc_sched = schedule(qc, backend) - pulse.call(qc_sched) - - - # It is also be possible to call a preexisting schedule - tmp_sched = pulse.Schedule() - tmp_sched += pulse.Play(gaussian_pulse, d0) - pulse.call(tmp_sched) - - # We also support: - - # frequency instructions - pulse.set_frequency(5.0e9, d0) - - # phase instructions - pulse.shift_phase(0.1, d0) - - # offset contexts - with pulse.phase_offset(math.pi, d0): - pulse.play(gaussian_pulse, d0) - - -The above is just a small taste of what is possible with the builder. See the rest of the module -documentation for more information on its capabilities. - .. autofunction:: build diff --git a/qiskit/visualization/timeline/interface.py b/qiskit/visualization/timeline/interface.py index 50dd006633a..1818f78a907 100644 --- a/qiskit/visualization/timeline/interface.py +++ b/qiskit/visualization/timeline/interface.py @@ -301,7 +301,7 @@ def draw( :alt: Output from the previous code. :include-source: - from qiskit import QuantumCircuit, transpile, schedule + from qiskit import QuantumCircuit, transpile from qiskit.visualization.timeline import draw from qiskit.providers.fake_provider import GenericBackendV2 @@ -318,7 +318,7 @@ def draw( :alt: Output from the previous code. :include-source: - from qiskit import QuantumCircuit, transpile, schedule + from qiskit import QuantumCircuit, transpile from qiskit.visualization.timeline import draw, IQXSimple from qiskit.providers.fake_provider import GenericBackendV2 @@ -335,7 +335,7 @@ def draw( :alt: Output from the previous code. :include-source: - from qiskit import QuantumCircuit, transpile, schedule + from qiskit import QuantumCircuit, transpile from qiskit.visualization.timeline import draw, IQXDebugging from qiskit.providers.fake_provider import GenericBackendV2 diff --git a/releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml b/releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml new file mode 100644 index 00000000000..b5284304008 --- /dev/null +++ b/releasenotes/notes/remove-schedule-sequence-a6249577da8d1c86.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + The functions ``sequence`` and ``schedule`` from the :mod:`.compiler` + module have been removed following their deprecation in Qiskit 1.3. + They relied on being able to translate circuits to pulse using backend + definitions, a capability that is no longer present. For this reason + they have been removed with no proposed alternative. + Note that this removals relate to the Pulse package which is + also being removed in Qiskit 2.0. \ No newline at end of file diff --git a/test/python/compiler/test_scheduler.py b/test/python/compiler/test_scheduler.py deleted file mode 100644 index c349bf054c3..00000000000 --- a/test/python/compiler/test_scheduler.py +++ /dev/null @@ -1,103 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# 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. - -"""Scheduler Test.""" - -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.pulse import InstructionScheduleMap, Schedule -from qiskit.providers.fake_provider import FakeOpenPulse3Q, GenericBackendV2 -from qiskit.compiler.scheduler import schedule -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestCircuitScheduler(QiskitTestCase): - """Tests for scheduling.""" - - def setUp(self): - super().setUp() - qr = QuantumRegister(2, name="q") - cr = ClassicalRegister(2, name="c") - self.circ = QuantumCircuit(qr, cr, name="circ") - self.circ.cx(qr[0], qr[1]) - self.circ.measure(qr, cr) - - qr2 = QuantumRegister(2, name="q") - cr2 = ClassicalRegister(2, name="c") - self.circ2 = QuantumCircuit(qr2, cr2, name="circ2") - self.circ2.cx(qr2[0], qr2[1]) - self.circ2.measure(qr2, cr2) - - with self.assertWarns(DeprecationWarning): - self.backend = GenericBackendV2( - 3, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) - - def test_instruction_map_and_backend_not_supplied(self): - """Test instruction map and backend not supplied.""" - with self.assertRaisesRegex( - QiskitError, - r"Must supply either a backend or InstructionScheduleMap for scheduling passes.", - ): - with self.assertWarns(DeprecationWarning): - schedule(self.circ) - - def test_instruction_map_and_backend_defaults_unavailable(self): - """Test backend defaults unavailable when backend is provided, but instruction map is not.""" - with self.assertWarns(DeprecationWarning): - self.backend = FakeOpenPulse3Q() - self.backend._defaults = None - with self.assertRaisesRegex( - QiskitError, r"The backend defaults are unavailable. The backend may not support pulse." - ): - with self.assertWarns(DeprecationWarning): - schedule(self.circ, self.backend) - - def test_measurement_map_and_backend_not_supplied(self): - """Test measurement map and backend not supplied.""" - with self.assertRaisesRegex( - QiskitError, - r"Must supply either a backend or a meas_map for scheduling passes.", - ): - with self.assertWarns(DeprecationWarning): - schedule(self.circ, inst_map=InstructionScheduleMap()) - - def test_schedules_single_circuit(self): - """Test scheduling of a single circuit.""" - with self.assertWarns(DeprecationWarning): - circuit_schedule = schedule(self.circ, self.backend) - - self.assertIsInstance(circuit_schedule, Schedule) - self.assertEqual(circuit_schedule.name, "circ") - - def test_schedules_multiple_circuits(self): - """Test scheduling of multiple circuits.""" - self.enable_parallel_processing() - - circuits = [self.circ, self.circ2] - with self.assertWarns(DeprecationWarning): - circuit_schedules = schedule(circuits, self.backend, method="asap") - self.assertEqual(len(circuit_schedules), len(circuits)) - - circuit_one_schedule = circuit_schedules[0] - circuit_two_schedule = circuit_schedules[1] - - with self.assertWarns(DeprecationWarning): - self.assertEqual( - circuit_one_schedule, - schedule(self.circ, self.backend, method="asap"), - ) - - self.assertEqual( - circuit_two_schedule, - schedule(self.circ2, self.backend, method="asap"), - ) diff --git a/test/python/compiler/test_sequencer.py b/test/python/compiler/test_sequencer.py deleted file mode 100644 index 3fcfc16674a..00000000000 --- a/test/python/compiler/test_sequencer.py +++ /dev/null @@ -1,109 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020, 2024. -# -# 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. - -# pylint: disable=missing-function-docstring - -"""Tests basic functionality of the sequence function""" -# TODO with the removal of pulses, this file can be removed too. - -import unittest - -from qiskit import QuantumCircuit, pulse -from qiskit.compiler import sequence, transpile, schedule -from qiskit.pulse.transforms import pad -from qiskit.providers.fake_provider import Fake127QPulseV1 -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestSequence(QiskitTestCase): - """Test sequence function.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = Fake127QPulseV1() - self.backend.configuration().timing_constraints = {} - - def test_sequence_empty(self): - with self.assertWarns(DeprecationWarning): - self.assertEqual(sequence([], self.backend), []) - - def test_transpile_and_sequence_agree_with_schedule(self): - qc = QuantumCircuit(2, name="bell") - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - sc = transpile(qc, self.backend, scheduling_method="alap") - with self.assertWarns(DeprecationWarning): - actual = sequence(sc, self.backend) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - expected = schedule(transpile(qc, self.backend), self.backend) - with self.assertWarns(DeprecationWarning): - # pad adds Delay which is deprecated - self.assertEqual(actual, pad(expected)) - - def test_transpile_and_sequence_agree_with_schedule_for_circuit_with_delay(self): - qc = QuantumCircuit(1, 1, name="t2") - qc.h(0) - qc.delay(500, 0, unit="ns") - qc.h(0) - qc.measure(0, 0) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - sc = transpile(qc, self.backend, scheduling_method="alap") - with self.assertWarns(DeprecationWarning): - actual = sequence(sc, self.backend) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - expected = schedule(transpile(qc, self.backend), self.backend) - with self.assertWarns(DeprecationWarning): - self.assertEqual( - actual.exclude(instruction_types=[pulse.Delay]), - expected.exclude(instruction_types=[pulse.Delay]), - ) - - @unittest.skip("not yet determined if delays on ancilla should be removed or not") - def test_transpile_and_sequence_agree_with_schedule_for_circuits_without_measures(self): - qc = QuantumCircuit(2, name="bell_without_measurement") - qc.h(0) - qc.cx(0, 1) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - sc = transpile(qc, self.backend, scheduling_method="alap") - with self.assertWarns(DeprecationWarning): - actual = sequence(sc, self.backend) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - with self.assertWarns(DeprecationWarning): - expected = schedule(transpile(qc, self.backend), self.backend) - self.assertEqual(actual, pad(expected)) diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 9501d176c9e..dcc113f5742 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -12,10 +12,9 @@ """Test pulse builder context utilities.""" -from math import pi import numpy as np -from qiskit import circuit, compiler, pulse +from qiskit import circuit, pulse from qiskit.pulse import builder, exceptions, macros from qiskit.pulse.instructions import directives from qiskit.pulse.transforms import target_qobj_transform @@ -764,102 +763,6 @@ def test_delay_qubits(self): class TestBuilderComposition(TestBuilder): """Test more sophisticated composite builder examples.""" - def test_complex_build(self): - """Test a general program build with nested contexts, - circuits and macros.""" - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - d2 = pulse.DriveChannel(2) - delay_dur = 30 - short_dur = 20 - long_dur = 49 - - def get_sched(qubit_idx: [int], backend): - qc = circuit.QuantumCircuit(2) - for idx in qubit_idx: - qc.append(circuit.library.U2Gate(0, pi / 2), [idx]) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - transpiled = compiler.transpile(qc, backend=backend, optimization_level=1) - with self.assertWarns(DeprecationWarning): - return compiler.schedule(transpiled, backend) - - with pulse.build(self.backend) as schedule: - with pulse.align_sequential(): - pulse.delay(delay_dur, d0) - pulse.call(get_sched([1], self.backend)) - - with pulse.align_right(): - pulse.play(library.Constant(short_dur, 0.1), d1) - pulse.play(library.Constant(long_dur, 0.1), d2) - pulse.call(get_sched([1], self.backend)) - - with pulse.align_left(): - pulse.call(get_sched([0, 1, 0], self.backend)) - - pulse.measure(0) - - # prepare and schedule circuits that will be used. - single_u2_qc = circuit.QuantumCircuit(2) - single_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - single_u2_qc = compiler.transpile(single_u2_qc, self.backend, optimization_level=1) - with self.assertWarns(DeprecationWarning): - single_u2_sched = compiler.schedule(single_u2_qc, self.backend) - - # sequential context - sequential_reference = pulse.Schedule() - sequential_reference += instructions.Delay(delay_dur, d0) - sequential_reference.insert(delay_dur, single_u2_sched, inplace=True) - - # align right - align_right_reference = pulse.Schedule() - align_right_reference += pulse.Play(library.Constant(long_dur, 0.1), d2) - align_right_reference.insert( - long_dur - single_u2_sched.duration, single_u2_sched, inplace=True - ) - align_right_reference.insert( - long_dur - single_u2_sched.duration - short_dur, - pulse.Play(library.Constant(short_dur, 0.1), d1), - inplace=True, - ) - - # align left - triple_u2_qc = circuit.QuantumCircuit(2) - triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) - triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [1]) - triple_u2_qc.append(circuit.library.U2Gate(0, pi / 2), [0]) - with self.assertWarnsRegex( - DeprecationWarning, - expected_regex="The `transpile` function will " - "stop supporting inputs of type `BackendV1`", - ): - triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend, optimization_level=1) - with self.assertWarns(DeprecationWarning): - align_left_reference = compiler.schedule(triple_u2_qc, self.backend, method="alap") - - # measurement - measure_reference = macros.measure( - qubits=[0], inst_map=self.inst_map, meas_map=self.configuration.meas_map - ) - reference = pulse.Schedule() - reference += sequential_reference - # Insert so that the long pulse on d2 occurs as early as possible - # without an overval on d1. - insert_time = reference.ch_stop_time(d1) - align_right_reference.ch_start_time(d1) - reference.insert(insert_time, align_right_reference, inplace=True) - reference.insert(reference.ch_stop_time(d0, d1), align_left_reference, inplace=True) - reference += measure_reference - - self.assertScheduleEqual(schedule, reference) - @decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSubroutineCall(TestBuilder): diff --git a/test/python/scheduler/__init__.py b/test/python/scheduler/__init__.py deleted file mode 100644 index b56983c88a1..00000000000 --- a/test/python/scheduler/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - -# pylint: disable=cyclic-import - -"""Qiskit pulse scheduling tests.""" diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py deleted file mode 100644 index 5adbb3fdc1f..00000000000 --- a/test/python/scheduler/test_basic_scheduler.py +++ /dev/null @@ -1,1218 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019, 2024. -# -# 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 cases for the pulse scheduler passes.""" - -import numpy as np -from numpy import pi -from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, schedule -from qiskit.circuit import Gate, Parameter -from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, SXGate -from qiskit.exceptions import QiskitError -from qiskit.pulse import ( - Schedule, - DriveChannel, - AcquireChannel, - Acquire, - MeasureChannel, - MemorySlot, - Gaussian, - GaussianSquare, - Play, - Waveform, - transforms, -) -from qiskit.pulse import build, macros, play, InstructionScheduleMap -from qiskit.providers.fake_provider import ( - FakeBackend, - FakeOpenPulse2Q, - FakeOpenPulse3Q, - GenericBackendV2, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestBasicSchedule(QiskitTestCase): - """Scheduling tests.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = FakeOpenPulse2Q() - self.inst_map = self.backend.defaults().instruction_schedule_map - - def test_unavailable_defaults(self): - """Test backend with unavailable defaults.""" - qr = QuantumRegister(1) - qc = QuantumCircuit(qr) - with self.assertWarns(DeprecationWarning): - backend = FakeBackend(None) - backend.defaults = backend.configuration - with self.assertWarns(DeprecationWarning): - self.assertRaises(QiskitError, lambda: schedule(qc, backend)) - - def test_alap_pass(self): - """Test ALAP scheduling.""" - - # ┌───────────────┐ ░ ┌─┐ - # q0_0: ┤ U2(3.14,1.57) ├────────────────────░───■──┤M├─── - # └┬──────────────┤ ░ ┌──────────────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ─┤ U2(0.5,0.25) ├─░─┤ U2(0.5,0.25) ├─░─┤ X ├─╫─┤M├ - # └──────────────┘ ░ └──────────────┘ ░ └───┘ ║ └╥┘ - # c0: 2/═════════════════════════════════════════════╩══╩═ - # 0 1 - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(3.14, 1.57), [q[0]]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[0], [q[1]]) - qc.cx(q[0], q[1]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - # X pulse on q0 should end at the start of the CNOT - with self.assertWarns(DeprecationWarning): - expected = Schedule( - (2, self.inst_map.get("u2", [0], 3.14, 1.57)), - self.inst_map.get("u2", [1], 0.5, 0.25), - (2, self.inst_map.get("u2", [1], 0.5, 0.25)), - (4, self.inst_map.get("cx", [0, 1])), - (26, self.inst_map.get("measure", [0, 1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_single_circuit_list_schedule(self): - """Test that passing a single circuit list to schedule() returns a list.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule([qc], self.backend, method="alap") - expected = Schedule() - self.assertIsInstance(sched, list) - self.assertEqual(sched[0].instructions, expected.instructions) - - def test_alap_with_barriers(self): - """Test that ALAP respects barriers on new qubits.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), (2, self.inst_map.get("u2", [1], 0, 0)) - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_empty_circuit_schedule(self): - """Test empty circuit being scheduled.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule() - self.assertEqual(sched.instructions, expected.instructions) - - def test_alap_aligns_end(self): - """Test that ALAP always acts as though there is a final global barrier.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U3Gate(0, 0, 0), [q[0]]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected_sched = Schedule( - (2, self.inst_map.get("u2", [1], 0, 0)), self.inst_map.get("u3", [0], 0, 0, 0) - ) - for actual, expected in zip(sched.instructions, expected_sched.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - with self.assertWarns(DeprecationWarning): - self.assertEqual( - sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) - ) - - def test_asap_pass(self): - """Test ASAP scheduling.""" - - # ┌───────────────┐ ░ ┌─┐ - # q0_0: ┤ U2(3.14,1.57) ├────────────────────░───■──┤M├─── - # └┬──────────────┤ ░ ┌──────────────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ─┤ U2(0.5,0.25) ├─░─┤ U2(0.5,0.25) ├─░─┤ X ├─╫─┤M├ - # └──────────────┘ ░ └──────────────┘ ░ └───┘ ║ └╥┘ - # c0: 2/═════════════════════════════════════════════╩══╩═ - # 0 1 - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(3.14, 1.57), [q[0]]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.barrier(q[0], q[1]) - qc.cx(q[0], q[1]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_soon_as_possible") - # X pulse on q0 should start at t=0 - expected = Schedule( - self.inst_map.get("u2", [0], 3.14, 1.57), - self.inst_map.get("u2", [1], 0.5, 0.25), - (2, self.inst_map.get("u2", [1], 0.5, 0.25)), - (4, self.inst_map.get("cx", [0, 1])), - (26, self.inst_map.get("measure", [0, 1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_alap_resource_respecting(self): - """Test that the ALAP pass properly respects busy resources when backwards scheduling. - For instance, a CX on 0 and 1 followed by an X on only 1 must respect both qubits' - timeline.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - insts = sched.instructions - self.assertEqual(insts[0][0], 0) - self.assertEqual(insts[6][0], 22) - - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.append(U2Gate(0.5, 0.25), [q[1]]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - self.assertEqual(sched.instructions[-1][0], 24) - - def test_inst_map_schedules_unaltered(self): - """Test that forward scheduling doesn't change relative timing with a command.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched1 = schedule(qc, self.backend, method="as_soon_as_possible") - sched2 = schedule(qc, self.backend, method="as_late_as_possible") - for asap, alap in zip(sched1.instructions, sched2.instructions): - self.assertEqual(asap[0], alap[0]) - self.assertEqual(asap[1], alap[1]) - insts = sched1.instructions - self.assertEqual(insts[0][0], 0) # shift phase - self.assertEqual(insts[1][0], 0) # ym_d0 - self.assertEqual(insts[2][0], 0) # x90p_d1 - self.assertEqual(insts[3][0], 2) # cr90p_u0 - self.assertEqual(insts[4][0], 11) # xp_d0 - self.assertEqual(insts[5][0], 13) # cr90m_u0 - - def test_measure_combined(self): - """ - Test to check for measure on the same qubit which generated another measure schedule. - - The measures on different qubits are combined, but measures on the same qubit - adds another measure to the schedule. - """ - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(3.14, 1.57), [q[0]]) - qc.cx(q[0], q[1]) - qc.measure(q[0], c[0]) - qc.measure(q[1], c[1]) - qc.measure(q[1], c[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_soon_as_possible") - expected = Schedule( - self.inst_map.get("u2", [0], 3.14, 1.57), - (2, self.inst_map.get("cx", [0, 1])), - (24, self.inst_map.get("measure", [0, 1])), - (34, self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(1)])), - (34, Acquire(10, AcquireChannel(1), MemorySlot(1))), - ) - self.assertEqual(sched.instructions, expected.instructions) - - # - def test_3q_schedule(self): - """Test a schedule that was recommended by David McKay :D""" - - # ┌─────────────────┐ - # q0_0: ─────────■─────────┤ U3(3.14,1.57,0) ├──────────────────────── - # ┌─┴─┐ └┬───────────────┬┘ - # q0_1: ───────┤ X ├────────┤ U2(3.14,1.57) ├───■───────────────────── - # ┌──────┴───┴──────┐ └───────────────┘ ┌─┴─┐┌─────────────────┐ - # q0_2: ┤ U2(0.778,0.122) ├───────────────────┤ X ├┤ U2(0.778,0.122) ├ - # └─────────────────┘ └───┘└─────────────────┘ - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - inst_map = backend.defaults().instruction_schedule_map - q = QuantumRegister(3) - c = ClassicalRegister(3) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.append(U2Gate(0.778, 0.122), [q[2]]) - qc.append(U3Gate(3.14, 1.57, 0), [q[0]]) - qc.append(U2Gate(3.14, 1.57), [q[1]]) - qc.cx(q[1], q[2]) - qc.append(U2Gate(0.778, 0.122), [q[2]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, backend) - expected = Schedule( - inst_map.get("cx", [0, 1]), - (22, inst_map.get("u2", [1], 3.14, 1.57)), - (22, inst_map.get("u2", [2], 0.778, 0.122)), - (24, inst_map.get("cx", [1, 2])), - (44, inst_map.get("u3", [0], 3.14, 1.57, 0)), - (46, inst_map.get("u2", [2], 0.778, 0.122)), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_schedule_multi(self): - """Test scheduling multiple circuits at once.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc0 = QuantumCircuit(q, c) - qc0.cx(q[0], q[1]) - qc1 = QuantumCircuit(q, c) - qc1.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - schedules = schedule([qc0, qc1], self.backend) - expected_insts = schedule(qc0, self.backend).instructions - for actual, expected in zip(schedules[0].instructions, expected_insts): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_circuit_name_kept(self): - """Test that the new schedule gets its name from the circuit.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c, name="CIRCNAME") - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual(sched.name, qc.name) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual(sched.name, qc.name) - - def test_can_add_gates_into_free_space(self): - """The scheduler does some time bookkeeping to know when qubits are free to be - scheduled. Make sure this works for qubits that are used in the future. This was - a bug, uncovered by this example: - - q0 = - - - - |X| - q1 = |X| |u2| |X| - - In ALAP scheduling, the next operation on qubit 0 would be added at t=0 rather - than immediately before the X gate. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.append(U2Gate(0, 0), [qr[i]]) - qc.append(U1Gate(3.14), [qr[i]]) - qc.append(U2Gate(0, 0), [qr[i]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), - self.inst_map.get("u2", [1], 0, 0), - (2, self.inst_map.get("u1", [0], 3.14)), - (2, self.inst_map.get("u1", [1], 3.14)), - (2, self.inst_map.get("u2", [0], 0, 0)), - (2, self.inst_map.get("u2", [1], 0, 0)), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_barriers_in_middle(self): - """As a follow on to `test_can_add_gates_into_free_space`, similar issues - arose for barriers, specifically. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.append(U2Gate(0, 0), [qr[i]]) - qc.barrier(qr[i]) - qc.append(U1Gate(3.14), [qr[i]]) - qc.barrier(qr[i]) - qc.append(U2Gate(0, 0), [qr[i]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), - self.inst_map.get("u2", [1], 0, 0), - (2, self.inst_map.get("u1", [0], 3.14)), - (2, self.inst_map.get("u1", [1], 3.14)), - (2, self.inst_map.get("u2", [0], 0, 0)), - (2, self.inst_map.get("u2", [1], 0, 0)), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_parametric_input(self): - """Test that scheduling works with parametric pulses as input.""" - qr = QuantumRegister(1) - qc = QuantumCircuit(qr) - qc.append(Gate("gauss", 1, []), qargs=[qr[0]]) - with self.assertWarns(DeprecationWarning): - custom_gauss = Schedule( - Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) - ) - self.inst_map.add("gauss", [0], custom_gauss) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=self.inst_map) - self.assertEqual(sched.instructions[0], custom_gauss.instructions[0]) - - def test_pulse_gates(self): - """Test scheduling calibrated pulse gates.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - qc.add_calibration( - "u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0] - ) - qc.add_calibration( - "u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0] - ) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - expected = Schedule( - Play(Gaussian(28, 0.2, 4), DriveChannel(0)), - (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_calibrated_measurements(self): - """Test scheduling calibrated measurements.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(U2Gate(0, 0), [q[0]]) - qc.measure(q[0], c[0]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - sched = schedule(qc, self.backend) - expected = Schedule(self.inst_map.get("u2", [0], 0, 0), (2, meas_sched)) - self.assertEqual(sched.instructions, expected.instructions) - - def test_subset_calibrated_measurements(self): - """Test that measurement calibrations can be added and used for some qubits, even - if the other qubits do not also have calibrated measurements.""" - qc = QuantumCircuit(3, 3) - qc.measure(0, 0) - qc.measure(1, 1) - qc.measure(2, 2) - meas_scheds = [] - for qubit in [0, 2]: - with self.assertWarns(DeprecationWarning): - meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( - 1200, AcquireChannel(qubit), MemorySlot(qubit) - ) - meas_scheds.append(meas) - with self.assertWarns(DeprecationWarning): - qc.add_calibration("measure", [qubit], meas) - - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - meas = macros.measure([1], backend) - meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) - with self.assertWarns(DeprecationWarning): - backend = FakeOpenPulse3Q() - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, backend) - expected = Schedule(meas_scheds[0], meas_scheds[1], meas) - self.assertEqual(sched.instructions, expected.instructions) - - def test_clbits_of_calibrated_measurements(self): - """Test that calibrated measurements are only used when the classical bits also match.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.measure(q[0], c[1]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - # Doesn't use the calibrated schedule because the classical memory slots do not match - with self.assertWarns(DeprecationWarning): - expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) - self.assertEqual(sched.instructions, expected.instructions) - - def test_metadata_is_preserved_alap(self): - """Test that circuit metadata is preserved in output schedule with alap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_metadata_is_preserved_asap(self): - """Test that circuit metadata is preserved in output schedule with asap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_scheduler_with_params_bound(self): - """Test scheduler with parameters defined and bound""" - x = Parameter("x") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - expected_schedule = Schedule() - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - qc = qc.assign_parameters({x: 1}) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - self.assertEqual(sched, expected_schedule) - - def test_scheduler_with_params_not_bound(self): - """Test scheduler with parameters defined but not bound""" - x = Parameter("amp") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - with build() as expected_schedule: - play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - sched = schedule(qc, self.backend) - self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) - - def test_schedule_block_in_instmap(self): - """Test schedule block in instmap can be scheduled.""" - duration = Parameter("duration") - - with self.assertWarns(DeprecationWarning): - with build() as pulse_prog: - play(Gaussian(duration, 0.1, 10), DriveChannel(0)) - - instmap = InstructionScheduleMap() - instmap.add("block_gate", (0,), pulse_prog, ["duration"]) - - qc = QuantumCircuit(1) - qc.append(Gate("block_gate", 1, [duration]), [0]) - qc.assign_parameters({duration: 100}, inplace=True) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=instmap) - - with self.assertWarns(DeprecationWarning): - ref_sched = Schedule() - ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) - - self.assertEqual(sched, ref_sched) - - -class TestBasicScheduleV2(QiskitTestCase): - """Scheduling tests.""" - - def setUp(self): - super().setUp() - with self.assertWarns(DeprecationWarning): - self.backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) - self.inst_map = self.backend.instruction_schedule_map - # self.pulse_2_samples is the pulse sequence used to calibrate "measure" in - # GenericBackendV2. See class construction for more details. - self.pulse_2_samples = np.linspace(0, 1.0, 32, dtype=np.complex128) - - def test_alap_pass(self): - """Test ALAP scheduling.""" - - # ┌────┐ ░ ┌─┐ - # q0_0: ┤ √X ├──────────░───■──┤M├─── - # ├────┤ ░ ┌────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ┤ √X ├─░─┤ √X ├─░─┤ X ├─╫─┤M├ - # └────┘ ░ └────┘ ░ └───┘ ║ └╥┘ - # c0: 2/════════════════════════╩══╩═ - # 0 1 - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - - qc.sx(q[0]) - qc.sx(q[1]) - qc.barrier(q[1]) - - qc.sx(q[1]) - qc.barrier(q[0], q[1]) - - qc.cx(q[0], q[1]) - qc.measure(q, c) - - with self.assertWarns(DeprecationWarning): - sched = schedule(circuits=qc, backend=self.backend, method="alap") - - # Since, the method of scheduling chosen here is 'as_late_as_possible' - # so all the π/2 pulse here should be right shifted. - # - # Calculations: - # Duration of the π/2 pulse for GenericBackendV2 backend is 16dt - # first π/2 pulse on q0 should start at 16dt because of 'as_late_as_possible'. - # first π/2 pulse on q1 should start 0dt. - # second π/2 pulse on q1 should start with a delay of 16dt. - # cx pulse( pulse on drive channel, control channel) should start with a delay - # of 16dt+16dt. - # measure pulse should start with a delay of 16dt+16dt+64dt(64dt for cx gate). - with self.assertWarns(DeprecationWarning): - expected = Schedule( - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("sx", [0])), # Right shifted because of alap. - (0 + 16, self.inst_map.get("sx", [1])), - (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", - ), - ), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", - ), - ), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_single_circuit_list_schedule(self): - """Test that passing a single circuit list to schedule() returns a list.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule([qc], self.backend, method="alap") - expected = Schedule() - self.assertIsInstance(sched, list) - self.assertEqual(sched[0].instructions, expected.instructions) - - def test_alap_with_barriers(self): - """Test that ALAP respects barriers on new qubits.""" - - # ┌────┐ ░ - # q0_0: ┤ √X ├─░─────── - # └────┘ ░ ┌────┐ - # q0_1: ───────░─┤ √X ├ - # ░ └────┘ - # c0: 2/═══════════════ - # - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.append(SXGate(), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(SXGate(), [q[1]]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - # If there wasn't a barrier the π/2 pulse on q1 would have started from 0dt, but since, - # there is a barrier so the π/2 pulse on q1 should start with a delay of 160dt. - expected = Schedule( - (0, self.inst_map.get("sx", [0])), (16, self.inst_map.get("sx", [1])) - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual, expected) - - def test_empty_circuit_schedule(self): - """Test empty circuit being scheduled.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule() - self.assertEqual(sched.instructions, expected.instructions) - - def test_alap_aligns_end(self): - """Test that ALAP always acts as though there is a final global barrier.""" - # ┌────┐ - # q1_0: ┤ √X ├ - # ├────┤ - # q1_1: ┤ √X ├ - # └────┘ - # c1: 2/══════ - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.sx(q[0]) - qc.sx(q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected_sched = Schedule( - (0, self.inst_map.get("sx", [1])), (0, self.inst_map.get("sx", [0])) - ) - for actual, expected in zip(sched.instructions, expected_sched.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - with self.assertWarns(DeprecationWarning): - self.assertEqual( - sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) - ) - - def test_asap_pass(self): - """Test ASAP scheduling.""" - - # ┌────┐ ░ ┌─┐ - # q0_0: ┤ √X ├──────────░───■──┤M├─── - # ├────┤ ░ ┌────┐ ░ ┌─┴─┐└╥┘┌─┐ - # q0_1: ┤ √X ├─░─┤ √X ├─░─┤ X ├─╫─┤M├ - # └────┘ ░ └────┘ ░ └───┘ ║ └╥┘ - # c0: 2/════════════════════════╩══╩═ - # 0 1 - - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - - qc.sx(q[0]) - qc.sx(q[1]) - qc.barrier(q[1]) - - qc.sx(q[1]) - qc.barrier(q[0], q[1]) - - qc.cx(q[0], q[1]) - qc.measure(q, c) - - with self.assertWarns(DeprecationWarning): - sched = schedule(circuits=qc, backend=self.backend, method="asap") - # Since, the method of scheduling chosen here is 'as_soon_as_possible' - # so all the π/2 pulse here should be left shifted. - # - # Calculations: - # Duration of the π/2 pulse for FakePerth backend is 16dt - # first π/2 pulse on q0 should start at 0dt because of 'as_soon_as_possible'. - # first π/2 pulse on q1 should start 0dt. - # second π/2 pulse on q1 should start with a delay of 16dt. - # cx pulse( pulse on drive channel, control channel) should start with a delay - # of 16dt+16dt. - # measure pulse should start with a delay of 16dt+16dt+64dt(64dt for cx gate). - with self.assertWarns(DeprecationWarning): - expected = Schedule( - (0, self.inst_map.get("sx", [1])), - (0, self.inst_map.get("sx", [0])), # Left shifted because of asap. - (0 + 16, self.inst_map.get("sx", [1])), - (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", - ), - ), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", - ), - ), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_alap_resource_respecting(self): - """Test that the ALAP pass properly respects busy resources when backwards scheduling. - For instance, a CX on 0 and 1 followed by an X on only 1 must respect both qubits' - timeline.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.sx(q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - insts = sched.instructions - - # This is ShiftPhase for the cx operation. - self.assertEqual(insts[0][0], 0) - - # It takes 4 pulse operations on DriveChannel and ControlChannel to do a - # cx operation on this backend. - # 64dt is duration of cx operation on this backend. - self.assertEqual(insts[4][0], 64) - - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.sx(q[1]) - qc.measure(q, c) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_late_as_possible") - - # 64dt for cx operation + 16dt for sx operation - # So, the pulses in MeasureChannel0 and 1 starts from 80dt. - self.assertEqual(sched.instructions[-1][0], 80) - - def test_inst_map_schedules_unaltered(self): - """Test that forward scheduling doesn't change relative timing with a command.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched1 = schedule(qc, self.backend, method="as_soon_as_possible") - sched2 = schedule(qc, self.backend, method="as_late_as_possible") - for asap, alap in zip(sched1.instructions, sched2.instructions): - self.assertEqual(asap[0], alap[0]) - self.assertEqual(asap[1], alap[1]) - insts = sched1.instructions - self.assertEqual(insts[0][0], 0) # ShiftPhase at DriveChannel(0) no dt required. - self.assertEqual(insts[1][0], 0) # ShiftPhase at ControlChannel(1) no dt required. - self.assertEqual(insts[2][0], 0) # Pulse pulse_2 of duration 32dt. - self.assertEqual(insts[3][0], 0) # Pulse pulse_3 of duration 160dt. - - def test_measure_combined(self): - """ - Test to check for measure on the same qubit which generated another measure schedule. - - The measures on different qubits are combined, but measures on the same qubit - adds another measure to the schedule. - """ - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.sx(q[0]) - qc.cx(q[0], q[1]) - qc.measure(q[0], c[0]) - qc.measure(q[1], c[1]) - qc.measure(q[1], c[1]) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="as_soon_as_possible") - - expected_sched = Schedule( - # This is the schedule to implement sx gate. - (0, self.inst_map.get("sx", [0])), - # This is the schedule to implement cx gate - (0 + 16, self.inst_map.get("cx", [0, 1])), - # This is the schedule for the measurements on qubits 0 and 1 (combined) - ( - 0 + 16 + 64, - self.inst_map.get("measure", [0]).filter( - channels=[MeasureChannel(0), MeasureChannel(1)] - ), - ), - ( - 0 + 16 + 64, - self.inst_map.get("measure", [0]).filter( - channels=[AcquireChannel(0), AcquireChannel(1)] - ), - ), - # This is the schedule for the second measurement on qubit 1 - ( - 0 + 16 + 64 + 1792, - self.inst_map.get("measure", [1]).filter(channels=[MeasureChannel(1)]), - ), - ( - 0 + 16 + 64 + 1792, - self.inst_map.get("measure", [1]).filter(channels=[AcquireChannel(1)]), - ), - ) - self.assertEqual(sched.instructions, expected_sched.instructions) - - def test_3q_schedule(self): - """Test a schedule that was recommended by David McKay :D""" - - # ┌────┐ - # q0_0: ──■───┤ √X ├─────────── - # ┌─┴─┐ ├───┬┘ - # q0_1: ┤ X ├─┤ X ├───■──────── - # ├───┴┐└───┘ ┌─┴─┐┌────┐ - # q0_2: ┤ √X ├──────┤ X ├┤ √X ├ - # └────┘ └───┘└────┘ - # c0: 3/═══════════════════════ - - q = QuantumRegister(3) - c = ClassicalRegister(3) - qc = QuantumCircuit(q, c) - qc.cx(q[0], q[1]) - qc.sx(q[0]) - qc.x(q[1]) - qc.sx(q[2]) - qc.cx(q[1], q[2]) - qc.sx(q[2]) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - expected = Schedule( - (0, self.inst_map.get("cx", [0, 1])), - (0, self.inst_map.get("sx", [2])), - (0 + 64, self.inst_map.get("sx", [0])), - (0 + 64, self.inst_map.get("x", [1])), - (0 + 64 + 16, self.inst_map.get("cx", [1, 2])), - (0 + 64 + 16 + 64, self.inst_map.get("sx", [2])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_schedule_multi(self): - """Test scheduling multiple circuits at once.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc0 = QuantumCircuit(q, c) - qc0.cx(q[0], q[1]) - qc1 = QuantumCircuit(q, c) - qc1.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - schedules = schedule([qc0, qc1], self.backend) - expected_insts = schedule(qc0, self.backend).instructions - for actual, expected in zip(schedules[0].instructions, expected_insts): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_circuit_name_kept(self): - """Test that the new schedule gets its name from the circuit.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c, name="CIRCNAME") - qc.cx(q[0], q[1]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual(sched.name, qc.name) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual(sched.name, qc.name) - - def test_can_add_gates_into_free_space(self): - """The scheduler does some time bookkeeping to know when qubits are free to be - scheduled. Make sure this works for qubits that are used in the future. This was - a bug, uncovered by this example: - - q0 = - - - - |X| - q1 = |X| |u2| |X| - - In ALAP scheduling, the next operation on qubit 0 would be added at t=0 rather - than immediately before the X gate. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.sx(qr[i]) - qc.x(qr[i]) - qc.sx(qr[i]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - (0, self.inst_map.get("sx", [0])), - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("x", [0])), - (0 + 16, self.inst_map.get("x", [1])), - (0 + 16 + 16, self.inst_map.get("sx", [0])), - (0 + 16 + 16, self.inst_map.get("sx", [1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_barriers_in_middle(self): - """As a follow on to `test_can_add_gates_into_free_space`, similar issues - arose for barriers, specifically. - """ - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - for i in range(2): - qc.sx(qr[i]) - qc.barrier(qr[i]) - qc.x(qr[i]) - qc.barrier(qr[i]) - qc.sx(qr[i]) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - (0, self.inst_map.get("sx", [0])), - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("x", [0])), - (0 + 16, self.inst_map.get("x", [1])), - (0 + 16 + 16, self.inst_map.get("sx", [0])), - (0 + 16 + 16, self.inst_map.get("sx", [1])), - ) - for actual, expected in zip(sched.instructions, expected.instructions): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) - - def test_parametric_input(self): - """Test that scheduling works with parametric pulses as input.""" - qr = QuantumRegister(1) - qc = QuantumCircuit(qr) - qc.append(Gate("gauss", 1, []), qargs=[qr[0]]) - with self.assertWarns(DeprecationWarning): - custom_gauss = Schedule( - Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) - ) - self.inst_map.add("gauss", [0], custom_gauss) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=self.inst_map) - self.assertEqual(sched.instructions[0], custom_gauss.instructions[0]) - - def test_pulse_gates(self): - """Test scheduling calibrated pulse gates.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.append(U2Gate(0, 0), [q[0]]) - qc.barrier(q[0], q[1]) - qc.append(U2Gate(0, 0), [q[1]]) - with self.assertWarns(DeprecationWarning): - qc.add_calibration( - "u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0] - ) - qc.add_calibration( - "u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0] - ) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - expected = Schedule( - Play(Gaussian(28, 0.2, 4), DriveChannel(0)), - (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), - ) - self.assertEqual(sched.instructions, expected.instructions) - - def test_calibrated_measurements(self): - """Test scheduling calibrated measurements.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.sx(0) - qc.measure(q[0], c[0]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play( - GaussianSquare( - duration=1472, - sigma=64, - width=1216, - amp=0.2400000000002, - angle=-0.247301694, - name="my_custom_calibration", - ), - MeasureChannel(0), - ) - meas_sched |= Acquire(1472, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - sched = schedule(qc, self.backend) - expected = Schedule(self.inst_map.get("sx", [0]), (16, meas_sched)) - self.assertEqual(sched.instructions, expected.instructions) - - def test_subset_calibrated_measurements(self): - """Test that measurement calibrations can be added and used for some qubits, even - if the other qubits do not also have calibrated measurements.""" - qc = QuantumCircuit(3, 3) - qc.measure(0, 0) - qc.measure(1, 1) - qc.measure(2, 2) - meas_scheds = [] - with self.assertWarns(DeprecationWarning): - for qubit in [0, 2]: - meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( - 1200, AcquireChannel(qubit), MemorySlot(qubit) - ) - meas_scheds.append(meas) - with self.assertWarns(DeprecationWarning): - qc.add_calibration("measure", [qubit], meas) - - meas = macros.measure(qubits=[1], backend=self.backend, qubit_mem_slots={0: 0, 1: 1}) - meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) - sched = schedule(qc, self.backend) - expected = Schedule(meas_scheds[0], meas_scheds[1], meas) - self.assertEqual(sched.instructions, expected.instructions) - - def test_clbits_of_calibrated_measurements(self): - """Test that calibrated measurements are only used when the classical bits also match.""" - q = QuantumRegister(2) - c = ClassicalRegister(2) - qc = QuantumCircuit(q, c) - qc.measure(q[0], c[1]) - - with self.assertWarns(DeprecationWarning): - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - sched = schedule(qc, self.backend) - # Doesn't use the calibrated schedule because the classical memory slots do not match - expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) - self.assertEqual(sched.instructions, expected.instructions) - - def test_metadata_is_preserved_alap(self): - """Test that circuit metadata is preserved in output schedule with alap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.sx(q[0]) - qc.barrier(q[0], q[1]) - qc.sx(q[1]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="alap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_metadata_is_preserved_asap(self): - """Test that circuit metadata is preserved in output schedule with asap.""" - q = QuantumRegister(2) - qc = QuantumCircuit(q) - qc.sx(q[0]) - qc.barrier(q[0], q[1]) - qc.sx(q[1]) - qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, method="asap") - self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) - - def test_scheduler_with_params_bound(self): - """Test scheduler with parameters defined and bound""" - x = Parameter("x") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - expected_schedule = Schedule() - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - qc = qc.assign_parameters({x: 1}) - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend) - self.assertEqual(sched, expected_schedule) - - def test_scheduler_with_params_not_bound(self): - """Test scheduler with parameters defined but not bound""" - x = Parameter("amp") - qc = QuantumCircuit(2) - qc.append(Gate("pulse_gate", 1, [x]), [0]) - with self.assertWarns(DeprecationWarning): - with build() as expected_schedule: - play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) - qc.add_calibration( - gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] - ) - sched = schedule(qc, self.backend) - with self.assertWarns(DeprecationWarning): - self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) - - def test_schedule_block_in_instmap(self): - """Test schedule block in instmap can be scheduled.""" - duration = Parameter("duration") - - with self.assertWarns(DeprecationWarning): - with build() as pulse_prog: - play(Gaussian(duration, 0.1, 10), DriveChannel(0)) - - instmap = InstructionScheduleMap() - instmap.add("block_gate", (0,), pulse_prog, ["duration"]) - - qc = QuantumCircuit(1) - qc.append(Gate("block_gate", 1, [duration]), [0]) - qc.assign_parameters({duration: 100}, inplace=True) - - with self.assertWarns(DeprecationWarning): - sched = schedule(qc, self.backend, inst_map=instmap) - - ref_sched = Schedule() - ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) - - self.assertEqual(sched, ref_sched) - - def test_inst_sched_map_get_measure_0(self): - """Test that Schedule returned by backend.instruction_schedule_map.get('measure', [0]) - is actually Schedule for just qubit_0""" - with self.assertWarns(DeprecationWarning): - sched_from_backend = self.backend.instruction_schedule_map.get("measure", [0]) - expected_sched = Schedule( - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", - ), - ), - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", - ), - ), - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(2), - name="pulse_2", - ), - ), - (0, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - (0, Acquire(1792, AcquireChannel(2), MemorySlot(2))), - name="measure", - ) - self.assertEqual(sched_from_backend, expected_sched) From 02850cfa95a2e724b6a1c64c74a15126761e72c3 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Mon, 10 Feb 2025 12:40:18 +0200 Subject: [PATCH 5/9] Add 2q fractional gates to the `UnitarySynthesis` transpiler pass (#13568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * TwoQubitControlledUDecomposer to _decomposer_2q_from_basis_gates * update (temporarily) basis gates in test * minor fix * add EulerBasis as a parameter to TwoQubitControlledUDecomposer * fix global_phase calculation in TwoQubitContolledUDecomposer * add TwoQubitControlledUDecomposer to the docs * make the choice of kak_gate deterministic * remove XXDecomposer from _decomposer_2q_from_basis_gates * make call_inner pub, add Clone, Debug * add TwoQubitControlledUDecomposer to unitary_synthesis.rs * Fix exit condition for GOODBYE_SET and PARAM_SET * make DEFAULT_ATOL public * add TwoQubitControlledUDecomposer to synth_su4_sequence * Add support for parametrized decomposer gate in apply_synth_sequence * change DecomposerType enum to fix clippy error * add a random unitary test to test_parametrized_basis_gate_in_target * add public new_inner for TwoQubitControlledUDecomposer * replace default 'ZYZ' by 'ZXZ' in TwoQubitControlledUDecomposer * remove using py in rust functions * minor update to test * make atol optional * add a test with fractional gates in the backend * add release notes * enhance tests following review * Add support for non-standard parametrized gates, add new tests. TODO: address TwoQubitControlledUDecomposer issue, it appends gates outside of basis set (h/s/sdg) * decompose S, Sdg, H into euler_basis * update test * Overwrite Python-side gate parameters as well as Rust-side parameters. * add examples to release notes --------- Co-authored-by: Elena Peña Tapia --- crates/accelerate/src/two_qubit_decompose.rs | 205 ++++++++++------ crates/accelerate/src/unitary_synthesis.rs | 227 +++++++++++++----- qiskit/synthesis/__init__.py | 2 + .../two_qubit/two_qubit_decompose.py | 24 +- .../passes/synthesis/unitary_synthesis.py | 48 +++- ...nitarysynthesis-pass-f66eee29903f5639.yaml | 37 +++ test/python/compiler/test_transpiler.py | 2 +- test/python/synthesis/test_synthesis.py | 30 ++- .../transpiler/test_unitary_synthesis.py | 107 ++++++++- 9 files changed, 528 insertions(+), 154 deletions(-) create mode 100644 releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index de20430d25b..5ce6d248c4e 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -54,7 +54,9 @@ use rand_pcg::Pcg64Mcg; use qiskit_circuit::circuit_data::CircuitData; use qiskit_circuit::circuit_instruction::OperationFromPython; -use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::gate_matrix::{ + CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SDG_GATE, SX_GATE, S_GATE, X_GATE, +}; use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; @@ -2448,23 +2450,25 @@ pub enum RXXEquivalent { } impl RXXEquivalent { - fn matrix(&self, py: Python, param: f64) -> PyResult> { + fn matrix(&self, param: f64) -> PyResult> { match self { Self::Standard(gate) => Ok(gate.matrix(&[Param::Float(param)]).unwrap()), - Self::CustomPython(gate_cls) => { + Self::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1((param,))?; let raw_matrix = gate_obj .call_method0(intern!(py, "to_matrix"))? .extract::>()?; Ok(raw_matrix.as_array().to_owned()) - } + }), } } } +#[derive(Clone, Debug)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitControlledUDecomposer { rxx_equivalent_gate: RXXEquivalent, + euler_basis: EulerBasis, #[pyo3(get)] scale: f64, } @@ -2479,7 +2483,6 @@ impl TwoQubitControlledUDecomposer { /// invert 2q gate sequence fn invert_2q_gate( &self, - py: Python, gate: (Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>), ) -> PyResult { let (gate, params, qubits) = gate; @@ -2516,7 +2519,7 @@ impl TwoQubitControlledUDecomposer { .collect::>(); Ok((Some(inv_gate.0), inv_gate_params, qubits)) } - RXXEquivalent::CustomPython(gate_cls) => { + RXXEquivalent::CustomPython(gate_cls) => Python::with_gil(|py: Python| { let gate_obj = gate_cls.bind(py).call1(PyTuple::new(py, params)?)?; let raw_inverse = gate_obj.call_method0(intern!(py, "inverse"))?; let inverse: OperationFromPython = raw_inverse.extract()?; @@ -2537,7 +2540,7 @@ impl TwoQubitControlledUDecomposer { "rxx gate inverse is not valid for this decomposer", )) } - } + }), } } } @@ -2550,20 +2553,19 @@ impl TwoQubitControlledUDecomposer { /// Circuit: Circuit equivalent to an RXXGate. /// Raises: /// QiskitError: If the circuit is not equivalent to an RXXGate. - fn to_rxx_gate(&self, py: Python, angle: f64) -> PyResult { + fn to_rxx_gate(&self, angle: f64) -> PyResult { // The user-provided RXXGate equivalent gate may be locally equivalent to the RXXGate // but with some scaling in the rotation angle. For example, RXXGate(angle) has Weyl // parameters (angle, 0, 0) for angle in [0, pi/2] but the user provided gate, i.e. // :code:`self.rxx_equivalent_gate(angle)` might produce the Weyl parameters // (scale * angle, 0, 0) where scale != 1. This is the case for the CPhaseGate. - let mat = self.rxx_equivalent_gate.matrix(py, self.scale * angle)?; + let mat = self.rxx_equivalent_gate.matrix(self.scale * angle)?; let decomposer_inv = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; - let euler_basis = EulerBasis::ZYZ; let mut target_1q_basis_list = EulerBasisSet::new(); - target_1q_basis_list.add_basis(euler_basis); + target_1q_basis_list.add_basis(self.euler_basis); // Express the RXXGate in terms of the user-provided RXXGate equivalent gate. let mut gates = Vec::with_capacity(13); @@ -2600,14 +2602,14 @@ impl TwoQubitControlledUDecomposer { gates.push((None, smallvec![self.scale * angle], smallvec![0, 1])); if let Some(unitary_k1r) = unitary_k1r { - global_phase += unitary_k1r.global_phase; + global_phase -= unitary_k1r.global_phase; for gate in unitary_k1r.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![0])); } } if let Some(unitary_k1l) = unitary_k1l { - global_phase += unitary_k1l.global_phase; + global_phase -= unitary_k1l.global_phase; for gate in unitary_k1l.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); gates.push((Some(inv_gate_name), inv_gate_params, smallvec![1])); @@ -2623,28 +2625,65 @@ impl TwoQubitControlledUDecomposer { /// Appends U_d(a, b, c) to the circuit. fn weyl_gate( &self, - py: Python, circ: &mut TwoQubitGateSequence, target_decomposed: TwoQubitWeylDecomposition, atol: f64, ) -> PyResult<()> { - let circ_a = self.to_rxx_gate(py, -2.0 * target_decomposed.a)?; + let circ_a = self.to_rxx_gate(-2.0 * target_decomposed.a)?; circ.gates.extend(circ_a.gates); let mut global_phase = circ_a.global_phase; + let mut target_1q_basis_list = EulerBasisSet::new(); + target_1q_basis_list.add_basis(self.euler_basis); + + let s_decomp = unitary_to_gate_sequence_inner( + aview2(&S_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + let sdg_decomp = unitary_to_gate_sequence_inner( + aview2(&SDG_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + let h_decomp = unitary_to_gate_sequence_inner( + aview2(&H_GATE), + &target_1q_basis_list, + 0, + None, + true, + None, + ); + // translate the RYYGate(b) into a circuit based on the desired Ctrl-U gate. if (target_decomposed.b).abs() > atol { - let circ_b = self.to_rxx_gate(py, -2.0 * target_decomposed.b)?; + let circ_b = self.to_rxx_gate(-2.0 * target_decomposed.b)?; global_phase += circ_b.global_phase; - circ.gates - .push((Some(StandardGate::SdgGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::SdgGate), smallvec![], smallvec![1])); + if let Some(sdg_decomp) = sdg_decomp { + global_phase += 2.0 * sdg_decomp.global_phase; + for gate in sdg_decomp.gates.into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } circ.gates.extend(circ_b.gates); - circ.gates - .push((Some(StandardGate::SGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::SGate), smallvec![], smallvec![1])); + if let Some(s_decomp) = s_decomp { + global_phase += 2.0 * s_decomp.global_phase; + for gate in s_decomp.gates.into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } // # translate the RZZGate(c) into a circuit based on the desired Ctrl-U gate. @@ -2656,36 +2695,57 @@ impl TwoQubitControlledUDecomposer { // circuit if c < 0. let mut gamma = -2.0 * target_decomposed.c; if gamma <= 0.0 { - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase += circ_c.global_phase; - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } circ.gates.extend(circ_c.gates); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } else { // invert the circuit above gamma *= -1.0; - let circ_c = self.to_rxx_gate(py, gamma)?; + let circ_c = self.to_rxx_gate(gamma)?; global_phase -= circ_c.global_phase; - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } for gate in circ_c.gates.into_iter().rev() { let (inv_gate_name, inv_gate_params, inv_gate_qubits) = - self.invert_2q_gate(py, gate)?; + self.invert_2q_gate(gate)?; circ.gates .push((inv_gate_name, inv_gate_params, inv_gate_qubits)); } - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![0])); - circ.gates - .push((Some(StandardGate::HGate), smallvec![], smallvec![1])); + if let Some(ref h_decomp) = h_decomp { + global_phase += 2.0 * h_decomp.global_phase; + for gate in h_decomp.gates.clone().into_iter() { + let gate_params = gate.1; + circ.gates + .push((Some(gate.0), gate_params.clone(), smallvec![0])); + circ.gates.push((Some(gate.0), gate_params, smallvec![1])); + } + } } } @@ -2695,18 +2755,16 @@ impl TwoQubitControlledUDecomposer { /// Returns the Weyl decomposition in circuit form. /// Note: atol is passed to OneQubitEulerDecomposer. - fn call_inner( + pub fn call_inner( &self, - py: Python, unitary: ArrayView2, - atol: f64, + atol: Option, ) -> PyResult { let target_decomposed = TwoQubitWeylDecomposition::new_inner(unitary, Some(DEFAULT_FIDELITY), None)?; - let euler_basis = EulerBasis::ZYZ; let mut target_1q_basis_list = EulerBasisSet::new(); - target_1q_basis_list.add_basis(euler_basis); + target_1q_basis_list.add_basis(self.euler_basis); let c1r = target_decomposed.K1r.view(); let c2r = target_decomposed.K2r.view(); @@ -2741,17 +2799,17 @@ impl TwoQubitControlledUDecomposer { gates, global_phase, }; - self.weyl_gate(py, &mut gates1, target_decomposed, atol)?; + self.weyl_gate(&mut gates1, target_decomposed, atol.unwrap_or(DEFAULT_ATOL))?; global_phase += gates1.global_phase; if let Some(unitary_c1r) = unitary_c1r { - global_phase -= unitary_c1r.global_phase; + global_phase += unitary_c1r.global_phase; for gate in unitary_c1r.gates.into_iter() { gates1.gates.push((Some(gate.0), gate.1, smallvec![0])); } } if let Some(unitary_c1l) = unitary_c1l { - global_phase -= unitary_c1l.global_phase; + global_phase += unitary_c1l.global_phase; for gate in unitary_c1l.gates.into_iter() { gates1.gates.push((Some(gate.0), gate.1, smallvec![1])); } @@ -2760,19 +2818,9 @@ impl TwoQubitControlledUDecomposer { gates1.global_phase = global_phase; Ok(gates1) } -} -#[pymethods] -impl TwoQubitControlledUDecomposer { - /// Initialize the KAK decomposition. - /// Args: - /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: - /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. - /// Raises: - /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. - #[new] - #[pyo3(signature=(rxx_equivalent_gate))] - pub fn new(py: Python, rxx_equivalent_gate: RXXEquivalent) -> PyResult { + /// Initialize the KAK decomposition. + pub fn new_inner(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { let atol = DEFAULT_ATOL; let test_angles = [0.2, 0.3, PI2]; @@ -2788,14 +2836,17 @@ impl TwoQubitControlledUDecomposer { } } RXXEquivalent::CustomPython(gate_cls) => { - if gate_cls.bind(py).call1((test_angle,)).ok().is_none() { + let takes_param = Python::with_gil(|py: Python| { + gate_cls.bind(py).call1((test_angle,)).ok().is_none() + }); + if takes_param { return Err(QiskitError::new_err( "Equivalent gate needs to take exactly 1 angle parameter.", )); } } }; - let mat = rxx_equivalent_gate.matrix(py, test_angle)?; + let mat = rxx_equivalent_gate.matrix(test_angle)?; let decomp = TwoQubitWeylDecomposition::new_inner(mat.view(), Some(DEFAULT_FIDELITY), None)?; let mat_rxx = StandardGate::RXXGate @@ -2836,17 +2887,35 @@ impl TwoQubitControlledUDecomposer { Ok(TwoQubitControlledUDecomposer { scale, rxx_equivalent_gate, + euler_basis: EulerBasis::__new__(euler_basis)?, }) } +} - #[pyo3(signature=(unitary, atol))] +#[pymethods] +impl TwoQubitControlledUDecomposer { + /// Initialize the KAK decomposition. + /// Args: + /// rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: + /// :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + /// euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + /// for 1Q synthesis. + /// Raises: + /// QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. + #[new] + #[pyo3(signature=(rxx_equivalent_gate, euler_basis="ZXZ"))] + pub fn new(rxx_equivalent_gate: RXXEquivalent, euler_basis: &str) -> PyResult { + TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, euler_basis) + } + + #[pyo3(signature=(unitary, atol=None))] fn __call__( &self, py: Python, unitary: PyReadonlyArray2, - atol: f64, + atol: Option, ) -> PyResult { - let sequence = self.call_inner(py, unitary.as_array(), atol)?; + let sequence = self.call_inner(unitary.as_array(), atol)?; match &self.rxx_equivalent_gate { RXXEquivalent::Standard(rxx_gate) => CircuitData::from_standard_gates( py, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 2721c58c923..b36715bd2cf 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -27,14 +27,14 @@ use smallvec::{smallvec, SmallVec}; use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyDict, PyString}; +use pyo3::types::{IntoPyDict, PyDict, PyString, PyType}; use pyo3::wrap_pyfunction; use pyo3::Python; use qiskit_circuit::converters::{circuit_to_dag, QuantumCircuitData}; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; use qiskit_circuit::imports; -use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::operations::{Operation, OperationRef, Param, PyGate, StandardGate}; use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; use qiskit_circuit::Qubit; @@ -44,7 +44,8 @@ use crate::euler_one_qubit_decomposer::{ use crate::nlayout::PhysicalQubit; use crate::target_transpiler::{NormalOperation, Target}; use crate::two_qubit_decompose::{ - TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition, + RXXEquivalent, TwoQubitBasisDecomposer, TwoQubitControlledUDecomposer, TwoQubitGateSequence, + TwoQubitWeylDecomposition, }; use crate::QiskitError; @@ -53,10 +54,12 @@ const PI4: f64 = PI / 4.; #[derive(Clone, Debug)] enum DecomposerType { - TwoQubitBasisDecomposer(Box), - XXDecomposer(PyObject), + TwoQubitBasis(Box), + TwoQubitControlledU(Box), + XX(PyObject), } +#[derive(Clone, Debug)] struct DecomposerElement { decomposer: DecomposerType, gate: NormalOperation, @@ -72,6 +75,7 @@ struct TwoQubitUnitarySequence { // then we know TwoQubitBasisDecomposer is an ideal decomposition and there is // no need to bother trying the XXDecomposer. static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; +static PARAM_SET: [&str; 8] = ["rzz", "rxx", "ryy", "rzx", "crx", "cry", "crz", "cphase"]; fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); @@ -133,17 +137,56 @@ fn apply_synth_sequence( ) -> PyResult<()> { let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len()); for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { - let gate_node = match gate { - None => sequence.decomp_gate.operation.standard_gate(), - Some(gate) => *gate, + let packed_op = match gate { + None => &sequence.decomp_gate.operation, + Some(gate) => &PackedOperation::from_standard_gate(*gate), }; let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); let new_params: Option>> = match gate { Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), - None => Some(Box::new(sequence.decomp_gate.params.clone())), + None => { + if !sequence.decomp_gate.params.is_empty() + && matches!(sequence.decomp_gate.params[0], Param::Float(_)) + { + Some(Box::new(sequence.decomp_gate.params.clone())) + } else { + Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())) + } + } + }; + + let new_op: PackedOperation = match packed_op.py_copy(py)?.view() { + OperationRef::Gate(gate) => { + gate.gate.setattr( + py, + "params", + new_params + .as_deref() + .map(SmallVec::as_slice) + .unwrap_or(&[]) + .iter() + .map(|param| param.clone_ref(py)) + .collect::>(), + )?; + Box::new(PyGate { + gate: gate.gate.clone(), + qubits: gate.qubits, + clbits: gate.clbits, + params: gate.params, + op_name: gate.op_name.clone(), + }) + .into() + } + OperationRef::StandardGate(_) => packed_op.clone(), + _ => { + return Err(QiskitError::new_err( + "Decomposed gate sequence contains unexpected operations.", + )) + } }; + let instruction = PackedInstruction { - op: PackedOperation::from_standard_gate(gate_node), + op: new_op, qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), params: new_params, @@ -407,8 +450,18 @@ fn run_2q_unitary_synthesis( coupling_edges, target, )?; + match decomposer_item.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::TwoQubitControlledU(_) => { let synth = synth_su4_sequence( &unitary, decomposer_item, @@ -417,7 +470,7 @@ fn run_2q_unitary_synthesis( )?; apply_synth_sequence(py, out_dag, out_qargs, &synth)?; } - DecomposerType::XXDecomposer(_) => { + DecomposerType::XX(_) => { let synth = synth_su4_dag( py, &unitary, @@ -442,7 +495,34 @@ fn run_2q_unitary_synthesis( target, )?; match &decomposer.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { + DecomposerType::TwoQubitBasis(_) => { + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let scoring_info = + sequence + .gate_sequence + .gates() + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = + qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence.decomp_gate.operation.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }); + let synth_error_from_target = synth_error(py, scoring_info, target); + synth_errors_sequence.push((sequence, synth_error_from_target)); + } + DecomposerType::TwoQubitControlledU(_) => { let sequence = synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; let scoring_info = @@ -460,12 +540,7 @@ fn run_2q_unitary_synthesis( inst_qubits, ), None => ( - sequence - .decomp_gate - .operation - .standard_gate() - .name() - .to_string(), + sequence.decomp_gate.operation.name().to_string(), Some(params.iter().map(|p| Param::Float(*p)).collect()), inst_qubits, ), @@ -474,7 +549,7 @@ fn run_2q_unitary_synthesis( let synth_error_from_target = synth_error(py, scoring_info, target); synth_errors_sequence.push((sequence, synth_error_from_target)); } - DecomposerType::XXDecomposer(_) => { + DecomposerType::XX(_) => { let synth_dag = synth_su4_dag( py, &unitary, @@ -543,6 +618,8 @@ fn get_2q_decomposers_from_target( let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); + let mut available_2q_param_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); + let mut available_2q_param_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); let mut qubit_gate_map = IndexMap::new(); @@ -565,28 +642,10 @@ fn get_2q_decomposers_from_target( } #[inline] - fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation { - if let Some(std_gate) = op.operation.try_standard_gate() { - match std_gate.name() { - "rxx" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI2) - } - } - "rzx" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI4) - } - } - "rzz" => { - if let Param::ParameterExpression(_) = op.params[0] { - op.params[0] = Param::Float(PI2) - } - } - _ => (), - } - } - op + fn check_parametrized_gate(op: &NormalOperation) -> bool { + // The gate counts as parametrized if there is any + // non-float parameter + !op.params.iter().all(|p| matches!(p, Param::Float(_))) } for (q_pair, gates) in qubit_gate_map { @@ -602,8 +661,21 @@ fn get_2q_decomposers_from_target( if op.operation.num_qubits() != 2 { continue; } - available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - + if check_parametrized_gate(op) { + available_2q_param_basis.insert(key, op.clone()); + if target.contains_key(key) { + available_2q_param_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } else { + continue; + } + } + available_2q_basis.insert(key, op.clone()); if target.contains_key(key) { available_2q_props.insert( key, @@ -620,7 +692,8 @@ fn get_2q_decomposers_from_target( } } } - if available_2q_basis.is_empty() { + + if available_2q_basis.is_empty() && available_2q_param_basis.is_empty() { return Err(QiskitError::new_err( "Target has no gates available on qubits to synthesize over.", )); @@ -655,7 +728,6 @@ fn get_2q_decomposers_from_target( } } - // Iterate over 1q and 2q supercontrolled basis, append TwoQubitBasisDecomposers let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis .iter() .filter(|(_, v)| is_supercontrolled(v)) @@ -680,26 +752,67 @@ fn get_2q_decomposers_from_target( )?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + decomposer: DecomposerType::TwoQubitBasis(Box::new(decomposer)), gate: gate.clone(), }); } } // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer - // is an ideal decomposition and there is no need to bother calculating the XX embodiments - // or try the XX decomposer + // is an ideal decomposition and there is no need to try other decomposers let available_basis_set: IndexSet<&str> = available_2q_basis.keys().copied().collect(); #[inline] fn check_goodbye(basis_set: &IndexSet<&str>) -> bool { - basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) + !basis_set.is_empty() && basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) } if check_goodbye(&available_basis_set) { return Ok(Some(decomposers)); } + for basis_1q in &available_1q_basis { + for (_basis_2q, gate) in available_2q_param_basis.iter() { + let rxx_equivalent_gate = if let Some(std_gate) = gate.operation.try_standard_gate() { + RXXEquivalent::Standard(std_gate) + } else { + let module = PyModule::import(py, "builtins")?; + let py_type = module.getattr("type")?; + let gate_type = py_type + .call1((gate.clone().into_pyobject(py)?,))? + .downcast_into::()? + .unbind(); + + RXXEquivalent::CustomPython(gate_type) + }; + + match TwoQubitControlledUDecomposer::new_inner(rxx_equivalent_gate, basis_1q) { + Ok(decomposer) => { + decomposers.push(DecomposerElement { + decomposer: DecomposerType::TwoQubitControlledU(Box::new(decomposer)), + gate: gate.clone(), + }); + } + Err(_) => continue, + }; + } + } + + // If our 2q basis gates are a subset of PARAM_SET, then we will use the TwoQubitControlledUDecomposer + // and there is no need to try other decomposers + + let available_basis_param_set: IndexSet<&str> = + available_2q_param_basis.keys().copied().collect(); + + #[inline] + fn check_parametrized_goodbye(basis_set: &IndexSet<&str>) -> bool { + !basis_set.is_empty() && basis_set.iter().all(|gate| PARAM_SET.contains(gate)) + } + + if check_parametrized_goodbye(&available_basis_param_set) { + return Ok(Some(decomposers)); + } + // Let's now look for possible controlled decomposers (i.e. XXDecomposer) let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis .iter() @@ -787,7 +900,7 @@ fn get_2q_decomposers_from_target( .extract::()?; decomposers.push(DecomposerElement { - decomposer: DecomposerType::XXDecomposer(decomposer.into()), + decomposer: DecomposerType::XX(decomposer.into()), gate: decomposer_gate, }); } @@ -887,8 +1000,10 @@ fn synth_su4_sequence( approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + let synth = if let DecomposerType::TwoQubitBasis(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; @@ -947,8 +1062,10 @@ fn reversed_synth_su4_sequence( let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + let synth = if let DecomposerType::TwoQubitBasis(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else if let DecomposerType::TwoQubitControlledU(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None)? } else { unreachable!( "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." @@ -982,7 +1099,7 @@ fn synth_su4_dag( approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let synth_dag = if let DecomposerType::XX(decomposer) = &decomposer_2q.decomposer { let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] .into_iter() .collect(); @@ -1048,7 +1165,7 @@ fn reversed_synth_su4_dag( let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let synth_dag = if let DecomposerType::XX(decomposer) = &decomposer_2q.decomposer { let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] .into_iter() .collect(); diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index a86ec668140..4b29c434b72 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -119,6 +119,7 @@ TwoQubitBasisDecomposer XXDecomposer TwoQubitWeylDecomposition + TwoQubitControlledUDecomposer .. autofunction:: two_qubit_cnot_decompose @@ -200,6 +201,7 @@ TwoQubitBasisDecomposer, two_qubit_cnot_decompose, TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, ) from .multi_controlled import ( synth_mcmt_vchain, diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py index 79a444e6220..4af8c3a7eef 100644 --- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py +++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py @@ -270,32 +270,46 @@ class TwoQubitControlledUDecomposer: :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate that is locally equivalent to an :class:`.RXXGate`.""" - def __init__(self, rxx_equivalent_gate: Type[Gate]): + def __init__(self, rxx_equivalent_gate: Type[Gate], euler_basis: str = "ZXZ"): r"""Initialize the KAK decomposition. Args: rxx_equivalent_gate: Gate that is locally equivalent to an :class:`.RXXGate`: - :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + :math:`U \sim U_d(\alpha, 0, 0) \sim \text{Ctrl-U}` gate. + Valid options are [:class:`.RZZGate`, :class:`.RXXGate`, :class:`.RYYGate`, + :class:`.RZXGate`, :class:`.CPhaseGate`, :class:`.CRXGate`, :class:`.CRYGate`, + :class:`.CRZGate`]. + euler_basis: Basis string to be provided to :class:`.OneQubitEulerDecomposer` + for 1Q synthesis. + Valid options are [``'ZXZ'``, ``'ZYZ'``, ``'XYX'``, ``'XZX'``, ``'U'``, ``'U3'``, + ``'U321'``, ``'U1X'``, ``'PSX'``, ``'ZSX'``, ``'ZSXX'``, ``'RR'``]. + Raises: QiskitError: If the gate is not locally equivalent to an :class:`.RXXGate`. """ if rxx_equivalent_gate._standard_gate is not None: self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( - rxx_equivalent_gate._standard_gate + rxx_equivalent_gate._standard_gate, euler_basis ) else: self._inner_decomposition = two_qubit_decompose.TwoQubitControlledUDecomposer( - rxx_equivalent_gate + rxx_equivalent_gate, euler_basis ) self.rxx_equivalent_gate = rxx_equivalent_gate self.scale = self._inner_decomposition.scale + self.euler_basis = euler_basis - def __call__(self, unitary: Operator | np.ndarray, *, atol=DEFAULT_ATOL) -> QuantumCircuit: + def __call__( + self, unitary: Operator | np.ndarray, approximate=False, use_dag=False, *, atol=DEFAULT_ATOL + ) -> QuantumCircuit: """Returns the Weyl decomposition in circuit form. + Args: unitary (Operator or ndarray): :math:`4 \times 4` unitary to synthesize. + Returns: QuantumCircuit: Synthesized quantum circuit. + Note: atol is passed to OneQubitEulerDecomposer. """ circ_data = self._inner_decomposition(np.asarray(unitary, dtype=complex), atol) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index ab66b38bd99..3dfd6caff65 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -38,6 +38,7 @@ RXXGate, RZXGate, RZZGate, + RYYGate, ECRGate, RXGate, SXGate, @@ -50,6 +51,10 @@ U3Gate, RYGate, RGate, + CRXGate, + CRYGate, + CRZGate, + CPhaseGate, ) from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.dagcircuit.dagcircuit import DAGCircuit @@ -61,6 +66,7 @@ from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, TwoQubitWeylDecomposition, + TwoQubitControlledUDecomposer, ) from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.coupling import CouplingMap @@ -87,16 +93,32 @@ "u3": U3Gate._standard_gate, "ry": RYGate._standard_gate, "r": RGate._standard_gate, + "rzz": RZZGate._standard_gate, + "ryy": RYYGate._standard_gate, + "rxx": RXXGate._standard_gate, + "rzx": RXXGate._standard_gate, + "cp": CPhaseGate._standard_gate, + "crx": RXXGate._standard_gate, + "cry": RXXGate._standard_gate, + "crz": RXXGate._standard_gate, } +KAK_GATE_PARAM_NAMES = { + "rxx": RXXGate, + "rzz": RZZGate, + "ryy": RYYGate, + "rzx": RZXGate, + "cphase": CPhaseGate, + "crx": CRXGate, + "cry": CRYGate, + "crz": CRZGate, +} KAK_GATE_NAMES = { "cx": CXGate(), "cz": CZGate(), "iswap": iSwapGate(), - "rxx": RXXGate(pi / 2), "ecr": ECRGate(), - "rzx": RZXGate(pi / 4), # typically pi/6 is also available } GateNameToGate = get_standard_gate_name_mapping() @@ -105,9 +127,14 @@ def _choose_kak_gate(basis_gates): """Choose the first available 2q gate to use in the KAK decomposition.""" kak_gate = None - kak_gates = set(basis_gates or []).intersection(KAK_GATE_NAMES.keys()) - if kak_gates: - kak_gate = KAK_GATE_NAMES[kak_gates.pop()] + kak_gates = sorted(set(basis_gates or []).intersection(KAK_GATE_NAMES.keys())) + kak_gates_params = sorted(set(basis_gates or []).intersection(KAK_GATE_PARAM_NAMES.keys())) + + if kak_gates_params: + kak_gate = KAK_GATE_PARAM_NAMES[kak_gates_params[0]] + + elif kak_gates: + kak_gate = KAK_GATE_NAMES[kak_gates[0]] return kak_gate @@ -150,14 +177,9 @@ def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximat kak_gate = _choose_kak_gate(basis_gates) euler_basis = _choose_euler_basis(basis_gates) basis_fidelity = approximation_degree or 1.0 - if isinstance(kak_gate, RZXGate): - backup_optimizer = TwoQubitBasisDecomposer( - CXGate(), - basis_fidelity=basis_fidelity, - euler_basis=euler_basis, - pulse_optimize=pulse_optimize, - ) - decomposer2q = XXDecomposer(euler_basis=euler_basis, backup_optimizer=backup_optimizer) + + if kak_gate in KAK_GATE_PARAM_NAMES.values(): + decomposer2q = TwoQubitControlledUDecomposer(kak_gate, euler_basis) elif kak_gate is not None: decomposer2q = TwoQubitBasisDecomposer( kak_gate, diff --git a/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml new file mode 100644 index 00000000000..a55fd7f3ea5 --- /dev/null +++ b/releasenotes/notes/add-2q-fractional-gates-to-unitarysynthesis-pass-f66eee29903f5639.yaml @@ -0,0 +1,37 @@ +--- +features_synthesis: + - | + Add a :class:`.TwoQubitControlledUDecomposer` that decomposes any two-qubit unitary + in terms of basis two-qubit fractional gates, such as :class:`.RZZGate` + (or two-gates gates which are locally equivalent to :class:`.RZZGate` up to single qubit gates). + + For example:: + + from qiskit.circuit.library import RZZGate + from qiskit.synthesis import TwoQubitControlledUDecomposer + from qiskit.quantum_info import random_unitary + + unitary = random_unitary(4, seed=1) + decomposer = TwoQubitControlledUDecomposer(RZZGate, euler_basis="ZXZ") + circ = decomposer(unitary) + circ.draw(output='mpl') + +features_transpiler: + - | + Added support for two-qubit fractional basis gates, such as :class:`.RZZGate`, to the + :class:`.UnitarySynthesis` transpiler pass. The decomposition is done using the + :class:`.TwoQubitControlledUDecomposer`, and supports both standard and custom basis gates. + + For example:: + + from qiskit import QuantumCircuit + from qiskit.quantum_info import random_unitary + from qiskit.transpiler.passes import UnitarySynthesis + from qiskit.converters import circuit_to_dag, dag_to_circuit + + unitary = random_unitary(4, seed=1) + qc = QuantumCircuit(2) + qc.append(unitary, [0, 1]) + dag = circuit_to_dag(qc) + circ = UnitarySynthesis(basis_gates=['rzz', 'rx', 'rz']).run(dag) + dag_to_circuit(circ).draw(output='mpl') diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 4bf2b437d05..5c707c00d88 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1276,7 +1276,7 @@ def test_block_collection_reduces_1q_gate(self, basis_gates, gate_counts): basis_gates=[ ["u3", "cx"], ["rx", "rz", "iswap"], - ["rx", "ry", "rxx"], + ["ry", "rz", "rxx"], ], ) def test_translation_method_synthesis(self, optimization_level, basis_gates): diff --git a/test/python/synthesis/test_synthesis.py b/test/python/synthesis/test_synthesis.py index b26a049b556..98e20d21c00 100644 --- a/test/python/synthesis/test_synthesis.py +++ b/test/python/synthesis/test_synthesis.py @@ -1426,14 +1426,32 @@ def test_approx_supercontrolled_decompose_phase_3_use_random(self, seed, delta=0 class TestTwoQubitControlledUDecompose(CheckDecompositions): """Test TwoQubitControlledUDecomposer() for exact decompositions and raised exceptions""" - @combine(seed=range(10), name="seed_{seed}") - def test_correct_unitary(self, seed): + @combine( + seed=range(5), + gate=[RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate], + euler_basis=[ + "ZYZ", + "ZXZ", + "XYX", + "XZX", + "RR", + "U", + "U3", + "U321", + "PSX", + "ZSX", + "ZSXX", + "U1X", + ], + name="seed_{seed}", + ) + def test_correct_unitary(self, seed, gate, euler_basis): """Verify unitary for different gates in the decomposition""" unitary = random_unitary(4, seed=seed) - for gate in [RXXGate, RYYGate, RZZGate, RZXGate, CPhaseGate, CRZGate, CRXGate, CRYGate]: - decomposer = TwoQubitControlledUDecomposer(gate) - circ = decomposer(unitary) - self.assertEqual(Operator(unitary), Operator(circ)) + + decomposer = TwoQubitControlledUDecomposer(gate, euler_basis) + circ = decomposer(unitary) + self.assertEqual(Operator(unitary), Operator(circ)) def test_not_rxx_equivalent(self): """Test that an exception is raised if the gate is not equivalent to an RXXGate""" diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index bc9e218e79c..d93d6800985 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -17,6 +17,7 @@ """ import unittest +import math import numpy as np import scipy from ddt import ddt, data @@ -25,6 +26,7 @@ from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.library import quantum_volume +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.transpiler.passes import UnitarySynthesis from qiskit.quantum_info.operators import Operator @@ -58,6 +60,7 @@ RZZGate, RXXGate, PauliEvolutionGate, + CPhaseGate, ) from qiskit.quantum_info import SparsePauliOp from qiskit.circuit import Measure @@ -130,7 +133,8 @@ def test_empty_basis_gates(self): @data( ["u3", "cx"], ["u1", "u2", "u3", "cx"], - ["rx", "ry", "rxx"], + ["ry", "rz", "rxx"], + ["rx", "rz", "rzz"], ["rx", "rz", "iswap"], ["u3", "rx", "rz", "cz", "iswap"], ) @@ -740,19 +744,110 @@ def test_iswap_no_cx_synthesis_succeeds(self): result_qc = dag_to_circuit(result_dag) self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat)) - def test_parameterized_basis_gate_in_target(self): - """Test synthesis with parameterized RXX gate.""" + @combine(is_random=[True, False], param_gate=[RXXGate, RZZGate, CPhaseGate]) + def test_parameterized_basis_gate_in_target(self, is_random, param_gate): + """Test synthesis with parameterized RZZ/RXX gate.""" theta = Parameter("θ") lam = Parameter("λ") + phi = Parameter("ϕ") target = Target(num_qubits=2) target.add_instruction(RZGate(lam)) - target.add_instruction(RXGate(theta)) - target.add_instruction(RXXGate(theta)) + target.add_instruction(RXGate(phi)) + target.add_instruction(param_gate(theta)) qc = QuantumCircuit(2) + if is_random: + qc.unitary(random_unitary(4, seed=1234), [0, 1]) qc.cp(np.pi / 2, 0, 1) qc_transpiled = transpile(qc, target=target, optimization_level=3, seed_transpiler=42) opcount = qc_transpiled.count_ops() - self.assertTrue(set(opcount).issubset({"rz", "rx", "rxx"})) + self.assertTrue(set(opcount).issubset({"rz", "rx", param_gate(theta).name})) + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + def test_custom_parameterized_gate_in_target(self): + """Test synthesis with custom parameterized gate in target.""" + + class CustomXXGate(RXXGate): + """Custom RXXGate subclass that's not a standard gate""" + + _standard_gate = None + + def __init__(self, theta, label=None): + super().__init__(theta, label) + self.name = "MyCustomXXGate" + + theta = Parameter("θ") + lam = Parameter("λ") + phi = Parameter("ϕ") + + target = Target(num_qubits=2) + target.add_instruction(RZGate(lam)) + target.add_instruction(RXGate(phi)) + target.add_instruction(CustomXXGate(theta)) + + qc = QuantumCircuit(2) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc_transpiled = UnitarySynthesis(target=target)(qc) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset({"rz", "rx", "MyCustomXXGate"})) + + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + def test_custom_parameterized_gate_in_target_skips(self): + """Test that synthesis is skipped with custom parameterized + gate in target that is not RXX equivalent.""" + + class CustomXYGate(Gate): + """Custom Gate subclass that's not a standard gate and not RXX equivalent""" + + _standard_gate = None + + def __init__(self, theta: ParameterValueType, label=None): + """Create new custom rotstion XY gate.""" + super().__init__("MyCustomXYGate", 2, [theta]) + + def __array__(self, dtype=None): + """Return a Numpy.array for the custom gate.""" + theta = self.params[0] + cos = math.cos(theta) + isin = 1j * math.sin(theta) + return np.array( + [[1, 0, 0, 0], [0, cos, -isin, 0], [0, -isin, cos, 0], [0, 0, 0, 1]], + dtype=dtype, + ) + + def inverse(self, annotated: bool = False): + return CustomXYGate(-self.params[0]) + + theta = Parameter("θ") + lam = Parameter("λ") + phi = Parameter("ϕ") + + target = Target(num_qubits=2) + target.add_instruction(RZGate(lam)) + target.add_instruction(RXGate(phi)) + target.add_instruction(CustomXYGate(theta)) + + qc = QuantumCircuit(2) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc_transpiled = UnitarySynthesis(target=target)(qc) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset({"unitary"})) + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + + @data( + ["rx", "ry", "rxx"], + ["rx", "rz", "rzz"], + ) + def test_parameterized_backend(self, basis_gates): + """Test synthesis with parameterized backend.""" + backend = GenericBackendV2(3, basis_gates=basis_gates, seed=0) + qc = QuantumCircuit(3) + qc.unitary(random_unitary(4, seed=1234), [0, 1]) + qc.unitary(random_unitary(4, seed=4321), [0, 2]) + qc.cp(np.pi / 2, 0, 1) + qc_transpiled = transpile(qc, backend, optimization_level=3, seed_transpiler=42) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset(basis_gates)) self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) @data(1, 2, 3) From 94504f21b0d82441e789a42714cb520d6e27fb48 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:06:24 +0200 Subject: [PATCH 6/9] Better decomposition for multi-controlled 1-qubit gates (#13801) * improve add_control for the gates H, SX, SXdg, U * update use_basis_gates=False * add SXdgGate to the test * add release notes * update release notes --- qiskit/circuit/add_control.py | 39 +++++++++++++------ ...-one-qubit-unitaries-3ae333a106274b79.yaml | 10 +++++ test/python/circuit/test_controlled_gate.py | 2 + 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 3e463c30ea9..8111fed9f70 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -109,7 +109,7 @@ def control( global_phase = 0 - basis = ["p", "u", "x", "z", "rx", "ry", "rz", "cx"] + basis = ["p", "u", "x", "z", "y", "h", "sx", "sxdg", "rx", "ry", "rz", "cx"] if operation.name in basis: apply_basic_controlled_gate(controlled_circ, operation, q_control, q_target[0]) @@ -187,7 +187,7 @@ def apply_basic_controlled_gate(circuit, gate, controls, target): This implements multi-control operations for the following basis gates: - ["p", "u", "x", "z", "rx", "ry", "rz", "cx"] + ["p", "u", "x", "z", "y", "h", "sx", "sxdg", "rx", "ry", "rz", "cx"] """ num_ctrl_qubits = len(controls) @@ -239,31 +239,46 @@ def apply_basic_controlled_gate(circuit, gate, controls, target): circuit.cu(theta, phi, lamb, 0, controls[0], target) else: if phi == -pi / 2 and lamb == pi / 2: - circuit.mcrx(theta, controls, target, use_basis_gates=True) + circuit.mcrx(theta, controls, target, use_basis_gates=False) elif phi == 0 and lamb == 0: circuit.mcry( theta, controls, target, - use_basis_gates=True, + use_basis_gates=False, ) elif theta == 0 and phi == 0: circuit.mcp(lamb, controls, target) else: - circuit.mcp(lamb, controls, target) - circuit.mcry( - theta, - controls, - target, - use_basis_gates=True, - ) - circuit.mcp(phi, controls, target) + circuit.mcrz(lamb, controls, target, use_basis_gates=False) + circuit.mcry(theta, controls, target, use_basis_gates=False) + circuit.mcrz(phi, controls, target, use_basis_gates=False) + circuit.mcp((phi + lamb) / 2, controls[1:], controls[0]) elif gate.name == "z": circuit.h(target) circuit.mcx(controls, target) circuit.h(target) + elif gate.name == "y": + circuit.sdg(target) + circuit.mcx(controls, target) + circuit.s(target) + + elif gate.name == "h": + circuit.mcry(pi / 2, controls, target, use_basis_gates=False) + circuit.mcx(controls, target) + + elif gate.name == "sx": + circuit.h(target) + circuit.mcp(pi / 2, controls, target) + circuit.h(target) + + elif gate.name == "sxdg": + circuit.h(target) + circuit.mcp(3 * pi / 2, controls, target) + circuit.h(target) + else: raise CircuitError(f"Gate {gate} not in supported basis.") diff --git a/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml b/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml new file mode 100644 index 00000000000..e7217229f3b --- /dev/null +++ b/releasenotes/notes/improve-decomposition-controlled-one-qubit-unitaries-3ae333a106274b79.yaml @@ -0,0 +1,10 @@ +--- +features_circuits: + - | + Reduce the number of two-qubit gates when decomposing some multi-controlled + single-qubit unitary gates. For example, + + * For multi-controlled :class:`.YGate` on 10 qubits, we reduce the :class:`.CXGate` count by 56%, + * For multi-controlled :class:`.HGate` on 10 qubits, we reduce the :class:`.CXGate` count by 44%, + * For multi-controlled :class:`.SXGate` and :class:`.SXdgGate` on 10 qubits, we reduce the :class:`.CXGate` count by 80%, + * For multi-controlled :class:`.UGate` on 10 qubits, we reduce the :class:`.CXGate` count by 31%. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 8ad5ca7d473..4b3e69d11e9 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -64,6 +64,7 @@ CU3Gate, CUGate, SXGate, + SXdgGate, CSXGate, MSGate, Barrier, @@ -1730,6 +1731,7 @@ class TestControlledGateLabel(QiskitTestCase): (CU1Gate, [0.1]), (SwapGate, []), (SXGate, []), + (SXdgGate, []), (CSXGate, []), (CCXGate, []), (RZGate, [0.1]), From 1b6fccfbbeb3b45e122dbcdc39a84f987f217e31 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 10 Feb 2025 09:19:47 -0500 Subject: [PATCH 7/9] Add: Implement `From` for `Param` (#13817) - Add a convenient way of converting an `f64` into a `Param` by implementing the `From` trait. This would enable us to simply call `f64::into()`. --- crates/circuit/src/operations.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index e0add717824..0b6de35ae01 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -130,6 +130,13 @@ impl AsRef for Param { } } +// Conveniently converts an f64 into a `Param`. +impl From for Param { + fn from(value: f64) -> Self { + Param::Float(value) + } +} + /// Struct to provide iteration over Python-space `Parameter` instances within a `Param`. pub struct ParamParameterIter<'py>(Option>); impl<'py> Iterator for ParamParameterIter<'py> { From 7995cabbdf47021b5d1b6d4c8f1daec206a4f6af Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 11 Feb 2025 10:12:15 +0200 Subject: [PATCH 8/9] Correctly updating global phase when removing gates that are identity up to a global phase (#13785) * correctly updating global phase when removing -I gates from the circuit * similar fix for specialized rotation gates * Correctly updating global phase when removing gates that are equivalent to identity up to a global phase * also handling UnitaryGates after the unitary gates PR was merged * applying review suggestions * removing code duplication --- .../accelerate/src/remove_identity_equiv.rs | 44 +++++++++++-------- .../optimization/remove_identity_equiv.py | 5 +-- ...remove-id-equivalent-6480da0c62f20df1.yaml | 8 ++++ .../test_remove_identity_equivalent.py | 37 +++++++++++++--- 4 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml diff --git a/crates/accelerate/src/remove_identity_equiv.rs b/crates/accelerate/src/remove_identity_equiv.rs index d94250f3c39..7a65a997201 100644 --- a/crates/accelerate/src/remove_identity_equiv.rs +++ b/crates/accelerate/src/remove_identity_equiv.rs @@ -9,7 +9,6 @@ // 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. - use num_complex::Complex64; use num_complex::ComplexFloat; use pyo3::prelude::*; @@ -27,11 +26,13 @@ use qiskit_circuit::packed_instruction::PackedInstruction; #[pyfunction] #[pyo3(signature=(dag, approx_degree=Some(1.0), target=None))] fn remove_identity_equiv( + py: Python, dag: &mut DAGCircuit, approx_degree: Option, target: Option<&Target>, ) { let mut remove_list: Vec = Vec::new(); + let mut global_phase_update: f64 = 0.; let get_error_cutoff = |inst: &PackedInstruction| -> f64 { match approx_degree { @@ -75,12 +76,17 @@ fn remove_identity_equiv( }; for (op_node, inst) in dag.op_nodes(false) { - match inst.op.view() { + if inst.is_parameterized() { + // Skip parameterized gates + continue; + } + let view = inst.op.view(); + match view { OperationRef::StandardGate(gate) => { let (dim, trace) = match gate { StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { if let Param::Float(theta) = inst.params_view()[0] { - let trace = (theta / 2.).cos() * 2.; + let trace = Complex64::new((theta / 2.).cos() * 2., 0.); (2., trace) } else { continue; @@ -91,20 +97,16 @@ fn remove_identity_equiv( | StandardGate::RZZGate | StandardGate::RZXGate => { if let Param::Float(theta) = inst.params_view()[0] { - let trace = (theta / 2.).cos() * 4.; + let trace = Complex64::new((theta / 2.).cos() * 4., 0.); (4., trace) } else { continue; } } _ => { - // Skip global phase gate - if gate.num_qubits() < 1 { - continue; - } if let Some(matrix) = gate.matrix(inst.params_view()) { let dim = matrix.shape()[0] as f64; - let trace = matrix.diag().iter().sum::().abs(); + let trace = matrix.diag().iter().sum::(); (dim, trace) } else { continue; @@ -112,34 +114,38 @@ fn remove_identity_equiv( } }; let error = get_error_cutoff(inst); - let f_pro = (trace / dim).powi(2); + let f_pro = (trace / dim).abs().powi(2); let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); if (1. - gate_fidelity).abs() < error { - remove_list.push(op_node) + remove_list.push(op_node); + global_phase_update += (trace / dim).arg(); } } - OperationRef::Gate(gate) => { - // Skip global phase like gate - if gate.num_qubits() < 1 { - continue; - } - if let Some(matrix) = gate.matrix(inst.params_view()) { + _ => { + let matrix = view.matrix(inst.params_view()); + // If view.matrix() returns None, then there is no matrix and we skip the operation. + if let Some(matrix) = matrix { let error = get_error_cutoff(inst); let dim = matrix.shape()[0] as f64; let trace: Complex64 = matrix.diag().iter().sum(); let f_pro = (trace / dim).abs().powi(2); let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); if (1. - gate_fidelity).abs() < error { - remove_list.push(op_node) + remove_list.push(op_node); + global_phase_update += (trace / dim).arg(); } } } - _ => continue, } } for node in remove_list { dag.remove_op_node(node); } + + if global_phase_update != 0. { + dag.add_global_phase(py, &Param::Float(global_phase_update)) + .expect("The global phase is guaranteed to be a float"); + } } pub fn remove_identity_equiv_mod(m: &Bound) -> PyResult<()> { diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py index fbf132d958a..17445eb5ad2 100644 --- a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -23,9 +23,8 @@ class RemoveIdentityEquivalent(TransformationPass): r"""Remove gates with negligible effects. - Removes gates whose effect is close to an identity operation, up to the specified - tolerance. Zero qubit gates such as :class:`.GlobalPhaseGate` are not considered - by this pass. + Removes gates whose effect is close to an identity operation up to a global phase + and up to the specified tolerance. Parameterized gates are not considered by this pass. For a cutoff fidelity :math:`f`, this pass removes gates whose average gate fidelity with respect to the identity is below :math:`f`. Concretely, diff --git a/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml new file mode 100644 index 00000000000..3ee4c4accc2 --- /dev/null +++ b/releasenotes/notes/fix-phase-in-remove-id-equivalent-6480da0c62f20df1.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.RemoveIdentityEquivalent` transpiler pass, where gates close + to identity up to a global phase were removed from the circuit, + but the global phase of the circuit was not updated. In particular, + :class:`.RemoveIdentityEquivalent` now removes non-parameterized :class:`.GlobalPhaseGate` + gates. diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py index 1db392d3654..736c02b765b 100644 --- a/test/python/transpiler/test_remove_identity_equivalent.py +++ b/test/python/transpiler/test_remove_identity_equivalent.py @@ -12,6 +12,7 @@ """Tests for the DropNegligible transpiler pass.""" +import ddt import numpy as np from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate @@ -26,6 +27,7 @@ XXMinusYYGate, XXPlusYYGate, GlobalPhaseGate, + UnitaryGate, ) from qiskit.quantum_info import Operator from qiskit.transpiler.passes import RemoveIdentityEquivalent @@ -34,6 +36,7 @@ from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt.ddt class TestDropNegligible(QiskitTestCase): """Test the DropNegligible pass.""" @@ -173,13 +176,33 @@ def to_matrix(self): expected = QuantumCircuit(3) self.assertEqual(expected, transpiled) - def test_global_phase_ignored(self): - """Test that global phase gate isn't considered.""" + @ddt.data( + RXGate(0), + RXGate(2 * np.pi), + RYGate(0), + RYGate(2 * np.pi), + RZGate(0), + RZGate(2 * np.pi), + UnitaryGate(np.array([[1, 0], [0, 1]])), + UnitaryGate(np.array([[-1, 0], [0, -1]])), + UnitaryGate(np.array([[np.exp(1j * np.pi / 4), 0], [0, np.exp(1j * np.pi / 4)]])), + GlobalPhaseGate(0), + GlobalPhaseGate(np.pi / 4), + ) + def test_remove_identity_up_to_global_phase(self, gate): + """Test that gates equivalent to identity up to a global phase are removed from the circuit, + and the global phase of the circuit is updated correctly. + """ + qc = QuantumCircuit(gate.num_qubits) + qc.append(gate, qc.qubits) + transpiled = RemoveIdentityEquivalent()(qc) + self.assertEqual(transpiled.size(), 0) + self.assertEqual(Operator(qc), Operator(transpiled)) + def test_parameterized_global_phase_ignored(self): + """Test that parameterized global phase gates are not removed by the pass.""" + theta = Parameter("theta") qc = QuantumCircuit(1) - qc.id(0) - qc.append(GlobalPhaseGate(0)) + qc.append(GlobalPhaseGate(theta), []) transpiled = RemoveIdentityEquivalent()(qc) - expected = QuantumCircuit(1) - expected.append(GlobalPhaseGate(0)) - self.assertEqual(transpiled, expected) + self.assertEqual(qc, transpiled) From 288d2f41b295e93114290a8c0c7e69cb18283aad Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 11 Feb 2025 16:49:18 +0000 Subject: [PATCH 9/9] Do not contract control-flow operations during `SabreSwap` (#13790) The previous implementation of control-flow handling for `SabreSwap` caused control-flow blocks to contract away from idle qubit wires as a side effect of its algorithm. This will no longer be always valid with the addition of `Box`, and besides, that optimisation isn't part of a routing algorithm's responsibilities, so it's cleaner to have it in a separate pass (now `ContractIdleWiresInControlFlow`) where it can be applied at times other than routing, like in the optimisation loop. --- .../transpiler/passes/routing/sabre_swap.py | 12 ++- .../sabre-contraction-cbb7bffaeb826d67.yaml | 6 ++ test/python/transpiler/test_sabre_swap.py | 100 ++++++++++++------ 3 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 23844406716..ce653f97880 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -416,6 +416,12 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): block_root_logical_map = { inner: root_logical_map[outer] for inner, outer in zip(block.qubits, node.qargs) } + # The virtual qubits originally incident to the block should be retained even if not + # actually used; the user might be marking them out specially (like in `box`). + # There are other transpiler passes to remove those dependencies if desired. + incident_qubits = { + layout.virtual_to_physical(block_root_logical_map[bit]) for bit in block.qubits + } block_dag, block_layout = recurse( empty_dag(block), circuit_to_dag_dict[id(block)], @@ -429,7 +435,11 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout): ) apply_swaps(block_dag, block_result.swap_epilogue, block_layout) mapped_block_dags.append(block_dag) - idle_qubits.intersection_update(block_dag.idle_wires()) + idle_qubits.intersection_update( + bit + for bit in block_dag.idle_wires() + if block_dag.find_bit(bit).index not in incident_qubits + ) mapped_blocks = [] for mapped_block_dag in mapped_block_dags: diff --git a/releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml b/releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml new file mode 100644 index 00000000000..fc54b83784e --- /dev/null +++ b/releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + :class:`.SabreSwap` will no longer contract idle qubit wires out of control-flow blocks during routing. + This was generally a valid optimization, but not an expected side effect of a routing pass. + You can now use the :class:`.ContractIdleWiresInControlFlow` pass to perform this contraction. diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index 7cf86356ab1..109053ee832 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -492,12 +492,12 @@ def test_pre_if_else_route(self): expected.swap(1, 2) expected.cx(0, 1) expected.measure(1, 2) - etrue_body = QuantumCircuit(qreg[[3, 4]], creg[[2]]) - etrue_body.x(0) - efalse_body = QuantumCircuit(qreg[[3, 4]], creg[[2]]) - efalse_body.x(1) + etrue_body = QuantumCircuit(qreg, creg[[2]]) + etrue_body.x(3) + efalse_body = QuantumCircuit(qreg, creg[[2]]) + efalse_body.x(4) new_order = [0, 2, 1, 3, 4] - expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg[[2]]) + expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg, creg[[2]]) expected.barrier(qreg) expected.measure(qreg, creg[new_order]) self.assertEqual(dag_to_circuit(cdag), expected) @@ -533,11 +533,11 @@ def test_pre_if_else_route_post_x(self): expected.cx(0, 1) expected.measure(1, 2) new_order = [0, 2, 1, 3, 4] - etrue_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) - etrue_body.x(0) - efalse_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) - efalse_body.x(1) - expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg[[0]]) + etrue_body = QuantumCircuit(qreg, creg[[0]]) + etrue_body.x(3) + efalse_body = QuantumCircuit(qreg, creg[[0]]) + efalse_body.x(4) + expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg, creg[[0]]) expected.x(2) expected.barrier(qreg) expected.measure(qreg, creg[new_order]) @@ -552,12 +552,12 @@ def test_post_if_else_route(self): qc = QuantumCircuit(qreg, creg) qc.h(0) qc.measure(0, 0) - true_body = QuantumCircuit(qreg, creg[[0]]) - true_body.x(3) - false_body = QuantumCircuit(qreg, creg[[0]]) - false_body.x(4) + true_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) + true_body.x(0) + false_body = QuantumCircuit(qreg[[3, 4]], creg[[0]]) + false_body.x(1) qc.barrier(qreg) - qc.if_else((creg[0], 0), true_body, false_body, qreg, creg[[0]]) + qc.if_else((creg[0], 0), true_body, false_body, qreg[[3, 4]], creg[[0]]) qc.barrier(qreg) qc.cx(0, 2) qc.barrier(qreg) @@ -596,10 +596,10 @@ def test_pre_if_else2(self): qc.cx(0, 2) qc.x(1) qc.measure(0, 0) - true_body = QuantumCircuit(qreg, creg[[0]]) + true_body = QuantumCircuit(qreg[[0]], creg[[0]]) true_body.x(0) - false_body = QuantumCircuit(qreg, creg[[0]]) - qc.if_else((creg[0], 0), true_body, false_body, qreg, creg[[0]]) + false_body = QuantumCircuit(qreg[[0]], creg[[0]]) + qc.if_else((creg[0], 0), true_body, false_body, qreg[[0]], creg[[0]]) qc.barrier(qreg) qc.measure(qreg, creg) @@ -748,16 +748,16 @@ def test_pre_intra_post_if_else(self): expected.swap(0, 1) expected.cx(1, 2) expected.measure(1, 0) - etrue_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg[[0]]) - etrue_body.cx(0, 1) - efalse_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg[[0]]) - efalse_body.swap(0, 1) - efalse_body.swap(2, 3) - efalse_body.cx(1, 2) - efalse_body.swap(0, 1) - efalse_body.swap(2, 3) + etrue_body = QuantumCircuit(qreg, creg[[0]]) + etrue_body.cx(1, 2) + efalse_body = QuantumCircuit(qreg, creg[[0]]) + efalse_body.swap(1, 2) + efalse_body.swap(3, 4) + efalse_body.cx(2, 3) + efalse_body.swap(1, 2) + efalse_body.swap(3, 4) - expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 2, 3, 4]], creg[[0]]) + expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg[[0]]) expected.swap(1, 2) expected.h(3) expected.cx(3, 2) @@ -834,11 +834,11 @@ def test_no_layout_change(self): expected.swap(1, 2) expected.cx(0, 1) expected.measure(0, 0) - etrue_body = QuantumCircuit(qreg[[1, 4]], creg[[0]]) - etrue_body.x(0) - efalse_body = QuantumCircuit(qreg[[1, 4]], creg[[0]]) - efalse_body.x(1) - expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 4]], creg[[0]]) + etrue_body = QuantumCircuit(qreg, creg[[0]]) + etrue_body.x(1) + efalse_body = QuantumCircuit(qreg, creg[[0]]) + efalse_body.x(4) + expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg[[0]]) expected.barrier(qreg) expected.measure(qreg, creg[[0, 2, 1, 3, 4]]) self.assertEqual(dag_to_circuit(cdag), expected) @@ -1336,6 +1336,42 @@ def test_if_no_else_restores_layout(self): running_layout.swap(*instruction.qubits) self.assertEqual(initial_layout, running_layout) + def test_idle_qubit_contraction(self): + """Incident virtual qubits to a control-flow block should be maintained, even if idle, but + the blocks shouldn't contain further unnecessary qubits.""" + qc = QuantumCircuit(8) + with qc.if_test(expr.lift(True)): + qc.cx(0, 3) + qc.noop(4) + # Both of these qubits will have been moved around by the prior necessary layout + # changes, so this is testing the recursion works for modified layouts. + with qc.if_test(expr.lift(True)) as else_: + qc.noop(0) + with else_: + qc.noop(3) + + coupling = CouplingMap.from_line(8) + + # With the `decay` heuristic set to penalise re-use of the same qubit swap, this expected + # circuit should be the only valid output (except for symmetries in the swap operation, + # which the equality check should handle). + expected = QuantumCircuit(8) + with expected.if_test(expr.lift(True)): + expected.noop(4) + expected.swap(0, 1) + expected.swap(2, 3) + expected.cx(1, 2) + with expected.if_test(expr.lift(True)) as else_: + expected.noop(1) + with else_: + expected.noop(2) + # We have to restore the output layout. + expected.swap(0, 1) + expected.swap(2, 3) + + pass_ = SabreSwap(coupling, "decay", seed=2025_02_05, trials=1) + self.assertEqual(pass_(qc), expected) + @ddt.ddt class TestSabreSwapRandomCircuitValidOutput(QiskitTestCase):