-
Notifications
You must be signed in to change notification settings - Fork 575
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
Add transform for quantum Monte Carlo #1316
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1316 +/- ##
=======================================
Coverage 98.17% 98.18%
=======================================
Files 155 156 +1
Lines 11631 11685 +54
=======================================
+ Hits 11419 11473 +54
Misses 212 212
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Beautiful addition. I'm suggesting minor improvements to the docstrings, otherwise looks good to go from my side, I couldn't see any issues
conventional multi-controlled-Z gate with an additional bit flip on each qubit before and after. | ||
|
||
This function performs the multi-controlled-Z gate via a multi-controlled-X gate by picking an | ||
arbitrary target wire to perform the X and adding a Hadamard on that wire either side of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this works for any target wire? I suppose, but just checking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the target wire can be any wire in wires
. Here is a sanity check:
import pennylane as qml
import numpy as np
wires = 3
dev = qml.device("default.qubit", wires=wires)
Z = np.zeros((2 ** wires, 2 ** wires))
np.fill_diagonal(Z, 1)
Z[-1, -1] = -1
target_wire = 0
control_wires = set(range(wires)) - set([target_wire])
@qml.qnode(dev)
def find_unitary(state):
qml.QubitStateVector(state, wires=range(wires))
qml.Hadamard(wires=target_wire)
qml.MultiControlledX(wires=target_wire, control_wires=control_wires)
qml.Hadamard(wires=target_wire)
qml.QubitUnitary(Z, wires=range(wires))
return qml.state()
I = np.eye(2 ** wires)
U = np.real_if_close(np.array([find_unitary(state) for state in I]).T)
assert np.allclose(U, I)
quantum phase estimation (see :class:`~.QuantumPhaseEstimation` for more details). | ||
|
||
Args: | ||
fn (Callable): a quantum function that applies quantum operations according to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any restrictions on this function? Can it have arbitrary inputs and outputs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can have any arguments and keyword arguments. The function itself should enact a quantum circuit, and it shouldn't return anything.
So far, we've been calling these things quantum functions, although I'm not sure how evolved or well-defined the concept is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'quantum function' was originally defined on the glossary as a function that applies quantum operations, and returns a measurement statistic. I think it has since been removed, but mention of it remains in the QNode entry:
A quantum node consists of a quantum function (such as a variational circuit), and a device on which it executes.
We've since started using the term a bit more loosely; the original definition of a quantum function was strictly
def qfunc(params):
quantum_operation()
quantum_operation()
quantum_operation()
return measurement_statistic()
but we have since broadened it (in usage, not strictly in definition) to also include
def qfunc(params):
quantum_operation()
quantum_operation()
quantum_operation()
This second usage is attractive because:
- It still tends to make sense to users; they still have a Python function that applies quantum operations
- It is more general than 'ansatz'; you often have a small quantum function that you use but wouldn't want to denote an 'ansatz'.
- With templates now being
Operation
objects, it tends to work quite nicely. In the above example,quantum_operation()
itself is a quantum function.
algorithm | ||
|
||
>>> qtape = qmc.qtape.expand(depth=1) | ||
>>> qtape.get_resources() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whaaat!?? 🤯 🤯 🤯 🤯
I had no idea we could do this. So cool!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes!! But don't advertise this yet as we don't want this to be user facing. Instead, we will be going with the following approaches:
In particular, the ability to do
>>> qml.specs(qmc)(*params) # get all specs
>>> qml.depth(qml)(*params) # get a specific spec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool! @josh146 do you recommend removing this part then? On the other hand, it's a shame to hide the gate count.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I would say - leave it for now, but we need to remember to update it when the preferred UI changes 🙂
@qml.qnode(dev) | ||
def unitary_z(basis_state): | ||
qml.BasisState(basis_state, wires=range(n_wires)) | ||
circ() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, what does circ()
do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
circ()
is the function passed into get_unitary()
. It'll be the usual "quantum function" style of applying some PL ops and not returning anything. Then get_unitary()
returns the unitary described by circ()
.
In practice in the tests below, circ()
will be, for example the qmc circuit or just the controlled Z.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it!
Got confused, I thought this was a python function; didn't realize it was an argument to get_unitary
A_wires = [0, "a", -1.1, -10, "bbb"] | ||
target_wire = "Ancilla" | ||
wires = A_wires + [target_wire] | ||
estimation_wires = ["bob", -3, 42, "penny", "lane"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤣 Maybe this will be the way I remember to test unconventional wire naming: give them fun names!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to get creative since I've used these names before!
Co-authored-by: ixfoduap <40441298+ixfoduap@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review @ixfoduap!
conventional multi-controlled-Z gate with an additional bit flip on each qubit before and after. | ||
|
||
This function performs the multi-controlled-Z gate via a multi-controlled-X gate by picking an | ||
arbitrary target wire to perform the X and adding a Hadamard on that wire either side of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the target wire can be any wire in wires
. Here is a sanity check:
import pennylane as qml
import numpy as np
wires = 3
dev = qml.device("default.qubit", wires=wires)
Z = np.zeros((2 ** wires, 2 ** wires))
np.fill_diagonal(Z, 1)
Z[-1, -1] = -1
target_wire = 0
control_wires = set(range(wires)) - set([target_wire])
@qml.qnode(dev)
def find_unitary(state):
qml.QubitStateVector(state, wires=range(wires))
qml.Hadamard(wires=target_wire)
qml.MultiControlledX(wires=target_wire, control_wires=control_wires)
qml.Hadamard(wires=target_wire)
qml.QubitUnitary(Z, wires=range(wires))
return qml.state()
I = np.eye(2 ** wires)
U = np.real_if_close(np.array([find_unitary(state) for state in I]).T)
assert np.allclose(U, I)
quantum phase estimation (see :class:`~.QuantumPhaseEstimation` for more details). | ||
|
||
Args: | ||
fn (Callable): a quantum function that applies quantum operations according to the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can have any arguments and keyword arguments. The function itself should enact a quantum circuit, and it shouldn't return anything.
So far, we've been calling these things quantum functions, although I'm not sure how evolved or well-defined the concept is.
if not wires.contains_wires(target_wire): | ||
raise ValueError("The target wire must be contained within wires") | ||
|
||
@wraps(fn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's defined here. The idea is to persist the name and docstring of the original function, rather than overwriting it with wrapper
.
For example:
from functools import wraps
def fn():
"""Docstring"""
...
def wrapper1():
"""Wrong Docstring"""
fn()
return
@wraps(fn)
def wrapper2():
"""Wrong Docstring"""
fn()
return
The output is:
>>> print("Name of wrapper1: ", wrapper1.__name__)
Name of wrapper1: wrapper1
>>> print("Docstring of wrapper1:", wrapper1.__doc__)
Docstring of wrapper1: Wrong Docstring
>>> print("Name of wrapper2: ", wrapper2.__name__)
Name of wrapper2: fn
>>> print("Docstring of wrapper2:", wrapper2.__doc__)
Docstring of wrapper2: Docstring
I'd never really used this before, so good chance for me to learn too!
@qml.qnode(dev) | ||
def unitary_z(basis_state): | ||
qml.BasisState(basis_state, wires=range(n_wires)) | ||
circ() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
circ()
is the function passed into get_unitary()
. It'll be the usual "quantum function" style of applying some PL ops and not returning anything. Then get_unitary()
returns the unitary described by circ()
.
In practice in the tests below, circ()
will be, for example the qmc circuit or just the controlled Z.
A_wires = [0, "a", -1.1, -10, "bbb"] | ||
target_wire = "Ancilla" | ||
wires = A_wires + [target_wire] | ||
estimation_wires = ["bob", -3, 42, "penny", "lane"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to get creative since I've used these names before!
@qml.qnode(dev) | ||
def unitary_z(basis_state): | ||
qml.BasisState(basis_state, wires=range(n_wires)) | ||
circ() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it!
Got confused, I thought this was a python function; didn't realize it was an argument to get_unitary
Context:
Adds a quantum Monte Carlo transform. Given an input circuit, the output will be a transformed circuit to do the full QMC algorithm.
Description of the Change:
Adds the transform to the
qml.transforms.qmc
module, along with supporting functionality.Benefits:
This transform can be used for resource estimation of the number of gates required to perform quantum Monte Carlo. It can also, in principle, be compatible with a hardware implementation (if a suitable low-noise, high-depth device were available).
Possible Drawbacks:
Potential confusion with the existing QuantumMonteCarlo template, which does the unitary-based version (not compatible with hardware or resource estimation).
The example uses
pennylane.templates.state_preparations.mottonen._uniform_rotation_dagger
for the R matrix. Should we consider pulling that out and giving a nicer name? Can we use an alternative in the example?