Skip to content

Ensemble Analysis

The analysis subpackage provides DefectEnsemble for managing a spatial ensemble of spin defects with their local E- and B-fields, and ParameterSweep for systematic parameter exploration.


SpinDefectSim.analysis.ensemble

DefectEnsemble

from SpinDefectSim.analysis.ensemble import DefectEnsemble
# or:
import SpinDefectSim as sds
sds.DefectEnsemble

An ensemble of N spin-defect centres distributed across a sensing patch. The class handles defect placement, local field computation (E and B), and wrapping the ensemble into a SensingExperiment.

Inherits: PhysicalParams, PlottingMixin, SerializationMixin, SweepMixin


Constructor

DefectEnsemble(
    N_def: Optional[int] = None,
    R_patch: Optional[float] = None,
    defaults: Optional[Defaults] = None,
)

Parameters:

Name Type Description
N_def int \| None Number of defects; if None, uses defaults.N_def
R_patch float \| None Sensing-patch radius (m); if None, uses defaults.R_patch
defaults Defaults \| None Global defaults object

Attributes after construction:

Attribute Type Set by
N_def int constructor
R_patch float constructor
defect_positions np.ndarray (N, 2) \| None generate_defects / set_defects
quantization_axes np.ndarray (N, 3) \| None generate_defects / set_quantization_axis
E_fields np.ndarray (N, 3) \| None compute_efields / set_efields
B_extra_fields np.ndarray (N, 3) \| None compute_bfields / set_bfields

Defect Placement

generate_defects

def generate_defects(
    seed: int = 0,
    quantization_axis=None,
) -> np.ndarray

Randomly place N_def defects with uniform probability inside a circular patch of radius R_patch.

Parameters:

Name Type Description
seed int NumPy random seed for reproducibility
quantization_axis None \| "random" \| array_like Defect orientation(s): None → all along z; "random" → uniform random on sphere; shape (3,) → all same; shape (N, 3) → per-defect

Returns: np.ndarray — defect positions, shape (N, 2), in metres

Example:

ens = DefectEnsemble(N_def=300, R_patch=200e-9)
positions = ens.generate_defects(seed=42, quantization_axis="random")


generate_defects_gaussian

def generate_defects_gaussian(
    beam_waist_m: float,
    seed: int = 0,
    n_sigma: float = 3.0,
) -> np.ndarray

Place defects with a Gaussian probability density (modelling the laser beam intensity profile). The patch is truncated at n_sigma × beam_waist_m.

Parameters:

Name Type Description
beam_waist_m float 1/e² beam radius (m)
seed int Random seed
n_sigma float Truncation radius in units of beam_waist_m

Returns: np.ndarray shape (N, 2)


set_defects

def set_defects(positions: np.ndarray) -> None

Manually supply defect positions instead of generating them randomly.

Parameters:

Name Type Description
positions np.ndarray Shape (N, 2), in metres

set_quantization_axis

def set_quantization_axis(spec, seed: int = 0) -> None

Set per-defect quantization axes after defect placement.

Parameters:

Name Type Description
spec "random" \| array_like "random" → uniform on sphere; shape (3,) → all same; shape (N, 3) → per-defect

n_defects_from_ppm (static method)

@staticmethod
def n_defects_from_ppm(
    ppm: float,
    beam_waist_m: float,
    hbn_thickness_m: float,
    layer_spacing_m: float = 0.334e-9,
    density_kg_m3: float = 2100.0,
    molar_mass_kg_mol: float = 24.82e-3,
) -> int

Estimate the number of defects inside a Gaussian laser beam from a defect concentration in parts per million (ppm).

Parameters:

Name Type Description
ppm float Defect concentration (ppm by substitution)
beam_waist_m float 1/e² beam radius (m)
hbn_thickness_m float Total hBN crystal thickness (m)
layer_spacing_m float Interlayer spacing (m); default 0.334 nm for hBN
density_kg_m3 float Crystal density (kg/m³); default 2100 kg/m³ for hBN
molar_mass_kg_mol float Molar mass (kg/mol); default for hBN

Returns: int — estimated number of defects


E-field Computation

compute_efields

def compute_efields(
    *,
    E0_gate=(0, 0, 0),
    gate_grad=None,
    disorder_xyzq=None,
    verbose: bool = False,
) -> np.ndarray

Compute the local E-field at each defect from the combination of a uniform (+ gradient) gate-bias field and optionally screened disorder charge contributions.

Parameters:

Name Type Description
E0_gate array_like (3,) Uniform gate-bias field (V/m)
gate_grad np.ndarray (2,2) \| None Linear gradient [[dEx/dx, dEx/dy], [dEy/dx, dEy/dy]] in V/m²
disorder_xyzq np.ndarray (M, 4) \| None Disorder point charges [x, y, z, q] in SI units
verbose bool Print progress

Returns: np.ndarray — local E-fields, shape (N, 3), in V/m. Also stored as self.E_fields.


set_efields

def set_efields(E_fields: np.ndarray) -> None

Manually supply pre-computed E-fields.

Parameters:

Name Type Description
E_fields np.ndarray Shape (N, 3), in V/m

efields_from_callable

def efields_from_callable(
    E_func: Callable,
    *,
    add: bool = False,
) -> np.ndarray

Evaluate a user-supplied function at each defect position to set the E-field.

Parameters:

Name Type Description
E_func Callable Function with signature E_func(x, y) -> (3,) array
add bool If True, add to existing self.E_fields rather than replacing

Returns: np.ndarray shape (N, 3) in V/m


efields_from_grid

def efields_from_grid(
    Ex: np.ndarray,
    Ey: np.ndarray,
    Ez: np.ndarray,
    x_coords: np.ndarray,
    y_coords: np.ndarray,
    z_coords: Optional[np.ndarray] = None,
    *,
    z_defect: Optional[float] = None,
    method: str = "linear",
    bounds_error: bool = False,
    fill_value: float = 0.0,
    add: bool = False,
) -> np.ndarray

Interpolate E-field components from a regular 2-D or 3-D grid onto the defect positions using scipy.interpolate.RegularGridInterpolator.

Parameters:

Name Type Description
Ex, Ey, Ez np.ndarray Field components; shape (ny, nx) for 2-D or (nz, ny, nx) for 3-D
x_coords, y_coords np.ndarray 1-D coordinate arrays (m)
z_coords np.ndarray \| None 1-D z coordinate array (m); required for 3-D grids
z_defect float \| None z-height at which to sample the field (m); uses defaults.z_defect if None
method str Interpolation method: "linear" (default) or "nearest"
bounds_error bool Raise if query points are out of bounds
fill_value float Value returned for out-of-bounds queries
add bool Add to existing self.E_fields rather than replacing

Returns: np.ndarray shape (N, 3) in V/m


B-field Computation

set_bfields

def set_bfields(B_fields: np.ndarray) -> None

Manually supply stray B-fields.

Parameters:

Name Type Description
B_fields np.ndarray Shape (N, 3), in T

compute_bfields

def compute_bfields(
    magnetization: Union[np.ndarray, Callable],
    geometry: SampleGeometry,
    *,
    n_pts: int = 100,
    include_bulk: bool = True,
    include_edge: bool = True,
    add: bool = False,
) -> np.ndarray

Compute the stray B-field at each defect from a 2-D in-plane magnetization distribution using the Biot-Savart law.

Parameters:

Name Type Description
magnetization np.ndarray \| Callable M_z grid, shape (ny, nx) in A, or callable (x, y) → float
geometry SampleGeometry Sample geometry (polygon, disk, square, …)
n_pts int Grid resolution for bulk current integration
include_bulk bool Include contribution from non-uniform bulk ∇×M
include_edge bool Include contribution from edge currents
add bool Add to existing self.B_extra_fields rather than replacing

Returns: np.ndarray shape (N, 3) in T


bfields_from_callable

def bfields_from_callable(B_func: Callable, *, add: bool = False) -> np.ndarray

Evaluate a user-supplied function at each defect position to set the B-field.

Parameters:

Name Type Description
B_func Callable Function B_func(x, y) -> (3,) array in T

bfields_from_grid

def bfields_from_grid(
    Bx: np.ndarray,
    By: np.ndarray,
    Bz: np.ndarray,
    x_coords: np.ndarray,
    y_coords: np.ndarray,
    z_coords: Optional[np.ndarray] = None,
    *,
    z_defect: Optional[float] = None,
    method: str = "linear",
    bounds_error: bool = False,
    fill_value: float = 0.0,
    add: bool = False,
) -> np.ndarray

Interpolate B-field components from a regular grid onto defect positions. Same grid conventions as efields_from_grid.


Running experiments

to_experiment

def to_experiment(sensing: str = "E") -> SensingExperiment

Wrap the ensemble into a SensingExperiment. The "with" and "no" branches differ by whether the E- or B-field signal is present.

Parameters:

Name Type Description
sensing str "E" — modulate E-field; "B" — modulate B-field; "both" — modulate both

Returns: SensingExperiment

Example:

ens = DefectEnsemble(N_def=400, R_patch=250e-9)
ens.generate_defects()
ens.compute_efields(E0_gate=(0, 0, 2e6))
exp = ens.to_experiment(sensing="E")
tau_s, S_w, S_no, dS, tau_opt, dS_peak = exp.ramsey()


SpinDefectSim.analysis.sweep

ParameterSweep

from SpinDefectSim.analysis.sweep import ParameterSweep

Convenience class for running Cartesian parameter sweeps. Inherits SweepMixin, PlottingMixin, SerializationMixin.

Constructor

ParameterSweep(
    N_def: int = 200,
    seed: int = 0,
    tau_echo_s: Optional[np.ndarray] = None,
    defaults: Optional[Defaults] = None,
)

Parameters:

Name Type Description
N_def int Default ensemble size
seed int Default random seed
tau_echo_s np.ndarray \| None Custom τ axis for echo sweeps
defaults Defaults \| None Global defaults

make_ensemble

def make_ensemble(**override_defaults) -> DefectEnsemble

Build a DefectEnsemble with defects already placed according to the sweep's N_def and seed, optionally overriding any Defaults fields.

Parameters:

Name Type Description
**override_defaults Any Defaults field name and value

Returns: DefectEnsemble (with defect_positions already set, E_fields not yet computed)

Example:

sweep = ParameterSweep(N_def=300, seed=7)

def run(E_gate, B_mT):
    ens = sweep.make_ensemble(B_mT=B_mT)
    ens.compute_efields(E0_gate=(0, 0, E_gate))
    exp = ens.to_experiment(sensing="E")
    _, _, _, dS, tau_opt, dS_peak = exp.ramsey()
    return {"dS_peak": dS_peak, "tau_opt_ns": tau_opt * 1e9}

results = sweep.sweep(run, E_gate=[1e6, 5e6, 10e6], B_mT=[1.0, 2.0, 5.0])