Skip to content

Tutorial: an X gate on a transmon

This tutorial walks through the daily task of a pulse engineer on the transmon/XGate-v0 task: start from a Gaussian pi pulse, add DRAG, optimise with GRAPE, then look at the same pulses with decoherence and export the result. All numbers on this page come from examples/tutorial_transmon_x_gate.py (float64, CPU, about 15 s), which tests/test_tutorials.py also runs in a reduced form.

The script uses the GRAPE helper in examples/grape_jax.py, which needs optax and, for the last step, gymnasium:

pip install "qenvs[gym]" optax
python examples/tutorial_transmon_x_gate.py

The task

import jax

jax.config.update("jax_enable_x64", True)  # errors of interest are 1e-6 to 1e-4
import jax.numpy as jnp
import numpy as np
import qenvs

env = qenvs.make("transmon/XGate-v0")
setting value
model three-level transmon, anharmonicity \(\alpha/2\pi = -0.34\) GHz, rotating frame of the drive, RWA
controls \([\Omega_I, \Omega_Q]\) in rad/ns, $
time grid 16 steps of 2 ns, a 32 ns gate
objective gain of the leakage-aware average gate fidelity to X on ${

See the transmon model page for the Hamiltonian, the sign conventions and the validation table.

1. A Gaussian pi pulse

from qenvs.models.transmon import gaussian_amplitude, gaussian_drag

T = env.horizon * env.dt
sigma = T / 4
amp = gaussian_amplitude(jnp.pi, env.horizon, env.dt, sigma)  # area pi
pulse = gaussian_drag(env.horizon, env.dt, amp, sigma, beta=0.0)
state, rewards = env.rollout(pulse)
info = env.info(state)  # fidelity, leakage, populations p0, p1, p2

The plain Gaussian reaches \(1 - F = 9.07\times10^{-4}\) with leakage \(2.4\times10^{-6}\): on a 32 ns gate the dominant error is not leakage but the phase error that the second excited level induces on the 0-1 transition.

2. DRAG

The quadrature \(\Omega_Q = \beta\,\dot\Omega_I\) cancels that error to first order. In this library's sign convention the coefficients are

from qenvs.models.transmon import drag_beta

drag_beta(-0.34, "phase")  # 0.234 ns  = -1/(2 alpha)
drag_beta(-0.34, "leakage")  # 0.468 ns  = -1/alpha

With the phase-error value the infidelity drops by a factor 187:

\(\beta\) (ns) 0 0.059 0.117 0.176 0.234 0.293 0.351 0.410 0.468
\(1 - F\) 9.07e-4 5.10e-4 2.28e-4 5.94e-5 4.85e-6 6.43e-5 2.38e-4 5.25e-4 9.25e-4

The minimum of the sweep sits exactly at the first-order value. The leakage-optimal value (\(-1/\alpha\)) is not the best choice for gate fidelity here, because at this gate length leakage is already small.

3. GRAPE

The environment is differentiable, so a gradient-based optimiser can shape the pulse directly. examples/grape_jax.py parametrises the 32 control amplitudes and runs Adam on the return of env.rollout:

from grape_jax import run_grape  # examples/ on sys.path

g = run_grape("transmon/XGate-v0", steps=300, lr=0.1, seed=0)
pulse = jnp.asarray(g["best_pulse"])  # (16, 2) in rad/ns

From a random start it passes \(F > 0.999\) within 30 Adam steps and ends at \(1 - F = 3.7\times10^{-15}\) with leakage \(3.6\times10^{-15}\), about 2 s on CPU. A closed three-level model has no fundamental limit at this gate length, so this is a check of the simulator and the optimiser rather than a statement about hardware.

4. Decoherence

Passing coherence times switches the environment to the superoperator of the Lindblad evolution:

env_open = qenvs.make("transmon/XGate-v0", t1_us=100.0, t2_us=100.0)
pulse \(1 - F\) with \(T_1 = T_2 = 100\ \mu\)s
idle for 32 ns, measured against the identity 1.600e-4
DRAG (\(\beta = 0.234\) ns) 1.652e-4
GRAPE pulse optimised without decoherence 1.674e-4
GRAPE optimised on the open task (300 steps) 1.673e-4

The idle value is a decoherence reference, not a strict bound: a driven gate spends time in \(|1\rangle\), where it relaxes. The table shows the practical conclusion for these parameters: the gate error is dominated by coherence, and pulse shaping changes it only at the \(10^{-6}\) level. GRAPE from a random start does not beat the analytic DRAG pulse here within 300 steps; it leaves more population in \(|0\rangle\) (\(2.0\times10^{-4}\) against \(1.6\times10^{-4}\)) and slightly more in \(|2\rangle\).

5. Export

The pulse is a list of piecewise-constant Rabi frequencies on a 2 ns grid. In MHz:

t_ns = (np.arange(env.horizon) + 0.5) * env.dt
omega_mhz = np.asarray(pulse) / (2 * np.pi) * 1e3  # columns I, Q

python examples/tutorial_transmon_x_gate.py --out pulse.npz writes t_ns, omega_i_mhz and omega_q_mhz. The GRAPE pulse peaks at 48.8 MHz. Mapping Rabi frequencies to AWG amplitudes needs a calibration of your hardware, which QEnvS does not model.

6. Reinforcement learning

The same task is a Gymnasium environment:

from qenvs.wrappers import GymnasiumEnv

gym_env = GymnasiumEnv(env, normalize_actions=True)  # actions in [-1, 1]

The observation has 19 entries (real and imaginary parts of the \(3\times3\) propagator and the elapsed time fraction); an episode ends after 16 steps. A random policy reaches \(F \approx 0.6\). See the RL libraries guide for vectorised environments and training loops.

What this model leaves out

Single qubit only (no couplers or cross-resonance), rotating-wave approximation, a harmonic \(T_1\) ladder, piecewise-constant controls without an AWG transfer function, and no virtual-Z frame updates (Z errors count as infidelity, whereas hardware calibrations usually absorb them in software).