From be6fca5899f516d74a5b59cf6331ab9aa3e1064c Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 23 Jun 2025 19:31:27 +0200 Subject: [PATCH 1/4] * Create cicuit module and circuit/library submodule * Add MidCircuitMeasure(Instruction) to new circuit library * Add unit tests --- qiskit_ibm_runtime/circuit/__init__.py | 15 ++++ .../circuit/library/__init__.py | 16 ++++ .../circuit/library/mid_circuit_measure.py | 24 +++++ test/unit/circuit/__init__.py | 11 +++ test/unit/circuit/test_mid_circ_meas.py | 90 +++++++++++++++++++ 5 files changed, 156 insertions(+) create mode 100644 qiskit_ibm_runtime/circuit/__init__.py create mode 100644 qiskit_ibm_runtime/circuit/library/__init__.py create mode 100644 qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py create mode 100644 test/unit/circuit/__init__.py create mode 100644 test/unit/circuit/test_mid_circ_meas.py diff --git a/qiskit_ibm_runtime/circuit/__init__.py b/qiskit_ibm_runtime/circuit/__init__.py new file mode 100644 index 000000000..1acf84d12 --- /dev/null +++ b/qiskit_ibm_runtime/circuit/__init__.py @@ -0,0 +1,15 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module for vendor-specific circuit objects.""" + +from .library import * diff --git a/qiskit_ibm_runtime/circuit/library/__init__.py b/qiskit_ibm_runtime/circuit/library/__init__.py new file mode 100644 index 000000000..3d707d69e --- /dev/null +++ b/qiskit_ibm_runtime/circuit/library/__init__.py @@ -0,0 +1,16 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module for vendor-specific instructions.""" + + +from .mid_circuit_measure import MidCircuitMeasure diff --git a/qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py b/qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py new file mode 100644 index 000000000..e4bad1f0d --- /dev/null +++ b/qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py @@ -0,0 +1,24 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +from qiskit.circuit import Instruction + + +class MidCircuitMeasure(Instruction): + def __init__(self, name="measure_2", label=None): + if not name.startswith("measure_"): + raise ValueError( + "Invalid name for mid-circuit measure instruction." + "The provided name must start with `measure_`" + ) + + super().__init__(name, 1, 1, [], label=label) diff --git a/test/unit/circuit/__init__.py b/test/unit/circuit/__init__.py new file mode 100644 index 000000000..60e93d8a9 --- /dev/null +++ b/test/unit/circuit/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/unit/circuit/test_mid_circ_meas.py b/test/unit/circuit/test_mid_circ_meas.py new file mode 100644 index 000000000..5cc2f404b --- /dev/null +++ b/test/unit/circuit/test_mid_circ_meas.py @@ -0,0 +1,90 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for MidCircuitMeasure instruction.""" + +from qiskit import QuantumCircuit, generate_preset_pass_manager +from qiskit.circuit import Instruction +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler.exceptions import TranspilerError + +from qiskit_ibm_runtime.circuit import MidCircuitMeasure +from qiskit_ibm_runtime.fake_provider import FakeVigoV2 + +from ...ibm_test_case import IBMTestCase + + +class TestMidCircuitMeasure(IBMTestCase): + """Test MidCircuitMeasure instruction.""" + + def test_instantiation(self): + """Test default instantiation.""" + mcm = MidCircuitMeasure() + self.assertIs(mcm.base_class, MidCircuitMeasure) + self.assertIsInstance(mcm, Instruction) + self.assertEqual(mcm.name, "measure_2") + self.assertEqual(mcm.num_qubits, 1) + self.assertEqual(mcm.num_clbits, 1) + + def test_instantiation_name(self): + """Test instantiation with custom name.""" + with self.subTest("measure_3"): + mcm = MidCircuitMeasure("measure_3") + self.assertIs(mcm.base_class, MidCircuitMeasure) + self.assertIsInstance(mcm, Instruction) + self.assertEqual(mcm.name, "measure_3") + self.assertEqual(mcm.num_qubits, 1) + self.assertEqual(mcm.num_clbits, 1) + + with self.subTest("measure_reset"): + mcm = MidCircuitMeasure("measure_reset") + self.assertIs(mcm.base_class, MidCircuitMeasure) + self.assertIsInstance(mcm, Instruction) + self.assertEqual(mcm.name, "measure_reset") + self.assertEqual(mcm.num_qubits, 1) + self.assertEqual(mcm.num_clbits, 1) + + with self.subTest("invalid_name"): + with self.assertRaises(ValueError): + mcm = MidCircuitMeasure("invalid_name") + + def test_circuit_integration(self): + """Test appending to circuit.""" + mcm = MidCircuitMeasure() + qc = QuantumCircuit(1, 2) + qc.append(mcm, [0], [0]) + qc.append(mcm, [0], [1]) + qc.reset(0) + self.assertIs(qc.data[0].operation, mcm) + self.assertIs(qc.data[1].operation, mcm) + + def test_transpiler_compat_without(self): + """Test that default pass manager FAILS if measure_2 not in Target.""" + mcm = MidCircuitMeasure() + backend = FakeVigoV2() + pm = generate_preset_pass_manager(backend=backend, seed_transpiler=0) + qc = QuantumCircuit(1, 2) + qc.append(mcm, [0], [0]) + with self.assertRaises(TranspilerError): + _ = pm.run(qc) + + def test_transpiler_compat_with(self): + """Test that default pass manager PASSES if measure_2 is in Target + and doesn't modify the instruction.""" + mcm = MidCircuitMeasure() + backend = GenericBackendV2(num_qubits=5, seed=0) + backend.target.add_instruction(mcm, {(i,): None for i in range(5)}) + pm = generate_preset_pass_manager(backend=backend, seed_transpiler=0) + qc = QuantumCircuit(1, 2) + qc.append(mcm, [0], [0]) + transpiled = pm.run(qc) + self.assertEqual(transpiled.data[0].operation.name, "measure_2") From 903320a862ed6988ff78892a27b537d9a9e3bbaa Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 9 Jul 2025 13:27:45 +0200 Subject: [PATCH 2/4] Add release note --- release-notes/unreleased/2316.feat.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 release-notes/unreleased/2316.feat.rst diff --git a/release-notes/unreleased/2316.feat.rst b/release-notes/unreleased/2316.feat.rst new file mode 100644 index 000000000..375f60e62 --- /dev/null +++ b/release-notes/unreleased/2316.feat.rst @@ -0,0 +1,9 @@ +The `qiskit-ibm-runtime` package has been extended with two new modules: :mod:`.circuit` and +:mod:`.circuit.library`. These modules are designed to mirror the structure of the +corresponding `qiskit` SDK modules, while providing vendor-specific implementations of +circuit objects and instructions. + +The first addition to this new circuit library is the :class:`.MidCircuitMeasure` class. +This class enables the creation of instructions that follow the naming convention +`measure_`, which are mapped to specific mid-circuit measurement + hardware instructions matching that pattern. \ No newline at end of file From ba8b4a4710c05bc55b7a0d514523aac4ed891e6c Mon Sep 17 00:00:00 2001 From: ElePT Date: Mon, 23 Jun 2025 19:31:27 +0200 Subject: [PATCH 3/4] Add preliminary transpiler pass to convert intermediate measurements into MidCircuitMeasure --- .../transpiler/passes/__init__.py | 3 +- .../transpiler/passes/basis/__init__.py | 1 + .../passes/basis/convert_mid_circ_meas.py | 46 +++++++++++++ .../basis/test_convert_mid_circ_meas.py | 69 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 qiskit_ibm_runtime/transpiler/passes/basis/convert_mid_circ_meas.py create mode 100644 test/unit/transpiler/passes/basis/test_convert_mid_circ_meas.py diff --git a/qiskit_ibm_runtime/transpiler/passes/__init__.py b/qiskit_ibm_runtime/transpiler/passes/__init__.py index da3699d1b..303a44f52 100644 --- a/qiskit_ibm_runtime/transpiler/passes/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/__init__.py @@ -28,11 +28,12 @@ ConvertIdToDelay ConvertISAToClifford FoldRzzAngle + ConvertToMidCircuitMeasure See :mod:`qiskit_ibm_runtime.transpiler.passes.scheduling` for a collection of scheduling passes. """ -from .basis import ConvertIdToDelay, FoldRzzAngle +from .basis import ConvertIdToDelay, FoldRzzAngle, ConvertToMidCircuitMeasure # circuit scheduling from .scheduling import ASAPScheduleAnalysis diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py index dbeac2851..b4e2c6be2 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py @@ -13,4 +13,5 @@ """Passes to layout circuits to IBM backend's instruction sets.""" from .convert_id_to_delay import ConvertIdToDelay +from .convert_mid_circ_meas import ConvertToMidCircuitMeasure from .fold_rzz_angle import FoldRzzAngle diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/convert_mid_circ_meas.py b/qiskit_ibm_runtime/transpiler/passes/basis/convert_mid_circ_meas.py new file mode 100644 index 000000000..789458bd2 --- /dev/null +++ b/qiskit_ibm_runtime/transpiler/passes/basis/convert_mid_circ_meas.py @@ -0,0 +1,46 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Pass to convert mreplaces terminal measures in the middle of the circuit with +MidCircuitMeasure instructions.""" + +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.circuit.measure import Measure +from qiskit.transpiler.passes.utils.remove_final_measurements import calc_final_ops + + +class ConvertToMidCircuitMeasure(TransformationPass): + """This pass replaces terminal measures in the middle of the circuit with + MidCircuitMeasure instructions. + """ + + def __init__(self, target): + super().__init__() + self.target = target + + def run(self, dag): + """Run the pass on a dag.""" + mid_circ_measure = None + for inst in self.target.instructions: + if inst[0].name.startswith("measure_"): + mid_circ_measure = inst[0] + break + if not mid_circ_measure: + return dag + + final_measure_nodes = calc_final_ops(dag, {"measure"}) + + for node in dag.op_nodes(Measure): + if node not in final_measure_nodes: + dag.substitute_node(node, mid_circ_measure, inplace=True) + + return dag diff --git a/test/unit/transpiler/passes/basis/test_convert_mid_circ_meas.py b/test/unit/transpiler/passes/basis/test_convert_mid_circ_meas.py new file mode 100644 index 000000000..1f94b0a86 --- /dev/null +++ b/test/unit/transpiler/passes/basis/test_convert_mid_circ_meas.py @@ -0,0 +1,69 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test the conversion of terminal Measure to MidCircuitMeasure.""" + +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import Measure +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler import PassManager + +from qiskit_ibm_runtime.circuit import MidCircuitMeasure +from qiskit_ibm_runtime.transpiler.passes.basis.convert_mid_circ_meas import ( + ConvertToMidCircuitMeasure, +) + +from .....ibm_test_case import IBMTestCase + + +class TestConvertToMidCircuitMeasure(IBMTestCase): + """Tests the ConvertToMidCircuitMeasure pass""" + + def setUp(self): + super().setUp() + + num_qubits = 5 + mcm = MidCircuitMeasure() + self.target_without = GenericBackendV2(num_qubits=num_qubits, seed=0).target + self.target_with = GenericBackendV2(num_qubits=num_qubits, seed=0).target + self.target_with.add_instruction(mcm, {(i,): None for i in range(num_qubits)}) + + self.qc = QuantumCircuit(2, 2) + self.qc.x(0) + self.qc.append(mcm, [0], [0]) + self.qc.measure([0], [0]) + self.qc.measure_all() + + def test_transpile_with(self): + custom_pass = ConvertToMidCircuitMeasure(self.target_with) + pm = PassManager([custom_pass]) + transpiled = pm.run(self.qc) + self.assertIsInstance(transpiled.data[1].operation, MidCircuitMeasure) + self.assertIsInstance(transpiled.data[2].operation, MidCircuitMeasure) + # [3] is the barrier + self.assertNotIsInstance(transpiled.data[4].operation, MidCircuitMeasure) + self.assertIsInstance(transpiled.data[4].operation, Measure) + self.assertNotIsInstance(transpiled.data[5].operation, MidCircuitMeasure) + self.assertIsInstance(transpiled.data[5].operation, Measure) + + def test_transpile_without(self): + custom_pass = ConvertToMidCircuitMeasure(self.target_without) + pm = PassManager([custom_pass]) + transpiled = pm.run(self.qc) + self.assertIsInstance(transpiled.data[1].operation, MidCircuitMeasure) + self.assertNotIsInstance(transpiled.data[2].operation, MidCircuitMeasure) + self.assertIsInstance(transpiled.data[2].operation, Measure) + # [3] is the barrier + self.assertNotIsInstance(transpiled.data[4].operation, MidCircuitMeasure) + self.assertIsInstance(transpiled.data[4].operation, Measure) + self.assertNotIsInstance(transpiled.data[5].operation, MidCircuitMeasure) + self.assertIsInstance(transpiled.data[5].operation, Measure) From d493ab88df45176cbd84acaa80c14de52319c547 Mon Sep 17 00:00:00 2001 From: ElePT Date: Wed, 9 Jul 2025 13:43:25 +0200 Subject: [PATCH 4/4] Add docstring --- qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py b/qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py index e4bad1f0d..998d7b253 100644 --- a/qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py +++ b/qiskit_ibm_runtime/circuit/library/mid_circuit_measure.py @@ -10,10 +10,18 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +"""MidCircuitMeasure gate.""" + from qiskit.circuit import Instruction class MidCircuitMeasure(Instruction): + """ + This instruction implements an alternative 'named' measurement definition + (1 classical bit, 1 quantum bit), whose name can be used to map to a corresponding + mid-circuit measurement instruction implementation on hardware. + """ + def __init__(self, name="measure_2", label=None): if not name.startswith("measure_"): raise ValueError(