Interfaces Module

The interfaces module provides hardware abstraction layers for experimental control, with realistic noise models.

AWG Interface

Arbitrary Waveform Generator (AWG) interface.

The AWG generates time-dependent microwave waveforms for spin control. It stores amplitude Ω(t), phase φ(t), and detuning Δ(t) as arrays and provides interpolation for arbitrary time points.

Waveform Structure

The MW field in the rotating frame is characterized by: - Amplitude (Rabi frequency) Ω(t) in MHz - Phase φ(t) in radians - Detuning Δ(t) in MHz (optional)

The resulting Hamiltonian is:

H_MW = (Ω/2)(S₊ e^(-iφ) + S₋ e^(iφ)) + Δ Sz

Pulse Shapes

Supported envelope shapes: - rect: Rectangular (constant amplitude) - gauss: Gaussian envelope - sinc: Sinc function (sin(x)/x) - blackman: Blackman window - hermite: Hermite-Gaussian - drag: DRAG pulse for leakage reduction

class sim.interfaces.awg.AWGInterface(sample_rate=1000000000.0, _amplitude=<factory>, _phase=<factory>, _detuning=<factory>)[source]

Bases: object

Arbitrary Waveform Generator for microwave pulse control.

Parameters:
sample_rate

AWG sample rate

Type:

float

dt

Time step between samples

Type:

float

duration

Total waveform duration

Type:

float

Examples

>>> awg = AWGInterface()
>>> awg.add_pulse("gauss", amplitude=10, duration=100e-9, phase=0)
>>> awg.add_delay(500e-9)
>>> awg.add_pulse("gauss", amplitude=10, duration=100e-9, phase=np.pi)
>>>
>>> # Get Rabi frequency at t=50 ns
>>> omega = awg.get_omega(50e-9)
sample_rate: float = 1000000000.0
clear()[source]

Clear all waveform data.

add_pulse(shape, amplitude, duration, phase=0.0, detuning=0.0, **kwargs)[source]

Add a pulse to the waveform.

Parameters:
  • shape (str) – Pulse shape: ‘rect’, ‘gauss’, ‘sinc’, ‘blackman’, ‘hermite’, ‘drag’

  • amplitude (float) – Peak Rabi frequency in MHz

  • duration (float) – Pulse duration in seconds

  • phase (float) – Pulse phase in radians. Default: 0

  • detuning (float) – Frequency detuning in MHz. Default: 0

  • **kwargs – Shape-specific parameters: - sigma: Gaussian width (default: duration/6) - beta: DRAG coefficient (default: 0.5) - order: Hermite order (default: 0)

add_delay(duration)[source]

Add a delay (zero amplitude) segment.

Parameters:

duration (float) – Delay duration in seconds

get_omega(t)[source]

Get Rabi frequency at time t.

Parameters:

t (float) – Time in seconds

Returns:

Rabi frequency in MHz

Return type:

float

get_phase(t)[source]

Get phase at time t.

Parameters:

t (float) – Time in seconds

Returns:

Phase in radians

Return type:

float

get_detuning(t)[source]

Get detuning at time t.

Parameters:

t (float) – Time in seconds

Returns:

Detuning in MHz

Return type:

float

to_mw_drive()[source]

Create a MicrowaveDrive term from this AWG waveform.

Returns:

Hamiltonian term with this waveform

Return type:

MicrowaveDrive

plot(ax=None)[source]

Plot the waveform.

Parameters:

ax (matplotlib axis, optional) – Axis to plot on. If None, creates new figure.

__init__(sample_rate=1000000000.0, _amplitude=<factory>, _phase=<factory>, _detuning=<factory>)
Parameters:
Return type:

None

The AWG (Arbitrary Waveform Generator) interface simulates a programmable pulse generator:

from sim.interfaces import AWGInterface

# Create AWG with 1 GS/s sample rate
awg = AWGInterface(sample_rate=1e9)

# Add pulses
awg.add_pulse(
    shape="gaussian",
    amplitude=1.0,          # Normalized amplitude
    duration=50e-9,         # 50 ns
    phase=0
)
awg.add_delay(100e-9)       # 100 ns delay
awg.add_pulse(
    shape="rect",
    amplitude=1.0,
    duration=100e-9,
    phase=np.pi/2
)

# Get waveform
t, waveform = awg.get_waveform()

# Plot
import matplotlib.pyplot as plt
plt.plot(t * 1e9, waveform.real)
plt.xlabel("Time (ns)")
plt.ylabel("Amplitude")

Waveform Generation

# Generate IQ waveforms
I, Q = awg.get_iq_waveform(carrier_freq=2.87e9)

# Export to file
awg.save_waveform("sequence.npz")

Laser Interface

Laser interface for optical control.

The laser provides optical excitation for: - Spin initialization (optical pumping) - Spin readout (fluorescence detection) - Coherent optical control (Rabi oscillations)

Laser Parameters

  • Wavelength: 532 nm (green) or 637 nm (ZPL)

  • Power: Typically 0.1-10 mW

  • Rabi frequency: Depends on power and beam waist

The optical Rabi frequency is related to laser power by:

Ω_L = d·E₀/ℏ = d·√(2P/(ε₀cπw₀²))/ℏ

where d is the dipole moment and w₀ is the beam waist.

class sim.interfaces.laser.LaserPulse(start, duration, power, shape='rect')[source]

Bases: object

Single laser pulse.

Parameters:
start: float
duration: float
power: float
shape: str = 'rect'
__init__(start, duration, power, shape='rect')
Parameters:
Return type:

None

class sim.interfaces.laser.LaserInterface(wavelength=532.0, power_to_rabi=10.0, linewidth=0.0, _pulses=<factory>)[source]

Bases: object

Laser interface for optical control.

Parameters:
  • wavelength (float) – Laser wavelength in nm. Default: 532 (green)

  • power_to_rabi (float) – Conversion factor from mW to MHz. Default: 10 MHz/mW

  • linewidth (float)

  • _pulses (List[LaserPulse])

wavelength

Laser wavelength in nm

Type:

float

pulses

List of scheduled laser pulses

Type:

list

Examples

>>> laser = LaserInterface()
>>> laser.add_pulse(start=0, duration=1e-6, power=1.0)
>>> laser.add_pulse(start=2e-6, duration=100e-9, power=0.5)
>>>
>>> # Get power at specific time
>>> p = laser.get_power(500e-9)  # 1.0 mW
wavelength: float = 532.0
power_to_rabi: float = 10.0
linewidth: float = 0.0
property pulses: List[LaserPulse]

List of scheduled pulses.

clear()[source]

Clear all pulses.

add_pulse(start, duration, power, shape='rect')[source]

Add a laser pulse.

Parameters:
  • start (float) – Start time in seconds

  • duration (float) – Pulse duration in seconds

  • power (float) – Laser power in mW

  • shape (str) – Pulse shape: ‘rect’, ‘gauss’. Default: ‘rect’

add_cw(power, t_start=0, t_end=0.001)[source]

Add continuous wave operation.

Parameters:
  • power (float) – CW power in mW

  • t_start (float) – Start time. Default: 0

  • t_end (float) – End time. Default: 1 ms

get_power(t)[source]

Get laser power at time t.

Parameters:

t (float) – Time in seconds

Returns:

Power in mW (0 if laser is off)

Return type:

float

get_rabi(t)[source]

Get optical Rabi frequency at time t.

Parameters:

t (float) – Time in seconds

Returns:

Rabi frequency in MHz

Return type:

float

is_on(t)[source]

Check if laser is on at time t.

Parameters:

t (float)

Return type:

bool

to_optical_coupling()[source]

Create an OpticalCoupling term from this laser.

Returns:

Hamiltonian term with this laser waveform

Return type:

OpticalCoupling

schedule_initialization(t_start=0, duration=1e-06, power=1.0)[source]

Schedule an initialization pulse.

Typical initialization uses a few microseconds of green light to optically pump into ms=0.

Parameters:
  • t_start (float) – Start time in seconds

  • duration (float) – Pulse duration in seconds. Default: 1 μs

  • power (float) – Laser power in mW. Default: 1 mW

schedule_readout(t_start, duration=3e-07, power=1.0)[source]

Schedule a readout pulse.

Readout typically uses a short pulse (~300 ns) to measure fluorescence before spin flips occur.

Parameters:
  • t_start (float) – Start time in seconds

  • duration (float) – Pulse duration. Default: 300 ns

  • power (float) – Laser power in mW. Default: 1 mW

__init__(wavelength=532.0, power_to_rabi=10.0, linewidth=0.0, _pulses=<factory>)
Parameters:
Return type:

None

The laser interface simulates optical excitation:

from sim.interfaces import LaserInterface

# Create laser (532 nm green)
laser = LaserInterface(wavelength=532e-9)

# Set power
laser.set_power(1e-3)  # 1 mW

# Convert to Rabi frequency
rabi = laser.power_to_rabi(power=1e-3)

# Pulse timing
laser.pulse(duration=1e-6, delay=0)

Photon Counter

Photon counter interface for fluorescence detection.

Models realistic single-photon detection including: - Detection efficiency - Dark counts - Dead time - Timing jitter - Afterpulsing

Detector Characteristics

Typical APD (Avalanche Photodiode) parameters: - Detection efficiency: 5-20% - Dark count rate: 10-1000 Hz - Dead time: 20-100 ns - Timing jitter: 0.3-1 ns - Afterpulsing probability: 0.1-5%

SNSPD (Superconducting Nanowire): - Detection efficiency: 70-95% - Dark count rate: <1 Hz - Dead time: 10-50 ns - Timing jitter: <100 ps

class sim.interfaces.photon_counter.PhotonCounter(efficiency=0.1, dark_count_rate=100.0, dead_time=5e-08, timing_jitter=5e-10, afterpulsing_prob=0.01, afterpulsing_delay=1e-07, _total_counts=0, _detection_times=<factory>, _last_detection=-1.0, _current_time=0.0)[source]

Bases: object

Realistic photon counter for fluorescence detection.

Parameters:
  • efficiency (float) – Detection efficiency (0-1). Default: 0.1 (10%)

  • dark_count_rate (float) – Dark count rate in Hz. Default: 100

  • dead_time (float) – Dead time in seconds. Default: 50e-9 (50 ns)

  • timing_jitter (float) – Timing jitter (std dev) in seconds. Default: 0.5e-9

  • afterpulsing_prob (float) – Afterpulsing probability. Default: 0.01 (1%)

  • afterpulsing_delay (float)

  • _total_counts (int)

  • _detection_times (List[float])

  • _last_detection (float)

  • _current_time (float)

total_counts

Total detected photons

Type:

int

detection_times

List of photon arrival times

Type:

list

Examples

>>> counter = PhotonCounter(efficiency=0.1)
>>>
>>> # Simulate detection from excited state population
>>> for rho in rho_trajectory:
...     result = counter.count(rho, dt=1e-9, gamma=8e7)
>>>
>>> print(f"Total photons: {counter.total_counts}")
efficiency: float = 0.1
dark_count_rate: float = 100.0
dead_time: float = 5e-08
timing_jitter: float = 5e-10
afterpulsing_prob: float = 0.01
afterpulsing_delay: float = 1e-07
property total_counts: int

Total number of detected photons.

property detection_times: List[float]

List of photon detection times.

reset()[source]

Reset all counters.

count(rho, dt, gamma, current_time=None)[source]

Simulate photon detection from the current state.

Parameters:
  • rho (np.ndarray) – 18×18 density matrix

  • dt (float) – Time step in seconds

  • gamma (float) – Spontaneous emission rate in Hz (~8×10⁷ for NV)

  • current_time (float, optional) – Current time. If None, auto-incremented.

Returns:

Detection result with keys: - ‘detected’: Number of photons detected in this step - ‘expected’: Expected number of photons - ‘pop_excited’: Excited state population

Return type:

dict

count_rate(window=1e-06)[source]

Calculate count rate over a time window.

Parameters:

window (float) – Time window in seconds

Returns:

Count rate in Hz

Return type:

float

histogram(bin_width=1e-09, t_range=None)[source]

Create histogram of detection times.

Parameters:
  • bin_width (float) – Bin width in seconds

  • t_range (tuple, optional) – (t_min, t_max) time range

Returns:

(counts, bin_edges)

Return type:

tuple

calculate_g2(tau_max=1e-06, n_bins=100)[source]

Calculate g²(τ) autocorrelation function.

Parameters:
  • tau_max (float) – Maximum delay time in seconds

  • n_bins (int) – Number of delay bins

Returns:

(tau, g2) arrays

Return type:

tuple

Notes

g²(0) < 1 indicates antibunching (single photon source) g²(0) > 1 indicates bunching (thermal light) g²(0) = 1 for coherent light

fano_factor(window=1e-06)[source]

Calculate Fano factor F = Var(n)/⟨n⟩.

Parameters:

window (float) – Counting window in seconds

Returns:

Fano factor (F<1: sub-Poissonian, F=1: Poissonian, F>1: super-Poissonian)

Return type:

float

__init__(efficiency=0.1, dark_count_rate=100.0, dead_time=5e-08, timing_jitter=5e-10, afterpulsing_prob=0.01, afterpulsing_delay=1e-07, _total_counts=0, _detection_times=<factory>, _last_detection=-1.0, _current_time=0.0)
Parameters:
Return type:

None

The photon counter simulates realistic single-photon detection:

from sim.interfaces import PhotonCounter

# Create APD-like detector
counter = PhotonCounter(
    efficiency=0.1,           # 10% detection efficiency
    dark_count_rate=100,      # 100 Hz dark counts
    dead_time=50e-9,          # 50 ns dead time
    timing_jitter=0.5e-9,     # 500 ps jitter
    afterpulsing_prob=0.01    # 1% afterpulsing
)

# Reset counters
counter.reset()

# Count photons from density matrix
for rho in trajectory:
    result = counter.count(
        rho=rho,
        dt=1e-9,              # 1 ns time step
        gamma=8e7             # Emission rate
    )
    print(f"Detected: {result['detected']}")

# Get total counts
print(f"Total: {counter.total_counts}")

Detector Types

APD (Avalanche Photodiode):

apd = PhotonCounter(
    efficiency=0.1,
    dark_count_rate=100,
    dead_time=50e-9,
    timing_jitter=0.5e-9,
    afterpulsing_prob=0.01
)

SNSPD (Superconducting Nanowire):

snspd = PhotonCounter(
    efficiency=0.9,
    dark_count_rate=0.1,
    dead_time=20e-9,
    timing_jitter=50e-12,
    afterpulsing_prob=0.001
)

Analysis Functions

# Count rate
rate = counter.count_rate(window=1e-6)  # Hz

# Histogram of detection times
counts, edges = counter.histogram(
    bin_width=1e-9,
    t_range=(0, 1e-6)
)

# g²(τ) autocorrelation
tau, g2 = counter.calculate_g2(
    tau_max=1e-6,
    n_bins=100
)

# Fano factor
F = counter.fano_factor(window=1e-6)

Complete Measurement

Example: ODMR measurement with all interfaces:

from sim import HamiltonianBuilder, LindbladSolver
from sim.hamiltonian.terms import ZFS, Zeeman, MicrowaveDrive
from sim.states import ground_state, projector_excited
from sim.interfaces import LaserInterface, PhotonCounter
import numpy as np

# Setup
laser = LaserInterface(wavelength=532e-9)
counter = PhotonCounter(efficiency=0.1)

# Sweep MW frequency
freqs = np.linspace(2.8, 2.94, 100)  # GHz
counts = []

for f in freqs:
    # Build Hamiltonian
    H = HamiltonianBuilder()
    H.add(ZFS(D=2.87))
    H.add(Zeeman(B=10))
    H.add(MicrowaveDrive(omega=1, detuning=f-2.87))

    # Simulate
    solver = LindbladSolver(H)
    solver.add_t2_dephasing(gamma=1e6)
    solver.add_optical_decay(gamma=8e7)

    rho0 = ground_state()
    result = solver.evolve(rho0, (0, 1e-6), n_steps=100)

    # Count photons
    counter.reset()
    for rho in result.rho_t:
        counter.count(rho, dt=10e-9, gamma=8e7)

    counts.append(counter.total_counts)

# Plot ODMR spectrum
import matplotlib.pyplot as plt
plt.plot(freqs, counts)
plt.xlabel("MW Frequency (GHz)")
plt.ylabel("Photon Counts")