Skip to content

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

  1. How QubitOS uses Hamiltonians to control qubits
  2. How to define a system Hamiltonian with Pauli strings
  3. How to optimize a pulse using GRAPE
  4. How to use gate presets as shortcuts
  5. 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:

\[ U = \mathcal{T}\exp\left(-i\int_0^T \left[H_0 + \sum_k u_k(t) H_k\right] dt\right) \]

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:

\[ \Omega(t) = u_I(t)\cos(\omega_d t) + u_Q(t)\sin(\omega_d t) \]
  • 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

Exercises

  1. Different Hamiltonians: Change the drift frequency and re-optimize. How does the pulse shape change?
  2. Two-qubit gate: Build a 2-qubit Hamiltonian and optimize a CNOT gate
  3. Error budget: Chain multiple gates and check whether the sequence fits in the T1/T2 budget
  4. Decoherence: Use qubitos.lindblad to simulate open-system dynamics