diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index c7328c69cc5..8dddd0f26c5 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,7 +4,11 @@

New features since last release

-* `DefaultQubit2` accepts a `max_workers` argument which controls multiprocessing. +* `qml.measure` now includes a boolean keyword argument `reset` to reset a wire to the + $|0\rangle$ computational basis state after measurement. + [(#4402)](https://github.com/PennyLaneAI/pennylane/pull/4402/) + +* `DefaultQubit2` accepts a `max_workers` argument which controls multiprocessing. A `ProcessPoolExecutor` executes tapes asynchronously using a pool of at most `max_workers` processes. If `max_workers` is `None` or not given, only the current process executes tapes. If you experience any @@ -105,7 +109,7 @@ array([False, False]) [#4331](https://github.com/PennyLaneAI/pennylane/pull/4331) * The `qchem` functions `primitive_norm` and `contracted_norm` are modified to be compatible with - higher versions of scipy. The private function `_fac2` for computing double factorials is added. + higher versions of scipy. The private function `_fac2` for computing double factorials is added. [#4321](https://github.com/PennyLaneAI/pennylane/pull/4321) * The default label for a `StatePrep` operator is now `|Ψ⟩`. @@ -158,6 +162,9 @@ array([False, False]) * The experimental `DefaultQubit2` device now supports computing VJPs and JVPs using the adjoint method. [(#4374)](https://github.com/PennyLaneAI/pennylane/pull/4374) + +* Provide users access to the logging configuration file path and improve the logging configuration structure. + [(#4377)](https://github.com/PennyLaneAI/pennylane/pull/4377) * Refactoring of `pennylane/interfaces`. The `execute_fn` passed to the machine learning framework boundaries is now responsible for converting parameters to numpy. The gradients module can now handle tensorflow parameters, @@ -236,8 +243,8 @@ array([False, False])

Deprecations 👋

-* ``qml.qchem.jordan_wigner`` is deprecated, use ``qml.jordan_wigner`` instead. - List input to define the fermionic operator is also deprecated; the fermionic +* ``qml.qchem.jordan_wigner`` is deprecated, use ``qml.jordan_wigner`` instead. + List input to define the fermionic operator is also deprecated; the fermionic operators in the ``qml.fermi`` module should be used instead. [(#4332)](https://github.com/PennyLaneAI/pennylane/pull/4332) @@ -245,11 +252,11 @@ array([False, False]) match the call signature of the operation. [(#4314)](https://github.com/PennyLaneAI/pennylane/pull/4314) -* The CV observables ``qml.X`` and ``qml.P`` have been deprecated. Use ``qml.QuadX`` +* The CV observables ``qml.X`` and ``qml.P`` have been deprecated. Use ``qml.QuadX`` and ``qml.QuadP`` instead. [(#4330)](https://github.com/PennyLaneAI/pennylane/pull/4330) -* The method ``tape.unwrap()`` and corresponding ``UnwrapTape`` and ``Unwrap`` classes +* The method ``tape.unwrap()`` and corresponding ``UnwrapTape`` and ``Unwrap`` classes are deprecated. Use ``convert_to_numpy_parameters`` instead. [(#4344)](https://github.com/PennyLaneAI/pennylane/pull/4344) @@ -368,6 +375,7 @@ This release contains contributions from (in alphabetical order): Utkarsh Azad, Isaac De Vlugt, +Amintor Dusko, Stepan Fomichev, Lillian M. A. Frederiksen, Soran Jahangiri, @@ -376,6 +384,7 @@ Korbinian Kottmann Christina Lee, Vincent Michaud-Rioux, Romain Moyard, +Lee James O'Riordan, Mudit Pandey, Borja Requena, Matthew Silverman, diff --git a/pennylane/drawer/drawable_layers.py b/pennylane/drawer/drawable_layers.py index 0042b9e3122..9a29b90e320 100644 --- a/pennylane/drawer/drawable_layers.py +++ b/pennylane/drawer/drawable_layers.py @@ -97,11 +97,6 @@ def drawable_layers(ops, wire_map=None): # loop over operations for op in ops: is_mid_measure = is_conditional = False - if set(measured_wires.values()).intersection({wire_map[w] for w in op.wires}): - raise ValueError( - f"Cannot apply operations on {op.wires} as some wires have been measured already." - ) - if isinstance(op, MidMeasureMP): if len(op.wires) > 1: raise ValueError("Cannot draw mid-circuit measurements with more than one wire.") diff --git a/pennylane/measurements/mid_measure.py b/pennylane/measurements/mid_measure.py index 2de61e178a8..d27a2e9a4fd 100644 --- a/pennylane/measurements/mid_measure.py +++ b/pennylane/measurements/mid_measure.py @@ -24,8 +24,8 @@ from .measurements import MeasurementProcess, MidMeasure -def measure(wires): # TODO: Change name to mid_measure - """Perform a mid-circuit measurement in the computational basis on the +def measure(wires: Wires, reset: Optional[bool] = False): + r"""Perform a mid-circuit measurement in the computational basis on the supplied qubit. Measurement outcomes can be obtained and used to conditionally apply @@ -38,7 +38,7 @@ def measure(wires): # TODO: Change name to mid_measure .. code-block:: python3 - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def func(x, y): @@ -55,16 +55,37 @@ def func(x, y): >>> func(*pars) tensor([0.90165331, 0.09834669], requires_grad=True) + Wires can be reused after measurement. Moreover, measured wires can be reset + to the :math:`|0 \rangle` by setting ``reset=True``. + + .. code-block:: python3 + + dev = qml.device("default.qubit", wires=3) + + @qml.qnode(dev) + def func(): + qml.PauliX(1) + m_0 = qml.measure(1, reset=True) + return qml.probs(wires=[1]) + + Executing this QNode: + + >>> func() + tensor([1., 0.], requires_grad=True) + Mid circuit measurements can be manipulated using the following dunder methods ``+``, ``-``, ``*``, ``/``, ``~`` (not), ``&`` (and), ``|`` (or), ``==``, ``<=``, ``>=``, ``<``, ``>`` with other mid-circuit measurements or scalars. - Note: - python ``not``, ``and``, ``or``, do not work since these do not have dunder - methods. Instead use ``~``, ``&``, ``|``. + .. Note :: + + Python ``not``, ``and``, ``or``, do not work since these do not have dunder methods. + Instead use ``~``, ``&``, ``|``. Args: wires (Wires): The wire of the qubit the measurement process applies to. + reset (Optional[bool]): Whether to reset the wire to the :math:`|0 \rangle` + state after measurement. Returns: MidMeasureMP: measurement process instance @@ -72,6 +93,7 @@ def func(x, y): Raises: QuantumFunctionError: if multiple wires were specified """ + wire = Wires(wires) if len(wire) > 1: raise qml.QuantumFunctionError( @@ -80,7 +102,7 @@ def func(x, y): # Create a UUID and a map between MP and MV to support serialization measurement_id = str(uuid.uuid4())[:8] - mp = MidMeasureMP(wires=wire, id=measurement_id) + mp = MidMeasureMP(wires=wire, reset=reset, id=measurement_id) return MeasurementValue([mp], processing_fn=lambda v: v) @@ -90,17 +112,23 @@ def func(x, y): class MidMeasureMP(MeasurementProcess): """Mid-circuit measurement. + This class additionally stores information about unknown measurement outcomes in the qubit model. + Measurements on a single qubit in the computational basis are assumed. + Please refer to :func:`measure` for detailed documentation. Args: wires (.Wires): The wires the measurement process applies to. This can only be specified if an observable was not provided. - id (str): custom label given to a measurement instance, can be useful for some applications - where the instance has to be identified + reset (bool): Whether to reset the wire after measurement. + id (str): Custom label given to a measurement instance. """ - def __init__(self, wires: Optional[Wires] = None, id: Optional[str] = None): - super().__init__(wires=wires, id=id) + def __init__( + self, wires: Optional[Wires] = None, reset: Optional[bool] = False, id: Optional[str] = None + ): + super().__init__(wires=Wires(wires), id=id) + self.reset = reset @property def return_type(self): diff --git a/pennylane/transforms/condition.py b/pennylane/transforms/condition.py index 1c3530b0eff..d5dcd4ea6e7 100644 --- a/pennylane/transforms/condition.py +++ b/pennylane/transforms/condition.py @@ -67,7 +67,7 @@ def cond(condition, true_fn, false_fn=None): :func:`defer_measurements` transform. Args: - condition (.MeasurementValue[bool]): a conditional expression involving a mid-circuit + condition (.MeasurementValue): a conditional expression involving a mid-circuit measurement value (see :func:`.pennylane.measure`) true_fn (callable): The quantum function of PennyLane operation to apply if ``condition`` is ``True`` @@ -114,6 +114,8 @@ def qnode(x, y): Expressions with boolean logic flow using operators like ``and``, ``or`` and ``not`` are not supported as the ``condition`` argument. + While such statements may not result in errors, they may result in + incorrect behaviour. .. details:: :title: Usage Details diff --git a/pennylane/transforms/defer_measurements.py b/pennylane/transforms/defer_measurements.py index 29c13cdfe27..3bf6ffa355e 100644 --- a/pennylane/transforms/defer_measurements.py +++ b/pennylane/transforms/defer_measurements.py @@ -16,12 +16,15 @@ from pennylane.measurements import MidMeasureMP from pennylane.ops.op_math import ctrl from pennylane.queuing import apply +from pennylane.tape import QuantumTape from pennylane.transforms import qfunc_transform from pennylane.wires import Wires +# pylint: disable=too-many-branches + @qfunc_transform -def defer_measurements(tape): +def defer_measurements(tape: QuantumTape): """Quantum function transform that substitutes operations conditioned on measurement outcomes to controlled operations. @@ -40,6 +43,14 @@ def defer_measurements(tape): that can be controlled as such depends on the set of operations supported by the chosen device. + .. note:: + + Devices that inherit from :class:`~pennylane.QubitDevice` **must** be initialized + with an additional wire for each mid-circuit measurement after which the measured + wire is reused or reset for ``defer_measurements`` to transform the quantum tape + correctly. Hence, devices and quantum tapes must also be initialized without custom + wire labels for correct behaviour. + .. note:: This transform does not change the list of terminal measurements returned by @@ -54,7 +65,7 @@ def defer_measurements(tape): post-measurement states are considered. Args: - qfunc (function): a quantum function + tape (.QuantumTape): a quantum tape **Example** @@ -84,8 +95,30 @@ def qfunc(par): >>> qml.grad(qnode)(par) tensor(-0.49622252, requires_grad=True) + + Reusing and reseting measured wires will work as expected with the + ``defer_measurements`` transform: + + .. code-block:: python3 + + dev = qml.device("default.qubit", wires=3) + + @qml.qnode(dev) + def func(x, y): + qml.RY(x, wires=0) + qml.CNOT(wires=[0, 1]) + m_0 = qml.measure(1, reset=True) + + qml.cond(m_0, qml.RY)(y, wires=0) + qml.RX(np.pi/4, wires=1) + return qml.probs(wires=[0, 1]) + + Executing this QNode: + + >>> pars = np.array([0.643, 0.246], requires_grad=True) + >>> func(*pars) + tensor([0.76960924, 0.13204407, 0.08394415, 0.01440254], requires_grad=True) """ - measured_wires = {} cv_types = (qml.operation.CVOperation, qml.operation.CVObservable) ops_cv = any(isinstance(op, cv_types) for op in tape.operations) @@ -93,25 +126,51 @@ def qfunc(par): if ops_cv or obs_cv: raise ValueError("Continuous variable operations and observables are not supported.") - for op in tape: - op_wires_measured = set(wire for wire in op.wires if wire in measured_wires.values()) - if len(op_wires_measured) > 0: - raise ValueError( - f"Cannot apply operations on {op.wires} as the following wires have been measured already: {op_wires_measured}." + # Find wires that are reused after measurement + measured_wires = set() + reused_measurement_wires = set() + + for op in tape.operations: + if isinstance(op, MidMeasureMP): + if op.wires[0] in measured_wires or op.reset is True: + reused_measurement_wires.add(op.wires[0]) + measured_wires.add(op.wires[0]) + + else: + reused_measurement_wires = reused_measurement_wires.union( + measured_wires.intersection(op.wires.toset()) ) + # Apply controlled operations to store measurement outcomes and replace + # classically controlled operations + control_wires = {} + cur_wire = max(tape.wires) + 1 + + for op in tape: if isinstance(op, MidMeasureMP): - measured_wires[op.id] = op.wires[0] + # Only store measurement outcome in new wire if wire gets reused + if op.wires[0] in reused_measurement_wires: + control_wires[op.id] = cur_wire + + qml.CNOT([op.wires[0], cur_wire]) + if op.reset: + qml.CNOT([cur_wire, op.wires[0]]) + + cur_wire += 1 + else: + control_wires[op.id] = op.wires[0] elif op.__class__.__name__ == "Conditional": - _add_control_gate(op, measured_wires) + _add_control_gate(op, control_wires) else: apply(op) + return tape._qfunc_output # pylint: disable=protected-access + -def _add_control_gate(op, measured_wires): +def _add_control_gate(op, control_wires): """Helper function to add control gates""" - control = [measured_wires[m.id] for m in op.meas_val.measurements] + control = [control_wires[m.id] for m in op.meas_val.measurements] for branch, value in op.meas_val._items(): # pylint: disable=protected-access if value: ctrl( diff --git a/tests/devices/qubit/test_preprocess.py b/tests/devices/qubit/test_preprocess.py index 9c1ad571a73..83d17f709e0 100644 --- a/tests/devices/qubit/test_preprocess.py +++ b/tests/devices/qubit/test_preprocess.py @@ -217,7 +217,7 @@ def test_expand_fn_expand_unsupported_op(self, shots): # pylint: disable=no-member def test_expand_fn_defer_measurement(self): """Test that expand_fn defers mid-circuit measurements.""" - mp = MidMeasureMP(wires=[0], id="test_id") + mp = MidMeasureMP(wires=[0], reset=True, id="test_id") mv = MeasurementValue([mp], processing_fn=lambda v: v) ops = [ qml.Hadamard(0), @@ -230,7 +230,9 @@ def test_expand_fn_defer_measurement(self): expanded_tape = expand_fn(tape) expected = [ qml.Hadamard(0), - qml.ops.Controlled(qml.RX(0.123, wires=1), 0), + qml.CNOT([0, 2]), + qml.CNOT([2, 0]), + qml.ops.Controlled(qml.RX(0.123, wires=1), 2), ] for op, exp in zip(expanded_tape, expected + measurements): diff --git a/tests/drawer/test_drawable_layers.py b/tests/drawer/test_drawable_layers.py index a842a3c915a..5bac4268d6a 100644 --- a/tests/drawer/test_drawable_layers.py +++ b/tests/drawer/test_drawable_layers.py @@ -173,23 +173,7 @@ def test_empty_layers_are_pruned(self): layers = drawable_layers(ops, wire_map={i: i for i in range(3)}) assert layers == [[ops[1]], [ops[2], ops[0]], [ops[3]]] - def test_cannot_reuse_wire_after_conditional(self): - """Tests that a wire cannot be re-used after using a mid-circuit measurement.""" - with AnnotatedQueue() as q: - m0 = qml.measure(0) - qml.cond(m0, qml.PauliX)(1) - qml.Hadamard(0) - - with pytest.raises(ValueError, match="some wires have been measured already"): - drawable_layers(q.queue) - def test_cannot_draw_multi_wire_MidMeasureMP(self): """Tests that MidMeasureMP is only supported with one wire.""" with pytest.raises(ValueError, match="mid-circuit measurements with more than one wire."): drawable_layers([MidMeasureMP([0, 1])]) - - def test_cannot_use_measured_wire(self): - """Tests error is raised when trying to use a measured wire.""" - ops = [MidMeasureMP([0]), qml.PauliX(0)] - with pytest.raises(ValueError, match="some wires have been measured already"): - drawable_layers(ops) diff --git a/tests/measurements/test_mid_measure.py b/tests/measurements/test_mid_measure.py index 06352b0a1de..6aa70a21457 100644 --- a/tests/measurements/test_mid_measure.py +++ b/tests/measurements/test_mid_measure.py @@ -26,7 +26,7 @@ def test_samples_computational_basis(): """Test that samples_computational_basis is always false for mid circuit measurements.""" - m = qml.measurements.MidMeasureMP(Wires(0)) + m = MidMeasureMP(Wires(0)) assert not m.samples_computational_basis @@ -44,19 +44,19 @@ def test_many_wires_error(self): def test_hash(self): """Test that the hash for `MidMeasureMP` is defined correctly.""" - m1 = MidMeasureMP(Wires(0), "m1") - m2 = MidMeasureMP(Wires(0), "m2") - m3 = MidMeasureMP(Wires(1), "m1") - m4 = MidMeasureMP(Wires(0), "m1") + m1 = MidMeasureMP(Wires(0), id="m1") + m2 = MidMeasureMP(Wires(0), id="m2") + m3 = MidMeasureMP(Wires(1), id="m1") + m4 = MidMeasureMP(Wires(0), id="m1") assert m1.hash != m2.hash assert m1.hash != m3.hash assert m1.hash == m4.hash -mp1 = MidMeasureMP(Wires(0), "m0") -mp2 = MidMeasureMP(Wires(1), "m1") -mp3 = MidMeasureMP(Wires(2), "m2") +mp1 = MidMeasureMP(Wires(0), id="m0") +mp2 = MidMeasureMP(Wires(1), id="m1") +mp3 = MidMeasureMP(Wires(2), id="m2") class TestMeasurementValueManipulation: diff --git a/tests/test_qnode.py b/tests/test_qnode.py index 32c4333e104..65c9011e92f 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -1077,7 +1077,7 @@ def test_defer_meas_if_mcm_unsupported(self, first_par, sec_par, return_type): """Tests that the transform using the deferred measurement principle is applied if the device doesn't support mid-circuit measurements natively.""" - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def cry_qnode(x, y): @@ -1105,7 +1105,7 @@ def conditional_ry_qnode(x, y): def test_sampling_with_mcm(self, basis_state): """Tests that a QNode with qml.sample and mid-circuit measurements returns the expected results.""" - dev = qml.device("default.qubit", wires=2, shots=1000) + dev = qml.device("default.qubit", wires=3, shots=1000) first_par = np.pi @@ -1135,7 +1135,7 @@ def test_conditional_ops_tensorflow(self, interface): """Test conditional operations with TensorFlow.""" import tensorflow as tf - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=3) @qml.qnode(dev, interface=interface, diff_method="parameter-shift") def cry_qnode(x): @@ -1178,7 +1178,7 @@ def test_conditional_ops_torch(self, interface): """Test conditional operations with Torch.""" import torch - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=3) @qml.qnode(dev, interface=interface, diff_method="parameter-shift") def cry_qnode(x): @@ -1217,7 +1217,7 @@ def test_conditional_ops_jax(self, jax_interface): import jax jnp = jax.numpy - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=3) @qml.qnode(dev, interface=jax_interface, diff_method="parameter-shift") def cry_qnode(x): @@ -1246,20 +1246,6 @@ def conditional_ry_qnode(x): assert np.allclose(r1, r2) assert np.allclose(jax.grad(cry_qnode)(x1), jax.grad(conditional_ry_qnode)(x2)) - def test_already_measured_error_operation(self): - """Test that attempting to apply an operation on a wires that has been - measured raises an error.""" - dev = qml.device("default.qubit", wires=3) - - @qml.qnode(dev) - def circuit(): - qml.measure(1) - qml.PauliX(1) - return qml.expval(qml.PauliZ(0)) - - with pytest.raises(ValueError, match="wires have been measured already: {1}"): - circuit() - def test_qnode_does_not_support_nested_queuing(self): """Test that operators in QNodes are not queued to surrounding contexts.""" dev = qml.device("default.qubit", wires=1) diff --git a/tests/transforms/test_defer_measurements.py b/tests/transforms/test_defer_measurements.py index 6e247e1c8f2..87ccf6a244c 100644 --- a/tests/transforms/test_defer_measurements.py +++ b/tests/transforms/test_defer_measurements.py @@ -47,19 +47,98 @@ def qnode2(): assert isinstance(res1, type(res2)) assert res1.shape == res2.shape - assert len(qnode1.qtape.operations) == len(qnode2.qtape.operations) + assert len(qnode2.qtape.operations) == 0 assert len(qnode1.qtape.measurements) == len(qnode2.qtape.measurements) - # Check the operations - for op1, op2 in zip(qnode1.qtape.operations, qnode2.qtape.operations): - assert isinstance(op1, type(op2)) - assert op1.data == op2.data - # Check the measurements for op1, op2 in zip(qnode1.qtape.measurements, qnode2.qtape.measurements): assert isinstance(op1, type(op2)) assert op1.data == op2.data + def test_reuse_wire_after_measurement(self): + """Test that wires can be reused after measurement.""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + @qml.defer_measurements + def qnode(): + qml.Hadamard(0) + qml.measure(0) + qml.PauliZ(0) + return qml.expval(qml.PauliX(0)) + + _ = qnode() + + def test_no_new_wires_without_reuse(self): + """Test that new wires are not added if a measured wire is not reused.""" + dev = qml.device("default.qubit", wires=3) + + # Quantum teleportation + @qml.qnode(dev) + def qnode1(phi): + qml.RX(phi, 0) + qml.Hadamard(1) + qml.CNOT([1, 2]) + qml.CNOT([0, 1]) + qml.Hadamard(0) + + m0 = qml.measure(0) + qml.cond(m0, qml.PauliZ)(2) + m1 = qml.measure(1) + qml.cond(m1, qml.PauliX)(2) + return qml.expval(qml.PauliZ(2)) + + # Prepare wire 0 in arbitrary state + @qml.qnode(dev) + def qnode2(phi): + qml.RX(phi, 0) + return qml.expval(qml.PauliZ(0)) + + # Outputs should match + assert np.isclose(qnode1(np.pi / 4), qnode2(np.pi / 4)) + + assert isinstance(qnode1.qtape.operations[5], qml.ops.Controlled) + assert qml.equal(qnode1.qtape.operations[5].base, qml.PauliZ(2)) + assert qnode1.qtape.operations[5].hyperparameters["control_wires"] == qml.wires.Wires(0) + + assert qml.equal(qnode1.qtape.operations[6], qml.CNOT([1, 2])) + + def test_new_wires_after_reuse(self): + """Test that a new wire is added for every measurement after which + the wire is reused.""" + dev = qml.device("default.qubit", wires=4) + + @qml.qnode(dev) + def qnode1(phi, theta): + qml.RX(phi, 0) + m0 = qml.measure(0, reset=True) # Reused measurement, one new wire added + qml.cond(m0, qml.Hadamard)(1) + m1 = qml.measure(1) # No reuse + qml.RY(theta, 2) + qml.cond(m1, qml.RY)(-theta, 2) + return qml.expval(qml.PauliZ(2)) + + res1 = qnode1(np.pi / 4, 3 * np.pi / 4) + + assert len(qnode1.qtape.wires) == 4 + assert len(qnode1.qtape.operations) == 6 + + @qml.qnode(dev) + def qnode2(phi, theta): + qml.RX(phi, 0) + m0 = qml.measure(0) # No reuse + qml.cond(m0, qml.Hadamard)(1) + m1 = qml.measure(1) # No reuse + qml.RY(theta, 2) + qml.cond(m1, qml.RY)(-theta, 2) + return qml.expval(qml.PauliZ(2)) + + res2 = qnode2(np.pi / 4, 3 * np.pi / 4) + assert np.allclose(res1, res2) + + assert len(qnode2.qtape.wires) == 3 + assert len(qnode2.qtape.operations) == 4 + def test_measure_between_ops(self): """Test that a quantum function that contains one operation before and after a mid-circuit measurement yields the correct results and is @@ -87,7 +166,7 @@ def func2(): assert isinstance(res1, type(res2)) assert res1.shape == res2.shape - assert len(qnode1.qtape.operations) == len(qnode2.qtape.operations) + assert len(qnode2.qtape.operations) == len(qnode1.qtape.operations) assert len(qnode1.qtape.measurements) == len(qnode2.qtape.measurements) # Check the operations @@ -100,13 +179,12 @@ def func2(): assert isinstance(op1, type(op2)) assert op1.data == op2.data - @pytest.mark.parametrize( - "mid_measure_wire, tp_wires", [(0, [1, 2, 3]), (0, [3, 1, 2]), ("a", ["b", "c", "d"])] - ) + @pytest.mark.parametrize("mid_measure_wire, tp_wires", [(0, [1, 2, 3]), (0, [3, 1, 2])]) def test_measure_with_tensor_obs(self, mid_measure_wire, tp_wires): """Test that the defer_measurements transform works well even with tensor observables in the tape.""" # pylint: disable=protected-access + with qml.queuing.AnnotatedQueue() as q: qml.measure(mid_measure_wire) qml.expval(qml.operation.Tensor(*[qml.PauliZ(w) for w in tp_wires])) @@ -115,7 +193,6 @@ def test_measure_with_tensor_obs(self, mid_measure_wire, tp_wires): tape = qml.defer_measurements(tape) # Check the operations and measurements in the tape - assert tape._ops == [] assert len(tape.measurements) == 1 measurement = tape.measurements[0] @@ -128,37 +205,6 @@ def test_measure_with_tensor_obs(self, mid_measure_wire, tp_wires): assert isinstance(ob, qml.PauliZ) assert ob.wires == qml.wires.Wires(tp_wires[idx]) - def test_already_measured_error_operation(self): - """Test that attempting to apply an operation on a wires that has been - measured raises an error.""" - dev = qml.device("default.qubit", wires=3) - - def qfunc(): - qml.measure(1) - qml.PauliX(1) - return qml.expval(qml.PauliZ(0)) - - tape_deferred_func = qml.defer_measurements(qfunc) - qnode = qml.QNode(tape_deferred_func, dev) - - with pytest.raises(ValueError, match="wires have been measured already: {1}"): - qnode() - - def test_already_measured_error_terminal_measurement(self): - """Test that attempting to measure a wire at the end of the circuit - that has been measured in the middle of the circuit raises an error.""" - dev = qml.device("default.qubit", wires=3) - - def qfunc(): - qml.measure(1) - return qml.expval(qml.PauliZ(1)) - - tape_deferred_func = qml.defer_measurements(qfunc) - qnode = qml.QNode(tape_deferred_func, dev) - - with pytest.raises(ValueError, match="Cannot apply operations"): - qnode() - def test_cv_op_error(self): """Test that CV operations are not supported.""" dev = qml.device("default.gaussian", wires=3) @@ -206,7 +252,7 @@ def test_correct_ops_in_tape(self, terminal_measurement): sec_par = 0.3 with qml.queuing.AnnotatedQueue() as q: - m_0 = qml.measure(4) + m_0 = qml.measure(4, reset=True) qml.cond(m_0, qml.RY)(first_par, wires=1) m_1 = qml.measure(3) @@ -216,15 +262,15 @@ def test_correct_ops_in_tape(self, terminal_measurement): tape = qml.tape.QuantumScript.from_queue(q) tape = qml.defer_measurements(tape) - assert len(tape.operations) == 2 + assert len(tape.operations) == 4 assert len(tape.measurements) == 1 # Check the two underlying Controlled instances - first_ctrl_op = tape.operations[0] + first_ctrl_op = tape.operations[2] assert isinstance(first_ctrl_op, qml.ops.op_math.Controlled) assert qml.equal(first_ctrl_op.base, qml.RY(first_par, 1)) - sec_ctrl_op = tape.operations[1] + sec_ctrl_op = tape.operations[3] assert isinstance(sec_ctrl_op, qml.ops.op_math.Controlled) assert qml.equal(sec_ctrl_op.base, qml.RZ(sec_par, 1)) @@ -301,7 +347,7 @@ def test_quantum_teleportation(self, rads): # Alice measures her qubits, obtaining one of four results, and sends this information to Bob. m_0 = qml.measure(0) - m_1 = qml.measure(1) + m_1 = qml.measure(1, reset=True) # Given Alice's measurements, Bob performs one of four operations on his half of the EPR pair and # recovers the original quantum state. @@ -312,7 +358,9 @@ def test_quantum_teleportation(self, rads): tape = qml.tape.QuantumScript.from_queue(q) tape = qml.defer_measurements(tape) - assert len(tape.operations) == 5 + 2 # 5 regular ops + 2 conditional ops + assert ( + len(tape.operations) == 5 + 1 + 1 + 2 + ) # 5 regular ops + 1 measurement op + 1 reset op + 2 conditional ops assert len(tape.measurements) == 1 # Check the each operation @@ -337,12 +385,22 @@ def test_quantum_teleportation(self, rads): assert isinstance(op5, qml.Hadamard) assert op5.wires == qml.wires.Wires([0]) - # Check the two underlying Controlled instances - ctrl_op1 = tape.operations[5] + # Check the two underlying CNOTs for storing measurement state + meas_op1 = tape.operations[5] + assert isinstance(meas_op1, qml.CNOT) + assert meas_op1.wires == qml.wires.Wires([1, 3]) + + meas_op2 = tape.operations[6] + assert isinstance(meas_op2, qml.CNOT) + assert meas_op2.wires == qml.wires.Wires([3, 1]) + + # Check the two underlying Controlled instances + ctrl_op1 = tape.operations[7] assert isinstance(ctrl_op1, qml.ops.op_math.Controlled) assert qml.equal(ctrl_op1.base, qml.RX(math.pi, 2)) + assert ctrl_op1.wires == qml.wires.Wires([3, 2]) - ctrl_op2 = tape.operations[6] + ctrl_op2 = tape.operations[8] assert isinstance(ctrl_op2, qml.ops.op_math.Controlled) assert qml.equal(ctrl_op2.base, qml.RZ(math.pi, 2)) assert ctrl_op2.wires == qml.wires.Wires([0, 2]) @@ -388,18 +446,28 @@ def test_hermitian_queued(self): measurement = qml.expval(qml.Hermitian(mat, wires=[3, 1, 2])) with qml.queuing.AnnotatedQueue() as q: - m_0 = qml.measure(0) + m_0 = qml.measure(0, reset=True) qml.cond(m_0, qml.RY)(rads, wires=4) qml.apply(measurement) tape = qml.tape.QuantumScript.from_queue(q) tape = qml.defer_measurements(tape) - assert len(tape.operations) == 1 + assert len(tape.operations) == 3 assert len(tape.measurements) == 1 + # Check the underlying CNOT for storing measurement state + meas_op1 = tape.operations[0] + assert isinstance(meas_op1, qml.CNOT) + assert meas_op1.wires == qml.wires.Wires([0, 5]) + + # Check the underlying CNOT for reseting measured wire + meas_op1 = tape.operations[1] + assert isinstance(meas_op1, qml.CNOT) + assert meas_op1.wires == qml.wires.Wires([5, 0]) + # Check the underlying Controlled instances - first_ctrl_op = tape.operations[0] + first_ctrl_op = tape.operations[2] assert isinstance(first_ctrl_op, qml.ops.op_math.Controlled) assert qml.equal(first_ctrl_op.base, qml.RY(rads, 4)) @@ -435,7 +503,7 @@ def test_hamiltonian_queued(self): assert qml.equal(first_ctrl_op.base, qml.RY(rads, 4)) assert len(tape.measurements) == 1 assert isinstance(tape.measurements[0], qml.measurements.MeasurementProcess) - assert tape.measurements[0].obs == H + assert qml.equal(tape.measurements[0].obs, H) @pytest.mark.parametrize("device", ["default.qubit", "default.mixed", "lightning.qubit"]) @pytest.mark.parametrize("ops", [(qml.RX, qml.CRX), (qml.RY, qml.CRY), (qml.RZ, qml.CRZ)]) @@ -470,7 +538,7 @@ def quantum_control_circuit(rads): @pytest.mark.parametrize("device", ["default.qubit", "default.mixed", "lightning.qubit"]) def test_conditional_rotations_with_else(self, device): """Test that an else operation can also defined using qml.cond.""" - dev = qml.device(device, wires=2) + dev = qml.device(device, wires=3) r = 2.345 op1, controlled_op1 = qml.RY, qml.CRY @@ -504,7 +572,7 @@ def test_keyword_syntax(self): keyword syntax works.""" op = qml.RY - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def qnode1(par): @@ -528,7 +596,7 @@ def qnode2(par): def test_condition_using_measurement_outcome(self, control_val, expected): """Apply a conditional bitflip by selecting the measurement outcome.""" - dev = qml.device("default.qubit", wires=2) + dev = qml.device("default.qubit", wires=3) @qml.qnode(dev) def qnode(): @@ -541,7 +609,7 @@ def qnode(): @pytest.mark.parametrize("device", ["default.qubit", "default.mixed", "lightning.qubit"]) def test_cond_qfunc(self, device): """Test that a qfunc can also used with qml.cond.""" - dev = qml.device(device, wires=2) + dev = qml.device(device, wires=3) r = 2.324 @@ -576,7 +644,7 @@ def quantum_control_circuit(r): def test_cond_qfunc_with_else(self, device): """Test that a qfunc can also used with qml.cond even when an else qfunc is provided.""" - dev = qml.device(device, wires=2) + dev = qml.device(device, wires=3) x = 0.3 y = 3.123 @@ -610,7 +678,25 @@ def cond_qnode(x, y): return qml.probs(wires=[0]) assert np.allclose(normal_circuit(x, y), cond_qnode(x, y)) - assert np.allclose(qml.matrix(normal_circuit)(x, y), qml.matrix(cond_qnode)(x, y)) + + def test_cond_on_measured_wire(self): + """Test that applying a conditional operation on the same wire + that is measured works as expected.""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + @qml.defer_measurements + def qnode(): + qml.Hadamard(0) + m = qml.measure(0) + qml.cond(m, qml.PauliX)(0) + return qml.density_matrix(0) + + # Above circuit will cause wire 0 to go back to the |0> computational + # basis state. We can inspect the reduced density matrix to confirm this + # without inspecting the extra wires + expected_dmat = np.array([[1, 0], [0, 0]]) + assert np.allclose(qnode(), expected_dmat) class TestExpressionConditionals: @@ -621,7 +707,7 @@ class TestExpressionConditionals: def test_conditional_rotations(self, r, op): """Test that the quantum conditional operations match the output of controlled rotations. And additionally that summing measurements works as expected.""" - dev = qml.device("default.qubit", wires=3) + dev = qml.device("default.qubit", wires=5) @qml.qnode(dev) def normal_circuit(rads): @@ -648,7 +734,7 @@ def quantum_control_circuit(rads): @pytest.mark.parametrize("r", np.linspace(0.1, 2 * np.pi - 0.1, 4)) def test_triple_measurement_condition_expression(self, r): """Test that combining the results of three mid-circuit measurements works as expected.""" - dev = qml.device("default.qubit", wires=4) + dev = qml.device("default.qubit", wires=7) @qml.qnode(dev) @qml.defer_measurements @@ -690,7 +776,7 @@ def test_multiple_conditions(self): """Test that when multiple "branches" of the mid-circuit measurements all satisfy the criteria then this translates to multiple control gates. """ - dev = qml.device("default.qubit", wires=4) + dev = qml.device("default.qubit", wires=7) @qml.qnode(dev) @qml.defer_measurements @@ -733,7 +819,7 @@ def quantum_control_circuit(rads): def test_composed_conditions(self): """Test that a complex nested expression gets resolved correctly to the corresponding correct control gates.""" - dev = qml.device("default.qubit", wires=4) + dev = qml.device("default.qubit", wires=7) @qml.qnode(dev) @qml.defer_measurements @@ -788,7 +874,7 @@ def test_basis_state_prep(self): basis_state = [0, 1, 1, 0] - dev = qml.device("default.qubit", wires=5) + dev = qml.device("default.qubit", wires=6) @qml.qnode(dev) def qnode1(): @@ -804,11 +890,9 @@ def qnode2(): qml.cond(m_0, template)(basis_state, wires=range(1, 5)) return qml.expval(qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliZ(3) @ qml.PauliZ(4)) - dev = qml.device("default.qubit", wires=2) - assert np.allclose(qnode1(), qnode2()) - assert len(qnode1.qtape.operations) == len(qnode2.qtape.operations) + assert len(qnode2.qtape.operations) == len(qnode1.qtape.operations) assert len(qnode1.qtape.measurements) == len(qnode2.qtape.measurements) # Check the operations @@ -827,7 +911,7 @@ def test_angle_embedding(self): template = qml.AngleEmbedding feature_vector = [1, 2, 3] - dev = qml.device("default.qubit", wires=5) + dev = qml.device("default.qubit", wires=6) @qml.qnode(dev) def qnode1(): @@ -843,13 +927,12 @@ def qnode2(): qml.cond(m_0, template)(features=feature_vector, wires=range(1, 5), rotation="Z") return qml.expval(qml.PauliZ(1) @ qml.PauliZ(2) @ qml.PauliZ(3) @ qml.PauliZ(4)) - dev = qml.device("default.qubit", wires=2) res1 = qnode1() res2 = qnode2() assert np.allclose(res1, res2) - assert len(qnode1.qtape.operations) == len(qnode2.qtape.operations) + assert len(qnode2.qtape.operations) == len(qnode1.qtape.operations) assert len(qnode1.qtape.measurements) == len(qnode2.qtape.measurements) # Check the operations @@ -865,7 +948,7 @@ def qnode2(): @pytest.mark.parametrize("template", [qml.StronglyEntanglingLayers, qml.BasicEntanglerLayers]) def test_layers(self, template): """Test layers conditioned on mid-circuit measurement outcomes.""" - dev = qml.device("default.qubit", wires=3) + dev = qml.device("default.qubit", wires=4) num_wires = 2 @@ -888,7 +971,7 @@ def qnode2(parameters): assert np.allclose(qnode1(weights), qnode2(weights)) - assert len(qnode1.qtape.operations) == len(qnode2.qtape.operations) + assert len(qnode2.qtape.operations) == len(qnode1.qtape.operations) assert len(qnode1.qtape.measurements) == len(qnode2.qtape.measurements) # Check the operations @@ -902,13 +985,120 @@ def qnode2(parameters): assert np.allclose(op1.data, op2.data) +class TestQubitReset: + """Tests for the qubit reset functionality of `qml.measure`.""" + + def test_correct_cnot_for_reset(self): + """Test that a CNOT is applied from the wire that stores the measurement + to the measured wire after the measurement.""" + dev = qml.device("default.qubit", wires=3) + + @qml.qnode(dev) + def qnode1(x): + qml.Hadamard(0) + qml.CRX(x, [0, 1]) + return qml.expval(qml.PauliZ(1)) + + @qml.qnode(dev) + @qml.defer_measurements + def qnode2(x): + qml.Hadamard(0) + m0 = qml.measure(0, reset=True) + qml.cond(m0, qml.RX)(x, 1) + return qml.expval(qml.PauliZ(1)) + + assert np.allclose(qnode1(0.123), qnode2(0.123)) + + expected_circuit = [ + qml.Hadamard(0), + qml.CNOT([0, 2]), + qml.CNOT([2, 0]), + qml.ops.Controlled(qml.RX(0.123, 1), 2), + qml.expval(qml.PauliZ(1)), + ] + + assert len(qnode2.qtape.circuit) == len(expected_circuit) + assert all( + qml.equal(actual, expected) + for actual, expected in zip(qnode2.qtape.circuit, expected_circuit) + ) + + def test_wire_is_reset(self): + """Test that a wire is reset to the |0> state without any local phases + after measurement if reset is requested.""" + dev = qml.device("default.qubit", wires=3) + + @qml.qnode(dev) + @qml.defer_measurements + def qnode(x): + qml.Hadamard(0) + qml.PhaseShift(np.pi / 4, 0) + m = qml.measure(0, reset=True) + qml.cond(m, qml.RX)(x, 1) + return qml.density_matrix(wires=[0]) + + # Expected reduced density matrix on wire 0 + expected_mat = np.array([[1, 0], [0, 0]]) + assert np.allclose(qnode(0.123), expected_mat) + + def test_multiple_measurements_mixed_reset(self): + """Test that a QNode with multiple mid-circuit measurements with + different resets is transformed correctly.""" + dev = qml.device("default.qubit", wires=6) + + @qml.qnode(dev) + def qnode(p, x, y): + qml.Hadamard(0) + qml.PhaseShift(p, 0) + # Set measurement_ids so that the order of wires in combined + # measurement values is consistent + + mp0 = qml.measurements.MidMeasureMP(0, reset=True, id=0) + m0 = qml.measurements.MeasurementValue([mp0], lambda v: v) + qml.cond(~m0, qml.RX)(x, 1) + mp1 = qml.measurements.MidMeasureMP(1, reset=True, id=1) + m1 = qml.measurements.MeasurementValue([mp1], lambda v: v) + qml.cond(m0 & m1, qml.Hadamard)(0) + mp2 = qml.measurements.MidMeasureMP(0, id=2) + m2 = qml.measurements.MeasurementValue([mp2], lambda v: v) + qml.cond(m1 | m2, qml.RY)(y, 2) + return qml.expval(qml.PauliZ(2)) + + _ = qnode(0.123, 0.456, 0.789) + + expected_circuit = [ + qml.Hadamard(0), + qml.PhaseShift(0.123, 0), + qml.CNOT([0, 3]), + qml.CNOT([3, 0]), + qml.ops.Controlled(qml.RX(0.456, 1), 3, [False]), + qml.CNOT([1, 4]), + qml.CNOT([4, 1]), + qml.ops.Controlled(qml.Hadamard(0), [3, 4]), + qml.CNOT([0, 5]), + qml.ops.Controlled(qml.RY(0.789, 2), [4, 5], [False, True]), + qml.ops.Controlled(qml.RY(0.789, 2), [4, 5], [True, False]), + qml.ops.Controlled(qml.RY(0.789, 2), [4, 5], [True, True]), + qml.expval(qml.PauliZ(2)), + ] + + assert len(qnode.qtape.circuit) == len(expected_circuit) + assert all( + qml.equal(actual, expected) + for actual, expected in zip(qnode.qtape.circuit, expected_circuit) + ) + + class TestDrawing: """Tests drawing circuits with mid-circuit measurements and conditional operations that have been transformed""" - def test_drawing(self): + def test_drawing_no_reuse(self): """Test that drawing a func with mid-circuit measurements works and - that controlled operations are drawn for conditional operations.""" + that controlled operations are drawn for conditional operations when + the measured wires are not reused.""" + + # TODO: Update after drawing for mid-circuit measurements is updated. def qfunc(): m_0 = qml.measure(0) @@ -929,3 +1119,31 @@ def qfunc(): "2: ───────────╰●────────┤ " ) assert qml.draw(transformed_qnode)() == expected + + def test_drawing_with_reuse(self): + """Test that drawing a func with mid-circuit measurements works and + that controlled operations are drawn for conditional operations when + the measured wires are reused.""" + + # TODO: Update after drawing for mid-circuit measurements is updated. + + def qfunc(): + m_0 = qml.measure(0, reset=True) + qml.cond(m_0, qml.RY)(0.312, wires=1) + + m_2 = qml.measure(2) + qml.cond(m_2, qml.RY)(0.312, wires=1) + return qml.expval(qml.PauliZ(1)) + + dev = qml.device("default.qubit", wires=4) + + transformed_qfunc = qml.transforms.defer_measurements(qfunc) + transformed_qnode = qml.QNode(transformed_qfunc, dev) + + expected = ( + "0: ─╭●─╭X─────────────────────┤ \n" + "1: ─│──│──╭RY(0.31)─╭RY(0.31)─┤ \n" + "2: ─│──│──│─────────╰●────────┤ \n" + "3: ─╰X─╰●─╰●──────────────────┤ " + ) + assert qml.draw(transformed_qnode)() == expected