Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix custom constraints in transpile with BackendV2 #12042

Merged
merged 10 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
ElePT marked this conversation as resolved.
Show resolved Hide resolved
or backend_properties is not None
or timing_constraints is not None
):
_skip_target = True
else:
target = getattr(backend, "target", None)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -43,6 +44,7 @@ def __init__(
self,
granularity: int = 1,
min_length: int = 1,
target: Target = None,
):
"""Create new pass.

Expand All @@ -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``.
Expand Down
10 changes: 9 additions & 1 deletion qiskit/transpiler/passes/scheduling/alignments/reschedule.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -63,6 +64,7 @@ def __init__(
self,
acquire_alignment: int = 1,
pulse_alignment: int = 1,
target: Target = None,
):
"""Create new rescheduler pass.

Expand All @@ -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]:
Expand Down
3 changes: 3 additions & 0 deletions qiskit/transpiler/preset_passmanagers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,13 +584,15 @@ def _require_alignment(property_set):
InstructionDurationCheck(
acquire_alignment=timing_constraints.acquire_alignment,
pulse_alignment=timing_constraints.pulse_alignment,
target=target,
)
)
scheduling.append(
ConditionalController(
ConstrainedReschedule(
acquire_alignment=timing_constraints.acquire_alignment,
pulse_alignment=timing_constraints.pulse_alignment,
target=target,
),
condition=_require_alignment,
)
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

150 changes: 147 additions & 3 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,26 @@
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
from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget
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
Expand Down Expand Up @@ -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."""
Expand Down
Loading