diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index d561218f70c0..c63b3f825119 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -948,8 +948,8 @@ def _parse_instruction_durations(backend, inst_durations, dt, circuits): if circ.calibrations: cal_durations = [] for gate, gate_cals in circ.calibrations.items(): - for (qubits, _), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, schedule.duration)) + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) circ_durations.update(cal_durations, circ_durations.dt) if inst_durations: diff --git a/qiskit/transpiler/instruction_durations.py b/qiskit/transpiler/instruction_durations.py index 423eb5146ea2..bc43c78b68d9 100644 --- a/qiskit/transpiler/instruction_durations.py +++ b/qiskit/transpiler/instruction_durations.py @@ -27,7 +27,11 @@ class InstructionDurations: It stores durations (gate lengths) and dt to be used at the scheduling stage of transpiling. It can be constructed from ``backend`` or ``instruction_durations``, - which is an argument of :func:`transpile`. + which is an argument of :func:`transpile`. The duration of an instruction depends on the + instruction (given by name), the qubits, and optionally the parameters of the instruction. + Note that these fields are used as keys in dictionaries that are used to retrieve the + instruction durations. Therefore, users must use the exact same parameter value to retrieve + an instruction duration as the value with which it was added. """ def __init__( @@ -35,6 +39,7 @@ def __init__( ): self.duration_by_name = {} self.duration_by_name_qubits = {} + self.duration_by_name_qubits_params = {} self.dt = dt if instruction_durations: self.update(instruction_durations) @@ -108,25 +113,48 @@ def update(self, inst_durations: Optional["InstructionDurationsType"], dt: float if isinstance(inst_durations, InstructionDurations): self.duration_by_name.update(inst_durations.duration_by_name) self.duration_by_name_qubits.update(inst_durations.duration_by_name_qubits) + self.duration_by_name_qubits_params.update( + inst_durations.duration_by_name_qubits_params + ) else: for i, items in enumerate(inst_durations): - if len(items) == 3: - inst_durations[i] = (*items, "dt") # set default unit - elif len(items) != 4: + + if not isinstance(items[-1], str): + items = (*items, "dt") # set default unit + + if len(items) == 4: # (inst_name, qubits, duration, unit) + inst_durations[i] = (*items[:3], None, items[3]) + else: + inst_durations[i] = items + + # assert (inst_name, qubits, duration, parameters, unit) + if len(inst_durations[i]) != 5: raise TranspilerError( "Each entry of inst_durations dictionary must be " "(inst_name, qubits, duration) or " - "(inst_name, qubits, duration, unit)" + "(inst_name, qubits, duration, unit) or" + "(inst_name, qubits, duration, parameters) or" + "(inst_name, qubits, duration, parameters, unit) " + f"received {inst_durations[i]}." ) - for name, qubits, duration, unit in inst_durations: + if inst_durations[i][2] is None: + raise TranspilerError(f"None duration for {inst_durations[i]}.") + + for name, qubits, duration, parameters, unit in inst_durations: if isinstance(qubits, int): qubits = [qubits] + if isinstance(parameters, (int, float)): + parameters = [parameters] + if qubits is None: self.duration_by_name[name] = duration, unit - else: + elif parameters is None: self.duration_by_name_qubits[(name, tuple(qubits))] = duration, unit + else: + key = (name, tuple(qubits), tuple(parameters)) + self.duration_by_name_qubits_params[key] = duration, unit return self @@ -135,13 +163,17 @@ def get( inst: Union[str, Instruction], qubits: Union[int, List[int], Qubit, List[Qubit]], unit: str = "dt", + parameters: Optional[List[float]] = None, ) -> float: - """Get the duration of the instruction with the name and the qubits. + """Get the duration of the instruction with the name, qubits, and parameters. + + Some instructions may have a parameter dependent duration. Args: inst: An instruction or its name to be queried. qubits: Qubits or its indices that the instruction acts on. unit: The unit of duration to be returned. It must be 's' or 'dt'. + parameters: The value of the parameters of the desired instruction. Returns: float|int: The duration of the instruction on the qubits. @@ -174,19 +206,31 @@ def get( qubits = [q.index for q in qubits] try: - return self._get(inst_name, qubits, unit) + return self._get(inst_name, qubits, unit, parameters) except TranspilerError as ex: raise TranspilerError( f"Duration of {inst_name} on qubits {qubits} is not found." ) from ex - def _get(self, name: str, qubits: List[int], to_unit: str) -> float: - """Get the duration of the instruction with the name and the qubits.""" + def _get( + self, + name: str, + qubits: List[int], + to_unit: str, + parameters: Optional[Iterable[float]] = None, + ) -> float: + """Get the duration of the instruction with the name, qubits, and parameters.""" if name == "barrier": return 0 - key = (name, tuple(qubits)) - if key in self.duration_by_name_qubits: + if parameters is not None: + key = (name, tuple(qubits), tuple(parameters)) + else: + key = (name, tuple(qubits)) + + if key in self.duration_by_name_qubits_params: + duration, unit = self.duration_by_name_qubits_params[key] + elif key in self.duration_by_name_qubits: duration, unit = self.duration_by_name_qubits[key] elif name in self.duration_by_name: duration, unit = self.duration_by_name[name] @@ -232,8 +276,10 @@ def units_used(self) -> Set[str]: InstructionDurationsType = Union[ + List[Tuple[str, Optional[Iterable[int]], float, Optional[Iterable[float]], str]], + List[Tuple[str, Optional[Iterable[int]], float, Optional[Iterable[float]]]], List[Tuple[str, Optional[Iterable[int]], float, str]], List[Tuple[str, Optional[Iterable[int]], float]], InstructionDurations, ] -"""List of tuples representing (instruction name, qubits indices, duration).""" +"""List of tuples representing (instruction name, qubits indices, parameters, duration).""" diff --git a/releasenotes/notes/instruction-durations-8d98369f89b48279.yaml b/releasenotes/notes/instruction-durations-8d98369f89b48279.yaml new file mode 100644 index 000000000000..c671ae706bfe --- /dev/null +++ b/releasenotes/notes/instruction-durations-8d98369f89b48279.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The `InstructionDurations` class is upgraded to accept gate parameters. Now, + an instruction duration is a tuple of `(inst_name, qubits, duration, parameters, unit)`. diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index 2440c013193a..16722a656f0a 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -15,6 +15,7 @@ import unittest import numpy as np from numpy import pi +from ddt import ddt, data from qiskit.circuit import QuantumCircuit, Delay from qiskit.circuit.library import XGate, YGate, RXGate, UGate @@ -24,9 +25,12 @@ from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError +import qiskit.pulse as pulse + from qiskit.test import QiskitTestCase +@ddt class TestDynamicalDecoupling(QiskitTestCase): """Tests DynamicalDecoupling pass.""" @@ -595,6 +599,29 @@ def test_insert_dd_bad_sequence(self): with self.assertRaises(TranspilerError): pm.run(self.ghz4) + @data(0.5, 1.5) + def test_dd_with_calibrations_with_parameters(self, param_value): + """Check that calibrations in a circuit with parameters work fine.""" + + circ = QuantumCircuit(2) + circ.x(0) + circ.cx(0, 1) + circ.rx(param_value, 1) + + rx_duration = int(param_value * 1000) + + with pulse.build() as rx: + pulse.play(pulse.Gaussian(rx_duration, 0.1, rx_duration // 4), pulse.DriveChannel(1)) + + circ.add_calibration("rx", (1,), rx, params=[param_value]) + + durations = InstructionDurations([("x", None, 100), ("cx", None, 300)]) + + dd_sequence = [XGate(), XGate()] + pm = PassManager([ALAPSchedule(durations), DynamicalDecoupling(durations, dd_sequence)]) + + self.assertEqual(pm.run(circ).duration, rx_duration + 100 + 300) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_instruction_durations.py b/test/python/transpiler/test_instruction_durations.py index 0c51cf98cec4..e7c296ae89dc 100644 --- a/test/python/transpiler/test_instruction_durations.py +++ b/test/python/transpiler/test_instruction_durations.py @@ -52,6 +52,14 @@ def test_from_backend_for_backend_without_dt(self): with self.assertRaises(TranspilerError): durations.get(gate, 0) + def test_update_with_parameters(self): + durations = InstructionDurations( + [("rzx", (0, 1), 150, (0.5,)), ("rzx", (0, 1), 300, (1.0,))] + ) + + self.assertEqual(durations.get("rzx", [0, 1], parameters=[0.5]), 150) + self.assertEqual(durations.get("rzx", [0, 1], parameters=[1.0]), 300) + def _find_gate_with_length(self, backend): """Find a gate that has gate length.""" props = backend.properties()