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 3 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
69 changes: 50 additions & 19 deletions qiskit/transpiler/instruction_durations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@

"""Durations of instructions, one of transpiler configurations."""
from __future__ import annotations
import logging
from typing import Optional, List, Tuple, Union, Iterable

import qiskit.circuit
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

logger = logging.getLogger(__name__)


class InstructionDurations:
"""Helper class to provide durations of instructions for scheduling.
Expand Down Expand Up @@ -75,26 +78,54 @@ def from_backend(cls, backend: 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 InstructionDurations(instruction_durations, dt=dt)
dt = 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:
pass

# Logic to handle if backend is sub-class is BackendV2
elif isinstance(backend, BackendV2):
target = backend.target
inst_with_no_props = {"delay"}
inst_duration = None
for name in target.operation_names:
for qubits, inst_props in target._gate_map[name].items():
if name in inst_with_no_props:
# Setting the duration for 'delay' to zero.
inst_duration = 0.0
else:
try:
inst_duration = inst_props.duration
except AttributeError:
logger.info("%s on %s did not report any duration", name, qubits)
continue
instruction_durations.append((name, qubits, inst_duration, "s"))

try:
dt = target.dt
except AttributeError:
logger.info("Backend Target didn't report any dt")
Copy link
Member

@mtreinish mtreinish Jan 9, 2024

Choose a reason for hiding this comment

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

Why not just use backend.target.durations() (https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.Target#durations)? It should already have most of this logic present in there so you can make this branch one line.

Copy link
Contributor Author

@MozammilQ MozammilQ Jan 9, 2024

Choose a reason for hiding this comment

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

@mtreinish , Haha! :) I was working on a different PR, I discovered InstructionDurations.from_backend() didn't have the BackendV2 and did this PR.
A much better way: backend.target.durations() didn't cross my mind :)
Thanks, for the suggestion :)
I will add the suggestion :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mtreinish , added suggestion, please see if this is good enough :)


return cls(instruction_durations, dt=dt)

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,18 @@
---
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`
with followig code.

.. code-block:: python
from qiskit.providers.fake_provder import FakePerth
from qiskit.transpiler import InstructionDurations
backendV2 = FakePerth()

inst_dur = InstructionDurations.from_backend(backendV2)



124 changes: 92 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,91 @@ 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])
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])


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(self.example_gate, self.example_qubit[0], "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])