Skip to content

Commit

Permalink
Parameters in InstructionDurations. (#7321)
Browse files Browse the repository at this point in the history
* * First draft of the instruction duration odification.

* * Adding suggestion by Itoko

* * Fix bug where duration and parameters were switched

* * Remove None from tests.

* * black.

* * Added check on None duration.

* * Added test.

* * Reno

* * Test fix.

* * Moved test and updated reno.

* * Docstring.

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
eggerdj and mergify[bot] authored Feb 25, 2022
1 parent 2600586 commit 15a109e
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 16 deletions.
4 changes: 2 additions & 2 deletions qiskit/compiler/transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
74 changes: 60 additions & 14 deletions qiskit/transpiler/instruction_durations.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ 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__(
self, instruction_durations: Optional["InstructionDurationsType"] = None, dt: float = None
):
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)
Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)."""
Original file line number Diff line number Diff line change
@@ -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)`.
27 changes: 27 additions & 0 deletions test/python/transpiler/test_dynamical_decoupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""

Expand Down Expand Up @@ -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()
8 changes: 8 additions & 0 deletions test/python/transpiler/test_instruction_durations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 15a109e

Please sign in to comment.