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

Conversation

josh146
Copy link
Member

@josh146 josh146 commented Nov 21, 2019

Context:

Currently, due to the 'expectation of observable' abstraction we use, the only way to return the probabilities from a QNode is to use a single QNode per computational basis state, and use return qml.expval(qml.Hermitian(|x><x|, wires=All)). This can be computationally expensive, especially if the size of each projector is huge.

However, probability is already computed via a single QNode evaluation (analytically on simulators and estimated on hardware), so there is no reason not to provide a way to return it from QNodes. Furthermore, since it is simply an array of expectation values of projectors, it supports analytic differentiation. That said, it does necessitate a slightly different UI, since it doesn't correspond to a single expectation of a single observable (as we currently require).

Example:

import numpy as np
import pennylane as qml

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

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

print("Marginal probability of wire 1:", circuit(0.2))
print("Analytic jacobian:", circuit.jacobian([0.2], method='A').flatten())
print("Numeric jacobian:", circuit.jacobian([0.2], method='F').flatten())

Output:

Marginal probability of wire 1: [0.40066533 0.59933467]
Analytic jacobian: [-0.49003329  0.49003329]
Numeric jacobian: [-0.49003329  0.49003328]

Description of the Change:

  • Adds the qml.probs() measurement function (from a UI perspective, roughly equivalent to the expval, sample, var measurement functions).

  • Unlike the other measurement functions, it does not accept an observable. Instead, probs() accepts a required argument wires, determining the marginal probabilities to return. prob(wires=range(num_wires)) will return the full joint probability distribution.

  • Unlike the other measurement functions, which return a scalar, probs(wires=range(num_wires)) will return an array of size (2**num_wires,)

  • Adds a new observable return type enum to qml.operation, Probability.

  • If an observable with obs.return_type=Probability is present, the QNode has a new logic flow:

    • self.output_conversion = np.squeeze (to avoid superfluous nesting)
    • self.output_dim = 2**len(obs.wires)
    • Device.execute() returns the result of self.probability(wires=wires).
  • The ability to return marginal probabilities has been added to DefaultQubit.probability().

Benefits:

  • Now possible to return and differentiate the probabilities of a quantum state, with very little modification.

  • UI should apply equally well to the CV case, with the addition of a cutoff argument.

Possible Drawbacks:

  • Ordering of the computational basis states is implicit (attempting to label the states would break autodiff). This is instead explained in the documentation.

Related GitHub Issues: n/a

@josh146 josh146 added enhancement ✨ New feature or request WIP 🚧 Work-in-progress labels Nov 21, 2019
@josh146 josh146 changed the title [WIP] Beta: adds a convenience prob() measurement function to return the probabilities of all computational basis states in a single QNode evaluation [WIP] adds a convenience prob() measurement function to return the probabilities of all computational basis states in a single QNode evaluation Nov 21, 2019
@codecov
Copy link

codecov bot commented Nov 21, 2019

Codecov Report

❗ No coverage uploaded for pull request base (master@6642817). Click here to learn what that means.
The diff coverage is 100%.

@@            Coverage Diff            @@
##             master     #432   +/-   ##
=========================================
  Coverage          ?   99.29%           
=========================================
  Files             ?       45           
  Lines             ?     3426           
  Branches          ?        0           
=========================================
  Hits              ?     3402           
  Misses            ?       24           
  Partials          ?        0
Impacted Files Coverage Δ
pennylane/plugins/default_qubit.py 99.45% <100%> (ø)
pennylane/_device.py 100% <100%> (ø)
pennylane/qnodes/base.py 100% <100%> (ø)
pennylane/measure.py 100% <100%> (ø)
pennylane/operation.py 99.62% <100%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6642817...df134b4. Read the comment docs.

@josh146 josh146 added review-ready 👌 PRs which are ready for review by someone from the core team. and removed WIP 🚧 Work-in-progress labels Nov 21, 2019
@josh146 josh146 changed the title [WIP] adds a convenience prob() measurement function to return the probabilities of all computational basis states in a single QNode evaluation Adds a convenience prob() measurement function to return the probabilities of all computational basis states in a single QNode evaluation Nov 21, 2019

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 🤔


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)]``.

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)
        ]

Comment on lines +27 to +36
@pytest.fixture
def init_state(scope="session"):
"""Fixture that creates an initial state"""
def _init_state(n):
"""An initial state over n wires"""
state = np.random.random([2 ** n]) + np.random.random([2 ** n]) * 1j
state /= np.linalg.norm(state)
return state

return _init_state
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it would be nice to have this in the conftest.py.

Copy link
Member Author

Choose a reason for hiding this comment

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

True, that's a good point. I'm a bit hesitant to put it in the conftest.py file right away, as the conftest file is a bit... messy, and needs cleaning up (there are two many fixtures which aren't used or aren't needed).

Copy link
Contributor

@antalszava antalszava left a comment

Choose a reason for hiding this comment

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

Looks really nice @josh146 ! Had a couple of minor comments, most we have already discussed

@josh146 josh146 merged commit 5dbbdcc into master Dec 24, 2019
@josh146 josh146 deleted the prob_fn branch December 24, 2019 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement ✨ New feature or request review-ready 👌 PRs which are ready for review by someone from the core team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants