Improve single-qubit gate count in TwoQubitControlledUDecomposer#16123
Improve single-qubit gate count in TwoQubitControlledUDecomposer#16123ShellyGarion wants to merge 24 commits intoQiskit:mainfrom
TwoQubitControlledUDecomposer#16123Conversation
Coverage Report for CI Build 25538423326Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.06%) to 87.627%Details
Uncovered Changes
Coverage Regressions761 previously-covered lines in 17 files lost coverage.
Coverage Stats
💛 - Coveralls |
|
One or more of the following people are relevant to this code:
|
TwoQubitControlledUDecomposerTwoQubitControlledUDecomposer
mtreinish
left a comment
There was a problem hiding this comment.
Overall this looks good, thanks for fixing this. I just had a few comments inline.
mtreinish
left a comment
There was a problem hiding this comment.
Thanks for the updates, I just had a few more small comments inline.
| .collect::<SmallVec<_>>(); | ||
| (inv_gate.0.into(), inv_gate_params) | ||
| } | ||
| type TwoQubitGateType = (PackedOperation, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>); |
There was a problem hiding this comment.
The 3rd type in this tuple, the SmallVec<[u8; 2]> is representing qubits right (I'm assuming either [0, 1] or [1, 0]? Is there ever a case where it could be two indices? I'm wondering if we should make this [u8; 2] (no small vec), or just a bool. I'm thinking maybe making this a struct like:
| type TwoQubitGateType = (PackedOperation, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>); | |
| struct TwoQubitGateType { | |
| op: PackedOperation, | |
| params: SmallVec<[f64; 3]>, | |
| reversed_qubits: bool, | |
| } |
might be better because then we have named access to make it clearer what the params are.
There was a problem hiding this comment.
actually we only use qubits [0, 1] here, so we don't need any parameter.
I updated the code in 11166c1
| let (inv_gate_name, inv_gate_params) = invert_1q_gate(gate); | ||
| gates.push((inv_gate_name, inv_gate_params, smallvec![1])); | ||
| if !k2l.try_inverse_mut() { | ||
| panic!("matrix k2l is not invertible"); |
There was a problem hiding this comment.
I was on the fence between asking to use unreachable!() here or not. But I think I agree panic!() is right here. But we should add a little more detail to message, because if we execute this panic this means the underlying error is actually in the TwoQubitWeylDecomposition because it somehow returned a non-unitary matrix for the 1q component which is an internal error in the weyl decomposition.
|
|
||
| if !is_inv_rxx { | ||
| // 1-qubit gates before the rxx_op, on qubits 0 and 1 respectively | ||
| if !k2r.try_inverse_mut() { |
There was a problem hiding this comment.
Does something like:
| if !k2r.try_inverse_mut() { | |
| if !k_mats[2].try_inverse_mut() { |
work here? The reason is we end up building two arrays of Matrix2 right now and if we do it in place on the existing k_mats we avoid creating a second copy of the array of matrices.
There was a problem hiding this comment.
note that we don't only invert the matrices, but also change their order.
namely, if is_inv_rxx=true then k_mats = [k1r, k1l, k2r, k2l],
and if is_inv_rxx=false then k_mats = [k2r_inv, k2l_inv, k1r_inv, k1l_inv],
so I think I need 2 different arrays in this case.
There was a problem hiding this comment.
You can still do it in a single array, just with a couple swap() calls to reorder the elements. So at the very end do k_mats.swap(0, 2) and k_mats.swap(1, 3).
| let mut c2r = c_mats[0]; // before weyl_gate, qubit 0 | ||
| let mut c2l = c_mats[1]; // before weyl_gate, qubit 1 | ||
| let mut c1r = c_mats[2]; // after weyl_gate, qubit 0 | ||
| let mut c1l = c_mats[3]; // after weyl_gate, qubit 1 | ||
|
|
||
| let rxx_k2r = rxx_mats[0]; // before RXX(a), qubit 0 | ||
| let rxx_k2l = rxx_mats[1]; // before RXX(a), qubit 1 | ||
| let rxx_k1r = rxx_mats[2]; // after RXX(a), qubit 0 | ||
| let rxx_k1l = rxx_mats[3]; // after RXX(a), qubit 1 | ||
|
|
||
| let mut ryy_k2r: Matrix2<Complex64>; // before RYY(b), qubit 0 | ||
| let mut ryy_k2l: Matrix2<Complex64>; // before RYY(b), qubit 1 | ||
| let mut ryy_k1r: Matrix2<Complex64>; // after RYY(b), qubit 0 | ||
| let mut ryy_k1l: Matrix2<Complex64>; // after RYY(b), qubit 1 | ||
|
|
||
| let mut rzz_k2r: Matrix2<Complex64>; // before RZZ(c), qubit 0 | ||
| let mut rzz_k2l: Matrix2<Complex64>; // before RZZ(c), qubit 1 | ||
| let mut rzz_k1r: Matrix2<Complex64>; // after RZZ(c), qubit 0 | ||
| let mut rzz_k1l: Matrix2<Complex64>; // after RZZ(c), qubit 1 |
There was a problem hiding this comment.
Just for a code comment it might be nice to have a little ascii art diagram that shows the structure of the output synthesis to explain these comments. it's clear to me but for future maintainers it might be helpful to explain visually.
There was a problem hiding this comment.
do you have an easy way to generate such diagrams?
There was a problem hiding this comment.
I'd use Qiskit! :D
from qiskit.circuit import QuantumCircuit, Gate, Parameter
from qiskit.circuit.library import RXXGate, RZZGate, RYYGate
a = Parameter("a")
b = Parameter("b")
c = Parameter("c")
rxx_a = RXXGate(a)
ryy_b = RYYGate(b)
rzz_c = RZZGate(c)
weyl_gate = Gate(name='Weyl', num_qubits=2, params=[])
c2r = Gate(name='c2r', num_qubits=1, params=[])
c2l = Gate(name='c2l', num_qubits=1, params=[])
c1r = Gate(name='c1r', num_qubits=1, params=[])
c1l = Gate(name='c1l', num_qubits=1, params=[])
k2r = Gate(name='k2r', num_qubits=1, params=[])
k2l = Gate(name='k2l', num_qubits=1, params=[])
k1r = Gate(name='k1r', num_qubits=1, params=[])
k1l = Gate(name='k1l', num_qubits=1, params=[])
circuit = QuantumCircuit(2)
circuit.append(c2r, [0])
circuit.append(c2l, [1])
circuit.append(weyl_gate, [0, 1])
circuit.append(c1r, [0])
circuit.append(c1l, [1])
print(circuit)
controlled_u = QuantumCircuit(2)
controlled_u.append(k2r, [0])
controlled_u.append(k2l, [1])
controlled_u.append(rxx_a, [0, 1])
controlled_u.append(k1r, [0])
controlled_u.append(k1l, [1])
controlled_u.append(k2r, [0])
controlled_u.append(k2l, [1])
controlled_u.append(ryy_b, [0, 1])
controlled_u.append(k1r, [0])
controlled_u.append(k1l, [1])
controlled_u.append(k2r, [0])
controlled_u.append(k2l, [1])
controlled_u.append(rzz_c, [0, 1])
controlled_u.append(k1r, [0])
controlled_u.append(k1l, [1])
print(controlled_u) ┌─────┐┌───────┐┌─────┐
q_0: ┤ c2r ├┤0 ├┤ c1r ├
├─────┤│ Weyl │├─────┤
q_1: ┤ c2l ├┤1 ├┤ c1l ├
└─────┘└───────┘└─────┘
┌─────┐┌─────────┐┌─────┐┌─────┐┌─────────┐┌─────┐┌─────┐ ┌─────┐
q_0: ┤ k2r ├┤0 ├┤ k1r ├┤ k2r ├┤0 ├┤ k1r ├┤ k2r ├─■──────┤ k1r ├
├─────┤│ Rxx(a) │├─────┤├─────┤│ Ryy(b) │├─────┤├─────┤ │ZZ(c) ├─────┤
q_1: ┤ k2l ├┤1 ├┤ k1l ├┤ k2l ├┤1 ├┤ k1l ├┤ k2l ├─■──────┤ k1l ├
└─────┘└─────────┘└─────┘└─────┘└─────────┘└─────┘└─────┘ └─────┘
There was a problem hiding this comment.
That's a great suggestion (although your picture isn't correct).
I added the pictures of the circuits in 3e39016
close #16036
This PR reduces the total number of 1-qubit unitaries needed to synthesize a general 2-qubit unitary using
TwoQubitControlledUDecomposerfrom 24 to 8.This is in particular useful for the peephole optimization in #13419.
Details:
The
TwoQubitControlledUDecomposerconsists of the following steps.We start from a general 4x4 unitary U.
Then we find a cannonical gate W (Weyl gate) s.t.
U(0,1) = c2r(0) c2l(1) W(0,1) c1r(0) c1l(1)this yields 4 1-qubit unitary gates.
W(0,1) = RXX(0,1) RYY(0,1) RZZ(0,1)RYY(0,1) = sdg(0) sdg(0,1) RXX(0,1) s(0,1) s(0,1)RZZ(0,1) = h(0) h(0,1) h(0,1) h(0,1) h(0,1)yielding 8 additional 1-qubit unitary gates.
RXX(0,1) = k2r(0) k2l(1) Equiv(0,1) k1r(0) k1l(1)yielding 3*4 1-qubit unitary gates.
This gives a total of:
3*4 + 8 + 4 = 24 1-qubit unitary gates.
In this PR we multiply the 1-qubit unitary matrices betwen the 2-qubit gates, to get at most 8 1-qubit unitary gates (in the general case where 3 2-qubit gates are needed).
AI/LLM disclosure