Skip to content

Spin Module

The spin subpackage contains everything related to single-spin physics: defect presets, spin Hamiltonians, nuclear hyperfine coupling, spin matrices, CW and echo ODMR spectra, and the optical rate model for contrast estimation.


SpinDefectSim.spin.defects

DefectType

from SpinDefectSim.spin.defects import DefectType

Dataclass holding the physical parameters for a single ODMR-active spin-defect species.

Fields

Field Type Default Description
name str Human-readable label
spin float Electron spin quantum number S
D0_Hz float 0.0 Axial zero-field splitting (Hz)
E0_Hz float 0.0 Transverse strain splitting (Hz)
d_perp float 0.0 Transverse E-field coupling (Hz / (V/m))
d_parallel float 0.0 Axial E-field coupling (Hz / (V/m))
gamma_Hz_T float 28e9 Electron gyromagnetic ratio (Hz/T)
ms0_index int 1 Index of the |m_s = 0⟩ state in the descending-Sz basis
nuclear_spins List[NuclearSpin] [] Coupled nuclear spins (nearest-neighbour environment)
rate_params RateParams \| None None Rate constants for the optical / ISC cycle
notes str "" Free-text notes

Built-in presets

Constant Name Host S D
VB_MINUS "vb_minus" hBN 1 3.46 GHz
NV_MINUS "nv_minus" diamond 1 2.87 GHz
V_SIC "v_sic" 4H-SiC 1 1.28 GHz
P1_CENTER "p1" diamond ½ 0
CR_GAN "cr_gaN" GaN 3/2 1.80 GHz

Example — custom defect

from SpinDefectSim.spin.defects import DefectType

my_defect = DefectType(
    name="my_V2",
    spin=1,
    D0_Hz=1.5e9,
    E0_Hz=20e6,
    d_perp=0.15,
    notes="Hypothetical spin-1 defect",
)

get_defect

from SpinDefectSim.spin.defects import get_defect

def get_defect(name: Union[str, DefectType]) -> DefectType

Return a built-in DefectType preset by name, or pass a DefectType instance through unchanged (identity pass-through for polymorphic code).

Parameters:

Name Type Description
name str \| DefectType Preset name string or an existing DefectType

Returns: DefectType

Raises: KeyError if the name is not recognized.

Example:

nv = get_defect("nv_minus")
print(nv.D0_Hz)  # 2.87e9


list_defects

from SpinDefectSim.spin.defects import list_defects

def list_defects() -> None

Print a formatted table of all built-in defect presets to stdout.


SpinDefectSim.spin.hamiltonian

SpinParams

from SpinDefectSim.spin.hamiltonian import SpinParams

Dataclass fully specifying a single-spin Hamiltonian — the minimal input needed by odmr_hamiltonian_Hz and related functions.

Fields

Field Type Description
D0 float Axial ZFS (Hz)
E0 float Transverse ZFS strain (Hz)
d_perp_Hz_per_Vpm float Transverse E-field coupling
B_T np.ndarray Lab-frame bias B-field, shape (3,), in T
gamma_e_Hz_per_T float Electron gyromagnetic ratio (Hz/T), default 28e9
d_parallel_Hz_per_Vpm float Axial E-field coupling (default 0)
B_extra_T np.ndarray Additional B-field (stray signal), shape (3,), in T
spin float Quantum number S (default 1.0)
ms0_index int Index of the |m_s = 0⟩-like basis state (default 1)
quantization_axis np.ndarray Defect z′-axis in the lab frame, shape (3,) (default [0,0,1])

odmr_hamiltonian_Hz

from SpinDefectSim.spin.hamiltonian import odmr_hamiltonian_Hz

def odmr_hamiltonian_Hz(sp: SpinParams, E_vec_lab: np.ndarray) -> np.ndarray

Build the electron-only spin Hamiltonian H/h (in Hz) for an arbitrary spin-S defect. The applied E- and B-fields are automatically rotated from the lab frame into the defect's local frame using the quantization_axis.

Parameters:

Name Type Description
sp SpinParams Spin parameters
E_vec_lab np.ndarray Electric field in the lab frame, shape (3,), in V/m

Returns: np.ndarray — complex128 Hermitian matrix, shape (2S+1, 2S+1), in Hz

Example:

import numpy as np
from SpinDefectSim.spin.hamiltonian import SpinParams, odmr_hamiltonian_Hz

sp = SpinParams(D0=3.46e9, E0=50e6, d_perp_Hz_per_Vpm=0.35,
                B_T=np.array([0, 0, 1.5e-3]))
H = odmr_hamiltonian_Hz(sp, E_vec_lab=np.zeros(3))
print(H.shape)   # (3, 3)


full_hyperfine_hamiltonian_Hz

from SpinDefectSim.spin.hamiltonian import full_hyperfine_hamiltonian_Hz

def full_hyperfine_hamiltonian_Hz(
    sp: SpinParams,
    E_vec_lab: np.ndarray,
    nuclear_spins: list,
) -> np.ndarray

Build the complete H/h (Hz) in the tensor-product Hilbert space H_e ⊗ H_n1 ⊗ H_n2 ⊗ …. Includes the electron-spin Hamiltonian plus hyperfine coupling, nuclear Zeeman, and quadrupole terms for each nuclear spin.

Parameters:

Name Type Description
sp SpinParams Electron spin parameters
E_vec_lab np.ndarray Electric field in lab frame (V/m)
nuclear_spins List[NuclearSpin] Nuclear spins to include

Returns: np.ndarray — complex128 Hermitian matrix, shape (dim_total, dim_total) where dim_total = (2S+1) × Π(2Iₖ+1)


odmr_transitions_Hz

from SpinDefectSim.spin.hamiltonian import odmr_transitions_Hz

def odmr_transitions_Hz(
    H: np.ndarray,
    electron_dim: int,
    ms0_basis_index: int,
    overlap_threshold: float = 0.1,
) -> np.ndarray

Diagonalise a (possibly full hyperfine) Hamiltonian and extract all ODMR transition frequencies originating from the |m_s = 0⟩-like eigenstate(s).

Parameters:

Name Type Description
H np.ndarray Hamiltonian matrix (from full_hyperfine_hamiltonian_Hz)
electron_dim int Electron Hilbert space dimension (2S+1)
ms0_basis_index int Index of the |ms=0⟩ state in the electron basis
overlap_threshold float Minimum |⟨ms=0|ψ⟩|² to qualify as an ms=0-like state

Returns: np.ndarray — float64 array of frequencies (Hz), sorted ascending


diagonalize_hamiltonian

from SpinDefectSim.spin.hamiltonian import diagonalize_hamiltonian

def diagonalize_hamiltonian(H: np.ndarray) -> tuple[np.ndarray, np.ndarray]

Diagonalise a Hermitian Hamiltonian using np.linalg.eigh (eigenvalues sorted ascending).

Parameters:

Name Type Description
H np.ndarray Hermitian matrix (H/h in Hz)

Returns: (eigenvalues_Hz, eigenvectors) — both as complex128 arrays; eigenvalues are sorted ascending


extract_ms0_like_transitions_Hz

from SpinDefectSim.spin.hamiltonian import extract_ms0_like_transitions_Hz

def extract_ms0_like_transitions_Hz(
    evals_Hz: np.ndarray,
    evecs: np.ndarray,
    ms0_basis_index: int = 1,
) -> tuple[np.ndarray, int]

Given pre-computed eigenvalues and eigenvectors, find the eigenstate with the highest overlap with the |ms=0⟩ basis state and return all transition frequencies from that state.

Parameters:

Name Type Description
evals_Hz np.ndarray Eigenvalues (Hz), sorted ascending
evecs np.ndarray Eigenvectors, column j is the j-th eigenstate
ms0_basis_index int Basis row index corresponding to |ms=0⟩

Returns: (freqs_Hz, i0) — frequency array in Hz and the eigenstate index i0


SpinDefect

from SpinDefectSim.spin.hamiltonian import SpinDefect
# or simply:
import SpinDefectSim as sds
sds.SpinDefect

High-level object representing a single ODMR-active spin defect. Inherits PhysicalParams.

Constructor

SpinDefect(
    defect_type: Union[str, DefectType, None] = None,
    *,
    spin: Optional[float] = None,
    B_mT: Optional[float] = None,
    B_vec_mT: Optional[array_like] = None,
    quantization_axis: Optional[array_like] = None,
    D0_Hz: Optional[float] = None,
    E0_Hz: Optional[float] = None,
    d_perp: Optional[float] = None,
    d_parallel: Optional[float] = None,
    nuclear_spins: Optional[List[NuclearSpin]] = None,
    defaults: Optional[Defaults] = None,
)

Parameters:

Name Type Description
defect_type str \| DefectType \| None Preset name or custom DefectType; None → use defaults.defect_type
spin float \| None Override spin quantum number S
B_mT float \| None Scalar bias B-field magnitude (mT), applied along z
B_vec_mT array_like \| None Full B-field vector (mT), shape (3,) — overrides B_mT
quantization_axis array_like \| None Defect z′-axis in lab frame, shape (3,)
D0_Hz float \| None Override axial ZFS (Hz)
E0_Hz float \| None Override transverse strain (Hz)
d_perp float \| None Override transverse E-field coupling
d_parallel float \| None Override axial E-field coupling
nuclear_spins list \| None Override nuclear-spin list
defaults Defaults \| None Global defaults object

Attributes:

  • spin_paramsSpinParams instance built from the constructor arguments
  • defect_type — resolved DefectType
  • nuclear_spins — list of NuclearSpin objects

Methods


hamiltonian
def hamiltonian(E_vec_Vpm=(0., 0., 0.)) -> np.ndarray

Return the electron-only Hamiltonian H/h (Hz), shape (2S+1, 2S+1).

Parameters:

Name Type Description
E_vec_Vpm array_like Electric field in lab frame (V/m)

Returns: np.ndarray complex128, shape (2S+1, 2S+1)


full_hamiltonian
def full_hamiltonian(E_vec_Vpm=(0., 0., 0.)) -> np.ndarray

Return H/h in the full electron ⊗ nuclear Hilbert space.

Returns: np.ndarray complex128, shape (dim_total, dim_total)


diagonalize
def diagonalize(E_vec_Vpm=(0., 0., 0.)) -> tuple[np.ndarray, np.ndarray]

Return (eigenvalues_Hz, eigenvectors) of the electron-only Hamiltonian.


transition_frequencies
def transition_frequencies(E_vec_Vpm=(0., 0., 0.)) -> np.ndarray

Return all ODMR transition frequencies from the |ms=0⟩-like state (Hz), sorted ascending.

Returns: np.ndarray float64, length 2S

Example:

d = SpinDefect("vb_minus", B_mT=2.0)
freqs = d.transition_frequencies()
# → array([3.403e9, 3.517e9]) Hz (approximate)


hyperfine_transitions
def hyperfine_transitions(E_vec_Vpm=(0., 0., 0.), overlap_threshold=0.1) -> np.ndarray

ODMR transition frequencies from the full hyperfine Hamiltonian, sorted ascending.

Returns: np.ndarray float64


zero_field_splitting
def zero_field_splitting() -> float

Return the maximum transition splitting at zero E-field (Hz).


to_experiment
def to_experiment(
    E_vec_Vpm=(0., 0., 0.),
    B_extra_T=(0., 0., 0.),
    *,
    sensing: str = "both",
) -> SensingExperiment

Wrap this single spin into a SensingExperiment with N_def=1.

Parameters:

Name Type Description
E_vec_Vpm array_like Applied E-field (V/m)
B_extra_T array_like Additional signal B-field (T)
sensing str "E", "B", or "both"

Returns: SensingExperiment


SpinDefectSim.spin.nuclear

NuclearSpin

from SpinDefectSim.spin.nuclear import NuclearSpin

Dataclass describing a single nuclear spin coupled to the electron spin via a hyperfine tensor.

Fields

Field Type Description
spin float Nuclear spin quantum number I
A_tensor_Hz np.ndarray 3×3 hyperfine tensor in Hz (in the defect local frame)
gamma_Hz_T float Nuclear gyromagnetic ratio γ_n (Hz/T)
label str Isotope label, e.g. "14N"
quadrupole_Hz float Nuclear quadrupole coupling P (Hz); only relevant for I ≥ 1

axial_A_tensor

from SpinDefectSim.spin.nuclear import axial_A_tensor

def axial_A_tensor(A_zz_Hz: float, A_perp_Hz: float) -> np.ndarray

Return a diag([A_perp, A_perp, A_zz]) 3×3 array — the standard form for axially symmetric hyperfine coupling.

Returns: np.ndarray shape (3, 3)

Example:

A = axial_A_tensor(A_zz_Hz=-2.14e6, A_perp_Hz=-2.70e6)


isotropic_A_tensor

from SpinDefectSim.spin.nuclear import isotropic_A_tensor

def isotropic_A_tensor(A_iso_Hz: float) -> np.ndarray

Return A_iso × I₃.


Isotope gyromagnetic ratios

Pre-defined constants (Hz/T) for common isotopes:

Constant Isotope I Value (Hz/T)
GAMMA_14N ¹⁴N 1 3.077e6
GAMMA_15N ¹⁵N ½ -4.316e6
GAMMA_11B ¹¹B 3/2 13.660e6
GAMMA_10B ¹⁰B 3 4.575e6
GAMMA_13C ¹³C ½ 10.708e6
GAMMA_29Si ²⁹Si ½ -5.319e6

SpinDefectSim.spin.matrices

spin_matrices

from SpinDefectSim.spin.matrices import spin_matrices

def spin_matrices(S: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]

Return the four spin operators Sx, Sy, Sz, and the identity I for an arbitrary spin quantum number S.

Parameters:

Name Type Description
S float Spin quantum number (0.5, 1, 1.5, 2, …)

Returns: (Sx, Sy, Sz, I) — each complex128, shape (2S+1, 2S+1)

Basis: states ordered as {|+S⟩, |+S−1⟩, …, |−S⟩} (descending Sz eigenvalues).

Example:

Sx, Sy, Sz, I = spin_matrices(1.0)
# Sz has eigenvalues +1, 0, -1


spin_1_matrices and spin_half_matrices

from SpinDefectSim.spin.matrices import spin_1_matrices, spin_half_matrices

Convenience functions returning the explicit spin-1 and spin-½ operators respectively, with the same (Sx, Sy, Sz, I) signature.


SpinDefectSim.spin.rates

RateParams

from SpinDefectSim.spin.rates import RateParams

Dataclass holding the optical / intersystem-crossing (ISC) rate constants for one defect species.

Fields

Field Type Description
spin float Electron spin quantum number S
k_optical float Laser excitation rate (s⁻¹)
k_rad float Radiative decay rate (s⁻¹)
k_isc_excited Sequence[float] ISC rates from each excited state, length 2S+1 (s⁻¹)
k_from_shelving Sequence[float] Return rates from shelving state to each ground state, length 2S+1 (s⁻¹)
k_nr float Non-radiative quenching rate (s⁻¹), default 0.0
notes str Free-text notes

Property:

  • has_shelving: bool — True if any ISC rate is nonzero

Built-in presets

Constant Defect S Approx. contrast
NV_RATES NV⁻ diamond 1 ~23%
VB_RATES VB⁻ hBN 1 ~2%
VSIC_RATES V_SiC 4H-SiC 1 ~15%
P1_RATES P1 diamond ½ 0%
CRGAN_RATES Cr GaN 3/2

RateModel

from SpinDefectSim.spin.rates import RateModel

Steady-state rate-equation solver for CW ODMR contrast.

Constructor

RateModel(rate_params: RateParams, ms0_index: int)

Parameters:

Name Type Description
rate_params RateParams Rate constants
ms0_index int Index of the ground |ms=0⟩ state

steady_state

def steady_state(k_mw: float = 0.0, mw_pair=None) -> np.ndarray

Solve for the steady-state population vector under CW illumination.

Parameters:

Name Type Description
k_mw float MW driving rate between two states (s⁻¹)
mw_pair tuple \| None (i, j) — which ground-state pair the MW drives

Returns: np.ndarray float, populations normalised to sum = 1


pl

def pl(k_mw: float = 0.0, mw_pair=None) -> float

Steady-state photoluminescence (proportional to photon count rate).


contrast

def contrast(mw_pair=None, k_mw=None) -> float

CW ODMR contrast C = (PL_off − PL_on) / PL_off.

Returns: float ∈ (0, 1)

Example:

from SpinDefectSim.spin.rates import RateModel, VB_RATES

model = RateModel(VB_RATES, ms0_index=1)
C = model.contrast()
print(f"VB⁻ contrast ≈ {C:.1%}")  # ≈ 2%


SpinDefectSim.spin.spectra

lorentzian

from SpinDefectSim.spin.spectra import lorentzian

def lorentzian(f: np.ndarray, f0: float, fwhm: float) -> np.ndarray

Normalised Lorentzian with unit peak: L(f; f0, fwhm).

Returns: np.ndarray same shape as f


PL_model

from SpinDefectSim.spin.spectra import PL_model

def PL_model(
    f_MW_Hz: np.ndarray,
    transitions_Hz: np.ndarray,
    linewidth_Hz: float,
    contrast: float = 0.02,
) -> np.ndarray

Compute the normalised CW photoluminescence spectrum for a single defect.

The model is:

\[\text{PL}(f) = 1 - C \sum_{i} \frac{L(f;\, f_i,\, \Delta f)}{N_\text{trans}}\]

Parameters:

Name Type Description
f_MW_Hz np.ndarray MW frequency axis (Hz)
transitions_Hz np.ndarray ODMR transition centre frequencies (Hz)
linewidth_Hz float Lorentzian FWHM (Hz)
contrast float Peak contrast C ∈ (0, 1)

Returns: np.ndarray float, PL values in approximately [1 − C, 1]


ensemble_transitions_from_Efields

from SpinDefectSim.spin.spectra import ensemble_transitions_from_Efields

def ensemble_transitions_from_Efields(
    E_fields_Vpm: np.ndarray,
    spin_params: SpinParams,
    ms0_basis_index: Optional[int] = None,
    quantization_axes: Optional[np.ndarray] = None,
    B_extra_fields: Optional[np.ndarray] = None,
) -> list

Compute the ODMR transition frequencies for each defect in an ensemble, given their local electric fields.

Parameters:

Name Type Description
E_fields_Vpm np.ndarray Local E-fields, shape (N, 3), in V/m
spin_params SpinParams Shared spin parameters (bias B-field, ZFS, etc.)
ms0_basis_index int \| None Override |ms=0⟩ basis index
quantization_axes np.ndarray \| None Per-defect z′-axes, shape (N, 3)
B_extra_fields np.ndarray \| None Per-defect stray B-fields, shape (N, 3), in T

Returns: list — length N; each element is a float64 array of transition frequencies (Hz)


ensemble_odmr_spectrum

from SpinDefectSim.spin.spectra import ensemble_odmr_spectrum

def ensemble_odmr_spectrum(
    f_axis: np.ndarray,
    transitions_list: list,
    fwhm: float,
    contrast: float = 0.10,
) -> np.ndarray

Build the inhomogeneously broadened ensemble CW ODMR spectrum by summing individual Lorentzians.

Parameters:

Name Type Description
f_axis np.ndarray MW frequency axis (Hz)
transitions_list list Output of ensemble_transitions_from_Efields
fwhm float Single-defect Lorentzian linewidth (Hz)
contrast float Per-defect contrast

Returns: np.ndarray float, same shape as f_axis (1 = no resonance, dips toward 1 − contrast)


SpinDefectSim.spin.echo

spin_echo_effective_fwhm

from SpinDefectSim.spin.echo import spin_echo_effective_fwhm

def spin_echo_effective_fwhm(T2_s: float) -> float

Return the Fourier-transform-limited echo linewidth: 1 / (π T₂).


echo_detected_odmr_spectrum

from SpinDefectSim.spin.echo import echo_detected_odmr_spectrum

def echo_detected_odmr_spectrum(
    f_axis: np.ndarray,
    transitions_list: list,
    T2_s: float,
    contrast: float = 0.10,
) -> np.ndarray

Echo-detected ensemble ODMR spectrum using T₂-limited Lorentzian linewidths.


ensemble_echo_signal

from SpinDefectSim.spin.echo import ensemble_echo_signal

def ensemble_echo_signal(
    freqs_list: list,
    tau_range_s: np.ndarray,
    T2_s: float,
    reference_Hz=None,
) -> np.ndarray

Ensemble-averaged Hahn-echo amplitude S(τ) vs free-precession time τ.

Parameters:

Name Type Description
freqs_list list Per-defect transition frequency arrays (Hz)
tau_range_s np.ndarray 1-D array of τ values (s)
T2_s float T₂ coherence time (s)
reference_Hz float \| np.ndarray \| None Lock-in demodulation reference frequency (Hz); None → auto-select

Returns: np.ndarray 1-D, values in [−1, 1]


lock_in_difference_echo

from SpinDefectSim.spin.echo import lock_in_difference_echo

def lock_in_difference_echo(
    freqs_with: list,
    freqs_no: list,
    tau_range_s: np.ndarray,
    T2_s: float,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]

Modulated lock-in echo signal ΔS(τ) = S_with(τ) − S_no(τ).

Returns: (S_with, S_no, S_diff) — each 1-D array over τ


lock_in_odmr_spectrum

from SpinDefectSim.spin.echo import lock_in_odmr_spectrum

def lock_in_odmr_spectrum(
    f_axis: np.ndarray,
    freqs_with: list,
    freqs_no: list,
    T2_s: float,
    contrast: float = 0.10,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]

Frequency-domain lock-in spectrum ΔPL(f) = PL_with(f) − PL_no(f).

Returns: (diff_pl, pl_with, pl_no) — each 1-D array over f_axis


lock_in_difference_ramsey

from SpinDefectSim.spin.echo import lock_in_difference_ramsey

def lock_in_difference_ramsey(
    freqs_with: list,
    freqs_no: list,
    tau_range_s: np.ndarray,
    T2_s: float,
    f_ref_Hz=None,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, float]

Modulated lock-in Ramsey signal ΔS(τ) = S_with(τ) − S_no(τ).

Returns: (S_with, S_no, S_diff, f_ref_Hz) — the demodulation reference frequency used is also returned