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

Adds a convenience prob() measurement function to return the probabilities of all computational basis states in a single QNode evaluation #432

Merged
merged 13 commits into from
Dec 24, 2019
15 changes: 13 additions & 2 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

### New features since last release

* Added `QAOAEmbedding` and its parameter initialization
* Added `QAOAEmbedding` and its parameter initialization
as a new trainable template.
[#442](https://github.com/XanaduAI/pennylane/pull/442)

* Added `qml.prob()` measurement function, allowing QNodes
to differentiate variational circuit probabilities
on simulators and hardware.
[#432](https://github.com/XanaduAI/pennylane/pull/432)

### Breaking changes

* Deprecated the old `QNode` such that only the new `QNode` and its syntax can be used,
Expand All @@ -19,7 +24,13 @@

### Documentation

* Improved documentation of ``AmplitudeEmbedding`` and ``BasisEmbedding`` templates.
* Improved documentation of `AmplitudeEmbedding` and `BasisEmbedding` templates.
[#441](https://github.com/XanaduAI/pennylane/pull/441)
[#439](https://github.com/XanaduAI/pennylane/pull/439)

* Codeblocks in the documentation now have a 'copy' button for easily
copying examples.
[#437](https://github.com/XanaduAI/pennylane/pull/437)

### Bug fixes

Expand Down
45 changes: 38 additions & 7 deletions doc/introduction/measurements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ Measurements
.. currentmodule:: pennylane.measure

PennyLane can extract different types of measurement results from quantum
devices: the expectation of an observable, its variance, or
samples of a single measurement.
devices: the expectation of an observable, its variance,
samples of a single measurement, or computational basis state probabilities.

For example, the following circuit returns the expectation value of the
:class:`~pennylane.PauliZ` observable on wire 1:
Expand All @@ -28,19 +28,19 @@ The available measurement functions are
:html:`<div class="summary-table">`

.. autosummary::
:nosignatures:

~pennylane.expval
~pennylane.sample
~pennylane.var
~pennylane.probs

:html:`</div>`

.. note::

Both :func:`~.pennylane.expval` and :func:`~.pennylane.var` support analytic
differentiation. :func:`~.pennylane.sample`, however, returns *stochastic*
results, and does not support differentiation.
All measurement functions support analytic differentiation, with the
exception of :func:`~.pennylane.sample`, as it returns *stochastic*
results.

Multiple measurements
---------------------
Expand Down Expand Up @@ -85,10 +85,41 @@ the ``@`` notation. For example, to measure the expectation value of
Note that we don't need to declare the identity observable on wire 1; this is
implicitly assumed.

The tensor observable notation can be used inside all measurement functions,
The tensor observable notation can be used inside all measurement functions that
accept observables as arguments,
including :func:`~.pennylane.expval`, :func:`~.pennylane.var`,
and :func:`~.pennylane.sample`.

Probability
-----------

You can also train QNodes on computational basis probabilities, by using
the :func:`~.pennylane.probs` measurement function. Unlike other
measurement functions, **this does not accept observables**.
Instead, it will return a flat array or tensor containing the (marginal)
probabilities of each quantum state.

.. code-block:: python3

def my_quantum_function(x, y):
qml.RZ(x, wires=0)
qml.CNOT(wires=[0, 1])
qml.RY(y, wires=1)
qml.CNOT(wires=[0, 2])
return qml.probs(wires=[0, 1])

For example:

>>> dev = qml.device("default.qubit", wires=3)
>>> qnode = qml.QNode(my_quantum_function, dev)
>>> qnode(0.56, 0.1)
array([0.99750208, 0.00249792, 0. , 0. ])

The returned probability array uses lexicographical ordering,
so corresponds to a :math:`99.75\%` probability of measuring
state :math:`|00\rangle`, and a :math:`0.25\%` probability of
measuring state :math:`|01\rangle`.

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

Expand Down
2 changes: 1 addition & 1 deletion pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from .circuit_graph import CircuitGraph
from .configuration import Configuration
from ._device import Device, DeviceError
from .measure import expval, var, sample
from .measure import expval, var, sample, probs
from .ops import *
from .optimize import *
from .qnodes import qnode, QNode, QuantumFunctionError
Expand Down
16 changes: 11 additions & 5 deletions pennylane/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import numpy as np

from pennylane.operation import Operation, Observable, Sample, Variance, Expectation, Tensor
from pennylane.operation import Operation, Observable, Sample, Variance, Expectation, Probability, Tensor
from pennylane.qnodes import QuantumFunctionError


Expand Down Expand Up @@ -184,6 +184,9 @@ def execute(self, queue, observables, parameters={}):
elif obs.return_type is Sample:
results.append(np.array(self.sample(obs.name, obs.wires, obs.parameters)))

elif obs.return_type is Probability:
results.append(list(self.probability(wires=obs.wires).values()))

elif obs.return_type is not None:
raise QuantumFunctionError("Unsupported return type specified for observable {}".format(obs.name))

Expand Down Expand Up @@ -455,11 +458,14 @@ def sample(self, observable, wires, par):
"""
raise NotImplementedError("Returning samples from QNodes not currently supported by {}".format(self.short_name))

def probability(self):
"""Return the full state probability of each computational basis state from the last run of the device.
def probability(self, wires=None):
"""Return the (marginal) probability of each computational basis
state from the last run of the device.

Raises:
NotImplementedError: if the device does not support returning probabilities
Args:
wires (Sequence[int]): Sequence of wires to return
marginal probabilities for. Wires not provided
are traced out of the system.

Returns:
OrderedDict[tuple, float]: Dictionary mapping a tuple representing the state
Expand Down
101 changes: 100 additions & 1 deletion pennylane/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
and measurement samples.
"""
import pennylane as qml
from .operation import Observable, Sample, Variance, Expectation, Tensor
from .operation import Observable, Sample, Variance, Expectation, Probability, Tensor
from .qnodes import QuantumFunctionError


Expand All @@ -31,6 +31,24 @@ def _remove_if_in_queue(op):
def expval(op):
r"""Expectation value of the supplied observable.

**Example:**

.. code-block:: python3

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

@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliY(0))

Executing this QNode:

>>> circuit(0.5)
-0.4794255386042029

Args:
op (Observable): a quantum observable object

Expand Down Expand Up @@ -63,6 +81,24 @@ def expval(op):
def var(op):
r"""Variance of the supplied observable.

**Example:**

.. code-block:: python3

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

@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[0, 1])
return qml.var(qml.PauliY(0))

Executing this QNode:

>>> circuit(0.5)
0.7701511529340698

Args:
op (Observable): a quantum observable object

Expand Down Expand Up @@ -96,6 +132,24 @@ def sample(op):
r"""Sample from the supplied observable, with the number of shots
determined from the ``dev.shots`` attribute of the corresponding device.

**Example:**

.. code-block:: python3

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

@qml.qnode(dev)
def circuit(x):
qml.RX(x, wires=0)
qml.Hadamard(wires=1)
qml.CNOT(wires=[0, 1])
return qml.sample(qml.PauliY(0))

Executing this QNode:

>>> circuit(0.5)
array([ 1., 1., 1., -1.])

Args:
op (Observable): a quantum observable object

Expand Down Expand Up @@ -123,3 +177,48 @@ def sample(op):
qml._current_context._append_op(op)

return op


def probs(wires):
r"""Probability of each computational basis state.

This measurement function accepts no observables, and instead
instructs the QNode to return a flat array containing the
probabilities of each quantum state.
Copy link
Contributor

Choose a reason for hiding this comment

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

As an extra sentence, it might be worth adding something like the explanation in the example such as
The dimension of the computational basis states is equal to the number of wires specified.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to say

Marginal probabilities may also be requested by restricting
the wires to a subset of the full system; the size of the
returned array will be ``[2**len(wires)]``.


Marginal probabilities may also be requested by restricting
the wires to a subset of the full system.

**Example:**

.. code-block:: python3

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

@qml.qnode(dev)
def circuit():
qml.Hadamard(wires=1)
qml.CNOT(wires=[0, 1])
return qml.probs(wires=[0, 1])

Executing this QNode:

>>> circuit()
array([0.5, 0.5, 0. , 0. ])

The returned array is in lexicographic order, so corresponds
to a :math:`50\%` chance of measuring either :math:`|00\rangle`
or :math:`|01\rangle`.

Args:
wires (Sequence[int] or int): the wire the operation acts on
"""
# pylint: disable=protected-access
op = qml.Identity(wires=wires, do_queue=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

Currently qml.Identity inherits from CVObservable, how come this works in the qubit case? Would have expected that there is a check somewhere that would throw an error

Copy link
Member Author

Choose a reason for hiding this comment

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

It looks the qnodes/base.py explicitly makes an exception for Identity:

        # True if op is a CV, False if it is a discrete variable (Identity could be either)
        are_cvs = [
            isinstance(op, CV) for op in self.queue + list(res) if not isinstance(op, qml.Identity)
        ]

op.return_type = Probability

if qml._current_context is not None:
# add observable to QNode observable queue
qml._current_context._append_op(op)

return op
4 changes: 4 additions & 0 deletions pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class ObservableReturnTypes(Enum):
Sample = 1
Variance = 2
Expectation = 3
Probability = 4


Sample = ObservableReturnTypes.Sample
Expand All @@ -157,6 +158,9 @@ class ObservableReturnTypes(Enum):
"""Enum: An enumeration which represents returning the expectation
value of an observable on specified wires."""

Probability = ObservableReturnTypes.Probability
"""Enum: An enumeration which represents returning probabilities
of all computational basis states."""

#=============================================================================
# Class property
Expand Down
13 changes: 9 additions & 4 deletions pennylane/plugins/default_qubit.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,11 +521,16 @@ def operations(self):
def observables(self):
return set(self._observable_map.keys())

def probability(self):
def probability(self, wires=None):
if self._state is None:
return None

states = itertools.product(range(2), repeat=self.num_wires)
probs = np.abs(self._state)**2
prob = np.abs(self._state.reshape([2] * self.num_wires)) ** 2

return OrderedDict(zip(states, probs))
wires = wires or range(self.num_wires)
wires = np.hstack(wires)
Copy link
Contributor

Choose a reason for hiding this comment

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

How come this line would be needed? One of the reasons could be flattening wires here, however, would it not be required to have wires already as a flat list or an array? (Also this performs the implicit conversion on range(self.num_wires))

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. I can't remember why I needed to add this line 🤔


basis_states = itertools.product(range(2), repeat=len(wires))
inactive_wires = list(set(range(self.num_wires)) - set(wires))
prob = np.apply_over_axes(np.sum, prob, inactive_wires).flatten()
return OrderedDict(zip(basis_states, prob))
8 changes: 7 additions & 1 deletion pennylane/qnodes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,14 +410,20 @@ def _check_circuit(self, res):

# check the return value
if isinstance(res, Observable):
self.output_dim = 1

if res.return_type is ObservableReturnTypes.Sample:
# Squeezing ensures that there is only one array of values returned
# when only a single-mode sample is requested
self.output_conversion = np.squeeze
elif res.return_type is ObservableReturnTypes.Probability:
self.output_conversion = np.squeeze
self.output_dim = 2**len(res.wires)
else:
self.output_conversion = float
self.output_dim = 1

res = (res,)

elif isinstance(res, Sequence) and res and all(isinstance(x, Observable) for x in res):
# for multiple observables values, any valid Python sequence of observables
# (i.e., lists, tuples, etc) are supported in the QNode return statement.
Expand Down
Loading