Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the conditional operations documentation #2294

Merged
merged 20 commits into from
Mar 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions doc/introduction/measurements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ Mid-circuit measurements and conditional operations
---------------------------------------------------

PennyLane allows specifying measurements in the middle of the circuit.
Operations can then be conditioned on the measurement outcome of such
mid-circuit measurements:
Quantum functions such as operations can then be conditioned on the measurement
outcome of such mid-circuit measurements:

.. code-block:: python

Expand All @@ -180,7 +180,7 @@ measurement on qubit 1 yielded ``1`` as an outcome, otherwise doing nothing
for the ``0`` measurement outcome.

PennyLane implements the deferred measurement principle to transform
conditional operations with the :func:`defer_measurements` quantum
conditional operations with the :func:`~.defer_measurements` quantum
function transform.

.. code-block:: python
Expand Down Expand Up @@ -226,6 +226,10 @@ differentiable and device-independent way. Performing true mid-circuit
measurements and conditional operations is dependent on the
quantum hardware and PennyLane device capabilities.

For more examples on applying quantum functions conditionally, refer to the
:func:`~.pennylane.cond` transform.


Changing the number of shots
----------------------------

Expand Down
1 change: 1 addition & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
[(#2211)](https://github.com/PennyLaneAI/pennylane/pull/2211)
[(#2236)](https://github.com/PennyLaneAI/pennylane/pull/2236)
[(#2275)](https://github.com/PennyLaneAI/pennylane/pull/2275)
[(#2294)](https://github.com/PennyLaneAI/pennylane/pull/2294)

The addition includes the `defer_measurements` device-independent transform
that can be applied on devices that have no native mid-circuit measurements
Expand Down
16 changes: 12 additions & 4 deletions pennylane/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,17 +634,25 @@ def branches(self):
return branch_dict

def __invert__(self):
"""Inverts the control value of the measurement."""
"""Return a copy of the measurement value with an inverted control
value."""
inverted_self = copy.copy(self)
zero = self._zero_case
one = self._one_case

self._control_value = one if self._control_value == zero else zero
inverted_self._control_value = one if self._control_value == zero else zero

return self
return inverted_self

def __eq__(self, control_value):
"""Allow asserting measurement values."""
measurement_outcomes = {self._zero_case, self._one_case}

if not isinstance(control_value, tuple(type(val) for val in measurement_outcomes)):
raise MeasurementValueError(
f"The equality operator is used to assert measurement outcomes, but got a value with type {type(control_value)}."
)

if control_value not in measurement_outcomes:
raise MeasurementValueError(
f"Unknown measurement value asserted; the set of possible measurement outcomes is: {measurement_outcomes}."
Expand Down Expand Up @@ -696,7 +704,7 @@ def func(x, y):
tensor([0.90165331, 0.09834669], requires_grad=True)

Args:
wires (Wires): The wires the measurement process applies to.
wires (Wires): The wire of the qubit the measurement process applies to.

Raises:
QuantumFunctionError: if multiple wires were specified
Expand Down
2 changes: 1 addition & 1 deletion pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,7 +788,7 @@ def terms(self):

Returns:
tuple[list[tensor_like or float], list[.Operation]]: list of coefficients :math:`c_i`
and list of operations :math:`O_i`
and list of operations :math:`O_i`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just something caught in master too.

"""
return self.compute_terms(*self.parameters, **self.hyperparameters)

Expand Down
147 changes: 136 additions & 11 deletions pennylane/transforms/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"""
Contains the condition transform.
"""
from copy import copy
from functools import wraps
from typing import Type

Expand Down Expand Up @@ -88,21 +87,148 @@ def cond(condition, true_fn, false_fn=None):

dev = qml.device("default.qubit", wires=3)

first_par = 0.1
sec_par = 0.3

@qml.qnode(dev)
def qnode():
def qnode(x, y):
qml.Hadamard(0)
m_0 = qml.measure(0)
qml.cond(m_0, qml.RY)(first_par, wires=1)
qml.cond(m_0, qml.RY)(x, wires=1)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wrong param used before


qml.Hadamard(2)
qml.RY(-np.pi/2, wires=[2])
m_1 = qml.measure(2)
qml.cond(m_0, qml.RZ)(sec_par, wires=1)
qml.cond(m_1 == 0, qml.RX)(y, wires=1)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wrong param used before, m_1 == 0 is just meant to showcase assertion.

return qml.expval(qml.PauliZ(1))

.. code-block :: pycon

>>> first_par = np.array(0.3, requires_grad=True)
>>> sec_par = np.array(1.23, requires_grad=True)
>>> qnode(first_par, sec_par)
tensor(0.32677361, requires_grad=True)

.. note::

If the first argument of ``cond`` is a measurement value (e.g., ``m_0``
in ``qml.cond(m_0, qml.RY)``), then ``m_0 == 1`` is considered
internally.

.. UsageDetails::

**Conditional quantum functions**

The ``cond`` transform allows conditioning quantum functions too:

.. code-block:: python3

dev = qml.device("default.qubit", wires=2)

def qfunc(par, wires):
qml.Hadamard(wires[0])
qml.RY(par, wires[0])

@qml.qnode(dev)
def qnode(x):
qml.Hadamard(0)
m_0 = qml.measure(0)
qml.cond(m_0, qfunc)(x, wires=[1])
return qml.expval(qml.PauliZ(1))

.. code-block :: pycon

>>> par = np.array(0.3, requires_grad=True)
>>> qnode(par)
tensor(0.3522399, requires_grad=True)

**Passing two quantum functions**

In the qubit model, single-qubit measurements may result in one of two
outcomes. Such measurement outcomes may then be used to create
conditional expressions.

According to the truth value of the conditional expression passed to
``cond``, the transform can apply a quantum function in both the
``True`` and ``False`` case:

.. code-block:: python3

dev = qml.device("default.qubit", wires=2)

def qfunc1(x, wires):
qml.Hadamard(wires[0])
qml.RY(x, wires[0])

def qfunc2(x, wires):
qml.Hadamard(wires[0])
qml.RZ(x, wires[0])

@qml.qnode(dev)
def qnode1(x):
qml.Hadamard(0)
m_0 = qml.measure(0)
qml.cond(m_0, qfunc1, qfunc2)(x, wires=[1])
return qml.expval(qml.PauliZ(1))

.. code-block :: pycon

>>> par = np.array(0.3, requires_grad=True)
>>> qnode1(par)
albi3ro marked this conversation as resolved.
Show resolved Hide resolved
tensor(-0.1477601, requires_grad=True)

The previous QNode is equivalent to using ``cond`` twice, inverting the
conditional expression in the second case using the ``~`` unary
operator:

.. code-block:: python3

@qml.qnode(dev)
def qnode2(x):
qml.Hadamard(0)
m_0 = qml.measure(0)
qml.cond(m_0, qfunc1)(x, wires=[1])
qml.cond(~m_0, qfunc2)(x, wires=[1])
return qml.expval(qml.PauliZ(1))

.. code-block :: pycon

>>> qnode2(par)
tensor(-0.1477601, requires_grad=True)
albi3ro marked this conversation as resolved.
Show resolved Hide resolved

**Quantum functions with different signatures**

It may be that the two quantum functions passed to ``qml.cond`` have
different signatures. In such a case, ``lambda`` functions taking no
arguments can be used with Python closure:

.. code-block:: python3

dev = qml.device("default.qubit", wires=2)

def qfunc1(x, wire):
qml.Hadamard(wire)
qml.RY(x, wire)

def qfunc2(x, y, z, wire):
qml.Hadamard(wire)
qml.Rot(x, y, z, wire)

@qml.qnode(dev)
def qnode(a, x, y, z):
qml.Hadamard(0)
m_0 = qml.measure(0)
qml.cond(m_0, lambda: qfunc1(a, wire=1), lambda: qfunc2(x, y, z, wire=1))()
return qml.expval(qml.PauliZ(1))

.. code-block :: pycon

>>> par = np.array(0.3, requires_grad=True)
>>> x = np.array(1.2, requires_grad=True)
>>> y = np.array(1.1, requires_grad=True)
>>> z = np.array(0.3, requires_grad=True)
>>> qnode(par, x, y, z)
tensor(-0.30922805, requires_grad=True)
"""
if callable(true_fn):
# We assume that the callable is an operation or a quantum function

with_meas_err = (
"Only quantum functions that contain no measurements can be applied conditionally."
)
Expand All @@ -127,11 +253,10 @@ def wrapper(*args, **kwargs):
if else_tape.measurements:
raise ConditionalTransformError(with_meas_err)

inverted_m = copy(condition)
inverted_m = ~inverted_m
inverted_condition = ~condition

for op in else_tape.operations:
Conditional(inverted_m, op)
Conditional(inverted_condition, op)

else:
raise ConditionalTransformError(
Expand Down
19 changes: 18 additions & 1 deletion tests/test_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,27 @@ def test_measurement_value_inversion(self, val_pair, num_inv, expected_idx):
one_case = val_pair[1]
mv = MeasurementValue(measurement_id="1234", zero_case=zero_case, one_case=one_case)
for _ in range(num_inv):
mv = mv.__invert__()
mv_new = mv.__invert__()

# Check that inversion involves creating a copy
assert not mv_new is mv

mv = mv_new

assert mv._control_value == val_pair[expected_idx]

def test_measurement_value_assertion_error_wrong_type(self):
"""Test that the return_type related info is updated for a
measurement."""
mv1 = MeasurementValue(measurement_id="1111")
mv2 = MeasurementValue(measurement_id="2222")

with pytest.raises(
MeasurementValueError,
match="The equality operator is used to assert measurement outcomes, but got a value with type",
):
mv1 == mv2

def test_measurement_value_assertion_error(self):
"""Test that the return_type related info is updated for a
measurement."""
Expand Down
42 changes: 34 additions & 8 deletions tests/transforms/test_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,32 @@ def f(x):

assert ops[4].return_type == qml.operation.Probability

def test_cond_queues_with_else(self):
"""Test that qml.cond queues Conditional operations as expected when an
else qfunc is also provided."""
def tape_with_else(f, g, r):
"""Tape that uses cond by passing both a true and false func."""
with qml.tape.QuantumTape() as tape:
m_0 = qml.measure(0)
qml.cond(m_0, f, g)(r)
qml.probs(wires=1)

return tape

def tape_uses_cond_twice(f, g, r):
"""Tape that uses cond twice such that it's equivalent to using cond
with two functions being passed (tape_with_else)."""
with qml.tape.QuantumTape() as tape:
m_0 = qml.measure(0)
qml.cond(m_0, f)(r)
qml.cond(~m_0, g)(r)
qml.probs(wires=1)

return tape

@pytest.mark.parametrize("tape", [tape_with_else, tape_uses_cond_twice])
def test_cond_queues_with_else(self, tape):
"""Test that qml.cond queues Conditional operations as expected in two cases:
1. When an else qfunc is provided;
2. When qml.cond is used twice equivalent to using an else qfunc.
"""
r = 1.234

def f(x):
Expand All @@ -82,11 +105,7 @@ def f(x):
def g(x):
qml.PauliY(1)

with qml.tape.QuantumTape() as tape:
m_0 = qml.measure(0)
qml.cond(m_0, f, g)(r)
qml.probs(wires=1)

tape = tape(f, g, r)
ops = tape.queue
target_wire = qml.wires.Wires(1)

Expand All @@ -111,6 +130,13 @@ def g(x):
assert isinstance(ops[4].then_op, qml.PauliY)
assert ops[4].then_op.wires == target_wire

# Check that: the measurement value is the same for true_fn conditional
# ops
assert ops[1].meas_val is ops[2].meas_val is ops[3].meas_val

# However, it is not the same for the false_fn
assert ops[3].meas_val is not ops[4].meas_val

assert ops[5].return_type == qml.operation.Probability

def test_cond_error(self):
Expand Down