Skip to content

Commit

Permalink
Update stable branch (#1387)
Browse files Browse the repository at this point in the history
* Update main branch version 0.19.1 (#1382)

* Porting qiskit-ibm-provider/787: Fix `DynamicCircuitInstructionDurations.from_backend` for both `Backend versions` (#1383)

* porting qiskit-ibm-provider/pull/787

* porting qiskit-ibm-provider/pull/787

* black

* oops

* monkey patch Qiskit/qiskit#11727

* black lynt

* mypy

---------

Co-authored-by: Kevin Tian <[email protected]>

* Cast use_symengine input to a bool (#1385)

* Cast use_symengine input to a bool

This commit works around a bug in Qiskit 0.45.x, 0.46.0, and 1.0.0rc1
with the `use_symengine` flag on `qpy.dump()`. The dump function has a
bug when it receives a truthy value instead of a bool literal that it
will generate a corrupt qpy because of a mismatch between how the
encoding was processed (the encoding is incorrectly set to sympy in the
file header but uses symengine encoding in the actual body of the
circuit.  This is being fixed in Qiskit/qiskit#11730 for 1.0.0, and will
be backported to 0.46.1. But to ensure compatibility with 0.45.x, 0.46.0,
and 1.0.0rc1 while waiting for those releases we can workaround this by
just casting the value to a boolean.

* Fix mypy failures

* Mypy fixes again

* Prepare release 0.19.1 (#1386)

---------

Co-authored-by: Luciano Bello <[email protected]>
Co-authored-by: Matthew Treinish <[email protected]>
  • Loading branch information
3 people authored Feb 7, 2024
1 parent bd7885c commit e975e16
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 49 deletions.
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ function-naming-style=snake_case
good-names=i,
j,
k,
dt,
ex,
Run,
_
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '0.19.0'
release = '0.19.1'

# -- General configuration ---------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion qiskit_ibm_runtime/VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.19.0
0.19.1
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,11 @@ def _pre_runhook(self, dag: DAGCircuit) -> None:
self._dd_sequence_lengths[qubit] = []

physical_index = dag.qubits.index(qubit)
if self._qubits and physical_index not in self._qubits:
if (
self._qubits
and physical_index not in self._qubits
or qubit in self._idle_qubits
):
continue

for index, gate in enumerate(seq):
Expand Down
112 changes: 92 additions & 20 deletions qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
InstructionDurations,
InstructionDurationsType,
)
from qiskit.transpiler.target import Target
from qiskit.transpiler.exceptions import TranspilerError
from qiskit.providers import Backend, BackendV1


def block_order_op_nodes(dag: DAGCircuit) -> Generator[DAGOpNode, None, None]:
Expand Down Expand Up @@ -150,6 +152,75 @@ def __init__(
self._enable_patching = enable_patching
super().__init__(instruction_durations=instruction_durations, dt=dt)

@classmethod
def from_backend(cls, backend: Backend) -> "DynamicCircuitInstructionDurations":
"""Construct a :class:`DynamicInstructionDurations` object from the backend.
Args:
backend: backend from which durations (gate lengths) and dt are extracted.
Returns:
DynamicInstructionDurations: The InstructionDurations constructed from backend.
"""
if isinstance(backend, BackendV1):
# TODO Remove once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1
# From here ---------------------------------------
def patch_from_backend(cls, backend: Backend): # type: ignore
"""
REMOVE me once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1
"""
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, # pylint: disable=invalid-name
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 patch_from_backend(DynamicCircuitInstructionDurations, backend)
# To here --------------------------------------- (remove comment ignore annotations too)
return super( # type: ignore # pylint: disable=unreachable
DynamicCircuitInstructionDurations, cls
).from_backend(backend)

# Get durations from target if BackendV2
return cls.from_target(backend.target)

@classmethod
def from_target(cls, target: Target) -> "DynamicCircuitInstructionDurations":
"""Construct a :class:`DynamicInstructionDurations` object from the target.
Args:
target: target from which durations (gate lengths) and dt are extracted.
Returns:
DynamicInstructionDurations: The InstructionDurations constructed from backend.
"""

instruction_durations_dict = target.durations().duration_by_name_qubits
instruction_durations = []
for instr_key, instr_value in instruction_durations_dict.items():
instruction_durations += [(*instr_key, *instr_value)]
try:
dt = target.dt
except AttributeError:
dt = None
return cls(instruction_durations, dt=dt)

def update(
self, inst_durations: Optional[InstructionDurationsType], dt: float = None
) -> "DynamicCircuitInstructionDurations":
Expand Down Expand Up @@ -206,15 +277,23 @@ def _patch_instruction(self, key: InstrKey) -> None:
elif name == "reset":
self._patch_reset(key)

def _convert_and_patch_key(self, key: InstrKey) -> None:
"""Convert duration to dt and patch key"""
prev_duration, unit = self._get_duration(key)
if unit != "dt":
prev_duration = self._convert_unit(prev_duration, unit, "dt")
# raise TranspilerError('Can currently only patch durations of "dt".')
odd_cycle_correction = self._get_odd_cycle_correction()
new_duration = prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction
if unit != "dt": # convert back to original unit
new_duration = self._convert_unit(new_duration, "dt", unit)
self._patch_key(key, new_duration, unit)

def _patch_measurement(self, key: InstrKey) -> None:
"""Patch measurement duration by extending duration by 160dt as temporarily
required by the dynamic circuit backend.
"""
prev_duration, unit = self._get_duration_dt(key)
if unit != "dt":
raise TranspilerError('Can currently only patch durations of "dt".')
odd_cycle_correction = self._get_odd_cycle_correction()
self._patch_key(key, prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction, unit)
self._convert_and_patch_key(key)
# Enforce patching of reset on measurement update
self._patch_reset(("reset", key[1], key[2]))

Expand All @@ -227,31 +306,24 @@ def _patch_reset(self, key: InstrKey) -> None:
# triggers the end of scheduling after the measurement pulse
measure_key = ("measure", key[1], key[2])
try:
measure_duration, unit = self._get_duration_dt(measure_key)
measure_duration, unit = self._get_duration(measure_key)
self._patch_key(key, measure_duration, unit)
except KeyError:
# Fall back to reset key if measure not available
prev_duration, unit = self._get_duration_dt(key)
if unit != "dt":
raise TranspilerError('Can currently only patch durations of "dt".')
odd_cycle_correction = self._get_odd_cycle_correction()
self._patch_key(
key,
prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction,
unit,
)
self._convert_and_patch_key(key)

def _get_duration_dt(self, key: InstrKey) -> Tuple[int, str]:
def _get_duration(self, key: InstrKey) -> Tuple[int, str]:
"""Handling for the complicated structure of this class.
TODO: This class implementation should be simplified in Qiskit. Too many edge cases.
"""
if key[1] is None and key[2] is None:
return self.duration_by_name[key[0]]
duration = self.duration_by_name[key[0]]
elif key[2] is None:
return self.duration_by_name_qubits[(key[0], key[1])]

return self.duration_by_name_qubits_params[key]
duration = self.duration_by_name_qubits[(key[0], key[1])]
else:
duration = self.duration_by_name_qubits_params[key]
return duration

def _patch_key(self, key: InstrKey, duration: int, unit: str) -> None:
"""Handling for the complicated structure of this class.
Expand Down
6 changes: 3 additions & 3 deletions qiskit_ibm_runtime/utils/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
if hasattr(obj, "to_json"):
return {"__type__": "to_json", "__value__": obj.to_json()}
if isinstance(obj, QuantumCircuit):
kwargs = {"use_symengine": optionals.HAS_SYMENGINE}
kwargs: dict[str, object] = {"use_symengine": bool(optionals.HAS_SYMENGINE)}
if _TERRA_VERSION[0] >= 1:
# NOTE: This can be updated only after the server side has
# updated to a newer qiskit version.
Expand All @@ -239,13 +239,13 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
data=obj,
serializer=_write_parameter_expression,
compress=False,
use_symengine=optionals.HAS_SYMENGINE,
use_symengine=bool(optionals.HAS_SYMENGINE),
)
return {"__type__": "ParameterExpression", "__value__": value}
if isinstance(obj, ParameterView):
return obj.data
if isinstance(obj, Instruction):
kwargs = {"use_symengine": optionals.HAS_SYMENGINE}
kwargs = {"use_symengine": bool(optionals.HAS_SYMENGINE)}
if _TERRA_VERSION[0] >= 1:
# NOTE: This can be updated only after the server side has
# updated to a newer qiskit version.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
fixes:
- |
Fix the patching of :class:`.DynamicCircuitInstructions` for instructions
with durations that are not in units of ``dt``.
upgrade:
- |
Extend :meth:`.DynamicCircuitInstructions.from_backend` to extract and
patch durations from both :class:`.BackendV1` and :class:`.BackendV2`
objects. Also add :meth:`.DynamicCircuitInstructions.from_target` to use a
:class:`.Target` object instead.
8 changes: 8 additions & 0 deletions releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
fixes:
- |
Fixed an issue with the :func:`.qpy.dump` function, when the
``use_symengine`` flag was set to a truthy object that evaluated to
``True`` but was not actually the boolean ``True`` the generated QPY
payload would be corrupt.
31 changes: 21 additions & 10 deletions test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -1038,35 +1038,46 @@ def test_disjoint_coupling_map(self):
self.assertEqual(delay_dict[0], delay_dict[2])

def test_no_unused_qubits(self):
"""Test DD with if_test circuit that unused qubits are untouched and not scheduled.
This ensures that programs don't have unnecessary information for unused qubits.
Which might hurt performance in later executon stages.
"""Test DD with if_test circuit that unused qubits are untouched and
not scheduled. Unused qubits may also have missing durations when
not operational.
This ensures that programs don't have unnecessary information for
unused qubits.
Which might hurt performance in later execution stages.
"""

# Here "x" on qubit 3 is not defined
durations = DynamicCircuitInstructionDurations(
[
("h", 0, 50),
("x", 0, 50),
("x", 1, 50),
("x", 2, 50),
("measure", 0, 840),
("reset", 0, 1340),
]
)

dd_sequence = [XGate(), XGate()]
pm = PassManager(
[
ASAPScheduleAnalysis(self.durations),
PadDynamicalDecoupling(
self.durations,
durations,
dd_sequence,
pulse_alignment=1,
sequence_min_length_ratios=[0.0],
),
]
)

qc = QuantumCircuit(3, 1)
qc = QuantumCircuit(4, 1)
qc.measure(0, 0)
qc.x(1)
with qc.if_test((0, True)):
qc.x(1)
qc.measure(0, 0)
with qc.if_test((0, True)):
qc.x(0)
qc.x(1)
qc_dd = pm.run(qc)
dont_use = qc_dd.qubits[-1]
dont_use = qc_dd.qubits[-2:]
for op in qc_dd.data:
self.assertNotIn(dont_use, op.qubits)
19 changes: 6 additions & 13 deletions test/unit/transpiler/passes/scheduling/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1774,23 +1774,16 @@ def test_transpile_both_paths(self):

qr = QuantumRegister(7, name="q")
expected = QuantumCircuit(qr, cr)
expected.delay(24080, qr[1])
expected.delay(24080, qr[2])
expected.delay(24080, qr[3])
expected.delay(24080, qr[4])
expected.delay(24080, qr[5])
expected.delay(24080, qr[6])
for q_ind in range(1, 7):
expected.delay(24240, qr[q_ind])
expected.measure(qr[0], cr[0])
with expected.if_test((cr[0], 1)):
expected.x(qr[0])
with expected.if_test((cr[0], 1)):
expected.delay(160, qr[0])
expected.x(qr[1])
expected.delay(160, qr[2])
expected.delay(160, qr[3])
expected.delay(160, qr[4])
expected.delay(160, qr[5])
expected.delay(160, qr[6])
for q_ind in range(7):
if q_ind != 1:
expected.delay(160, qr[q_ind])
self.assertEqual(expected, scheduled)

def test_c_if_plugin_conversion_with_transpile(self):
Expand Down Expand Up @@ -1837,7 +1830,7 @@ def test_no_unused_qubits(self):
"""Test DD with if_test circuit that unused qubits are untouched and not scheduled.
This ensures that programs don't have unnecessary information for unused qubits.
Which might hurt performance in later executon stages.
Which might hurt performance in later execution stages.
"""

durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)])
Expand Down
28 changes: 28 additions & 0 deletions test/unit/transpiler/passes/scheduling/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from qiskit_ibm_runtime.transpiler.passes.scheduling.utils import (
DynamicCircuitInstructionDurations,
)
from qiskit_ibm_runtime.fake_provider import FakeKolkata, FakeKolkataV2
from .....ibm_test_case import IBMTestCase


Expand Down Expand Up @@ -51,6 +52,33 @@ def test_patch_measure(self):
self.assertEqual(short_odd_durations.get("measure", (0,)), 1224)
self.assertEqual(short_odd_durations.get("reset", (0,)), 1224)

def test_durations_from_backend_v1(self):
"""Test loading and patching durations from a V1 Backend"""

durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkata())

self.assertEqual(durations.get("x", (0,)), 160)
self.assertEqual(durations.get("measure", (0,)), 3200)
self.assertEqual(durations.get("reset", (0,)), 3200)

def test_durations_from_backend_v2(self):
"""Test loading and patching durations from a V2 Backend"""

durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkataV2())

self.assertEqual(durations.get("x", (0,)), 160)
self.assertEqual(durations.get("measure", (0,)), 3200)
self.assertEqual(durations.get("reset", (0,)), 3200)

def test_durations_from_target(self):
"""Test loading and patching durations from a target"""

durations = DynamicCircuitInstructionDurations.from_target(FakeKolkataV2().target)

self.assertEqual(durations.get("x", (0,)), 160)
self.assertEqual(durations.get("measure", (0,)), 3200)
self.assertEqual(durations.get("reset", (0,)), 3200)

def test_patch_disable(self):
"""Test if schedules circuits with c_if after measure with a common clbit.
See: https://github.com/Qiskit/qiskit-terra/issues/7654"""
Expand Down

0 comments on commit e975e16

Please sign in to comment.