v2/crates/nvsim/README.md
Deterministic Rust simulator for NV-diamond ensemble magnetometers. Synthesise the magnetic-field trace a real sensor would have produced — without the hardware, the lab, or the $8 K vendor receipt.
NV-diamond magnetometers are exotic but real: they detect magnetic fields by
shining green laser at a diamond and watching how its red fluorescence shifts
under microwave excitation. They are sensitive enough to feel a person's
heartbeat from across a room — when they work. The catch: a working ensemble
sensor costs ~$8 K and lives in a lab. nvsim runs the same forward
pipeline in software, end-to-end, deterministically, so you can ask "what
would my magnetometer have seen if a steel rebar walked past it" without
wiring up any of it.
It is not a hardware-control stack, microscope simulator, full Hamiltonian solver, or claim of fT-level sensitivity. This crate does not control lasers, microwave sources, ADC hardware, or real NV sensors. It is a deterministic Rust simulator with explicit physics approximations and no hidden mocks — every formula is cited; every conjectural default is flagged in code; every random number comes from a seeded ChaCha20 PRNG.
| If you are a… | …nvsim lets you… |
|---|---|
| Sensor researcher evaluating a new pipeline | Replay a synthetic trace through your own DSP and check it against a published-physics ground truth before buying hardware |
| DSP / ML engineer building anomaly detectors | Generate magnetic-anomaly traces with a known answer key — useful for regression replay, deterministic CI, and "did my detector regress?" gates |
| Educator teaching magnetometry / NV physics | Run real Biot-Savart, Lorentzian ODMR, and 4-axis projection in Rust without standing up a Python+QuTiP environment |
| RuView pipeline contributor | Get a binary MagFrame shape (0xC51A_6E70) you can plumb into existing observability, with optional ruvector trace compression behind a feature flag |
| Auditor / compliance reviewer | Re-run the included determinism check (same scene + seed → byte-identical proof bundle) and verify the simulator's output across machines without re-running the whole pipeline |
| Capability | What's in the crate |
|---|---|
| Scene primitives | DipoleSource, CurrentLoop, FerrousObject, EddyCurrent, Scene aggregate. JSON round-trip safe. |
| Magnetic-field synthesis | Closed-form analytic dipole, numerical Biot-Savart over 64-segment current loops, linearly-induced ferrous-object moment, multi-source aggregation. All in f64 for near-field stability; clamped at 1 mm with a saturation flag. |
| Per-material attenuation | Air / drywall / brick / dry concrete / reinforced concrete / sheet steel — with a HEAVY_ATTENUATION flag for the materials whose loss values are admittedly conjectural. NaN-safe on adversarial input (negative or non-finite path lengths). |
| NV-ensemble physics | ODMR Lorentzian (FWHM ≈ 1 MHz), shot-noise floor δB ∝ 1/(γ_e·C·√(N·t·T₂*)), T₂ decay envelope, 4-axis 〈111〉 crystallographic projection with closed-form LSQ inversion. Defaults match Barry et al. Rev. Mod. Phys. 92 (2020) Table III for COTS bulk diamond. |
| Determinism | Same (B_in, dt, seed) → byte-identical NvReading. ChaCha20-seeded shot noise; no global state, no time-of-day field, no allocator randomness. |
| Binary frame format | MagFrame — 60-byte fixed-layout record, magic 0xC51A_6E70 (distinct from ADR-018 CSI 0xC51F... and ADR-084 sketch 0xC511_0084). Round-trips byte-exact, deserialiser rejects bad magic / bad version / wrong length without panicking. |
digitiser.rs — ADC quantization + 4ᵗʰ-order Butterworth anti-alias + lockin demodulationpipeline.rs — wires every stage end-to-end and emits a MagFrame streamproof.rs + criterion bench — deterministic SHA-256 witness bundle + ≥ 1 kHz wall-clock throughput targetThese complete the six-pass plan in
docs/research/quantum-sensing/15-nvsim-implementation-plan.md.
The closest existing tools each cover one slice of what nvsim covers
end-to-end. Nothing in the open-source ecosystem (as of early 2026) covers
the whole forward pipeline at once — see
docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md §2.2.
| Tool | Source synthesis | Material attenuation | NV ensemble physics | Digitiser + lockin | Witness bundle | Language |
|---|---|---|---|---|---|---|
| Magpylib | ✅ analytic dipole + Biot-Savart | ❌ | ❌ | ❌ | ❌ | Python |
| QuTiP NV scripts | ❌ | ❌ | ✅ full Hamiltonian + Lindblad | ❌ | ❌ | Python |
| Vendor sims (Element Six, etc.) | partial | partial | ✅ proprietary | partial | ❌ | closed |
nvsim | ✅ analytic + Biot-Savart | ✅ 6 materials, NaN-safe | ✅ leading-order ensemble proxy | 🚧 Pass 5 | 🚧 Pass 6 | Rust, deterministic |
nvsim deliberately does not try to compete with QuTiP on Hamiltonian
fidelity (full Lindblad solver is plan §6 out-of-scope). It picks the
linear-readout proxy that Barry 2020 §III.A validates as adequate for
ensemble magnetometers in the linear regime, and ships that path
end-to-end with witness-anchored reproducibility.
You get three things at once that no other open simulator combines:
The cost: nvsim is a forward simulator only. It does not do inverse
problems (estimating field sources from sensor readings), full Hamiltonian
dynamics, or hardware control. If you need those, you escalate to QuTiP,
COMSOL, or a real lab respectively.
# Inside the workspace:
cargo build -p nvsim --no-default-features
cargo test -p nvsim --no-default-features # currently 34 passing
nvsim is a standalone leaf crate. It depends only on serde, thiserror,
tracing, rand, and rand_chacha. RuView ecosystem integrations
(wifi-densepose-core frame alignment, ruvector-core trace compression)
land behind feature flags after the core simulator is shipping. None are
required to use this crate.
use nvsim::{Scene, DipoleSource, scene_field_at};
let mut scene = Scene::new();
// 1 mA·m² dipole at (0,0,0.5 m) pointing along +ẑ
scene.add_dipole(DipoleSource::new([0.0, 0.0, 0.5], [0.0, 0.0, 1.0e-3]));
// Field at the origin
let (b_tesla, near_field_flag) = scene_field_at(&scene, [0.0, 0.0, 0.0]);
println!("B = {:?} T (near-field saturated: {})", b_tesla, near_field_flag);
use nvsim::{NvSensor, NvSensorConfig};
let sensor = NvSensor::cots_defaults();
let b_in = [1.0e-9, 0.0, 0.0]; // 1 nT along +x̂
let dt = 1.0e-3; // 1 ms integration
let seed = 0xCAFE_BABE;
let reading = sensor.sample(b_in, dt, seed);
println!("recovered B = {:?}", reading.b_recovered);
println!("σ per axis = {:?} T", reading.sigma_per_axis);
println!("δB floor = {:e} T/√Hz", reading.noise_floor_t_sqrt_hz);
use nvsim::{attenuate, LosSegment, Material};
let b_in = [1.0e-9, 0.0, 0.0];
let segments = [
LosSegment { material: Material::Air, path_m: 1.0 },
LosSegment { material: Material::Drywall, path_m: 0.1 },
LosSegment { material: Material::ReinforcedConcrete, path_m: 0.2 }, // raises HEAVY flag
];
let (b_attenuated, heavy) = attenuate(b_in, &segments);
use nvsim::{MagFrame, MAG_FRAME_MAGIC};
use nvsim::frame::flag;
let mut f = MagFrame::empty(7); // sensor_id 7
f.b_pt = [1500.0, -250.0, 800.0]; // pT
f.set_flag(flag::ADC_SATURATED);
let bytes = f.to_bytes(); // 60 bytes, deterministic
let parsed = MagFrame::from_bytes(&bytes)
.expect("round-trip must succeed");
assert_eq!(parsed, f);
These are the four numbers nvsim commits to as a finished simulator:
(scene, seed) produces byte-identical proof-bundle output across runs and machines.The first and last numbers come online with Pass 5/6. The middle two are already enforced in the test suite.
For the full SOTA survey and the build/skip verdict, see
docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md. For the
six-pass implementation plan that drives the build, see
docs/research/quantum-sensing/15-nvsim-implementation-plan.md.
Per 15-nvsim-implementation-plan.md §6:
nvsim is room-scale, not nm.nvsim follows.nvsim is forward-only.nvsim commits to a pT-floor honestly.If your use case needs any of the above, nvsim is the wrong starting
point. If your use case is forward simulation of a deterministic NV
magnetometer pipeline you can run in CI, it is the right one.
nvsim is WASM-ready by construction. Zero std::time / std::fs /
std::env / std::process / std::thread / Mutex / RwLock calls in
the crate's source — every dependency in the tree (serde, thiserror,
tracing, rand, rand_chacha, sha2, ndarray) compiles cleanly to
wasm32-unknown-unknown. The shot-noise PRNG is seeded from a
caller-supplied u64 so no OS-entropy bridge is needed.
rustup target add wasm32-unknown-unknown # one-time, on the dev machine
cargo build -p nvsim --target wasm32-unknown-unknown --no-default-features
Why it matters: cluster-Pi inference, browser-side sensor demos, and
Cloudflare-Worker / Deno-deploy edge workloads can all run the
deterministic pipeline. A 28-byte MagFrame shape and a 32-byte SHA-256
witness make it straightforward to ship simulator output across any
HTTP / WebSocket / IPC channel.
MIT OR Apache-2.0 (matches workspace default).