End-to-End Tutorial

QUASAR (QUantum Assembler for Star-topology ARchitectures) is a hardware-aware compiler for star-topology quantum devices. Its main target is the NV center: one central electron spin, coupled to several nuclear spins around it.

This notebook runs the whole pipeline once:

  1. pick a native gate set;

  2. compile a circuit onto it at the gate level, with two strategies;

  3. lower the native circuit to hardware pulses, and check them in simulation.

The other tutorials go deeper into each step.

Setup

This notebook needs quasar, simphony for the pulse-level section, and the plotting extra for the circuit diagrams:

pip install --find-links=wheels/ ".[simphony-backend,plot]"
%matplotlib inline

import quasar
from qiskit.circuit.random import random_circuit

1. Gate set

A GateSet describes the native gates of a device and its star connectivity. We use the default CROTGateSet. Its native gates are the single-qubit rotations rz and r, and the two-qubit controlled rotation crot on every star edge (0, target).

gateset = quasar.CROTGateSet(n_qubits=3)
gateset
CROTGateSet(name='crot', n_qubits=3)

The circuit

The running example is a random 3-qubit circuit with a fixed seed. Every compile below starts from it.

qc = random_circuit(num_qubits=3, depth=4, seed=4)
qc.draw(output="mpl");
../../_images/8f131b9588b28f9dbabbeee4b96c9a341b566da1c95259108c9b86de0e5a3092.png

2. Gate-level compiling

A GateCompiler binds a gate set and a configuration. The configuration picks the compilation path. We compare two paths:

  • Qiskit’s transpiler is the baseline. It starts from the gates of the circuit and rewrites them into native gates.

  • Adaptive synthesis starts from the unitary of the circuit. It searches for a native circuit with the same unitary, whatever gates the original circuit uses.

Baseline: Qiskit routing

QiskitConfig routes and translates the circuit with Qiskit’s transpiler. Then it lowers the result to native rotations.

config_qiskit = quasar.QiskitConfig(optimization_level=3, seed=1)
compiler_qiskit = quasar.GateCompiler(gateset, config=config_qiskit)
qc_qiskit = compiler_qiskit.compile(qc)
dict(qc_qiskit.count_ops())
{'rz': 43, 'r': 43, 'crot': 20}

The compiled circuit holds only the native gates rz, r and crot:

qc_qiskit.draw(output="mpl");
../../_images/d0d8a03e951bb6b6a529ab76a6eb60cd3526bb74c7c097f4d9cc798da5f1ddd3.png

GateCompiler.compilation_error compares the compiled circuit with the original. It is one minus the average gate fidelity, so 0 is a perfect match. It takes into account any qubit relabeling the compiler chose:

compiler_qiskit.compilation_error(qc, qc_qiskit)
7.771561172376096e-16

Adaptive synthesis

The adaptive strategy adds entanglers until the compilation error falls below tolerance_error. A small tolerance (here 1e-8) makes the compilation numerically exact.

config_adaptive = quasar.AdaptiveConfig(tolerance_error=1e-8, seed=1)
compiler_adaptive = quasar.GateCompiler(gateset, config=config_adaptive)
qc_adaptive = compiler_adaptive.compile(qc)
dict(qc_adaptive.count_ops())
{'rz': 13, 'r': 13, 'crot': 5}

The synthesized circuit:

qc_adaptive.draw(output="mpl");
../../_images/1a3de57218a52d561e95d3e483460d9df1e3e395051a5a34ebc2563b943327aa.png

The tolerance is small, so the compilation error is close to 0:

compiler_adaptive.compilation_error(qc, qc_adaptive)
5.705247385634493e-11

Synthesis usually needs fewer native two-qubit gates than the Qiskit baseline. Compare the crot counts:

{"qiskit": qc_qiskit.count_ops().get("crot", 0), "adaptive": qc_adaptive.count_ops().get("crot", 0)}
{'qiskit': 20, 'adaptive': 5}

QUASAR has five configurations: the Qiskit path and four synthesis strategies. This notebook uses two of them. In short:

  • Tree search also works to a tolerance_error. It tries more placement orders per depth, so it can find a shorter circuit, but it takes longer.

  • The error-aware strategies, one for adaptive synthesis and one for tree search, pick the depth with the smallest overall error. The overall error combines the compilation error with the error the gates of the device add.

  • On any synthesis strategy, a looser tolerance_error trades accuracy for fewer gates.

The Gate Compilers tutorial compares them. A layout search can lower the count further (see the Qubit Layout tutorial).

3. Pulse-level compiling

A native circuit can be lowered further, to hardware control pulses. This works for any gate set with a registered pulse backend. CROTGateSet lowers to pulses on a Simphony device model, which simulates them. FixedCROTGateSet lowers to a Simphony model too, and also to XeedQ hardware (see the Pulse Compilers tutorial).

The device model

We use a default NV-center model with three spins: the electron spin of the NV center, its nitrogen-14 nuclear spin, and one nearby carbon-13 nuclear spin. They form a 3-qubit register, and the electron is the central qubit 0. The model also holds the driving fields that pulse compilation needs.

import simphony

model = simphony.default_nv_model(
    nitrogen_isotope=14,
    static_field_strength=0.05,
    carbon_atom_indices=[(2, -2, -1, 1)],
)
model
Model(spin_names=['e', 'N', 'C'], num_static_fields=1, driving_field_names=['MW_x', 'RF_x'], dimension=18)

A PulseCompiler binds the gate set to a pulse backend, here a SimphonyBackend on the model. It lowers a native circuit to pulses. We pulse-compile the synthesized circuit from above:

backend = quasar.SimphonyBackend(model=model)
pulse_compiler = quasar.PulseCompiler(gateset=gateset, backend=backend)
pulse_compiler.compile(qc_adaptive)
PulseList(n_pulses=114)

The model now holds the pulses of this circuit. Their microwave and RF envelopes:

model.plot_driving_fields(function="complex_envelope")
../../_images/6fc9e04f03e50c2bf4bf7ea5d2d33ba864e38d0a97139e789c611c4fae1a343b.png

Finally, benchmark_circuit simulates the whole pulse sequence and compares it with the ideal circuit. It returns the average gate fidelity of the pulses:

pulse_compiler.benchmark_circuit(qc_adaptive)
0.9586046567475894

The fidelity is about 96 %. Each native gate has a small error on this device model, and over the whole circuit the errors add up. The Pulse Compilers tutorial measures the error of each gate, and lets the gate-level compiler take it into account.

Where to go next

  • Gate Sets tutorial — the native gate sets, and the gate errors and durations they carry.

  • Gate Compilers tutorial — every compilation strategy, and which one to choose.

  • Pulse Compilers tutorial — pulse-level compiling and benchmarking, including the hardware-only XeedQBackend.

  • Qubit Layout tutorial — letting the compiler choose which qubit sits where.

  • State Preparation and Pauli Readout tutorial — compiling a state preparation, a readout and a reset.

  • The API reference for the full surface.