Gate Sets

QUASAR compiles a circuit for a star-topology device in two steps: first to the device’s native gates, then to the control pulses that realize them. Both steps start from a GateSet, which describes the device’s native operations: which single-qubit rotations and which two-qubit entangler it supports, and the connectivity they’re available on. Every gate set in QUASAR is a star topology — one central qubit (0) connected to every other qubit, with no direct connections between the outer qubits — but they differ in which native gates realize that connectivity.

You will see:

  • the four gate sets QUASAR ships, and their native gates;

  • which of them lower to pulses;

  • how FixedCROTGateSet chooses between a U3 and a U2 rotation;

  • the gate errors, durations and reserved qubits a gate set carries for its device.

For how a gate set is used to compile a circuit, see the Gate Compilers and Pulse Compilers tutorials.

Setup

This notebook needs quasar alone:

pip install --find-links=wheels/ .
import quasar

The single-qubit gates

Every gate set below builds its single-qubit rotations from one of three bases: rz/r, rz/rx or rz/sx.

\[ R_z(\lambda) = e^{-i\lambda Z/2}, \qquad R(\theta, \phi) = e^{-i\frac{\theta}{2}\left(\cos\phi\,X + \sin\phi\,Y\right)}, \qquad R_x(\theta) = e^{-i\theta X/2}, \qquad SX = \sqrt{X} \]

rz rotates about \(Z\), and hardware that tracks it as a frame change spends no time on it. r rotates by \(\theta\) about an axis in the \(XY\) plane at angle \(\phi\) from \(X\), and rx is the same rotation with \(\phi = 0\). sx is the square root of \(X\): \(SX^2 = X\), and \(SX = e^{i\pi/4} R_x(\pi/2)\), so it is a fixed rotation, with no angle to tune.

CROTGateSet

The default gate set: single-qubit rz/r rotations, and the two-qubit controlled rotation crot — a continuously-parameterized entangler — on every star edge.

\[ \mathrm{CROT}(\theta, \phi) = R(\theta, \phi) \otimes |0\rangle\langle 0| + R(-\theta, \phi) \otimes |1\rangle\langle 1| \]

The right factor is qubit 0, the centre of the star, which acts as the control. The formulas follow Qiskit’s order, which puts qubit 0 on the right. Many textbooks put the control on the left (see the Conventions section of the Concepts page). Both branches rotate: the control flips the sign of the rotation angle rather than switching the target rotation on and off.

gateset_crot = quasar.CROTGateSet(n_qubits=3)
gateset_crot.topology
((0, 1), (0, 2))

The native_gates list is the full native-gate catalogue: every single-qubit gate on every qubit, plus the two-qubit gate on every edge.

gateset_crot.native_gates
(Gate(name='rz', qubits=(0,), num_params=1),
 Gate(name='r', qubits=(0,), num_params=2),
 Gate(name='rz', qubits=(1,), num_params=1),
 Gate(name='r', qubits=(1,), num_params=2),
 Gate(name='rz', qubits=(2,), num_params=1),
 Gate(name='r', qubits=(2,), num_params=2),
 Gate(name='crot', qubits=(0, 1), num_params=2),
 Gate(name='crot', qubits=(0, 2), num_params=2))

CROTGateSet lowers to simulated hardware pulses through SimphonyBackend. The End-to-End and Pulse Compilers tutorials use it throughout.

FixedCROTGateSet

A single-qubit basis of rz/sx instead of rz/r, and a fixed-angle entangler, crotxpi2 (crot at a hardwired \(\theta = \pi/2\)) rather than a free one. This models a device whose two-qubit interaction can’t be tuned continuously — only turned on for a fixed duration.

\[ \mathrm{CROTX}_{\pi/2} = R_x(\pi/2) \otimes |0\rangle\langle 0| + R_x(-\pi/2) \otimes |1\rangle\langle 1| \]

This is crot at \(\theta = \pi/2\) and \(\phi = 0\), with both angles hardwired.

gateset_fixed_crot = quasar.FixedCROTGateSet(n_qubits=3)
gateset_fixed_crot.native_gates
(Gate(name='rz', qubits=(0,), num_params=1),
 Gate(name='sx', qubits=(0,), num_params=0),
 Gate(name='rz', qubits=(1,), num_params=1),
 Gate(name='sx', qubits=(1,), num_params=0),
 Gate(name='rz', qubits=(2,), num_params=1),
 Gate(name='sx', qubits=(2,), num_params=0),
 Gate(name='crotxpi2', qubits=(0, 1), num_params=0),
 Gate(name='crotxpi2', qubits=(0, 2), num_params=0))

crotxpi2 takes no angle parameter. The compiler cannot scale a rotation angle as it does on CROTGateSet. It has to find a circuit of fixed-angle gates that reproduces the target unitary.

FixedCROTGateSet has two registered pulse backends: SimphonyBackend, which simulates the pulses, and XeedQBackend, for the XeedQ cloud device.

Rotation ansatz

The synthesis builds a circuit from layers: a general rotation on both qubits of an edge, then the entangler. On FixedCROTGateSet a general rotation, a U3, takes two sx. A U2 fixes \(\theta\) at \(\pi/2\) and takes one sx:

\[ U3(\theta, \phi, \lambda) = R_z(\phi)\, R_y(\theta)\, R_z(\lambda), \qquad U2(\phi, \lambda) = U3(\pi/2, \phi, \lambda) \]

Three settings choose between them, each "u3" (the default) or "u2":

  • control_rotation_ansatz — the rotation on the control of each edge, the centre;

  • target_rotation_ansatz — the rotation on the target of each edge;

  • closing_rotation_ansatz — the rotations after the last entangler.

A U2 on a target saves one sx on that nucleus, and there one sx is a whole DDRF sequence. It pays on a circuit whose rotations sit at multiples of \(\pi/2\), such as a Clifford circuit. The Gate Compilers tutorial introduces the GateCompiler used here:

from qiskit.quantum_info import random_clifford

qc_clifford = random_clifford(num_qubits=3, seed=4).to_circuit()
config = quasar.AdaptiveConfig(seed=1)
counts = {}
for ansatz in ("u3", "u2"):
    gateset_ansatz = quasar.FixedCROTGateSet(n_qubits=3, target_rotation_ansatz=ansatz)
    compiled = quasar.GateCompiler(gateset_ansatz, config=config).compile(qc_clifford)
    counts[f"{ansatz} on the targets"] = dict(compiled.count_ops())
counts
{'u3 on the targets': {'rz': 23, 'sx': 12, 'crotxpi2': 4},
 'u2 on the targets': {'rz': 21, 'sx': 10, 'crotxpi2': 4}}

The U2 version needs two sx fewer, with the same four entanglers. On a general circuit, a U2 layer fits less of the target, so the compile needs more entanglers instead. U3 is the default for that reason.

CNOTGateSet

rz/rx single-qubit gates, and the standard controlled-NOT (cx) as the two-qubit entangler — the gate most quantum-computing tooling already assumes as native.

\[ \mathrm{CX} = I \otimes |0\rangle\langle 0| + X \otimes |1\rangle\langle 1| \]

Unlike crot, this leaves the target untouched on the \(|0\rangle\) branch.

gateset_cnot = quasar.CNOTGateSet(n_qubits=3)
gateset_cnot.native_gates
(Gate(name='rz', qubits=(0,), num_params=1),
 Gate(name='rx', qubits=(0,), num_params=1),
 Gate(name='rz', qubits=(1,), num_params=1),
 Gate(name='rx', qubits=(1,), num_params=1),
 Gate(name='rz', qubits=(2,), num_params=1),
 Gate(name='rx', qubits=(2,), num_params=1),
 Gate(name='cx', qubits=(0, 1), num_params=0),
 Gate(name='cx', qubits=(0, 2), num_params=0))

CNOTGateSet is gate-level only — no pulse backend is registered for it. It’s useful as a synthesis target when you want native-gate counts comparable to CNOT-based hardware, without needing a pulse-level device model.

CRXGateSet

rz/rx single-qubit gates again, with the standard controlled-RX rotation (crx) as the entangler.

\[ \mathrm{CRX}(\theta) = I \otimes |0\rangle\langle 0| + R_x(\theta) \otimes |1\rangle\langle 1| \]
gateset_crx = quasar.CRXGateSet(n_qubits=3)
gateset_crx.native_gates
(Gate(name='rz', qubits=(0,), num_params=1),
 Gate(name='rx', qubits=(0,), num_params=1),
 Gate(name='rz', qubits=(1,), num_params=1),
 Gate(name='rx', qubits=(1,), num_params=1),
 Gate(name='rz', qubits=(2,), num_params=1),
 Gate(name='rx', qubits=(2,), num_params=1),
 Gate(name='crx', qubits=(0, 1), num_params=1),
 Gate(name='crx', qubits=(0, 2), num_params=1))

Like CNOTGateSet, CRXGateSet is gate-level only. Its crx entangler’s default angle is \(\theta = \pi\) — the angle at which cx/cz decompose into a single crx exactly, so Qiskit-path compiling (QiskitConfig) can translate cx/cz-based circuits onto it with no approximation.

Comparing them side by side

The four gate sets side by side:

gate set

single-qubit gates

entangler

pulse backends

CROTGateSet

rz, r

crot

SimphonyBackend

FixedCROTGateSet

rz, sx

crotxpi2

SimphonyBackend, XeedQBackend

CNOTGateSet

rz, rx

cx

none

CRXGateSet

rz, rx

crx

none

Every gate set takes spam_qubits, the qubits whose state preparation and measurement (SPAM) the device performs: it resets and reads out these qubits, and only these. The default, (0,), is the central qubit alone. The State Preparation and Pauli Readout tutorial uses spam_qubits=(0, 1).

Gate properties

A gate set can also carry what each native gate costs on the device:

  • its error, the average gate infidelity on its own qubits;

  • its duration, the time the gate takes. QUASAR does not specify the unit, but all gates must use the same one.

Both are optional. Give them uniformly, for every single-qubit gate and every entangler, or per gate. A per-gate value replaces the uniform one. Here edge (0, 2) is worse than the others, and durations are in microseconds:

gateset_with_costs = quasar.CROTGateSet(
    n_qubits=3,
    single_qubit_gate_error=0.001,
    entangler_gate_error=0.01,
    gate_errors={("crot", (0, 2)): 0.03},
    single_qubit_gate_duration=0.1,
    entangler_gate_duration=2.0,
)
{f"{gate.gate_type.name}{gate.qubits}": error for gate, error in gateset_with_costs.gate_errors.items()}
{'rz(0,)': 0.0,
 'r(0,)': 0.001,
 'rz(1,)': 0.0,
 'r(1,)': 0.001,
 'rz(2,)': 0.0,
 'r(2,)': 0.001,
 'crot(0, 1)': 0.01,
 'crot(0, 2)': 0.03}

A frame change, rz, takes no time and carries no error, so its entries are 0. There is no default value. If a value is not given and a method needs it, the method raises an error.

The error-aware strategies and estimate_errors read the errors (see the Gate Compilers tutorial). A layout search can rank layouts by either property (see the Qubit Layout tutorial). The Pulse Compilers tutorial measures the errors on a simulated device.

Reserved qubits

A gate also reserves qubits while it runs: its own, and any other qubit the device cannot use meanwhile. Two gates can run at once only when their reserved qubits are disjoint.

Unlike errors and durations, reserved qubits have a default. On CROTGateSet and FixedCROTGateSet, a gate on a nucleus also reserves the electron, because its DDRF sequence decouples the electron. So every gate there runs after the one before. A frame change reserves no qubit:

gates = [("r", (0,)), ("r", (1,)), ("crot", (0, 2)), ("rz", (1,))]
{gate: gateset_with_costs.reserved_qubits(gate) for gate in gates}
{('r', (0,)): (0,),
 ('r', (1,)): (0, 1),
 ('crot', (0, 2)): (0, 2),
 ('rz', (1,)): ()}

reserved_qubits_by_gate replaces the default for the gates it names. Here a device drives both nuclei directly, on one shared RF line and without the electron:

gateset_direct = quasar.CROTGateSet(
    n_qubits=3,
    single_qubit_gate_duration=0.1,
    entangler_gate_duration=2.0,
    reserved_qubits_by_gate={("r", (1,)): (1, 2), ("r", (2,)): (1, 2)},
)
gates = [("r", (0,)), ("r", (1,)), ("r", (2,))]
{gate: gateset_direct.reserved_qubits(gate) for gate in gates}
{('r', (0,)): (0,), ('r', (1,)): (1, 2), ('r', (2,)): (1, 2)}

An r on the electron can now run during an r on a nucleus, but the two nuclei still take turns. A layout search by duration reads the reserved qubits (see the Qubit Layout tutorial).

Where to go next

  • Gate Compilers tutorial — how a GateCompiler targets any of these gate sets, with every compilation strategy.

  • Pulse Compilers tutorial — lowering CROTGateSet/FixedCROTGateSet circuits to hardware pulses.

  • The API reference for the full surface.