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¶
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¶
Manually supply defect positions instead of generating them randomly.
Parameters:
| Name | Type | Description |
|---|---|---|
positions |
np.ndarray |
Shape (N, 2), in metres |
set_quantization_axis¶
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¶
Manually supply pre-computed E-fields.
Parameters:
| Name | Type | Description |
|---|---|---|
E_fields |
np.ndarray |
Shape (N, 3), in V/m |
efields_from_callable¶
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¶
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¶
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¶
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¶
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¶
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])