# Pulse-Level Simulation of Superconducting Qubits with QuTiP-QIP

This notebook demonstrates how to use
[qutip-qip](https://qutip-qip.readthedocs.io/)
[[LCM22](#id41)] to perform
**pulse-level simulations** of superconducting transmon qubits.
It bridges the gap between the Hamiltonian-level design covered in the
companion notebooks
([scQubits parameter calculation](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/scqubits_parameter_calculation.html.md)
and [pymablock dispersive shift](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/pymablock_dispersive_shift.html.md))
and the actual control pulses that drive quantum gates on hardware.

We use [qutip-jax](https://qutip-jax.readthedocs.io/) as the JAX backend
for QuTiP, leveraging the existing JAX integration in qpdk.

## Why Pulse-Level Simulation?

Ideal quantum gate models assume instantaneous, perfect operations.
In practice, gates on superconducting qubits are realised by shaped
microwave pulses applied over tens to hundreds of nanoseconds.
Pulse-level simulation captures the actual time dynamics of the system,
including:

- **Leakage** to non-computational states (the third level of the transmon)
- **Gate errors** from finite pulse duration and bandwidth
- **Decoherence** from $T_1$ relaxation and $T_2$ dephasing

These effects are critical for designing high-fidelity quantum processors
[[KSB+20](#id36), [KKY+19](#id38)].

![A shaped microwave pulse drives the zero-to-one transition but may also cause leakage to the second excited state](notebooks/figures/pulse-leakage.svg)

For some recent examples of `qutip` pulse-level simulation in the context of superconducting
qubits, see [[And24](#id6), [Sal23](#id62)].

```ipython3
import sys

if "google.colab" in sys.modules:
    import subprocess

    print("Running in Google Colab. Installing QPDK...")
    subprocess.check_call([
        sys.executable,
        "-m",
        "pip",
        "install",
        "-q",
        "qpdk[models,qutip] @ git+https://github.com/gdsfactory/quantum-rf-pdk.git",
    ])
```

```ipython3
import warnings
from typing import cast

import jax.numpy as jnp
import matplotlib.pyplot as plt
import qutip
import qutip_jax  # ruff: ignore[unused-import] — registers JAX backend for QuTiP
from IPython.display import Math, display
from qutip_qip.circuit import QubitCircuit
from qutip_qip.device import SCQubits

from qpdk import PDK
from qpdk.cells.transmon import xmon_transmon
from qpdk.models.perturbation import ej_ec_to_frequency_and_anharmonicity

core_options = qutip.settings.core
if core_options is None:
    raise RuntimeError("qutip.settings.core is set when qutip.core is imported")
core_options["numpy_backend"] = jnp
core_options["default_dtype"] = "jax"

PDK.activate()

# Suppress FutureWarnings from qutip internals
warnings.filterwarnings("ignore", category=FutureWarning, module="qutip")
```

## From Layout to Simulation: Design Mapping

In a real quantum PDK (qpdk), the physical layout parameters directly
determine the Hamiltonian parameters used in the pulse-level simulation.

### Step 1: Layout Component

We start with an **Xmon transmon** design. The sizes of arms
and the gap between them determine the total shunt capacitance $C_\Sigma$,
which sets the charging energy $E_\text{C} = e^2 / (2 C_\Sigma)$.

```ipython3
c_transmon = xmon_transmon(arm_width=(10.0,) * 4, arm_lengths=(100.0,) * 4)
c_transmon.plot()
plt.title("Xmon Transmon Layout (qpdk)")
plt.show()
```

```myst-ansi
<Figure size 800x600 with 1 Axes>
```

### Step 2: Parameter Extraction

Changing the `arm_width` and `arm_length` in the layout modifies $E_\text{C}$.
For a transmon, the Josephson energy $E_\text{J}$ is determined by the critical
current of the junction.

Below we see how a change in $E_\text{C}$ (e.g., from making the pads larger)
affects the qubit frequency $\omega_\text{q}$ and anharmonicity $\alpha$.

```ipython3
# Fixed EJ = 15 GHz
EJ_val = 15.0

# EC = 0.3 GHz (standard pads) vs EC = 0.2 GHz (larger pads)
ec_values = [0.3, 0.2]

for ec in ec_values:
    wq_calc, alpha_calc = ej_ec_to_frequency_and_anharmonicity(EJ_val, ec)
    display(
        Math(rf"""
\text{{For }} E_\text{{C}} = {ec:.1f}\,\text{{GHz}} \implies
\omega_\text{{q}} \approx {wq_calc:.3f}\,\text{{GHz}}, \quad
\alpha \approx {alpha_calc:.3f}\,\text{{GHz}}
""")
    )
```

```myst-ansi
<IPython.core.display.Math object>
```

```myst-ansi
<IPython.core.display.Math object>
```

**Design Trade-off:**

- **Larger pads (Lower $E_\text{C}$)**: Reduces $\alpha$, which requires longer
  pulses to avoid leakage to $|2\rangle$ (slower gates).
- **Smaller pads (Higher $E_\text{C}$)**: Increases $\alpha$, allowing faster
  gates, but makes the qubit more sensitive to charge noise.

## Pulse-Level Simulation: Single-Qubit X Gate

We now simulate an $X$ gate on a qubit with the extracted parameters.
The `SCQubits` processor captures the time-domain pulse envelopes and
the population dynamics.

```ipython3
# Using parameters extracted from layout (EC = 0.3 GHz, EJ = 15 GHz)
# QuTiP-QIP SCQubits expects negative anharmonicity for transmons,
# while the qpdk perturbation model returns positive values by convention.
wq0, a0 = ej_ec_to_frequency_and_anharmonicity(EJ_val, 0.3)
wq = [float(wq0), float(wq0) - 0.1]
alpha = [-float(a0), -float(a0)]

g_coupling = 0.1
wr = 6.5

qc_x = QubitCircuit(2)
qc_x.add_gate("X", targets=0)

processor_x = SCQubits(2, wq=wq, alpha=alpha, g=g_coupling, wr=wr)
processor_x.load_circuit(qc_x)

init_state = qutip.basis([3, 3], [0, 0])
result_x = cast(qutip.Result, processor_x.run_state(init_state))
```

### Population Dynamics and Leakage

We track the population in each basis state. The 3-level transmon model
reveals any **leakage** to the $|2\rangle$ state, which is heavily
influenced by the anharmonicity $\alpha$ set in the layout.

```ipython3
# Define projection operators for population tracking
proj_ops = {
    r"$|0,0\rangle$": qutip.tensor(qutip.fock_dm(3, 0), qutip.fock_dm(3, 0)),
    r"$|1,0\rangle$": qutip.tensor(qutip.fock_dm(3, 1), qutip.fock_dm(3, 0)),
    r"$|2,0\rangle$": qutip.tensor(qutip.fock_dm(3, 2), qutip.fock_dm(3, 0)),
}

tlist_x = jnp.asarray(result_x.times)

fig, ax = plt.subplots(figsize=(8, 4))
for label, proj in proj_ops.items():
    pops = [qutip.expect(proj, s) for s in result_x.states]
    ax.plot(tlist_x, pops, linewidth=2, label=label)

ax.set_xlabel("Time (ns)")
ax.set_ylabel("Population")
ax.set_title("X gate: population dynamics with leakage tracking")
ax.legend(loc="center right")
ax.grid(True, alpha=0.3)
plt.show()
```

```myst-ansi
<Figure size 2400x1200 with 1 Axes>
```

## Bell State Preparation

Entanglement between two qubits is realized via a CNOT gate. In this
processor, the CNOT is implemented using **Cross-Resonance (CR)** pulses.
The efficiency of the CR interaction depends on the frequency detuning
$\Delta = \omega_{\text{q},1} - \omega_{\text{q},2}$, which is set by the junction
sizing in the layout.

```ipython3
qc_bell = QubitCircuit(2)
qc_bell.add_gate("SNOT", targets=0)  # Hadamard
qc_bell.add_gate("CNOT", controls=0, targets=1)

processor_bell = SCQubits(2, wq=wq, alpha=alpha, g=g_coupling, wr=wr)
processor_bell.load_circuit(qc_bell)

result_bell = cast(qutip.Result, processor_bell.run_state(init_state))
```

```ipython3
fig, axes = processor_bell.plot_pulses(figsize=(10, 8))
fig.suptitle(
    r"Control pulses for Bell state preparation ($H + \text{CNOT}$)",
    fontsize=14,
    y=1.02,
)
fig.tight_layout()
plt.show()
```

```myst-ansi
<Figure size 3000x2400 with 8 Axes>
```

### Fidelity Analysis

We compute the state fidelity with the ideal Bell state.

```ipython3
# Ideal Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2
bell_ideal = (
    qutip.tensor(qutip.basis(3, 0), qutip.basis(3, 0))
    + qutip.tensor(qutip.basis(3, 1), qutip.basis(3, 1))
).unit()

final_state = result_bell.states[-1]
fidelity_bell = qutip.fidelity(final_state, bell_ideal)

display(
    Math(rf"""
\textbf{{Bell State Fidelity:}} \\
F(|\Phi^+\rangle) = {fidelity_bell:.6f}
""")
)
```

```myst-ansi
<IPython.core.display.Math object>
```

## Decoherence Effects: $T_2$ Sweep

Real qubits are limited by decoherence. If our layout has higher
loss (e.g., from narrow gaps), the $T_1$ and $T_2$ times decrease,
directly reducing the gate fidelity.

```ipython3
# Sweep T2 values (in ns) with fixed T1 = 80 μs
t1_fixed = 80_000.0  # 80 μs in ns
t2_values_us = jnp.array([5, 10, 20, 40, 80, 150])
t2_values_ns = t2_values_us * 1e3

fidelities_t2 = []
for t2 in t2_values_ns:
    proc = SCQubits(
        2, wq=wq, alpha=alpha, g=g_coupling, wr=wr, t1=t1_fixed, t2=float(t2)
    )
    proc.load_circuit(qc_bell)
    res = cast(qutip.Result, proc.run_state(init_state))
    f = qutip.fidelity(res.states[-1], bell_ideal)
    fidelities_t2.append(f)

fig, ax = plt.subplots(figsize=(8, 4))
ax.semilogx(t2_values_us, fidelities_t2, "o-", linewidth=2, markersize=8)
ax.set_xlabel(r"$T_2$ (µs)")
ax.set_ylabel(r"Bell state fidelity $F$")
ax.set_title(
    rf"Bell state fidelity vs. $T_2$ ($T_1 = {t1_fixed / 1e3:.0f}\,\text{{µs}}$)"
)
ax.grid(True, alpha=0.3, which="both")
plt.show()
```

```myst-ansi
<Figure size 2400x1200 with 1 Axes>
```

## Summary: The Design Cycle

This notebook demonstrated the connection between physical layout and
pulse-level performance:

1. **Layout (qpdk)**: Geometric parameters (pad size, gap) determine $C_\Sigma$ and $L_\text{J}$.
2. **Hamiltonian (qpdk.models)**: Parameters $E_\text{C}, E_\text{J}$ set $\omega_\text{q}$ and $\alpha$.
3. **Simulation (QuTiP)**: Pulse-level dynamics reveal gate fidelity and leakage.
4. **Iterate**: If leakage is too high ($|2\rangle$ population), we go back to
   step 1 and increase $E_\text{C}$ in the layout.

For more details on parameter extraction, see the companion notebooks
on [scQubits parameter calculation](https://gdsfactory.github.io/quantum-rf-pdk/notebooks/scqubits_parameter_calculation.html.md).

## References
