Quickstart Guide¶
Time to complete: 20-30 minutes
This guide walks you through generating and executing your first quantum gate pulse with QubitOS. We start from physics — the Hamiltonian that governs your qubit — and show how QubitOS optimizes pulse shapes to implement any desired unitary evolution.
What You'll Learn¶
- How QubitOS uses Hamiltonians to control qubits
- How to define a system Hamiltonian with Pauli strings
- How to optimize a pulse using GRAPE
- How to use gate presets as shortcuts
- How to execute pulses and interpret results
Prerequisites¶
- QubitOS installed (Installation Guide)
- Basic Python knowledge
- Basic understanding of quantum mechanics
1. What is QubitOS?¶
QubitOS is a Hamiltonian-level quantum control system. It works at the physics layer: you specify the Hamiltonian of your system, the target unitary evolution you want, and QubitOS optimizes the electromagnetic control pulses to achieve it.
The Core Idea¶
Every quantum gate is a unitary evolution generated by a Hamiltonian:
where: - \(H_0\) is your drift Hamiltonian (always-on qubit frequencies, couplings) - \(H_k\) are control Hamiltonians (how your drive lines couple to qubits) - \(u_k(t)\) are the control amplitudes — what QubitOS optimizes
Standard gates like X, Y, Z, CNOT are just specific target unitaries. QubitOS treats them as convenient presets, not fundamental primitives.
QubitOS Workflow:
Hamiltonian + Target Unitary → GRAPE Optimizer → Calibrated Pulse → Execution
↑ ↑
Your physics Pulse engineering
2. Defining Your System Hamiltonian¶
QubitOS uses Pauli string notation to define Hamiltonians concisely.
Pauli String Basics¶
from qubitos.pulsegen.hamiltonians import build_hamiltonian
# A transmon qubit with 5 GHz frequency:
# H_0 = (ω/2) σ_z = 2π × 5.0 × (σ_z / 2)
# Controlled by X and Y drives:
# H_c = [σ_x, σ_y]
H0, Hc = build_hamiltonian(
drift="5.0 * Z0", # Drift: 5 GHz qubit frequency (Z on qubit 0)
controls=["X0", "Y0"], # Control: X and Y drive on qubit 0
num_qubits=1,
)
print(f"Drift shape: {H0.shape}") # (2, 2)
print(f"Controls: {len(Hc)} operators") # 2 operators
Multi-Qubit Systems¶
# Two coupled transmons:
# H_0 = 5.0 Z₀ + 5.2 Z₁ + 0.02 Z₀Z₁ (ZZ coupling)
# H_c = [X₀, Y₀, X₁, Y₁]
H0, Hc = build_hamiltonian(
drift="5.0 * Z0 + 5.2 * Z1 + 0.02 * Z0Z1",
controls=["X0", "Y0", "X1", "Y1"],
num_qubits=2,
)
print(f"Drift shape: {H0.shape}") # (4, 4)
print(f"Controls: {len(Hc)} operators") # 4 operators
3. Optimizing Your First Pulse¶
Let's optimize a pulse to implement an X gate (quantum NOT) on a single qubit.
The Hamiltonian-First Way¶
import numpy as np
from qubitos.pulsegen import GrapeOptimizer, GrapeConfig
from qubitos.pulsegen.hamiltonians import build_hamiltonian, get_target_unitary
# Step 1: Define the physics
H0, Hc = build_hamiltonian(
drift="5.0 * Z0", # Qubit frequency
controls=["X0", "Y0"], # Drive channels
num_qubits=1,
)
# Step 2: Define the target evolution
# X gate: U = exp(-i π/2 σ_x) = [[0, 1], [1, 0]] (up to global phase)
target = get_target_unitary("X", num_qubits=1)
# Step 3: Configure GRAPE optimization
config = GrapeConfig(
num_time_steps=100, # Pulse discretization (100 samples)
duration_ns=50, # Total gate time: 50 nanoseconds
max_iterations=200, # Maximum optimization steps
target_fidelity=0.999, # Stop when fidelity ≥ 99.9%
learning_rate=0.1, # Gradient ascent step size
)
# Step 4: Optimize
optimizer = GrapeOptimizer(config)
result = optimizer.optimize(target, num_qubits=1)
print(f"Converged: {result.converged}")
print(f"Fidelity: {result.fidelity:.6f}")
print(f"Iterations: {result.iterations}")
print(f"I envelope: {result.i_envelope.shape}") # (100,)
print(f"Q envelope: {result.q_envelope.shape}") # (100,)
Why start with the Hamiltonian? Because it's the physics. When you move to real hardware, the drift Hamiltonian changes (different qubit frequency, different coupling strength), and you re-optimize against the actual physics of your device.
Gate Presets: The Shortcut¶
For standard gates, QubitOS provides presets that wrap the Hamiltonian setup:
# This does the same thing as above, with default Hamiltonians
result = optimizer.optimize(
get_target_unitary("X", num_qubits=1),
num_qubits=1,
)
Gate presets are convenience wrappers — they construct a default Hamiltonian and target unitary for you. Available presets:
| Gate | Matrix | Hamiltonian evolution |
|---|---|---|
X |
\(\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\) | \(e^{-i\pi\sigma_x/2}\) |
Y |
\(\begin{pmatrix} 0 & -i \\ i & 0 \end{pmatrix}\) | \(e^{-i\pi\sigma_y/2}\) |
Z |
\(\begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}\) | \(e^{-i\pi\sigma_z/2}\) |
H |
\(\frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix}\) | \((e^{-i\pi\sigma_x/2})(e^{-i\pi\sigma_z/4})\) |
4. Understanding the Pulse Output¶
The GRAPE optimizer returns two envelope arrays that define the control pulse:
- I envelope (
result.i_envelope): In-phase control amplitudes \(u_I(t)\) - Q envelope (
result.q_envelope): Quadrature control amplitudes \(u_Q(t)\) - Fidelity: \(F = \frac{1}{d^2}|\text{Tr}(U_{\text{target}}^\dagger U_{\text{achieved}})|^2\)
Visualizing¶
import matplotlib.pyplot as plt
t = np.linspace(0, 50, len(result.i_envelope))
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 5), sharex=True)
ax1.plot(t, result.i_envelope, 'b-', lw=1.5)
ax1.set_ylabel('I Amplitude')
ax1.set_title('GRAPE-Optimized X-Gate Pulse')
ax1.grid(True, alpha=0.3)
ax2.plot(t, result.q_envelope, 'r-', lw=1.5)
ax2.set_ylabel('Q Amplitude')
ax2.set_xlabel('Time (ns)')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
5. Checking the Error Budget¶
Before executing on hardware, check whether the pulse fits within your decoherence error budget:
from qubitos.error_budget import ErrorBudget
budget = ErrorBudget(
target_fidelity=0.99,
t1_us={0: 50.0}, # T1 = 50 μs for qubit 0
t2_us={0: 30.0}, # T2 = 30 μs for qubit 0
)
budget.add_gate(
infidelity=1 - result.fidelity,
qubit=0,
duration_ns=50,
label="X gate",
)
print(f"Projected fidelity: {budget.projected_fidelity:.4f}")
print(f"Within budget: {budget.can_append(infidelity=0.001, qubit=0, duration_ns=20)}")
6. Executing on a Backend¶
Using the CLI¶
# Generate a pulse
qubit-os pulse generate --gate X --duration 50 --fidelity 0.999
# Execute on the QuTiP simulator
qubit-os pulse execute x_gate.json --shots 1024
Using Python¶
from qubitos.client import HALClientSync
with HALClientSync("localhost:50051") as client:
execution = client.execute_pulse(
i_envelope=result.i_envelope.tolist(),
q_envelope=result.q_envelope.tolist(),
duration_ns=50,
target_qubits=[0],
num_shots=1024,
)
print(f"Bitstring counts: {execution.bitstring_counts}")
# Expected: ~99% in |1⟩ (X gate flips |0⟩ → |1⟩)
7. CLI Reference¶
| Command | Description |
|---|---|
qubit-os hal health |
Check HAL server status |
qubit-os hal backends |
List available backends |
qubit-os pulse generate |
Generate optimized pulse |
qubit-os pulse execute |
Execute a pulse |
qubit-os --help |
Show all commands |
Next Steps¶
You've learned the core QubitOS workflow: Hamiltonian → Target Unitary → GRAPE → Pulse → Execution
Continue Learning¶
- Pulse Generation Tutorial: Pulse shapes and parametric gates
- GRAPE Optimizer Deep Dive: Optimization strategies and convergence
- Custom Hamiltonians: Multi-qubit systems and advanced Hamiltonians
- API Reference: Complete API documentation
Exercises¶
- Different Hamiltonians: Change the drift frequency and re-optimize. How does the pulse shape change?
- Two-qubit gate: Build a 2-qubit Hamiltonian and optimize a CNOT gate
- Error budget: Chain multiple gates and check whether the sequence fits in the T1/T2 budget
- Decoherence: Use
qubitos.lindbladto simulate open-system dynamics