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

InstructionDurations from BackendV2 #11528

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
52 changes: 30 additions & 22 deletions qiskit/transpiler/instruction_durations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from qiskit.circuit import Barrier, Delay
from qiskit.circuit import Instruction, ParameterExpression
from qiskit.circuit.duration import duration_in_dt
from qiskit.providers import Backend
from qiskit.providers import Backend, BackendV1, BackendV2
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.utils.units import apply_prefix

Expand Down Expand Up @@ -71,30 +71,38 @@ def from_backend(cls, backend: Backend):

Returns:
InstructionDurations: The InstructionDurations constructed from backend.

Raises:
TranspilerError: If dt and dtm is different in the backend.
"""

# All durations in seconds in gate_length
instruction_durations = []
backend_properties = backend.properties()
if hasattr(backend_properties, "_gates"):
for gate, insts in backend_properties._gates.items():
for qubits, props in insts.items():
if "gate_length" in props:
gate_length = props["gate_length"][0] # Throw away datetime at index 1
instruction_durations.append((gate, qubits, gate_length, "s"))
for q, props in backend.properties()._qubits.items():
if "readout_length" in props:
readout_length = props["readout_length"][0] # Throw away datetime at index 1
instruction_durations.append(("measure", [q], readout_length, "s"))

try:
dt = backend.configuration().dt
except AttributeError:
dt = None

return cls(instruction_durations, dt=dt)
return_durations = None

# Logic to handle if backend is sub-class of old BackendV1
if isinstance(backend, BackendV1):
backend_properties = backend.properties()
if hasattr(backend_properties, "_gates"):
for gate, insts in backend_properties._gates.items():
for qubits, props in insts.items():
if "gate_length" in props:
gate_length = props["gate_length"][0] # Ignore datetime at index 1
instruction_durations.append((gate, qubits, gate_length, "s"))
for q, props in backend.properties()._qubits.items():
if "readout_length" in props:
readout_length = props["readout_length"][0] # Ignore datetime at index 1
instruction_durations.append(("measure", [q], readout_length, "s"))

try:
dt = backend.configuration().dt
except AttributeError:
dt = None

return_durations = cls(instruction_durations, dt=dt)

# Logic to handle if backend is sub-class is BackendV2
elif isinstance(backend, BackendV2):
return_durations = backend.target.durations()

return return_durations

def update(self, inst_durations: "InstructionDurationsType" | None, dt: float = None):
"""Update self with inst_durations (inst_durations overwrite self).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
features:
- |
This enables the support for :class:`.BackendV2` in :meth:`.from_backend`
of :class:`.InstructionDurations`.
MozammilQ marked this conversation as resolved.
Show resolved Hide resolved

Users can have an object of :class:`.InstructionDurations` using :class:`.BackendV2`
from :meth:`.from_backend` with followig code.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from :meth:`.from_backend` with followig code.
from :meth:`.InstructionDurations.from_backend` with followig code.

to fix doc error


.. code-block:: python

from qiskit.providers.fake_provider import FakePerth
from qiskit.transpiler import InstructionDurations
backendV2 = FakePerth()

inst_dur = InstructionDurations.from_backend(backendV2)

upgrade:
- |
Given :code:`dt` and :code:`dtm` for a :class:`.BackendV1` or :class:`.BackendV2` are unequal,
:meth:`~.InstructionDurations.from_backend` does not raise any error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any change in code behavior with this PR? dtm is acquisition sampling rate and doesn't impact the instruction duration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nkanazawa1989 ,
According to,

TranspilerError: If dt and dtm is different in the backend.

I should get TranspilerError if dt!=dtm, I just checked it, and found it doesn't.
So, I added this line in release note and added test test_works_unequal_dt_dtm to make sure this case is covered :)

Copy link
Contributor

@nkanazawa1989 nkanazawa1989 Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The document should be wrong. See p.33 of https://arxiv.org/abs/1809.03452. dtm defines the sampling rate of digitizer, which can be independent of dt that defines the sampling rate of AWG.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this upgrade note is not necessary because TranspilerError was not actually raised (just accidentally documented).


146 changes: 114 additions & 32 deletions test/python/transpiler/test_instruction_durations.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
# pylint: disable=missing-function-docstring

"""Test InstructionDurations class."""

from copy import deepcopy
from qiskit.circuit import Delay, Parameter
from qiskit.providers.fake_provider import FakeParis, FakeTokyo
from qiskit.providers.fake_provider import FakeParis, FakePerth
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.transpiler.instruction_durations import InstructionDurations

from qiskit.test.base import QiskitTestCase


class TestInstructionDurationsClass(QiskitTestCase):
"""Test Test InstructionDurations class."""
class TestInstructionDurations(QiskitTestCase):
"""Test InstructionDurations class."""

def test_empty(self):
durations = InstructionDurations()
Expand All @@ -36,41 +36,13 @@ def test_fail_if_invalid_dict_is_supplied_when_construction(self):
with self.assertRaises(TranspilerError):
InstructionDurations(invalid_dic)

def test_from_backend_for_backend_with_dt(self):
backend = FakeParis()
gate = self._find_gate_with_length(backend)
durations = InstructionDurations.from_backend(backend)
self.assertGreater(durations.dt, 0)
self.assertGreater(durations.get(gate, 0), 0)

def test_from_backend_for_backend_without_dt(self):
backend = FakeTokyo()
gate = self._find_gate_with_length(backend)
durations = InstructionDurations.from_backend(backend)
self.assertIsNone(durations.dt)
self.assertGreater(durations.get(gate, 0, "s"), 0)
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()
for gate in props.gates:
try:
if props.gate_length(gate.gate, 0):
return gate.gate
except Exception: # pylint: disable=broad-except
pass
raise ValueError("Unable to find a gate with gate length.")

def test_can_get_unbounded_duration_without_unit_conversion(self):
param = Parameter("t")
parameterized_delay = Delay(param, "dt")
Expand All @@ -88,3 +60,113 @@ def test_fail_if_get_unbounded_duration_with_unit_conversion_when_dt_is_not_prov
parameterized_delay = Delay(param, "s")
with self.assertRaises(TranspilerError):
InstructionDurations().get(parameterized_delay, 0)


class TestInstrctionDurationsFromBackendV1(QiskitTestCase):
"""Test :meth:`~.from_backend` of :class:`.InstructionDurations` with
:class:`.BackendV1`"""

def setUp(self):
super().setUp()

self.backend = FakeParis()
self.backend_config = self.backend.configuration()
self.backend_props = self.backend.properties()
MozammilQ marked this conversation as resolved.
Show resolved Hide resolved
self.example_qubit = (0,)
self.example_gate = "x"

# Setting dt for the copy of backend to be None
self.backend_cpy = deepcopy(self.backend)
self.backend_cpy.configuration().dt = None

def test_backend_dt_equals_inst_dur_dt(self):
durations = InstructionDurations.from_backend(self.backend)
self.assertEqual(durations.dt, self.backend_config.dt)

def test_backend_gate_length_equals_inst_dur(self):
durations = InstructionDurations.from_backend(self.backend)
inst_dur_duration = durations.get(self.example_gate, self.example_qubit[0], "s")
backend_inst_dur = self.backend_props.gate_length(
gate=self.example_gate, qubits=self.example_qubit
)
self.assertEqual(inst_dur_duration, backend_inst_dur)

def test_backend_without_dt_sets_inst_dur_None(self):
durations = InstructionDurations.from_backend(self.backend_cpy)
self.assertIsNone(durations.dt)

def test_get_dur_s_with_dt_None(self):
durations = InstructionDurations.from_backend(self.backend_cpy)
self.assertEqual(
durations.get(self.example_gate, self.example_qubit[0], "s"), 3.5555555555555554e-08
)

def test_raise_dur_get_dt_with_backend_dt_None(self):
durations = InstructionDurations.from_backend(self.backend_cpy)
with self.assertRaises(TranspilerError):
durations.get(self.example_gate, self.example_qubit[0])

def test_works_unequal_dt_dtm(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this test? InstructionDuration is the object used for scheduling, and dtm doesn't matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nkanazawa1989 , This line was the motivation:

TranspilerError: If dt and dtm is different in the backend.

Am I missing something

self.backend_cpy.configuration().dt = 1.0

# This is expcted to fail
InstructionDurations.from_backend(self.backend_cpy)

self.backend_cpy.configuration().dt = None # Resetting to None
MozammilQ marked this conversation as resolved.
Show resolved Hide resolved
# Check if dt and dtm were indeed unequal
self.assertNotEqual(self.backend_cpy.configuration().dtm, 1.0)


class TestInstrctionDurationsFromBackendV2(QiskitTestCase):
"""Test :meth:`~.from_backend` of :class:`.InstructionDurations` with
:class:`.BackendV2`"""

def setUp(self):
super().setUp()

self.backend = FakePerth()
self.example_gate = "x"
self.example_qubit = (0,)

# Setting dt for the copy for BackendV2 to None
self.backend_cpy = deepcopy(self.backend)
self.backend_cpy.target.dt = None

def test_backend_dt_equals_inst_dur_dt(self):
durations = InstructionDurations.from_backend(self.backend)
self.assertEqual(durations.dt, self.backend.dt)

def test_backend_gate_length_equals_inst_dur(self):
durations = InstructionDurations.from_backend(self.backend)
inst_dur_duration = durations.get(
inst=self.example_gate, qubits=self.example_qubit[0], unit="s"
)
backend_inst_dur = self.backend.target._gate_map[self.example_gate][
self.example_qubit
].duration
self.assertEqual(inst_dur_duration, backend_inst_dur)

def test_backend_without_dt_sets_inst_dur_None(self):
durations = InstructionDurations.from_backend(self.backend_cpy)
self.assertIsNone(durations.dt)

def test_get_dur_s_with_dt_None(self):
durations = InstructionDurations.from_backend(self.backend_cpy)
self.assertEqual(
durations.get(self.example_gate, self.example_qubit[0], "s"), 3.5555555555555554e-08
)

def test_raise_dur_get_dt_with_backend_dt_None(self):
durations = InstructionDurations.from_backend(self.backend_cpy)
with self.assertRaises(TranspilerError):
durations.get(self.example_gate, self.example_qubit[0])

def test_works_unequal_dt_dtm(self):
self.backend_cpy.target.dt = 1.0

# This is expcted to fail
InstructionDurations.from_backend(self.backend_cpy)

self.backend_cpy.target.dt = None # Resetting to None
MozammilQ marked this conversation as resolved.
Show resolved Hide resolved
# Check if dt and dtm were indeed unequal
self.assertNotEqual(self.backend_cpy.dtm, 1.0)