From bf608bdff0000d7ba360f0914778beddfaa15213 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Tue, 6 Feb 2024 10:33:11 -0500 Subject: [PATCH 1/4] Update main branch version 0.19.1 (#1382) --- docs/conf.py | 2 +- qiskit_ibm_runtime/VERSION.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9aeae035e..769cfb73a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 --------------------------------------------------- diff --git a/qiskit_ibm_runtime/VERSION.txt b/qiskit_ibm_runtime/VERSION.txt index 1cf0537c3..41915c799 100644 --- a/qiskit_ibm_runtime/VERSION.txt +++ b/qiskit_ibm_runtime/VERSION.txt @@ -1 +1 @@ -0.19.0 +0.19.1 From a78c5ed981808efc0ba0f36fcebbf31dd8906935 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 6 Feb 2024 23:17:54 +0100 Subject: [PATCH 2/4] 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 https://github.com/Qiskit/qiskit/pull/11727 * black lynt * mypy --------- Co-authored-by: Kevin Tian --- .pylintrc | 1 + .../passes/scheduling/dynamical_decoupling.py | 6 +- .../transpiler/passes/scheduling/utils.py | 112 ++++++++++++++---- ...ix-duration-patching-b80d45d77481dfa6.yaml | 11 ++ .../scheduling/test_dynamical_decoupling.py | 31 +++-- .../passes/scheduling/test_scheduler.py | 19 +-- .../passes/scheduling/test_utils.py | 28 +++++ 7 files changed, 164 insertions(+), 44 deletions(-) create mode 100644 releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml diff --git a/.pylintrc b/.pylintrc index 5d5cb1eb0..a455fd605 100644 --- a/.pylintrc +++ b/.pylintrc @@ -379,6 +379,7 @@ function-naming-style=snake_case good-names=i, j, k, + dt, ex, Run, _ diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py index b7406263a..77d893f57 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py @@ -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): diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py index bf7665cd1..ec4710492 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py @@ -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]: @@ -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": @@ -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])) @@ -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. diff --git a/releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml b/releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml new file mode 100644 index 000000000..bfe9cf25e --- /dev/null +++ b/releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml @@ -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. diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py index df42b629b..d548665a1 100644 --- a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -1038,18 +1038,32 @@ 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], @@ -1057,16 +1071,13 @@ def test_no_unused_qubits(self): ] ) - 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) diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index 3170a3e5f..5903fec8e 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -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): @@ -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)]) diff --git a/test/unit/transpiler/passes/scheduling/test_utils.py b/test/unit/transpiler/passes/scheduling/test_utils.py index 50cd79ff7..e53cf59e6 100644 --- a/test/unit/transpiler/passes/scheduling/test_utils.py +++ b/test/unit/transpiler/passes/scheduling/test_utils.py @@ -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 @@ -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""" From 789017082bb9abdcc3db1d4e744fb86996647fb2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 7 Feb 2024 09:49:00 -0500 Subject: [PATCH 3/4] 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 --- qiskit_ibm_runtime/utils/json.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index 1c2e7c287..6b1468bea 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -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. @@ -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. From 72ed540f912d21c3f8f9fe3bda522e28930ec407 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 7 Feb 2024 10:08:14 -0500 Subject: [PATCH 4/4] Prepare release 0.19.1 (#1386) --- .../fix-duration-patching-b80d45d77481dfa6.yaml | 0 releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml | 8 ++++++++ 2 files changed, 8 insertions(+) rename releasenotes/notes/{ => 0.19}/fix-duration-patching-b80d45d77481dfa6.yaml (100%) create mode 100644 releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml diff --git a/releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml b/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml similarity index 100% rename from releasenotes/notes/fix-duration-patching-b80d45d77481dfa6.yaml rename to releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml diff --git a/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml b/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml new file mode 100644 index 000000000..8969fba28 --- /dev/null +++ b/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml @@ -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. +