Skip to content

Commit e975e16

Browse files
kt4741ucian0mtreinish
authored
Update stable branch (#1387)
* 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]>
1 parent bd7885c commit e975e16

File tree

11 files changed

+177
-49
lines changed

11 files changed

+177
-49
lines changed

.pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ function-naming-style=snake_case
379379
good-names=i,
380380
j,
381381
k,
382+
dt,
382383
ex,
383384
Run,
384385
_

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# The short X.Y version
2626
version = ''
2727
# The full version, including alpha/beta/rc tags
28-
release = '0.19.0'
28+
release = '0.19.1'
2929

3030
# -- General configuration ---------------------------------------------------
3131

qiskit_ibm_runtime/VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.19.0
1+
0.19.1

qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,11 @@ def _pre_runhook(self, dag: DAGCircuit) -> None:
325325
self._dd_sequence_lengths[qubit] = []
326326

327327
physical_index = dag.qubits.index(qubit)
328-
if self._qubits and physical_index not in self._qubits:
328+
if (
329+
self._qubits
330+
and physical_index not in self._qubits
331+
or qubit in self._idle_qubits
332+
):
329333
continue
330334

331335
for index, gate in enumerate(seq):

qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
InstructionDurations,
2222
InstructionDurationsType,
2323
)
24+
from qiskit.transpiler.target import Target
2425
from qiskit.transpiler.exceptions import TranspilerError
26+
from qiskit.providers import Backend, BackendV1
2527

2628

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

155+
@classmethod
156+
def from_backend(cls, backend: Backend) -> "DynamicCircuitInstructionDurations":
157+
"""Construct a :class:`DynamicInstructionDurations` object from the backend.
158+
Args:
159+
backend: backend from which durations (gate lengths) and dt are extracted.
160+
Returns:
161+
DynamicInstructionDurations: The InstructionDurations constructed from backend.
162+
"""
163+
if isinstance(backend, BackendV1):
164+
# TODO Remove once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1
165+
# From here ---------------------------------------
166+
def patch_from_backend(cls, backend: Backend): # type: ignore
167+
"""
168+
REMOVE me once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1
169+
"""
170+
instruction_durations = []
171+
backend_properties = backend.properties()
172+
if hasattr(backend_properties, "_gates"):
173+
for gate, insts in backend_properties._gates.items():
174+
for qubits, props in insts.items():
175+
if "gate_length" in props:
176+
gate_length = props["gate_length"][
177+
0
178+
] # Throw away datetime at index 1
179+
instruction_durations.append((gate, qubits, gate_length, "s"))
180+
for (
181+
q, # pylint: disable=invalid-name
182+
props,
183+
) in backend.properties()._qubits.items():
184+
if "readout_length" in props:
185+
readout_length = props["readout_length"][
186+
0
187+
] # Throw away datetime at index 1
188+
instruction_durations.append(("measure", [q], readout_length, "s"))
189+
try:
190+
dt = backend.configuration().dt
191+
except AttributeError:
192+
dt = None
193+
194+
return cls(instruction_durations, dt=dt)
195+
196+
return patch_from_backend(DynamicCircuitInstructionDurations, backend)
197+
# To here --------------------------------------- (remove comment ignore annotations too)
198+
return super( # type: ignore # pylint: disable=unreachable
199+
DynamicCircuitInstructionDurations, cls
200+
).from_backend(backend)
201+
202+
# Get durations from target if BackendV2
203+
return cls.from_target(backend.target)
204+
205+
@classmethod
206+
def from_target(cls, target: Target) -> "DynamicCircuitInstructionDurations":
207+
"""Construct a :class:`DynamicInstructionDurations` object from the target.
208+
Args:
209+
target: target from which durations (gate lengths) and dt are extracted.
210+
Returns:
211+
DynamicInstructionDurations: The InstructionDurations constructed from backend.
212+
"""
213+
214+
instruction_durations_dict = target.durations().duration_by_name_qubits
215+
instruction_durations = []
216+
for instr_key, instr_value in instruction_durations_dict.items():
217+
instruction_durations += [(*instr_key, *instr_value)]
218+
try:
219+
dt = target.dt
220+
except AttributeError:
221+
dt = None
222+
return cls(instruction_durations, dt=dt)
223+
153224
def update(
154225
self, inst_durations: Optional[InstructionDurationsType], dt: float = None
155226
) -> "DynamicCircuitInstructionDurations":
@@ -206,15 +277,23 @@ def _patch_instruction(self, key: InstrKey) -> None:
206277
elif name == "reset":
207278
self._patch_reset(key)
208279

280+
def _convert_and_patch_key(self, key: InstrKey) -> None:
281+
"""Convert duration to dt and patch key"""
282+
prev_duration, unit = self._get_duration(key)
283+
if unit != "dt":
284+
prev_duration = self._convert_unit(prev_duration, unit, "dt")
285+
# raise TranspilerError('Can currently only patch durations of "dt".')
286+
odd_cycle_correction = self._get_odd_cycle_correction()
287+
new_duration = prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction
288+
if unit != "dt": # convert back to original unit
289+
new_duration = self._convert_unit(new_duration, "dt", unit)
290+
self._patch_key(key, new_duration, unit)
291+
209292
def _patch_measurement(self, key: InstrKey) -> None:
210293
"""Patch measurement duration by extending duration by 160dt as temporarily
211294
required by the dynamic circuit backend.
212295
"""
213-
prev_duration, unit = self._get_duration_dt(key)
214-
if unit != "dt":
215-
raise TranspilerError('Can currently only patch durations of "dt".')
216-
odd_cycle_correction = self._get_odd_cycle_correction()
217-
self._patch_key(key, prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction, unit)
296+
self._convert_and_patch_key(key)
218297
# Enforce patching of reset on measurement update
219298
self._patch_reset(("reset", key[1], key[2]))
220299

@@ -227,31 +306,24 @@ def _patch_reset(self, key: InstrKey) -> None:
227306
# triggers the end of scheduling after the measurement pulse
228307
measure_key = ("measure", key[1], key[2])
229308
try:
230-
measure_duration, unit = self._get_duration_dt(measure_key)
309+
measure_duration, unit = self._get_duration(measure_key)
231310
self._patch_key(key, measure_duration, unit)
232311
except KeyError:
233312
# Fall back to reset key if measure not available
234-
prev_duration, unit = self._get_duration_dt(key)
235-
if unit != "dt":
236-
raise TranspilerError('Can currently only patch durations of "dt".')
237-
odd_cycle_correction = self._get_odd_cycle_correction()
238-
self._patch_key(
239-
key,
240-
prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction,
241-
unit,
242-
)
313+
self._convert_and_patch_key(key)
243314

244-
def _get_duration_dt(self, key: InstrKey) -> Tuple[int, str]:
315+
def _get_duration(self, key: InstrKey) -> Tuple[int, str]:
245316
"""Handling for the complicated structure of this class.
246317
247318
TODO: This class implementation should be simplified in Qiskit. Too many edge cases.
248319
"""
249320
if key[1] is None and key[2] is None:
250-
return self.duration_by_name[key[0]]
321+
duration = self.duration_by_name[key[0]]
251322
elif key[2] is None:
252-
return self.duration_by_name_qubits[(key[0], key[1])]
253-
254-
return self.duration_by_name_qubits_params[key]
323+
duration = self.duration_by_name_qubits[(key[0], key[1])]
324+
else:
325+
duration = self.duration_by_name_qubits_params[key]
326+
return duration
255327

256328
def _patch_key(self, key: InstrKey, duration: int, unit: str) -> None:
257329
"""Handling for the complicated structure of this class.

qiskit_ibm_runtime/utils/json.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
215215
if hasattr(obj, "to_json"):
216216
return {"__type__": "to_json", "__value__": obj.to_json()}
217217
if isinstance(obj, QuantumCircuit):
218-
kwargs = {"use_symengine": optionals.HAS_SYMENGINE}
218+
kwargs: dict[str, object] = {"use_symengine": bool(optionals.HAS_SYMENGINE)}
219219
if _TERRA_VERSION[0] >= 1:
220220
# NOTE: This can be updated only after the server side has
221221
# updated to a newer qiskit version.
@@ -239,13 +239,13 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
239239
data=obj,
240240
serializer=_write_parameter_expression,
241241
compress=False,
242-
use_symengine=optionals.HAS_SYMENGINE,
242+
use_symengine=bool(optionals.HAS_SYMENGINE),
243243
)
244244
return {"__type__": "ParameterExpression", "__value__": value}
245245
if isinstance(obj, ParameterView):
246246
return obj.data
247247
if isinstance(obj, Instruction):
248-
kwargs = {"use_symengine": optionals.HAS_SYMENGINE}
248+
kwargs = {"use_symengine": bool(optionals.HAS_SYMENGINE)}
249249
if _TERRA_VERSION[0] >= 1:
250250
# NOTE: This can be updated only after the server side has
251251
# updated to a newer qiskit version.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
fixes:
3+
- |
4+
Fix the patching of :class:`.DynamicCircuitInstructions` for instructions
5+
with durations that are not in units of ``dt``.
6+
upgrade:
7+
- |
8+
Extend :meth:`.DynamicCircuitInstructions.from_backend` to extract and
9+
patch durations from both :class:`.BackendV1` and :class:`.BackendV2`
10+
objects. Also add :meth:`.DynamicCircuitInstructions.from_target` to use a
11+
:class:`.Target` object instead.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed an issue with the :func:`.qpy.dump` function, when the
5+
``use_symengine`` flag was set to a truthy object that evaluated to
6+
``True`` but was not actually the boolean ``True`` the generated QPY
7+
payload would be corrupt.
8+

test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,35 +1038,46 @@ def test_disjoint_coupling_map(self):
10381038
self.assertEqual(delay_dict[0], delay_dict[2])
10391039

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

1049+
# Here "x" on qubit 3 is not defined
1050+
durations = DynamicCircuitInstructionDurations(
1051+
[
1052+
("h", 0, 50),
1053+
("x", 0, 50),
1054+
("x", 1, 50),
1055+
("x", 2, 50),
1056+
("measure", 0, 840),
1057+
("reset", 0, 1340),
1058+
]
1059+
)
1060+
10471061
dd_sequence = [XGate(), XGate()]
10481062
pm = PassManager(
10491063
[
10501064
ASAPScheduleAnalysis(self.durations),
10511065
PadDynamicalDecoupling(
1052-
self.durations,
1066+
durations,
10531067
dd_sequence,
10541068
pulse_alignment=1,
10551069
sequence_min_length_ratios=[0.0],
10561070
),
10571071
]
10581072
)
10591073

1060-
qc = QuantumCircuit(3, 1)
1074+
qc = QuantumCircuit(4, 1)
10611075
qc.measure(0, 0)
10621076
qc.x(1)
1063-
with qc.if_test((0, True)):
1064-
qc.x(1)
1065-
qc.measure(0, 0)
10661077
with qc.if_test((0, True)):
10671078
qc.x(0)
10681079
qc.x(1)
10691080
qc_dd = pm.run(qc)
1070-
dont_use = qc_dd.qubits[-1]
1081+
dont_use = qc_dd.qubits[-2:]
10711082
for op in qc_dd.data:
10721083
self.assertNotIn(dont_use, op.qubits)

test/unit/transpiler/passes/scheduling/test_scheduler.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,23 +1774,16 @@ def test_transpile_both_paths(self):
17741774

17751775
qr = QuantumRegister(7, name="q")
17761776
expected = QuantumCircuit(qr, cr)
1777-
expected.delay(24080, qr[1])
1778-
expected.delay(24080, qr[2])
1779-
expected.delay(24080, qr[3])
1780-
expected.delay(24080, qr[4])
1781-
expected.delay(24080, qr[5])
1782-
expected.delay(24080, qr[6])
1777+
for q_ind in range(1, 7):
1778+
expected.delay(24240, qr[q_ind])
17831779
expected.measure(qr[0], cr[0])
17841780
with expected.if_test((cr[0], 1)):
17851781
expected.x(qr[0])
17861782
with expected.if_test((cr[0], 1)):
1787-
expected.delay(160, qr[0])
17881783
expected.x(qr[1])
1789-
expected.delay(160, qr[2])
1790-
expected.delay(160, qr[3])
1791-
expected.delay(160, qr[4])
1792-
expected.delay(160, qr[5])
1793-
expected.delay(160, qr[6])
1784+
for q_ind in range(7):
1785+
if q_ind != 1:
1786+
expected.delay(160, qr[q_ind])
17941787
self.assertEqual(expected, scheduled)
17951788

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

18431836
durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)])

test/unit/transpiler/passes/scheduling/test_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from qiskit_ibm_runtime.transpiler.passes.scheduling.utils import (
1616
DynamicCircuitInstructionDurations,
1717
)
18+
from qiskit_ibm_runtime.fake_provider import FakeKolkata, FakeKolkataV2
1819
from .....ibm_test_case import IBMTestCase
1920

2021

@@ -51,6 +52,33 @@ def test_patch_measure(self):
5152
self.assertEqual(short_odd_durations.get("measure", (0,)), 1224)
5253
self.assertEqual(short_odd_durations.get("reset", (0,)), 1224)
5354

55+
def test_durations_from_backend_v1(self):
56+
"""Test loading and patching durations from a V1 Backend"""
57+
58+
durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkata())
59+
60+
self.assertEqual(durations.get("x", (0,)), 160)
61+
self.assertEqual(durations.get("measure", (0,)), 3200)
62+
self.assertEqual(durations.get("reset", (0,)), 3200)
63+
64+
def test_durations_from_backend_v2(self):
65+
"""Test loading and patching durations from a V2 Backend"""
66+
67+
durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkataV2())
68+
69+
self.assertEqual(durations.get("x", (0,)), 160)
70+
self.assertEqual(durations.get("measure", (0,)), 3200)
71+
self.assertEqual(durations.get("reset", (0,)), 3200)
72+
73+
def test_durations_from_target(self):
74+
"""Test loading and patching durations from a target"""
75+
76+
durations = DynamicCircuitInstructionDurations.from_target(FakeKolkataV2().target)
77+
78+
self.assertEqual(durations.get("x", (0,)), 160)
79+
self.assertEqual(durations.get("measure", (0,)), 3200)
80+
self.assertEqual(durations.get("reset", (0,)), 3200)
81+
5482
def test_patch_disable(self):
5583
"""Test if schedules circuits with c_if after measure with a common clbit.
5684
See: https://github.com/Qiskit/qiskit-terra/issues/7654"""

0 commit comments

Comments
 (0)