Lazy synthesis¶

LazySynthesis is an algorithm that can compile quantum circuit for some particular hardware connectivity constraints by lazily synthesizing pieces of circuits.

It works by maintaining a classical data structure representing a unitary operator in some simple class of operators (Clifford operators). Gates belonging to this simple set of operators are aggregated in the data structure while gates outside of it will trigger partial synthesis of the current operator.

Overall, the algorithm tries to minimize the CNOT count of the final circuit.

Let us try to compile some circuits!

In [1]:
from qat.opt import MaxCut
import networkx as nx

inst_graph = nx.generators.erdos_renyi_graph(16, 0.2)
problem = MaxCut(inst_graph)
job = problem.to_job("qaoa", 1) # '1' is the depth
LazySynthesis compile method can be replaced by the shorter compile_circuit method, when compiling a single circuit. When working directly on the QLM machine, the following syntaxes otherwise a similar purpose:
compiled_circuit = LazySynthesis().compile(Batch(jobs=[circuit.to_job()], device).jobs[0].circuit
compiled_circuit, table = LazySynthesis().compile_circuit(circuit, device)
In this notebook, we use the more verbose compile method to stay most compatible with the myQML Power Acess mode.
In [2]:
from qat.plugins import LazySynthesis
from qat.devices import RIGETTI_ASPEN as device
from qat.core import Batch
print("Using:\n", device)
compiled_circuit = LazySynthesis().compile(Batch(jobs=[job]), device).jobs[0].circuit
print("CNOT count:", (sum(1 if op[0] == "CNOT" else 0 for op in compiled_circuit.iterate_simple())))
Using:
 
  4 -- 3         12 -- 11
 /      \       /        \
5        2 -- 13          10
|        |     |          |
6        1 -- 14          9
 \      /       \        /
  7 -- 0         15 -- 8
CNOT count: 254

Synthesis using Clifford operator is more expressive than the one using linear operators. But it really shines when dealing with circuit containing many arbitrary Pauli rotations (like, for instance, VQE circuits).

In [3]:
import numpy as np
from qat.lang.AQASM import Program, RX, H, CNOT, PH

prog = Program()
qbits = prog.qalloc(16)
for _ in range(10):
    args = np.random.choice(qbits, size=3, replace=False)
    paulis = np.random.choice(["X", "Y", "Z"], size=3)
    for qbit, pauli in zip(args, paulis):
        if pauli == "X":
            H(qbit)
        if pauli == "Y":
            RX(np.pi/2)(qbit)
    CNOT(args[0], args[1])
    CNOT(args[1], args[2])
    PH(np.random.random() * 2 * np.pi)(args[2])
    
    CNOT(args[1], args[2])
    CNOT(args[0], args[1])
    for qbit, pauli in zip(args, paulis):
        if pauli == "X":
            H(qbit)
        if pauli == "Y":
            RX(-np.pi/2)(qbit)
circuit = prog.to_circ()
circuit.display()
No description has been provided for this image
In [4]:
print("Original CNOT count:", sum(1 if op[0] == "CNOT" else 0 for op in circuit.iterate_simple()))

compiled_circuit = LazySynthesis().compile(Batch(jobs=[circuit.to_job()]), device).jobs[0].circuit
print("CNOT count:", (sum(1 if op[0] == "CNOT" else 0 for op in compiled_circuit.iterate_simple())))
Original CNOT count: 40
CNOT count: 113

Seach depth¶

The main parameter of the algorithm is the depth of local recursive search. Increasing this parameter might greatly reduce the final CNOT count, while increasing the compilation time:

In [5]:
print("Original CNOT count:", sum(1 if op[0] == "CNOT" else 0 for op in circuit.iterate_simple()))

for depth in [0, 1, 2, 3, 4]:
    print(f"============   Depth = {depth}")
    compiled_circuit = LazySynthesis(depth=depth).compile(Batch(jobs=[circuit.to_job()]), device).jobs[0].circuit
    print("CNOT count:", (sum(1 if op[0] == "CNOT" else 0 for op in compiled_circuit.iterate_simple())))
Original CNOT count: 40
============   Depth = 0
CNOT count: 121
============   Depth = 1
CNOT count: 117
============   Depth = 2
CNOT count: 114
============   Depth = 3
CNOT count: 108
============   Depth = 4
CNOT count: 113

Using LazySynthesis as a Plugin¶

Of course, in practical applications, it is more convenient to include the algorithms as a Plugin in a stack.

Notice how we included the target device using QuameleonPlugin.

When working directly on the QLM (ie. not using Qaptiva Access), the Quameleon Plugin below can be created using the following shorthand method:
device.as_quameleon()
In [6]:
inst_graph = nx.generators.erdos_renyi_graph(8, 0.5)
problem = MaxCut(inst_graph)
job = problem.to_job("qaoa", 1) # '1' is the depth

from qat.plugins import ScipyMinimizePlugin, QuameleonPlugin
from qat.qpus import get_default_qpu

stack = LazySynthesis(depth=3) | ScipyMinimizePlugin(method="COBYLA", tol=1e-5, options={"maxiter": 350}) | QuameleonPlugin(specs=device) | get_default_qpu()
result = stack.submit(job)
print("Final energy:", result.value)
Final energy: -9.369615287251847