Skip to content

Measurement: Remove targets and classical_store#386

Open
mudit06mah wants to merge 6 commits intoqutip:masterfrom
mudit06mah:measurement-refactor/targets
Open

Measurement: Remove targets and classical_store#386
mudit06mah wants to merge 6 commits intoqutip:masterfrom
mudit06mah:measurement-refactor/targets

Conversation

@mudit06mah
Copy link
Copy Markdown
Contributor

Description
This PR removes targets and classical_store from Measurement class.

Changes

  • Deprecates(Removes) targets and classical_store from Measurement class
  • Changes measurement_comp_basis() to accept targets as arg
  • Changes _apply_measurement to accept targets and classical_store as arg
  • Modifies related test cases

Related issues or PRs
Part of #331

AI Usage Disclosure
To fix docstring grammar

Comment thread tests/test_qasm.py Outdated
final_measurement = Measurement("start", targets=[2])
initial_measurement = Measurement("start", targets=[0])
final_measurement = Measurement("start")
initial_measurement = Measurement("start")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was curious as to why both of these measurements have the name "start", it is the same in other tests as well so It is intentional I think.

Copy link
Copy Markdown
Member

@Mayank447 Mayank447 Apr 20, 2026

Choose a reason for hiding this comment

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

The name argument in Measurement object doesn't matter and should be removed.

Ideally both of these should be

Suggested change
initial_measurement = Measurement("start")
final_measurement = Mz
initial_measurement = Mz

If someone wants to define a custom measurement operator, it should be via inheritance

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I haven't yet introduced measurement operators, but we can remove the name prop from the class I guess

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sounds good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Mayank447 I have left the name prop but removed assigning the passed the name prop in the __init__ method, so when operators are introduced they can have the name prop to "MZ', "MX", etc.

Copy link
Copy Markdown
Member

@Mayank447 Mayank447 Apr 21, 2026

Choose a reason for hiding this comment

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

Instead of defining the measurement operators "Mz", "Mx" as name prop, we can make Measurement class an abstract class and define any measurement operators by inheriting the Measurement. This is what we follow for Gate class and in future plan to do the same for device classes. You don't need to make any change for this in this PR.

I see Measurement() being repeated at quite a few places. What you can do is to define Mz = Measurement() in measurement.py, import that wherever qc.add_measurement is being called use Mz instead of Measurement(). This will fix the interface for the other files, we won't have to touch them. Later all our future changes like making Mz inherit from Measurement etc. would only need to be made in measurement.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we can make Measurement class an abstract class and define any measurement operators

Yes that is what I meant as well, I am not removing the name prop right now and leaving it to use for the Mz and other operator classes which will be abstracted by Measurement.

Nice idea about defining Mz, I have made new changes according to that :)

@Mayank447
Copy link
Copy Markdown
Member

Hi, thanks for this PR. I admit I am a bit lost. All the changes seem to be squashed in a single commit, could you explain a bit more the reasoning behind the different changes.

@mudit06mah
Copy link
Copy Markdown
Contributor Author

mudit06mah commented Apr 20, 2026

Hey @Mayank447, since the changes weren't that big I only made one commit.

This PR focuses on removing targets and classical_store from the Measurement class.
Since measurement_comp_basis was accessing Measurement.targets I had to pass it from _apply_measurementwhich uses the 'classical_store` so I passed both targets and classical_store from the step function.
I changed the test cases accordingly.

@BoxiLi
Copy link
Copy Markdown
Member

BoxiLi commented Apr 20, 2026

Glad to see this coming, just wanna add that, please let me know if you find it better to create a separate dev branch for this. E.g. it is hard or cumbersome to keep the tests passing for this refactor in every single PR.

@mudit06mah
Copy link
Copy Markdown
Contributor Author

mudit06mah commented Apr 20, 2026

@BoxiLi I did fix all the tests for this PR, didn't appear to be too cumbersome but maybe when I introduced measurement operators which might cause a lot of changes in tests, though the PRs could very well just be bigger in size. I guess you can make that call :)

@mudit06mah mudit06mah requested a review from Mayank447 April 21, 2026 13:16
@mudit06mah mudit06mah force-pushed the measurement-refactor/targets branch from 210fa03 to 73c1ecd Compare April 26, 2026 13:56
@Mayank447
Copy link
Copy Markdown
Member

Doctest appears to be failing. @mudit06mah could you make the same change in qip-basics.rst

@mudit06mah mudit06mah force-pushed the measurement-refactor/targets branch from 73c1ecd to f4bc91e Compare April 27, 2026 17:39
@mudit06mah
Copy link
Copy Markdown
Contributor Author

@Mayank447 looks like I forgot to add the qip-basics.rst file in my commit, should be fine now :)

Copy link
Copy Markdown
Member

@Mayank447 Mayank447 left a comment

Choose a reason for hiding this comment

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

Good work!

Comment thread src/qutip_qip/circuit/circuit.py Outdated
parameters are unpacked and added.
targets: list
measurement : :class:`.Measurement`
`Measurement` object
Copy link
Copy Markdown
Member

@Mayank447 Mayank447 Apr 28, 2026

Choose a reason for hiding this comment

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

Suggested change
`Measurement` object
`Measurement` subclasses

Comment thread src/qutip_qip/circuit/circuit.py Outdated
Comment on lines +279 to +286
if isinstance(measurement, str):
warnings.warn(
"Passing a string name to add_measurement is deprecated. "
"Please pass an instantiated Measurement object instead.",
DeprecationWarning,
stacklevel=2,
)
measurement = Measurement(name=measurement)
Copy link
Copy Markdown
Member

@Mayank447 Mayank447 Apr 28, 2026

Choose a reason for hiding this comment

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

Suggested change
if isinstance(measurement, str):
warnings.warn(
"Passing a string name to add_measurement is deprecated. "
"Please pass an instantiated Measurement object instead.",
DeprecationWarning,
stacklevel=2,
)
measurement = Measurement(name=measurement)
if isinstance(measurement, str):
warnings.warn(
"Passing a string name to add_measurement has been removed. "
"Defaulting to measurement in the Z- basis.",
DeprecationWarning,
stacklevel=2,
)
measurement = Mz

Object instantiation won't work since Measurement class will be abstracted in next couple of PRs.

Comment on lines +294 to +296
if targets is None or classical_store is None:
raise ValueError("'targets' and 'classical_store' must not be None")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if targets is None or classical_store is None:
raise ValueError("'targets' and 'classical_store' must not be None")
if not isinstance(targets, IntSequence):
raise ValueError("'targets' must be a sequence of positive integer")
if not isinstance(classical_store, IntSequence):
raise ValueError("'classical_store' must be a sequence of positive integer")

Also need to check for each targets, classical_store is >=0 and less than num_qubits for the circuit. I wrote some helper functions for this, they were used in add_gate. You can reuse those functions.

Comment thread src/qutip_qip/operations/measurement.py Outdated
from qutip.measurement import measurement_statistics
from qutip_qip.operations import expand_operator

__all__ = ["Measurement", "Mz"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
__all__ = ["Measurement", "Mz"]
__all__ = ["Mz"]

Measurement is already exported in __init__.py. Although not relevant to this PR, later measurement.py can be renamed to measurementclass.py and a directory measurement can be created which stores all the standard measurements like Pauli basis, POVMs

Comment thread src/qutip_qip/operations/measurement.py Outdated
raise ValueError("Index of a qubit must be an integer")

def measurement_comp_basis(self, state):
def measurement_comp_basis(self, state, targets):
Copy link
Copy Markdown
Member

@Mayank447 Mayank447 Apr 29, 2026

Choose a reason for hiding this comment

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

This should be made a classmethod

Comment on lines +323 to +327
targets : tuple of int
The indices of the qubits to be measured.
classical_store : tuple of int
The indices of the classical registers where the measurement
results will be stored.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
targets : tuple of int
The indices of the qubits to be measured.
classical_store : tuple of int
The indices of the classical registers where the measurement
results will be stored.
qubits : tuple of int
The indices of the qubits to be measured.
cbits : tuple of int
The indices of the classical registers where the measurement
results will be stored.

Same change needs to be made in the function.

Comment thread tests/test_circuit.py Outdated
Comment on lines +534 to +535
initial_measurement = Mz
_, initial_probabilities = initial_measurement.measurement_comp_basis(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
initial_measurement = Mz
_, initial_probabilities = initial_measurement.measurement_comp_basis(
_, initial_probabilities = Mz.measurement_comp_basis(

Comment thread tests/test_circuit.py Outdated
Comment on lines +543 to +544
final_measurement = Mz
_, final_probabilities = final_measurement.measurement_comp_basis(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
final_measurement = Mz
_, final_probabilities = final_measurement.measurement_comp_basis(
_, final_probabilities = Mz.measurement_comp_basis(

Same change needs to be made at few more places in this file as well.

@mudit06mah mudit06mah requested a review from Mayank447 May 2, 2026 20:47
Comment thread src/qutip_qip/operations/__init__.py Outdated
"get_controlled_gate",
"AngleParametricGate",
"Measurement",
"Mz",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
"Mz",

Why are you exporting Mz in both measurement and operation's __init__.py.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh yeah, sorry I guess I got a bit confused since I was thinking on where to keep and where to remove it, I have removed it now from operations.__init__

@mudit06mah mudit06mah force-pushed the measurement-refactor/targets branch from f2ad83a to 9ab3243 Compare May 3, 2026 19:23
Copy link
Copy Markdown
Member

@Mayank447 Mayank447 left a comment

Choose a reason for hiding this comment

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

LGTM, just two minor changes.

Comment thread src/qutip_qip/operations/measurement.py Outdated

def measurement_comp_basis(self, state):
@classmethod
def measurement_comp_basis(cls, state, targets):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
def measurement_comp_basis(cls, state, targets):
def measurement_comp_basis(cls, state, qubits):

Same change in function definition.

Comment thread src/qutip_qip/circuit/circuit.py Outdated
def add_measurement(
self,
measurement: str | Measurement,
measurement: Measurement,
Copy link
Copy Markdown
Member

@Mayank447 Mayank447 May 3, 2026

Choose a reason for hiding this comment

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

Suggested change
measurement: Measurement,
measurement: Type[Measurement],

Measurement -> instantiated object from Measurement class, Type[Measurement] -> a Measurement subclass

@mudit06mah mudit06mah force-pushed the measurement-refactor/targets branch from 9ab3243 to 4c93e05 Compare May 3, 2026 19:51
@mudit06mah
Copy link
Copy Markdown
Contributor Author

@Mayank447 I have made the necessary changes now, could you please take a look?

@Mayank447
Copy link
Copy Markdown
Member

LGTM

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors how measurements are represented and attached to circuits by removing targets/classical_store state from the Measurement object and passing these via MeasurementInstruction/add_measurement arguments, aligning with the ongoing gate/instruction refactor (#331).

Changes:

  • Introduces a shared Z-basis measurement object (Mz) and updates call sites to pass Mz into QubitCircuit.add_measurement.
  • Updates measurement execution to use instruction-provided qubit/cbit indices (simulator changes) and adapts measurement_comp_basis to accept qubits as an argument.
  • Updates tests, QASM/Qiskit conversion, and documentation examples to use the new measurement API.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/test_renderer.py Switches circuit measurement usage from string names to Mz.
tests/test_qasm.py Updates QASM tests to use Mz and pass explicit qubits into measurement_comp_basis.
tests/test_measurement.py Updates measurement tests to call Mz.measurement_comp_basis(state, qubits=...).
tests/test_circuit.py Updates circuit tests to add measurements via Mz and adjusts assertions around instruction operations.
src/qutip_qip/qiskit/utils/converter.py Converts Qiskit measure instructions into add_measurement(Mz, ...).
src/qutip_qip/qasm.py QASM import final pass now adds measurements using Mz.
src/qutip_qip/operations/measurement.py Deprecates Measurement initialization args, makes measurement_comp_basis take qubits, and defines singleton Mz.
src/qutip_qip/circuit/simulator/matrix_mul_simulator.py Updates measurement application to use instruction qubits/cbits and new measurement_comp_basis signature.
src/qutip_qip/circuit/circuit.py Refactors add_measurement to accept measurement objects and normalizes targets/cbits to sequences with bounds checks.
src/qutip_qip/algorithms/phase_flip.py Updates syndrome measurement to use Mz.
src/qutip_qip/algorithms/bit_flip.py Updates syndrome measurement to use Mz.
doc/source/qip-basics.rst Updates measurement examples/expected output to reflect Mz usage (but still contains at least one remaining string-based example).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

The collapsed state after the measurement.
"""
states, probabilities = operation.measurement_comp_basis(self.state)
states, probabilities = operation.measurement_comp_basis(self.state, qubits)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have removed the state argument from the _apply_measurement.py function.

Comment on lines 41 to 79
@@ -61,6 +51,8 @@ def measurement_comp_basis(self, state):
state : ket or oper
state to be measured on specified by
ket vector or density matrix
qubits : list or tuple of int
The indices of the qubits to be measured.

Returns
-------
@@ -74,7 +66,7 @@ def measurement_comp_basis(self, state):
"""

n = int(np.log2(state.shape[0]))
target = self.targets[0]
target = qubits[0]
if target < n:
op0 = basis(2, 0) * basis(2, 0).dag()
op1 = basis(2, 1) * basis(2, 1).dag()
@@ -83,8 +75,7 @@ def measurement_comp_basis(self, state):
raise ValueError("target is not valid")

measurement_ops = [
expand_operator(op, dims=[2] * n, targets=self.targets)
for op in measurement_ops
expand_operator(op, dims=[2] * n, targets=qubits) for op in measurement_ops
]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ignore this, expanding Measurement beyond one qubit is unrrelated to this PR.

Comment on lines +19 to +28
def __init__(self, name=None, targets=None, index=None, classical_store=None):
"""
Create a measurement with specified parameters.
"""
if name is not None:
warnings.warn(
"'name' argument in Measurement has been deprecated.",
DeprecationWarning,
stacklevel=2,
)
Comment thread doc/source/qip-basics.rst
Comment on lines 289 to 312
.. plot::
:include-source:

from qutip_qip.circuit import QubitCircuit
from qutip_qip.operations.gates import H, CX, ISWAP

# create the quantum circuit
qc = QubitCircuit(2, num_cbits=1)
qc.add_gate(CX, controls=0, targets=1)
qc.add_gate(H, targets=1)
qc.add_gate(ISWAP, targets=[0,1])
qc.add_measurement("M0", targets=1, classical_store=0)

qc.draw("matplotlib", dpi=300)

**Customization Examples**:

.. plot::
:include-source:

from qutip_qip.circuit import QubitCircuit
from qutip_qip.operations.gates import H, CX, ISWAP
from qutip_qip.operations.measurement import Mz

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I missed this earlier, check line 300. There might be more instances.

Comment thread src/qutip_qip/circuit/circuit.py Outdated
Comment on lines 251 to 255
def add_measurement(
self,
measurement: str | Measurement,
measurement: Type[Measurement],
targets: int | IntSequence,
classical_store: int,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ignore this

Comment on lines +253 to 256
measurement: Type[Measurement],
targets: int | IntSequence,
classical_store: int,
index: None = None,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with Copilot on this

Suggested change
measurement: Type[Measurement],
targets: int | IntSequence,
classical_store: int,
index: None = None,
measurement: Type[Measurement],
targets: int | IntSequence,
classical_store: int | IntSequence,
index: None = None,

Comment on lines +289 to 305
if targets is None or classical_store is None:
raise ValueError("'targets' and 'classical_store' must not be None")

if type(targets) is int:
targets = [targets]
targets = convert_type_input_to_sequence(int, "targets", targets)
classical_store = convert_type_input_to_sequence(
int, "classical_store", classical_store
)

if type(classical_store) is int:
classical_store = [classical_store]
check_limit("targets", targets, 0, self.num_qubits - 1)
check_limit("classical_store", classical_store, 0, self.num_cbits - 1)

self._instructions.append(
MeasurementInstruction(
operation=meas,
operation=measurement,
qubits=tuple(targets),
cbits=tuple(classical_store),
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ignore

@mudit06mah
Copy link
Copy Markdown
Contributor Author

@BoxiLi Could you take a look as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants