-
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
[OpRefactor] Make decomposition
a method of the main Operator
#1873
[OpRefactor] Make decomposition
a method of the main Operator
#1873
Conversation
qml.OrbitalRotation.decomposition(weights[layer][idx][1], wires=qwires[idx]) | ||
|
||
with qml.tape.stop_recording(): | ||
op1 = qml.OrbitalRotation(include_pi_param, wires=qwires[idx]) |
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.
This is super ugly. Is there any way around this?
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.
I fear this is a very typical example for the weaknesses of the queuing system, and it does not only appear here, but is a general pattern! Every time we create an operation to call its method which creates other operations (i.e. to simplify an already created hamiltonian) this happens.
But the alternative, to keep all methods static, is not a scalable option I fear.
I hope we can solve this elsewhere.
Codecov Report
@@ Coverage Diff @@
## master #1873 +/- ##
=======================================
Coverage 98.98% 98.98%
=======================================
Files 219 219
Lines 16509 16513 +4
=======================================
+ Hits 16341 16345 +4
Misses 168 168
Continue to review full report at Codecov.
|
decomposition
a method of the main Operator
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.
Fantastic, thanks for tackling this.
Does it feel right? I think it is much better to use the internal information in decomposition
instead of arguments, and the only disadvantage is a structural problem of queuing, so I am inclined to ignore it for now and rather make a better queueing system.
pennylane/_qubit_device.py
Outdated
@@ -920,7 +920,7 @@ def adjoint_jacobian(self, tape, starting_state=None, use_device_state=False): | |||
for op in reversed(tape.operations): | |||
if op.num_params > 1: | |||
if isinstance(op, qml.Rot) and not op.inverse: | |||
ops = op.decomposition(*op.parameters, wires=op.wires) | |||
ops = op.decomposition() |
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.
I like this a lot!
pennylane/ops/qubit/matrix_ops.py
Outdated
def decomposition(D, wires): | ||
return [QubitUnitary(qml.math.diag(D), wires=wires)] | ||
def decomposition(self): | ||
return [QubitUnitary(qml.math.diag(self.parameters[0]), wires=self.wires)] |
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.
I know, this pattern that the developer needs to know that parameters[0]
is D
is not optimal, but I can't think of a better option.
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.
On the other hand, if PennyLane feeds op.parameters
into decomposition
then PennyLane makes the assumption that D
is parameters[0]
, which is probably a lot less safe!
qml.OrbitalRotation.decomposition(weights[layer][idx][1], wires=qwires[idx]) | ||
|
||
with qml.tape.stop_recording(): | ||
op1 = qml.OrbitalRotation(include_pi_param, wires=qwires[idx]) |
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.
I fear this is a very typical example for the weaknesses of the queuing system, and it does not only appear here, but is a general pattern! Every time we create an operation to call its method which creates other operations (i.e. to simplify an already created hamiltonian) this happens.
But the alternative, to keep all methods static, is not a scalable option I fear.
I hope we can solve this elsewhere.
I must admit, I can't stop thinking about this 🤔
Would it make sense to keep E.g., class MyOp(Operation):
def __init__(self, H, x, t, wires):
super().__init__(H.coeffs, x, t, wires)
self.hamiltonian = H
@Operation.decomposition()
def decomposition(self, H, x, t, wires):
return [qml.ApproxTimeEvolution(H, t, wires), qml.RX(x, wires[0])] where So you could then do either: >>> MyOp.decomposition(H, x, t, wires=[0, 1]) or >>> MyOp(H, x, t, wires=[0, 1]).decomposition() The ambiguity is also removed, since neither the user nor the base class makes any 'assumptions' about the signature of |
Co-authored-by: Maria Schuld <mariaschuld@gmail.com>
doc/releases/changelog-dev.md
Outdated
with qml.tape.stop_recording(): | ||
op = qml.CRX(x, wires=[0, 1]) | ||
op.decomposition() | ||
``` |
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.
Is this still valid, since it's only relevant for tests and there one can do _decomposition
?
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.
Perfect!
It may feel weird for now, but I up once we have all bits tidied up in a systematic manner the benefit will become obvious!
Oh, only consider my suggestion for the changelog, I think that would need to be updated? |
My bad, forgot to update that! Thanks :) |
pennylane/operation.py
Outdated
if self.num_params == 0: | ||
return self._decomposition(wires=self.wires) | ||
return self._decomposition(*self.parameters, wires=self.wires) |
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.
I have one worry here, but I'm not sure of the solution. Given that we are planning on making it so that only trainable parameters are stored in self.parameters
, and devs should instead store non-trainable parameters as static attributes... how will we pass non-trainable parameters here?
@mariaschuld, any ideas? I am drawing blanks.
For example, consider the following:
class MyOp(Operation):
def __init__(self, H, t, boolean_arg=True, wires=None):
# store trainable parameters
super().__init__(H.coeffs, t, wires=None)
# after calling __init__, trainable parameters
# are now stored as self.parameters
assert self.parameters == H.coeffs, t
# store non-trainable parameters as an attribute
self.boolean_arg = boolean_arg
self.H = H
@staticmethod
def _decomposition(H, t, boolean_arg=True, wires=None):
# the parent class will attempt to pass H.coeffs and t,
# not H and boolean_arg!
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.
The only solution I can think of, is something along the lines of what PyTorch does (https://pytorch.org/docs/stable/notes/extending.html):
# Inherit from Function
class LinearFunction(Function):
# Note that both forward and backward are @staticmethods
@staticmethod
# bias is an optional argument
def forward(ctx, input, weight, bias=None):
ctx.save_for_backward(input, weight, bias)
output = input.mm(weight.t())
if bias is not None:
output += bias.unsqueeze(0).expand_as(output)
return output
# This function has only a single output, so it gets only one gradient
@staticmethod
def backward(ctx, grad_output):
# This is a pattern that is very convenient - at the top of backward
# unpack saved_tensors and initialize all gradients w.r.t. inputs to
# None. Thanks to the fact that additional trailing Nones are
# ignored, the return statement is simple even when the function has
# optional inputs.
input, weight, bias = ctx.saved_tensors
grad_input = grad_weight = grad_bias = None
# These needs_input_grad checks are optional and there only to
# improve efficiency. If you want to make your code simpler, you can
# skip them. Returning gradients for inputs that don't require it is
# not an error.
if ctx.needs_input_grad[0]:
grad_input = grad_output.mm(weight)
if ctx.needs_input_grad[1]:
grad_weight = grad_output.t().mm(input)
if bias is not None and ctx.needs_input_grad[2]:
grad_bias = grad_output.sum(0)
return grad_input, grad_weight, grad_bias
When defining custom PyTorch operations, you create a class with static methods.
However, the first argument of each static method is always ctx
. This is a 'context' object that
- provides helper methods, and
- allows the developers to store anything
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.
If we take a similar approach ourselves, the operator class could look like this:
class MyOp(Operation):
@staticmethod
def create(ctx, H, t, boolean_arg=True, wires=None):
# store trainable parameters
ctx.save_parameters(H.coeffs, t)
# store non-trainable parameters as an attribute
self.ctx.boolean_arg = boolean_arg
self.ctx.H = H
@staticmethod
def _decomposition(*parameters, wires=None, ctx=None):
H.coeffs, t = parameters
H = ctx.Hamiltonian
boolean_arg = ctx.boolean_arg
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.
Forgot to pick this when I left my review :(
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.
Looks good @glassnotes!
@@ -13,13 +13,27 @@ | |||
|
|||
<h3>Improvements</h3> | |||
|
|||
* A `decompose()` method has been added to the `Operator` class such that we can | |||
obtain (and queue) decompositions directly from instances of operations. | |||
[(#1873)](https://github.com/PennyLaneAI/pennylane/pull/1873) |
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.
🚀
* ``qml.circuit_drawer.draw_mpl`` produces a matplotlib figure and axes given a tape. | ||
[(#1787)](https://github.com/PennyLaneAI/pennylane/pull/1787) | ||
|
||
* AngleEmbedding now supports `batch_params` decorator. [(#1812)](https://github.com/PennyLaneAI/pennylane/pull/1812) | ||
|
||
<h3>Breaking changes</h3> | ||
|
||
* The static method `decomposition()`, formerly in the `Operation` class, has | ||
been moved to the base `Operator` class. | ||
[(#1873)](https://github.com/PennyLaneAI/pennylane/pull/1873) |
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.
🚀
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.
Nice and simple!
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.
I turned "approve" to "request changes" due to a minor codecov issue. I think since we now test decomposition
instead of decompose
there is no test where an int is fed as a wire. Maybe a test like this should help:
def test_decomposition_with_int_wire()
op1 = qml.Hadamard.decomposition(1)
op2 = qml.Hadamard.decomposition([1])
assert op1 == op2
Co-authored-by: Maria Schuld <mariaschuld@gmail.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.
Approval for if CI is happy now :)
Context: Operator overhaul.
Description of the Change: The static
decomposition
has been moved to theOperator
class, and a new methoddecompose
has been added.Benefits: Decompositions can now be obtained from specific instances of operations, and can use their stored parameters and wires. The move to
Operator
will allow the decomposition to be used as an arithmetic operation when theOperator
class is overhauled.Possible Drawbacks: Josh has found some issues with the
decompose
method in the cases where some of the non-trainable parameters are required for constructing the decomposition.Related GitHub Issues: N/A