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!
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
compiled_circuit = LazySynthesis().compile(Batch(jobs=[circuit.to_job()], device).jobs[0].circuit
compiled_circuit, table = LazySynthesis().compile_circuit(circuit, device)
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).
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()
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:
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
.
device.as_quameleon()
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