diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index dd6758acc161..93a04b18bad7 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (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 @@ -72,9 +72,32 @@ def transpile( # pylint: disable=too-many-return-statements """Transpile one or more circuits, according to some desired transpilation targets. Transpilation is potentially done in parallel using multiprocessing when ``circuits`` - is a list with > 1 :class:`~.QuantumCircuit` object depending on the local environment + is a list with > 1 :class:`~.QuantumCircuit` object, depending on the local environment and configuration. + The prioritization of transpilation target constraints works as follows: if a ``target`` + input is provided, it will take priority over any ``backend`` input or loose constraints + (``basis_gates``, ``inst_map``, ``coupling_map``, ``backend_properties``, ``instruction_durations``, + ``dt`` or ``timing_constraints``). If a ``backend`` is provided together with any loose constraint + from the list above, the loose constraint will take priority over the corresponding backend + constraint. This behavior is independent of whether the ``backend`` instance is of type + :class:`.BackendV1` or :class:`.BackendV2`, as summarized in the table below. The first column + in the table summarizes the potential user-provided constraints, and each cell shows whether + the priority is assigned to that specific constraint input or another input + (`target`/`backend(V1)`/`backend(V2)`). + + ============================ ========= ======================== ======================= + User Provided target backend(V1) backend(V2) + ============================ ========= ======================== ======================= + **basis_gates** target basis_gates basis_gates + **coupling_map** target coupling_map coupling_map + **instruction_durations** target instruction_durations instruction_durations + **inst_map** target inst_map inst_map + **dt** target dt dt + **timing_constraints** target timing_constraints timing_constraints + **backend_properties** target backend_properties backend_properties + ============================ ========= ======================== ======================= + Args: circuits: Circuit(s) to transpile backend: If set, the transpiler will compile the input circuit to this target @@ -325,8 +348,16 @@ def callback_func(**kwargs): backend_properties = target_to_backend_properties(target) # If target is not specified and any hardware constraint object is # manually specified then do not use the target from the backend as - # it is invalidated by a custom basis gate list or a custom coupling map - elif basis_gates is not None or coupling_map is not None: + # it is invalidated by a custom basis gate list, custom coupling map, + # custom dt or custom instruction_durations + elif ( + basis_gates is not None # pylint: disable=too-many-boolean-expressions + or coupling_map is not None + or dt is not None + or instruction_durations is not None + or backend_properties is not None + or timing_constraints is not None + ): _skip_target = True else: target = getattr(backend, "target", None) diff --git a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py index 818342c2277c..5c4e2b331eb8 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py +++ b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (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 @@ -14,6 +14,7 @@ from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler import Target class InstructionDurationCheck(AnalysisPass): @@ -28,11 +29,7 @@ class InstructionDurationCheck(AnalysisPass): of the hardware alignment constraints, which is true in general. """ - def __init__( - self, - acquire_alignment: int = 1, - pulse_alignment: int = 1, - ): + def __init__(self, acquire_alignment: int = 1, pulse_alignment: int = 1, target: Target = None): """Create new duration validation pass. The alignment values depend on the control electronics of your quantum processor. @@ -42,10 +39,16 @@ def __init__( trigger acquisition instruction in units of ``dt``. pulse_alignment: Integer number representing the minimum time resolution to trigger gate instruction in units of ``dt``. + target: The :class:`~.Target` representing the target backend, if + ``target`` is specified then this argument will take + precedence and ``acquire_alignment`` and ``pulse_alignment`` will be ignored. """ super().__init__() self.acquire_align = acquire_alignment self.pulse_align = pulse_alignment + if target is not None: + self.acquire_align = target.acquire_alignment + self.pulse_align = target.pulse_alignment def run(self, dag: DAGCircuit): """Run duration validation passes. diff --git a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py index 4bf55913450e..a4c23a645e17 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py +++ b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py @@ -16,6 +16,7 @@ from qiskit.pulse import Play from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler import Target class ValidatePulseGates(AnalysisPass): @@ -43,6 +44,7 @@ def __init__( self, granularity: int = 1, min_length: int = 1, + target: Target = None, ): """Create new pass. @@ -53,10 +55,16 @@ def __init__( 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``. diff --git a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py index b53d0f864cef..618186a34f9c 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py +++ b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -20,6 +20,7 @@ from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGOutNode from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler import Target class ConstrainedReschedule(AnalysisPass): @@ -63,6 +64,7 @@ def __init__( self, acquire_alignment: int = 1, pulse_alignment: int = 1, + target: Target = None, ): """Create new rescheduler pass. @@ -73,10 +75,16 @@ def __init__( trigger acquisition instruction in units of ``dt``. pulse_alignment: Integer number representing the minimum time resolution to trigger gate instruction in units of ``dt``. + target: The :class:`~.Target` representing the target backend, if + ``target`` is specified then this argument will take + precedence and ``acquire_alignment`` and ``pulse_alignment`` will be ignored. """ super().__init__() self.acquire_align = acquire_alignment self.pulse_align = pulse_alignment + if target is not None: + self.acquire_align = target.acquire_alignment + self.pulse_align = target.pulse_alignment @classmethod def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> Generator[DAGOpNode, None, None]: diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index d59a6271cbf1..dcd9f27c2bc0 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -584,6 +584,7 @@ def _require_alignment(property_set): InstructionDurationCheck( acquire_alignment=timing_constraints.acquire_alignment, pulse_alignment=timing_constraints.pulse_alignment, + target=target, ) ) scheduling.append( @@ -591,6 +592,7 @@ def _require_alignment(property_set): ConstrainedReschedule( acquire_alignment=timing_constraints.acquire_alignment, pulse_alignment=timing_constraints.pulse_alignment, + target=target, ), condition=_require_alignment, ) @@ -599,6 +601,7 @@ def _require_alignment(property_set): ValidatePulseGates( granularity=timing_constraints.granularity, min_length=timing_constraints.min_length, + target=target, ) ) if scheduling_method: diff --git a/releasenotes/notes/fix-custom-transpile-constraints-5defa36d540d1608.yaml b/releasenotes/notes/fix-custom-transpile-constraints-5defa36d540d1608.yaml new file mode 100644 index 000000000000..88babaecaf99 --- /dev/null +++ b/releasenotes/notes/fix-custom-transpile-constraints-5defa36d540d1608.yaml @@ -0,0 +1,21 @@ +--- +fixes: + - | + A bug in :func:`.transpile` has been fixed where custom ``instruction_durations``, ``dt`` and ``backend_properties`` + constraints would be ignored when provided at the same time as a backend of type :class:`.BackendV2`. The behavior + after the fix is now independent of whether the provided backend is of type :class:`.BackendV1` or + type :class:`.BackendV2`. Similarly, custom ``timing_constraints`` are now overridden by ``target`` inputs + but take precedence over :class:`.BackendV1` and :class:`.BackendV2` inputs. + +features_transpiler: + - | + The following analysis passes now accept constraints encoded in a :class:`.Target` thanks to a new ``target`` + input argument: + + * :class:`.InstructionDurationCheck` + * :class:`.ConstrainedReschedule` + * :class:`.ValidatePulseGates` + + The target constraints will have priority over user-provided constraints, for coherence with the rest of + the transpiler pipeline. + diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index e18f483fb959..b260be9a8ca1 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -74,10 +74,11 @@ from qiskit.dagcircuit import DAGOpNode, DAGOutNode from qiskit.exceptions import QiskitError from qiskit.providers.backend import BackendV2 -from qiskit.providers.fake_provider import Fake20QV1, GenericBackendV2 +from qiskit.providers.backend_compat import BackendV2Converter +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 +from qiskit.pulse import InstructionScheduleMap, Schedule, Play, Gaussian, DriveChannel from qiskit.quantum_info import Operator, random_unitary from qiskit.utils import parallel from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass @@ -85,7 +86,14 @@ from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager -from qiskit.transpiler.target import InstructionProperties, Target +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 @@ -1498,6 +1506,142 @@ 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.""" + + backend_v1 = Fake27QPulseV1() + backend_v2 = BackendV2Converter(backend_v1) + # 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() + 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]): + _ = transpile( + qc, + backend=backend, + timing_constraints=timing_constraints, + ) + + def test_scheduling_instruction_constraints(self): + """Test that scheduling-related loose transpile constraints + work with both BackendV1 and BackendV2.""" + + backend_v1 = Fake27QPulseV1() + backend_v2 = BackendV2Converter(backend_v1) + qc = QuantumCircuit(2) + qc.h(0) + qc.delay(500, 1, "dt") + qc.cx(0, 1) + # update durations + durations = InstructionDurations.from_backend(backend_v1) + durations.update([("cx", [0, 1], 1000, "dt")]) + + for backend in [backend_v1, backend_v2]: + with self.subTest(backend=backend): + scheduled = transpile( + qc, + backend=backend, + scheduling_method="alap", + instruction_durations=durations, + layout_method="trivial", + ) + self.assertEqual(scheduled.duration, 1500) + + def test_scheduling_dt_constraints(self): + """Test that scheduling-related loose transpile constraints + work with both BackendV1 and BackendV2.""" + + backend_v1 = Fake27QPulseV1() + backend_v2 = BackendV2Converter(backend_v1) + qc = QuantumCircuit(1, 1) + qc.x(0) + qc.measure(0, 0) + original_dt = 2.2222222222222221e-10 + original_duration = 3504 + + for backend in [backend_v1, backend_v2]: + with self.subTest(backend=backend): + # halve dt in sec = double duration in dt + scheduled = transpile( + qc, backend=backend, scheduling_method="asap", dt=original_dt / 2 + ) + self.assertEqual(scheduled.duration, original_duration * 2) + + def test_backend_props_constraints(self): + """Test that loose transpile constraints + work with both BackendV1 and BackendV2.""" + + backend_v1 = Fake20QV1() + backend_v2 = BackendV2Converter(backend_v1) + qr1 = QuantumRegister(3, "qr1") + qr2 = QuantumRegister(2, "qr2") + qc = QuantumCircuit(qr1, qr2) + qc.cx(qr1[0], qr1[1]) + qc.cx(qr1[1], qr1[2]) + qc.cx(qr1[2], qr2[0]) + qc.cx(qr2[0], qr2[1]) + + # generate a fake backend with same number of qubits + # but different backend properties + fake_backend = GenericBackendV2(num_qubits=20, seed=42) + custom_backend_properties = target_to_backend_properties(fake_backend.target) + + # expected layout for custom_backend_properties + # (different from expected layout for Fake20QV1) + vf2_layout = { + 18: Qubit(QuantumRegister(3, "qr1"), 1), + 13: Qubit(QuantumRegister(3, "qr1"), 2), + 19: Qubit(QuantumRegister(3, "qr1"), 0), + 14: Qubit(QuantumRegister(2, "qr2"), 0), + 9: Qubit(QuantumRegister(2, "qr2"), 1), + 0: Qubit(QuantumRegister(15, "ancilla"), 0), + 1: Qubit(QuantumRegister(15, "ancilla"), 1), + 2: Qubit(QuantumRegister(15, "ancilla"), 2), + 3: Qubit(QuantumRegister(15, "ancilla"), 3), + 4: Qubit(QuantumRegister(15, "ancilla"), 4), + 5: Qubit(QuantumRegister(15, "ancilla"), 5), + 6: Qubit(QuantumRegister(15, "ancilla"), 6), + 7: Qubit(QuantumRegister(15, "ancilla"), 7), + 8: Qubit(QuantumRegister(15, "ancilla"), 8), + 10: Qubit(QuantumRegister(15, "ancilla"), 9), + 11: Qubit(QuantumRegister(15, "ancilla"), 10), + 12: Qubit(QuantumRegister(15, "ancilla"), 11), + 15: Qubit(QuantumRegister(15, "ancilla"), 12), + 16: Qubit(QuantumRegister(15, "ancilla"), 13), + 17: Qubit(QuantumRegister(15, "ancilla"), 14), + } + + for backend in [backend_v1, backend_v2]: + with self.subTest(backend=backend): + result = transpile( + qc, + backend=backend, + backend_properties=custom_backend_properties, + optimization_level=2, + seed_transpiler=42, + ) + + self.assertEqual(result._layout.initial_layout._p2v, vf2_layout) + @data(1, 2, 3) def test_no_infinite_loop(self, optimization_level): """Verify circuit cost always descends and optimization does not flip flop indefinitely."""