Skip to content

Commit

Permalink
Fix delay padding to respect target's constraints (#10007)
Browse files Browse the repository at this point in the history
* Add and update tests

* Fix padding passes to respect target's constraints

* Fix transpile with scheduling to respect target's constraints

* Add release note

* fix reno

* use target.instruction_supported

* simplify

* Add check if all DD gates are supported on each qubit

* Add logging

* Update DD tests

* Make DD gates check target-aware

* Fix legacy DD pass
  • Loading branch information
itoko authored Apr 25, 2023
1 parent 116575a commit 117d188
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 27 deletions.
5 changes: 3 additions & 2 deletions qiskit/transpiler/passes/scheduling/alap.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def run(self, dag):

for bit in node.qargs:
delta = t0 - idle_before[bit]
if delta > 0:
if delta > 0 and self._delay_supported(bit_indices[bit]):
new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [])
idle_before[bit] = t1

Expand All @@ -148,7 +148,8 @@ def run(self, dag):
delta = circuit_duration - before
if not (delta > 0 and isinstance(bit, Qubit)):
continue
new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [])
if self._delay_supported(bit_indices[bit]):
new_dag.apply_operation_front(Delay(delta, time_unit), [bit], [])

new_dag.name = dag.name
new_dag.metadata = dag.metadata
Expand Down
5 changes: 3 additions & 2 deletions qiskit/transpiler/passes/scheduling/asap.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def run(self, dag):
# Add delay to qubit wire
for bit in node.qargs:
delta = t0 - idle_after[bit]
if delta > 0 and isinstance(bit, Qubit):
if delta > 0 and isinstance(bit, Qubit) and self._delay_supported(bit_indices[bit]):
new_dag.apply_operation_back(Delay(delta, time_unit), [bit], [])
idle_after[bit] = t1

Expand All @@ -161,7 +161,8 @@ def run(self, dag):
delta = circuit_duration - after
if not (delta > 0 and isinstance(bit, Qubit)):
continue
new_dag.apply_operation_back(Delay(delta, time_unit), [bit], [])
if self._delay_supported(bit_indices[bit]):
new_dag.apply_operation_back(Delay(delta, time_unit), [bit], [])

new_dag.name = dag.name
new_dag.metadata = dag.metadata
Expand Down
15 changes: 11 additions & 4 deletions qiskit/transpiler/passes/scheduling/base_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,16 @@ def __init__(
"""
super().__init__()
self.durations = durations
# Ensure op node durations are attached and in consistent unit
if target is not None:
self.durations = target.durations()
self.requires.append(TimeUnitConversion(self.durations))

# Control flow constraints.
self.clbit_write_latency = clbit_write_latency
self.conditional_latency = conditional_latency

# Ensure op node durations are attached and in consistent unit
self.requires.append(TimeUnitConversion(durations))
if target is not None:
self.durations = target.durations()
self.target = target

@staticmethod
def _get_node_duration(
Expand Down Expand Up @@ -281,5 +282,11 @@ def _get_node_duration(

return duration

def _delay_supported(self, qarg: int) -> bool:
"""Delay operation is supported on the qubit (qarg) or not."""
if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)):
return True
return False

def run(self, dag: DAGCircuit):
raise NotImplementedError
27 changes: 21 additions & 6 deletions qiskit/transpiler/passes/scheduling/dynamical_decoupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
import warnings

import numpy as np
from qiskit.circuit.delay import Delay
from qiskit.circuit.reset import Reset
from qiskit.circuit import Gate, Delay, Reset
from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate
from qiskit.dagcircuit import DAGOpNode, DAGInNode
from qiskit.quantum_info.operators.predicates import matrix_equal
Expand Down Expand Up @@ -128,8 +127,14 @@ def __init__(
self._qubits = qubits
self._spacing = spacing
self._skip_reset_qubits = skip_reset_qubits
self._target = target
if target is not None:
self._durations = target.durations()
for gate in dd_sequence:
if gate.name not in target.operation_names:
raise TranspilerError(
f"{gate.name} in dd_sequence is not supported in the target"
)

def run(self, dag):
"""Run the DynamicalDecoupling pass on dag.
Expand Down Expand Up @@ -178,18 +183,22 @@ def run(self, dag):
end = mid / 2
self._spacing = [end] + [mid] * (num_pulses - 1) + [end]

new_dag = dag.copy_empty_like()
for qarg in list(self._qubits):
for gate in self._dd_sequence:
if not self.__gate_supported(gate, qarg):
self._qubits.discard(qarg)
break

qubit_index_map = {qubit: index for index, qubit in enumerate(new_dag.qubits)}
index_sequence_duration_map = {}
for qubit in new_dag.qubits:
physical_qubit = qubit_index_map[qubit]
for physical_qubit in self._qubits:
dd_sequence_duration = 0
for gate in self._dd_sequence:
gate.duration = self._durations.get(gate, physical_qubit)
dd_sequence_duration += gate.duration
index_sequence_duration_map[physical_qubit] = dd_sequence_duration

new_dag = dag.copy_empty_like()
qubit_index_map = {qubit: index for index, qubit in enumerate(new_dag.qubits)}
for nd in dag.topological_op_nodes():
if not isinstance(nd.op, Delay):
new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs)
Expand Down Expand Up @@ -252,6 +261,12 @@ def run(self, dag):

return new_dag

def __gate_supported(self, gate: Gate, qarg: int) -> bool:
"""A gate is supported on the qubit (qarg) or not."""
if self._target is None or self._target.instruction_supported(gate.name, qargs=(qarg,)):
return True
return False


def _mod_2pi(angle: float, atol: float = 0):
"""Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π"""
Expand Down
36 changes: 33 additions & 3 deletions qiskit/transpiler/passes/scheduling/padding/base_padding.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@

"""Padding pass to fill empty timeslot."""

import logging
from typing import List, Optional, Union

from qiskit.circuit import Qubit, Clbit, Instruction
from qiskit.circuit.delay import Delay
from qiskit.dagcircuit import DAGCircuit, DAGNode
from qiskit.transpiler.basepasses import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.target import Target

logger = logging.getLogger(__name__)


class BasePadding(TransformationPass):
Expand Down Expand Up @@ -49,6 +53,20 @@ class BasePadding(TransformationPass):
which may result in violation of hardware alignment constraints.
"""

def __init__(
self,
target: Target = None,
):
"""BasePadding initializer.
Args:
target: The :class:`~.Target` representing the target backend.
If it supplied and it does not support delay instruction on a qubit,
padding passes do not pad any idle time of the qubit.
"""
super().__init__()
self.target = target

def run(self, dag: DAGCircuit):
"""Run the padding pass on ``dag``.
Expand Down Expand Up @@ -83,6 +101,7 @@ def run(self, dag: DAGCircuit):
new_dag.calibrations = dag.calibrations
new_dag.global_phase = dag.global_phase

bit_indices = {q: index for index, q in enumerate(dag.qubits)}
idle_after = {bit: 0 for bit in dag.qubits}

# Compute fresh circuit duration from the node start time dictionary and op duration.
Expand All @@ -104,9 +123,8 @@ def run(self, dag: DAGCircuit):
continue

for bit in node.qargs:

# Fill idle time with some sequence
if t0 - idle_after[bit] > 0:
if t0 - idle_after[bit] > 0 and self.__delay_supported(bit_indices[bit]):
# Find previous node on the wire, i.e. always the latest node on the wire
prev_node = next(new_dag.predecessors(new_dag.output_map[bit]))
self._pad(
Expand All @@ -129,7 +147,7 @@ def run(self, dag: DAGCircuit):

# Add delays until the end of circuit.
for bit in new_dag.qubits:
if circuit_duration - idle_after[bit] > 0:
if circuit_duration - idle_after[bit] > 0 and self.__delay_supported(bit_indices[bit]):
node = new_dag.output_map[bit]
prev_node = next(new_dag.predecessors(node))
self._pad(
Expand All @@ -145,6 +163,12 @@ def run(self, dag: DAGCircuit):

return new_dag

def __delay_supported(self, qarg: int) -> bool:
"""Delay operation is supported on the qubit (qarg) or not."""
if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)):
return True
return False

def _pre_runhook(self, dag: DAGCircuit):
"""Extra routine inserted before running the padding pass.
Expand All @@ -159,6 +183,12 @@ def _pre_runhook(self, dag: DAGCircuit):
f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes "
f"before running the {self.__class__.__name__} pass."
)
for qarg, _ in enumerate(dag.qubits):
if not self.__delay_supported(qarg):
logger.debug(
"No padding on qubit %d as delay is not supported on it",
qarg,
)

def _apply_scheduled_op(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"""Dynamical Decoupling insertion pass."""

import logging
from typing import List, Optional

import numpy as np
Expand All @@ -29,6 +30,8 @@

from .base_padding import BasePadding

logger = logging.getLogger(__name__)


class PadDynamicalDecoupling(BasePadding):
"""Dynamical decoupling insertion pass.
Expand Down Expand Up @@ -152,7 +155,7 @@ def __init__(
non-multiple of the alignment constraint value is found.
TypeError: If ``dd_sequence`` is not specified
"""
super().__init__()
super().__init__(target=target)
self._durations = durations
if dd_sequence is None:
raise TypeError("required argument 'dd_sequence' is not specified")
Expand All @@ -163,10 +166,16 @@ def __init__(
self._spacing = spacing
self._extra_slack_distribution = extra_slack_distribution

self._no_dd_qubits = set()
self._dd_sequence_lengths = {}
self._sequence_phase = 0
if target is not None:
self._durations = target.durations()
for gate in dd_sequence:
if gate.name not in target.operation_names:
raise TranspilerError(
f"{gate.name} in dd_sequence is not supported in the target"
)

def _pre_runhook(self, dag: DAGCircuit):
super()._pre_runhook(dag)
Expand Down Expand Up @@ -200,10 +209,18 @@ def _pre_runhook(self, dag: DAGCircuit):
raise TranspilerError("The DD sequence does not make an identity operation.")
self._sequence_phase = np.angle(noop[0][0])

# Compute no DD qubits on which any gate in dd_sequence is not supported in the target
for qarg, _ in enumerate(dag.qubits):
for gate in self._dd_sequence:
if not self.__gate_supported(gate, qarg):
self._no_dd_qubits.add(qarg)
logger.debug(
"No DD on qubit %d as gate %s is not supported on it", qarg, gate.name
)
break
# Precompute qubit-wise DD sequence length for performance
for qubit in dag.qubits:
physical_index = dag.qubits.index(qubit)
if self._qubits and physical_index not in self._qubits:
for physical_index, qubit in enumerate(dag.qubits):
if not self.__is_dd_qubit(physical_index):
continue

sequence_lengths = []
Expand Down Expand Up @@ -231,6 +248,20 @@ def _pre_runhook(self, dag: DAGCircuit):
gate.duration = gate_length
self._dd_sequence_lengths[qubit] = sequence_lengths

def __gate_supported(self, gate: Gate, qarg: int) -> bool:
"""A gate is supported on the qubit (qarg) or not."""
if self.target is None or self.target.instruction_supported(gate.name, qargs=(qarg,)):
return True
return False

def __is_dd_qubit(self, qubit_index: int) -> bool:
"""DD can be inserted in the qubit or not."""
if (qubit_index in self._no_dd_qubits) or (
self._qubits and qubit_index not in self._qubits
):
return False
return True

def _pad(
self,
dag: DAGCircuit,
Expand Down Expand Up @@ -268,7 +299,7 @@ def _pad(
# Y3: 288 dt + 160 dt + 80 dt = 528 dt (33 x 16 dt)
# Y4: 368 dt + 160 dt + 80 dt = 768 dt (48 x 16 dt)
#
# As you can see, constraints on t0 are all satified without explicit scheduling.
# As you can see, constraints on t0 are all satisfied without explicit scheduling.
time_interval = t_end - t_start
if time_interval % self._alignment != 0:
raise TranspilerError(
Expand All @@ -277,7 +308,7 @@ def _pad(
f"on qargs {next_node.qargs}."
)

if self._qubits and dag.qubits.index(qubit) not in self._qubits:
if not self.__is_dd_qubit(dag.qubits.index(qubit)):
# Target physical qubit is not the target of this DD sequence.
self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit)
return
Expand Down
8 changes: 6 additions & 2 deletions qiskit/transpiler/passes/scheduling/padding/pad_delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from qiskit.circuit import Qubit
from qiskit.circuit.delay import Delay
from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode
from qiskit.transpiler.target import Target

from .base_padding import BasePadding

Expand Down Expand Up @@ -50,13 +51,16 @@ class PadDelay(BasePadding):
See :class:`BasePadding` pass for details.
"""

def __init__(self, fill_very_end: bool = True):
def __init__(self, fill_very_end: bool = True, target: Target = None):
"""Create new padding delay pass.
Args:
fill_very_end: Set ``True`` to fill the end of circuit with delay.
target: The :class:`~.Target` representing the target backend.
If it supplied and it does not support delay instruction on a qubit,
padding passes do not pad any idle time of the qubit.
"""
super().__init__()
super().__init__(target=target)
self.fill_very_end = fill_very_end

def _pad(
Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/preset_passmanagers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ def _require_alignment(property_set):
)
if scheduling_method:
# Call padding pass if circuit is scheduled
scheduling.append(PadDelay())
scheduling.append(PadDelay(target=target))

return scheduling

Expand Down
13 changes: 13 additions & 0 deletions releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
fixes:
- |
Fixed an issue in tranpiler passes for padding delays, which did not respect target's constraints
and inserted delays even for qubits not supporting :class:`~.circuit.Delay` instruction.
:class:`~.PadDelay` and :class:`~.PadDynamicalDecoupling` are fixed
so that they do not pad any idle time of qubits such that the target does not support
``Delay`` instructions for the qubits.
Also legacy scheduling passes ``ASAPSchedule`` and ``ALAPSchedule``,
which pad delays internally, are fixed in the same way.
In addition, :func:`transpile` is fixed to call ``PadDelay`` with a ``target`` object
so that it works correctly when called with ``scheduling_method`` option.
Fixed `#9993 <https://github.com/Qiskit/qiskit-terra/issues/9993>`__
Loading

0 comments on commit 117d188

Please sign in to comment.