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

Inconsistencies in using ControlledGate(s) and ControlledOperation(s) #4172

Closed
tanujkhattar opened this issue Jun 8, 2021 · 2 comments · Fixed by #4167
Closed

Inconsistencies in using ControlledGate(s) and ControlledOperation(s) #4172

tanujkhattar opened this issue Jun 8, 2021 · 2 comments · Fixed by #4167
Labels
area/gates kind/design-issue A conversation around design needs agreed design We want to do this, but it needs an agreed upon design before implementation triage/accepted there is consensus amongst maintainers that this is a real bug or a reasonable feature to add

Comments

@tanujkhattar
Copy link
Collaborator

tanujkhattar commented Jun 8, 2021

We currently have ControlledGate and ControlledOperation as generic types which are useful to represent controlled versions of an underlying sub_type. For many common gates, we also have explicit types defined for controlled versions of those gates (eg: X, CX, CCX etc.).

The general pattern to get a controlled_gate from a gate is to call gate.controlled() and to get a controlled_operation from an operation, one should call operation.controlled_by().

There are a few inconsistencies in user experience while dealing with these types, which should be fixed. Some examples are as follows:

Equality of ControlledOperations with explicitly defined controlled types as subtype

q = cirq.LineQubit.range(5)
op = cirq.X(q[0]).controlled_by(*q[1:4]) # Way-1: gate.on().controlled_by()
gate = cirq.X.controlled(4) # Way-2: gate.controlled().on()
op1 = gate(*q[1:5], q[0]) # op1 & op2 should be same, but are not.
op2 = gate(*q[5:0:-1], q[0])
print(repr(gate), repr(op), sep = '\n')
print(op1, op2, op1 == op2)

[output]: 
cirq.ControlledGate(sub_gate=cirq.TOFFOLI, num_controls=2)
cirq.ControlledOperation(sub_operation=cirq.X(cirq.LineQubit(0)),control_values=((1,), (1,), (1,)),controls=(cirq.LineQubit(1), cirq.LineQubit(2), cirq.LineQubit(3)))
CCTOFFOLI(1, 2, 3, 4, 0) CCTOFFOLI(4, 3, 2, 1, 0) False

Issue: Both op1 and op2 are same operations (control on qubits 1, 2, 3, 4 and target on 0), but the equality tests false because their subtypes & controls are different (TOFFOLI(3, 4, 0) vs TOFFOLI(2, 1, 0)).

Proposed Solution: If num_controls > 2, cirq.X.controlled() should return a cirq.ControlledGate(sub_gate = 'X', num_controls = num_controls) instead of first converting to a TOFFOLI. Similar changes to should be done for other gates like Z, CZ, CX etc.

gate.controlled().on() and gate.on().controlled_by() should return the same operations.

q = cirq.LineQubit.range(3)
op1 = cirq.X.controlled(2).on(*q[1:3], q[0]) # Way-1: gate.controlled().on()
op2 = cirq.X(q[0]).controlled_by(*q[1:3]) # Way-2: gate.on().controlled_by()
print(repr(op1), repr(op2), op1 == op2, sep = '\n')

[output]: 
cirq.TOFFOLI(cirq.LineQubit(1), cirq.LineQubit(2), cirq.LineQubit(0))
cirq.ControlledOperation(sub_operation=cirq.X(cirq.LineQubit(0)),control_values=((1,), (1,)),controls=(cirq.LineQubit(1), cirq.LineQubit(2)))
False

Issue: Both op1 and op2 are the same operations, but their types are different. We have custom overrides for gate.controlled for many common gates to return specialized versions of the controlled gate, but these overrides are ignored when we call controlled_by from the corresponding operation.

Proposed Solution: GateOperation should override the controlled_by function to first construct the controlled_gate via gate.controlled() and then apply this controlled_gate on qubits to get a controlled_operation.

Controlled Gates / Ops which have specific types defined should always use/return them by default

It would be nice if can make it harder for instances like cirq.ControlledGate(sub_gate = cirq.X, num_controls = 1) to exist, and always have cirq.CNOT in it's place. Maybe by constructing the controlled gates / ops via a factory that always forwards the requests to gate.controlled / op.controlled_by ?

Update: Part of #3242

@tanujkhattar tanujkhattar added kind/design-issue A conversation around design triage/discuss Needs decision / discussion, bring these up during Cirq Cynque labels Jun 8, 2021
@balopat
Copy link
Contributor

balopat commented Jun 9, 2021

Discussed on Cirq Cynque:

  • the issues are definitely valid - in fact we should add tests to the standard gate consistency tests that ensure that
    • ordering of the qubits doesn't matter --- e.g. TOFFOLI(a,b,c).controlled_by(d) == TOFFOLI(d,b,c).controlled_by(a)
    • controlled operations created from gates or operations should end up equivalent, meaning TODO (types? python equality?)
  • opinions/ideas:
    • cirq.ControlledGate() should be allowed to create "weird" / "non-canonical" gates
    • maybe we should change the definition of equality on these types somehow?

@balopat balopat added needs agreed design We want to do this, but it needs an agreed upon design before implementation triage/accepted there is consensus amongst maintainers that this is a real bug or a reasonable feature to add and removed triage/discuss Needs decision / discussion, bring these up during Cirq Cynque labels Jun 9, 2021
@cduck
Copy link
Collaborator

cduck commented Jul 9, 2021

I have a related point that may or may not make sense to be part of #4167.

I consider GlobalPhase to be part of the C*Z family and would expect cirq.GlobalPhaseOperation(exp(1j*pi*ht).controlled_by(q) to be equal to cirq.Z(q)**ht. Should GlobalPhaseOperation also be special-cased?

op = cirq.GlobalPhaseOperation(-1).controlled_by(cirq.LineQubit(0))
assert np.allclose(cirq.unitary(op), cirq.unitary(cirq.Z))
print(op)  # C(0, -1)
print(type(op))  # cirq.ControlledOperation

CirqBot pushed a commit that referenced this issue Oct 1, 2021
…ions (#4167)

This PR tries to resolve the inconsistencies mentioned in #4172. Specifically, 

- Equality b/w controlled `TOFFOLI`s with different qubit ordering on controls, i.e. `TOFFOLI(a,b,c).controlled_by(d)  == TOFFOLI(d,b,c).controlled_by(a)`  (and other 3Q controlled gates like CCZ, CSWAP). To achieve this, we override the `controlled` method on `CCX`, `CCZ` etc. to return a `ControlledGate` with `sub_gate = X` in case of `CCX` s.t. `TOFFOLI(a,b,c).controlled_by(d) == CCCX(a, b, d, c) == TOFFOLI(d,b,c).controlled_by(a)` instead of `CTOFFOLI`
- `gate_operation.controlled_by` now forwards the request to `gate.controlled` to first create a controlled gate and then apply it on qubits to create a `ControlledOperation`. This solves the original use case of adding specialized controls, requested in #2142, i.e. `cirq.Z(q0).controlled_by(q1) == cirq.CZ(q1, q0)`. 
- Fixes #4515


Note, this is a breaking change because
- `gate_operation.controlled_by()` can now return a `cirq.GateOperation` instead of `cirq.ControlledOperation` in cases where the underlying gates have specialized `gate.controlled()` implementations. 
  - This also leads to a change in diagram of the controlled gates with specialized controlled implementations. For eg: Controlled S gate is now plotted as `CZPowGate` (`@ --- @ ** 0.5`) instead of `ControlledOperation` with Z ** 0.5 as subgate(`@ ---- S`)
- `op.controlled_by` for 3Q gates like `CCX`, `CCZ`,  `CSWAP` will now return `ControlledOperation` with `sub_operation = <underlying non-controlled gate>`. Eg: `CCCX` (i.e. `sub_gate = X`) instead of `CTOFFOLI` (i.e. `sub_gate = TOFFOLI`) etc.
- Diagrams for `ControlledOperations` will now always have the exponent drawn on the target qubit (in case of multi qubit `sub_operation`, the exponent will always be on the first qubit if not the underlying gate does not explicitly specify a target index).
rht pushed a commit to rht/Cirq that referenced this issue May 1, 2023
…ions (quantumlib#4167)

This PR tries to resolve the inconsistencies mentioned in quantumlib#4172. Specifically, 

- Equality b/w controlled `TOFFOLI`s with different qubit ordering on controls, i.e. `TOFFOLI(a,b,c).controlled_by(d)  == TOFFOLI(d,b,c).controlled_by(a)`  (and other 3Q controlled gates like CCZ, CSWAP). To achieve this, we override the `controlled` method on `CCX`, `CCZ` etc. to return a `ControlledGate` with `sub_gate = X` in case of `CCX` s.t. `TOFFOLI(a,b,c).controlled_by(d) == CCCX(a, b, d, c) == TOFFOLI(d,b,c).controlled_by(a)` instead of `CTOFFOLI`
- `gate_operation.controlled_by` now forwards the request to `gate.controlled` to first create a controlled gate and then apply it on qubits to create a `ControlledOperation`. This solves the original use case of adding specialized controls, requested in quantumlib#2142, i.e. `cirq.Z(q0).controlled_by(q1) == cirq.CZ(q1, q0)`. 
- Fixes quantumlib#4515


Note, this is a breaking change because
- `gate_operation.controlled_by()` can now return a `cirq.GateOperation` instead of `cirq.ControlledOperation` in cases where the underlying gates have specialized `gate.controlled()` implementations. 
  - This also leads to a change in diagram of the controlled gates with specialized controlled implementations. For eg: Controlled S gate is now plotted as `CZPowGate` (`@ --- @ ** 0.5`) instead of `ControlledOperation` with Z ** 0.5 as subgate(`@ ---- S`)
- `op.controlled_by` for 3Q gates like `CCX`, `CCZ`,  `CSWAP` will now return `ControlledOperation` with `sub_operation = <underlying non-controlled gate>`. Eg: `CCCX` (i.e. `sub_gate = X`) instead of `CTOFFOLI` (i.e. `sub_gate = TOFFOLI`) etc.
- Diagrams for `ControlledOperations` will now always have the exponent drawn on the target qubit (in case of multi qubit `sub_operation`, the exponent will always be on the first qubit if not the underlying gate does not explicitly specify a target index).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/gates kind/design-issue A conversation around design needs agreed design We want to do this, but it needs an agreed upon design before implementation triage/accepted there is consensus amongst maintainers that this is a real bug or a reasonable feature to add
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants