Compressing single qubit gates

The KAKCompression plugin can be used to automatically compress sequences of consecutive single qubit gates into a fixed universal pattern of your choice. In practice, it can be used after various compilation stages in order to optimally reduce the number of single qubit gates in a circuit.

Predefined patterns

The plugin comes with predefined universal patterns: - \(RZ-RX-RZ\) (called ZXZ) - \(RX-RZ-RX\) (called XZX) - \(RZ-RY-RZ\) (called ZYZ) - \(RZ - RX(\pi/2) - RZ - RX(\pi/2) - RZ\) (called ions or rx+) - \(U3\) (called u3 or ibm)

One can directly construct a KAKCompression from one of these patterns using a known pattern (i.e. a pattern listed by list_decompositions()):

# declaring a circuit
from qat.pbo.kak import list_decompositions
from qat.plugins import KAKCompression
from qat.lang import H, RX, RZ, CNOT, qrout

@qrout
def circuit():
    "Create a quantum circuit"
    H(0)
    RX(0.232)(0)
    CNOT(0, 1)
    RZ(1.89)(1)
    H(1)

print("== Initial circuit ==")
for op in circuit.iterate_simple():
    print(op)

job = circuit.to_job()

for decomposition in list_decompositions():
    with KAKCompression(decomposition=decomposition):
        compiled_job = job.compile()

    print(f"\n== Circuit compiled with {decomposition!r} ==")
    for op in compiled_job.circuit.iterate_simple():
        print(op)
== Initial circuit ==
('H', [], [0])
('RX', [0.232], [0])
('CNOT', [], [0, 1])
('RZ', [1.89], [1])
('H', [], [1])

== Circuit compiled with 'ions' ==
('RZ', [6.515185307179586], [0])
('RX', [1.5707963267948966], [0])
('RZ', [1.5707963267948966], [0])
('RX', [1.5707963267948966], [0])
('RZ', [0.0], [0])
('CNOT', [], [0, 1])
('RZ', [8.173185307179587], [1])
('RX', [1.5707963267948966], [1])
('RZ', [1.5707963267948966], [1])
('RX', [1.5707963267948966], [1])
('RZ', [0.0], [1])

== Circuit compiled with 'ZXZ' ==
('RZ', [1.8027963267948968], [0])
('RX', [1.5707963267948966], [0])
('RZ', [1.5707963267948966], [0])
('CNOT', [], [0, 1])
('RZ', [3.4607963267948962], [1])
('RX', [1.5707963267948966], [1])
('RZ', [1.5707963267948966], [1])

== Circuit compiled with 'XZX' ==
('RX', [1.5707963267948966], [0])
('RZ', [1.5707963267948966], [0])
('RX', [1.8027963267948968], [0])
('CNOT', [], [0, 1])
('RX', [-4.71238898038469], [1])
('RZ', [1.5707963267948966], [1])
('RX', [3.4607963267948962], [1])

== Circuit compiled with 'ZYZ' ==
('RZ', [3.3735926535897933], [0])
('RY', [1.5707963267948966], [0])
('RZ', [0.0], [0])
('CNOT', [], [0, 1])
('RZ', [5.031592653589793], [1])
('RY', [1.5707963267948966], [1])
('RZ', [0.0], [1])

== Circuit compiled with 'u3' ==
('U3', [1.5707963267948966, 0.0, 3.3735926535897933], [0])
('CNOT', [], [0, 1])
('U3', [1.5707963267948966, 0.0, 5.031592653589793], [1])

== Circuit compiled with 'ibm' ==
('U3', [1.5707963267948966, 0.0, 3.3735926535897933], [0])
('CNOT', [], [0, 1])
('U3', [1.5707963267948966, 0.0, 5.031592653589793], [1])

== Circuit compiled with 'rx+' ==
('RZ', [6.515185307179586], [0])
('RX', [1.5707963267948966], [0])
('RZ', [1.5707963267948966], [0])
('RX', [1.5707963267948966], [0])
('RZ', [0.0], [0])
('CNOT', [], [0, 1])
('RZ', [8.173185307179587], [1])
('RX', [1.5707963267948966], [1])
('RZ', [1.5707963267948966], [1])
('RX', [1.5707963267948966], [1])
('RZ', [0.0], [1])

Declaring custom patterns

It is also possible to declare custom decompositions. In order to do so we need to specify:

  • the universal pattern to used

  • a way to compute the angles to feed to this pattern given the coefficient of a \(SU(2)\) matrix

Let us give an example where we would like to use the following patter:

\[X - RZ(\alpha) - RX(\beta) - RZ(\gamma) - X\]

It is easy to see that our pattern is pretty close from a ZXZ decomposition except that we need to flip the sign of the two Z rotations. We need to know how to compute values for \(\alpha, \beta, \gamma\) given the entries of the matrix to decompose. Luckily, we can import method qat.pbo.decompositions.decompositions.get_euler_angles() that, given values for the 4 coefficient of a unitary matrix returns the value of angles for a \(ZXZ\) decomposition.

from qat.pbo.decompositions.decompositions import get_euler_angles

def my_angles(a, b, c, d): # here a,b,c,d are the coefficient of the matrix to decompose
    d, e, f = get_euler_angles(a, b, c, d)
    return -d, e, -f

We can now declare our plugin. We need to declare 4 variables that will correspond to the 4 entries of the matrix, and build a pattern that uses these variables (thanks to our function):

from qat.pbo import VAR

matrix = [VAR() for _ in range(4)]
alpha, beta, gamma = my_angles(*matrix)
pattern = [('X', [0]), ('RZ', [0], alpha), ('RX', [0], beta), ('RZ', [0], gamma), ('X', [0])]

We can now safely declare our plugin:

from qat.plugins import KAKCompression

plugin = KAKCompression(variables=matrix, pattern=pattern)

And run it on the previous example:

# declaring a circuit
from qat.plugins import KAKCompression
from qat.lang import H, RX, RZ, CNOT, qrout

@qrout
def circuit():
    "Create a quantum circuit"
    H(0)
    RX(0.232)(0)
    CNOT(0, 1)
    RZ(1.89)(1)
    H(1)

print("== Initial circuit ==")
for op in circuit.iterate_simple():
    print(op)

job = circuit.to_job()

with plugin:
    compiled_job = job.compile()

print("\n== Compiled circuit ==")
for op in compiled_job.circuit.iterate_simple():
    print(op)
== Initial circuit ==
('H', [], [0])
('RX', [0.232], [0])
('CNOT', [], [0, 1])
('RZ', [1.89], [1])
('H', [], [1])

== Compiled circuit ==
('X', [], [0])
('RZ', [-3.3735926535897933], [0])
('RX', [1.5707963267948966], [0])
('RZ', [-0.0], [0])
('X', [], [0])
('CNOT', [], [0, 1])
('X', [], [1])
('RZ', [-5.031592653589793], [1])
('RX', [1.5707963267948966], [1])
('RZ', [-0.0], [1])
('X', [], [1])