Measurement: Remove targets and classical_store#386
Measurement: Remove targets and classical_store#386mudit06mah wants to merge 6 commits intoqutip:masterfrom
Conversation
| final_measurement = Measurement("start", targets=[2]) | ||
| initial_measurement = Measurement("start", targets=[0]) | ||
| final_measurement = Measurement("start") | ||
| initial_measurement = Measurement("start") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
The name argument in Measurement object doesn't matter and should be removed.
Ideally both of these should be
| initial_measurement = Measurement("start") | |
| final_measurement = Mz | |
| initial_measurement = Mz |
If someone wants to define a custom measurement operator, it should be via inheritance
There was a problem hiding this comment.
I haven't yet introduced measurement operators, but we can remove the name prop from the class I guess
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
we can make
Measurementclass 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 :)
|
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. |
|
Hey @Mayank447, since the changes weren't that big I only made one commit. This PR focuses on removing |
|
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. |
|
@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 :) |
210fa03 to
73c1ecd
Compare
|
Doctest appears to be failing. @mudit06mah could you make the same change in |
73c1ecd to
f4bc91e
Compare
|
@Mayank447 looks like I forgot to add the |
| parameters are unpacked and added. | ||
| targets: list | ||
| measurement : :class:`.Measurement` | ||
| `Measurement` object |
There was a problem hiding this comment.
| `Measurement` object | |
| `Measurement` subclasses |
| 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) |
There was a problem hiding this comment.
| 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.
| if targets is None or classical_store is None: | ||
| raise ValueError("'targets' and 'classical_store' must not be None") | ||
|
|
There was a problem hiding this comment.
| 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.
| from qutip.measurement import measurement_statistics | ||
| from qutip_qip.operations import expand_operator | ||
|
|
||
| __all__ = ["Measurement", "Mz"] |
There was a problem hiding this comment.
| __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
| raise ValueError("Index of a qubit must be an integer") | ||
|
|
||
| def measurement_comp_basis(self, state): | ||
| def measurement_comp_basis(self, state, targets): |
There was a problem hiding this comment.
This should be made a classmethod
| 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. |
There was a problem hiding this comment.
| 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.
| initial_measurement = Mz | ||
| _, initial_probabilities = initial_measurement.measurement_comp_basis( |
There was a problem hiding this comment.
| initial_measurement = Mz | |
| _, initial_probabilities = initial_measurement.measurement_comp_basis( | |
| _, initial_probabilities = Mz.measurement_comp_basis( |
| final_measurement = Mz | ||
| _, final_probabilities = final_measurement.measurement_comp_basis( |
There was a problem hiding this comment.
| 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.
| "get_controlled_gate", | ||
| "AngleParametricGate", | ||
| "Measurement", | ||
| "Mz", |
There was a problem hiding this comment.
| "Mz", |
Why are you exporting Mz in both measurement and operation's __init__.py.
There was a problem hiding this comment.
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__
f2ad83a to
9ab3243
Compare
Mayank447
left a comment
There was a problem hiding this comment.
LGTM, just two minor changes.
|
|
||
| def measurement_comp_basis(self, state): | ||
| @classmethod | ||
| def measurement_comp_basis(cls, state, targets): |
There was a problem hiding this comment.
| def measurement_comp_basis(cls, state, targets): | |
| def measurement_comp_basis(cls, state, qubits): |
Same change in function definition.
| def add_measurement( | ||
| self, | ||
| measurement: str | Measurement, | ||
| measurement: Measurement, |
There was a problem hiding this comment.
| measurement: Measurement, | |
| measurement: Type[Measurement], |
Measurement -> instantiated object from Measurement class, Type[Measurement] -> a Measurement subclass
9ab3243 to
4c93e05
Compare
|
@Mayank447 I have made the necessary changes now, could you please take a look? |
|
LGTM |
There was a problem hiding this comment.
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 passMzintoQubitCircuit.add_measurement. - Updates measurement execution to use instruction-provided qubit/cbit indices (simulator changes) and adapts
measurement_comp_basisto 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) |
There was a problem hiding this comment.
I have removed the state argument from the _apply_measurement.py function.
| @@ -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 | |||
| ] | |||
There was a problem hiding this comment.
Ignore this, expanding Measurement beyond one qubit is unrrelated to this PR.
| 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, | ||
| ) |
| .. 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 | ||
|
|
There was a problem hiding this comment.
I missed this earlier, check line 300. There might be more instances.
| def add_measurement( | ||
| self, | ||
| measurement: str | Measurement, | ||
| measurement: Type[Measurement], | ||
| targets: int | IntSequence, | ||
| classical_store: int, |
| measurement: Type[Measurement], | ||
| targets: int | IntSequence, | ||
| classical_store: int, | ||
| index: None = None, |
There was a problem hiding this comment.
I agree with Copilot on this
| 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, |
| 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), | ||
| ) |
|
@BoxiLi Could you take a look as well? |
Description
This PR removes
targetsandclassical_storefromMeasurementclass.Changes
targetsandclassical_storefromMeasurementclassmeasurement_comp_basis()to accepttargetsas arg_apply_measurementtoaccept targetsandclassical_storeas argRelated issues or PRs
Part of #331
AI Usage Disclosure
To fix docstring grammar