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¶
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:
list_defects¶
Print a formatted table of all built-in defect presets to stdout.
SpinDefectSim.spin.hamiltonian¶
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_params—SpinParamsinstance built from the constructor argumentsdefect_type— resolvedDefectTypenuclear_spins— list ofNuclearSpinobjects
Methods¶
hamiltonian¶
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¶
Return H/h in the full electron ⊗ nuclear Hilbert space.
Returns: np.ndarray complex128, shape (dim_total, dim_total)
diagonalize¶
Return (eigenvalues_Hz, eigenvectors) of the electron-only Hamiltonian.
transition_frequencies¶
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¶
ODMR transition frequencies from the full hyperfine Hamiltonian, sorted ascending.
Returns: np.ndarray float64
zero_field_splitting¶
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¶
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:
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:
spin_1_matrices and 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¶
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¶
Steady-state rate-equation solver for CW ODMR contrast.
Constructor¶
Parameters:
| Name | Type | Description |
|---|---|---|
rate_params |
RateParams |
Rate constants |
ms0_index |
int |
Index of the ground |ms=0⟩ state |
steady_state¶
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¶
Steady-state photoluminescence (proportional to photon count rate).
contrast¶
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:
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