-
Notifications
You must be signed in to change notification settings - Fork 603
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
Implement transform to push commuting operations through controlled gates #1464
Implement transform to push commuting operations through controlled gates #1464
Conversation
Hello. You may have forgotten to update the changelog!
|
pennylane/operation.py
Outdated
""" | ||
|
||
@property | ||
def comp_control_wires(self): |
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 originally named this property control_wires
, however this caused issues with variables of the same name used in operations like MultiControlledX
. I prefixed with comp_
to indicate compilation, but we should figure out a better way of incorporating this information for all gates (e.g., inheritance from a class similar to ControlledOperation
)
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.
Hmm, I would suggest renaming MultiControlledX.control_wires
-> MultiControlledX._control_wires
.
Then, you could have this property do two things:
-
By default,
Operation.control_wires
could simply returnself._control_wires
if it exists, otherwise raisingNotImplementedError
-
Inheriting devices can defined this class property directly.
Codecov Report
@@ Coverage Diff @@
## master #1464 +/- ##
========================================
Coverage 98.29% 98.30%
========================================
Files 169 170 +1
Lines 12360 12483 +123
========================================
+ Hits 12149 12271 +122
- Misses 211 212 +1
Continue to review full report at Codecov.
|
@property | ||
def comp_control_wires(self): | ||
return Wires(self.wires[0]) | ||
|
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.
Not sure how to handle testing for this one, it seems redundant to do it for every gate.
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.
Agree 🤔 What you could do is add # pragma: no cover
to this property, to indicate it is intentionally not covered?
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've added the # pragma
(L595 of operation.py
) but the warning still occurs. Was it added incorrectly?
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 okay to me 🤔
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.
Huh :( Well... since it is not causing any issues with the CI and doesn't require an override, I am going to just merge 👍
if op.wires == qml.wires.Wires(wire_order): | ||
mat = qml.math.dot(op.matrix, mat) | ||
else: | ||
mat = qml.math.dot(qml.SWAP(wires=[0, 1]).matrix, qml.math.dot(op.matrix, mat)) |
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 should have caught this when I wrote it originally. Emphasizes the need for that get_unitary_matrix
transform.
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 @glassnotes! I left some suggestions and thoughts regarding the structure. Looking forward to having all these transforms in!
pennylane/operation.py
Outdated
""" | ||
|
||
@property | ||
def comp_control_wires(self): |
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.
Hmm, I would suggest renaming MultiControlledX.control_wires
-> MultiControlledX._control_wires
.
Then, you could have this property do two things:
-
By default,
Operation.control_wires
could simply returnself._control_wires
if it exists, otherwise raisingNotImplementedError
-
Inheriting devices can defined this class property directly.
@property | ||
def comp_control_wires(self): | ||
return Wires(self.wires[0]) | ||
|
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.
Agree 🤔 What you could do is add # pragma: no cover
to this property, to indicate it is intentionally not covered?
.github/CHANGELOG.md
Outdated
single-qubit gates ahead of controlled operations. | ||
[(#1464)](https://github.com/PennyLaneAI/pennylane/pull/1464) | ||
|
||
The `commute_through_controls_targets` transform works as follows: |
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.
Relatively minor, but is there more succinct name we could use for this transform? I tend to dislike the verbosity/typing of functions with more than 2 or so words!
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.
commute_controlled
?
# Gates that commute among each particular basis | ||
commuting_gates = { | ||
"X": ["PauliX", "RX", "SX"], | ||
"Y": ["PauliY", "RY"], | ||
"Z": ["PauliZ", "RZ", "PhaseShift", "S", "T"], | ||
} |
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.
Out of curiosity: could we not have a basis
property on gates instead? e.g.:
class RX(Operation):
basis = "X"
...
class CRX(Operation):
basis = "X"
...
This could replace the dictionary here as well as the target_basis
property?
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.
Alternatively, you could instead define the controlled target basis as a dictionary of gates here?
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.
Not really suggesting one over the other, I think both approaches have their place. Just gathering my thoughts: I guess I'm trying to determine why commuting_gates
is transform-specific data, while target_basis
is a Operation-specific data.
Is commuting_gates
explicitly linked to the compilation transform?
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.
Awesome, I like the idea of specifying a basis; then we can just check that they match, and there is no need to have any of this info here. Nothing is compilation-specific, the gates either commute or don't, it's just we aren't right now making use of that information anywhere else.
Args: | ||
qfunc (function): A quantum function. |
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 can't recall if other compilation transforms have this, but does it make sense to have
Returns:
function: the transformed quantum function
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.
They don't, but I think that's a good idea. Will update!
with ``CRY``). We can use the transform to push single-qubit gates as | ||
far as possible through the controlled operations: |
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.
Question: could we have
@commute_through_controls_target(direction="left"|"right")
in case you want to move the single-qubit operations in a particular direction? :D
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 love this!! After I wrote the transform I was still debating whether I should not rewrite it the other way (in fact, the precursors to it worked by pushing the controlled operations to the right and thus the single-qubit gates to the left, but didn't work nearly as well).
Also, this implicitly solves the long name problem 😁
# We will go through the list backwards; whenever we find a single-qubit | ||
# gate, we will extract it and push it through 2-qubit gates as far as | ||
# possible to the right. |
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.
Your comments are always super helpful to understand the logical flow!
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.
💯
# Find the next gate that contains an overlapping wire | ||
next_gate_idx = find_next_gate(current_gate.wires, list_copy[current_location + 1 :]) | ||
|
||
where_should_we_put_it = current_location |
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.
lol
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.
Perhaps new_location
?
except KeyError: | ||
break |
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.
When does this happen? Shouldn't this be AttributeError
? 🤔
if op.wires == qml.wires.Wires(wire_order): | ||
mat = qml.math.dot(op.matrix, mat) | ||
else: | ||
mat = qml.math.dot(qml.SWAP(wires=[0, 1]).matrix, qml.math.dot(op.matrix, mat)) |
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.
👍
compare_operation_lists, | ||
compute_matrix_from_ops_two_qubit, | ||
check_matrix_equivalence, |
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.
Really useful to see these functions being used that I'd otherwise remain unaware of!
def test_commute_through_controls_targets_autograd(self): | ||
"""Test QNode and gradient in autograd interface.""" | ||
|
||
original_qnode = qml.QNode(qfunc, dev) |
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.
might be worth explicitly passing interface="autograd"
in case the default interface changes in future
|
||
Diagonal gates on either side of control qubits do not affect the outcome | ||
of controlled gates; thus we can push all the single-qubit gates on the | ||
first qubit together on the right (and fuse them if desired). Similarly, X |
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 there any plan to combine this and fusion in a single function?
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.
Not at the moment - for now, trying to keep the transforms as modular as possible so they can be chained in different ways, but this might change in the future.
# We will go through the list backwards; whenever we find a single-qubit | ||
# gate, we will extract it and push it through 2-qubit gates as far as | ||
# possible to the right. |
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.
💯
return op_list | ||
|
||
|
||
def _commute_controlled_left(op_list): |
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.
Since this is so close to the _right
version, I have not added a full set of comments in order to keep things more streamlined. I can add them back if preferred though 🙂
@josh146 @anthayes92 thank you both for the feedback, updated version is cleaner and more flexible now 😁 |
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.
💯 Happy to approve this!
Only blocker it looks like there are a few places where the left function is not being tested?
# Only go ahead if information is available | ||
if next_gate.basis is None: | ||
break |
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 guess this never happens in the test suite?
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.
Coverage has been added for this and the instances in _commute_controlled_left
👍
Context: Addition of compilation transforms to PennyLane.
Description of the Change: Adds a new transform,
commute_controlled
that pushes commuting gates through controls/targets of controlled operations. All diagonal gates are pushed ahead (or behind, if specified) of control qubits, andX
/Y
/Z
-type gates are pushed through targets when appropriate. Additional compilation attributes and properties are also added to support the transform.Benefits: Moving as many gates as possible through controlled operations will enable more single-qubit gate simplifications to be performed, as well as fusion and cancellation of two-qubit controlled rotations once those in-between gates are out of the way.
Possible Drawbacks: Rather than adding attributes, it would be nice if we could instead resolve #1447 and have things inherit from
ControlledOperation
. However after a preliminary look, it seems that would be a pretty extensive refactor.Related GitHub Issues: Uses some of the code and ideas from #1446 🎉