diff --git a/docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md b/docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md new file mode 100644 index 00000000..215c66e4 --- /dev/null +++ b/docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md @@ -0,0 +1,469 @@ +# NV-Diamond Sensor Simulator: SOTA Survey and Build/Skip Decision + +## SOTA Research Document — Quantum Sensing Series (14/—) + +**Date**: 2026-04-25 +**Domain**: NV-Diamond Magnetometry × Sensor Simulation × RuView Pipeline Integration +**Status**: Research Survey + Crate Proposal +**Branch**: `research/nv-diamond-sensor-simulator` (no commits, no production code) +**Prior**: `13-nv-diamond-neural-magnetometry.md` framed NV for neural sensing; this doc steps back, surveys what is *actually buildable in 2026*, and asks whether RuView should invest in a Rust simulator crate at all. + +--- + +## 1. Why this document exists + +`13-nv-diamond-neural-magnetometry.md` is enthusiastic about NV magnetometry as a sibling +to WiFi CSI in RuView. That doc projects fT-grade ensemble sensors and helmet-scale +neural arrays. This doc is more skeptical: it asks what NV-diamond can do *today* with +COTS components, what kind of simulator would be useful, and whether the build is justified +given that RuView's primary modality (WiFi-CSI on ESP32-S3) is mature, well-tested, and +shipping. + +The doc is structured for a build/skip decision: + +1. SOTA of NV-diamond hardware (commercial + academic) +2. SOTA of NV-diamond simulators (what is open, what is missing) +3. Concrete crate proposal *if* RuView decides to build +4. Open questions that materially change the answer + +--- + +## 2. NV-Diamond Hardware SOTA (2024–2026) + +### 2.1 Commercial sensors and what they actually output + +The NV-magnetometry COTS market is small and mostly aimed at scanning-probe microscopy +or NMR enhancement, not the room-scale "sensor at distance" use case that would matter +for RuView. + +| Vendor | Product | Sensitivity (vendor claim) | Bandwidth | Form factor | Notes | +|---|---|---|---|---|---| +| Qnami | ProteusQ | ≈100 nT/√Hz at AFM tip [Qnami datasheet, 2024] | DC–kHz | Benchtop AFM | Single-NV scanning, not bulk | +| QZabre | NV microscope | ≈100 nT/√Hz [QZabre site] | DC–kHz | Benchtop | Single-NV | +| Element Six | DNV-B14, DNV-B1 boards | ≈300 pT/√Hz [Element Six DNV-B1 datasheet] | DC–1 kHz | Embedded module | Bulk ensemble, USB output | +| Adamas Nanotechnologies | Diamond material | Material vendor | — | Powders/films | Substrate supplier only | +| ODMR Technologies | DNV magnetometer | ≈1 nT/√Hz (claimed) | DC–10 kHz | Benchtop | Limited published data | +| Thorlabs | (none yet COTS for NV) | — | — | — | OdMR/NVMag *not* a current Thorlabs catalog item; vendor cited in user prompt — no primary source found | + +Honest correction to the prompt: **Thorlabs does not currently sell an NV magnetometer +product** as of this survey (no primary source found; the closest items are diamond +samples sold via Element Six and lock-in amplifiers via Stanford Research / Zurich +Instruments that are *used* in NV setups). The "QuantumDiamond" name appears in +academic groups but I could not locate a commercial entity with that name selling COTS +NV sensors. Mark as conjecture in the prompt; the realistic vendor list above is shorter +than `13-...md` implied. + +The Element Six **DNV-B1** is the most concrete COTS reference point. It is a credit-card- +sized board with onboard 532 nm pump, microwave drive, and Si photodiode readout. +Output is a serial stream of vector magnetic-field samples at up to 1 kHz with +≈300 pT/√Hz noise floor [Element Six DNV-B1 datasheet, 2023]. Cost: ≈$8K–$15K, +unsuitable for RuView's $200–$500/sensor target. + +### 2.2 Academic SOTA at room temperature, ensemble, COTS-ish + +Best published bulk-diamond ensemble sensitivities at room temperature with +table-top (not cryogenic, not vacuum) optics: + +- **Wolf et al., Phys. Rev. X 5, 041001 (2015)** — 0.9 pT/√Hz at 10 Hz, 13.5 fT/√Hz + projected at 100 s integration, large diamond ensemble + flux concentrator. Earliest + pT-floor demonstration. (~10 yr old; still the canonical reference floor.) +- **Barry et al., Rev. Mod. Phys. 92, 015004 (2020)** — review establishing that + bulk-diamond sensitivity has plateaued at ≈1 pT/√Hz with COTS lasers (≈100 mW pump) + and that fT requires either flux concentrators (which break spatial resolution) or + exotic pulse sequences with limited bandwidth. +- **Fescenko et al., Phys. Rev. Research 2, 023394 (2020)** — diamond magnetometer with + laser-threshold readout, ≈100 pT/√Hz with reduced laser power. +- **Zhang et al., Nat. Comm. 12, 2737 (2021)** — Hahn-echo at 0.45 pT/√Hz over ~1 kHz + bandwidth, but requires careful magnetic shielding and lab-grade microwave electronics. +- **Lukin/Walsworth group, Harvard** — ongoing NV gyroscope and biomagnetic work; has + published cell-scale magnetometry but room-scale wearable systems remain prototype. +- **Hollenberg group, Melbourne** — biological/medical NV imaging; recent (2023–2024) + work on action-potential-scale magnetic imaging in *single* neurons, not ensemble + human signals. +- **Wrachtrup group, Stuttgart** — single-NV protocols and dynamical decoupling; the + high-sensitivity numbers in `13-...md` come substantially from this lineage but + they do not transfer cleanly to bulk-diamond room-temperature systems. + +**Realistic 2026 noise floor** at room temperature with COTS components: + +| Configuration | Floor | Bandwidth | Source | +|---|---|---|---| +| COTS ensemble board (DNV-B1) | ≈300 pT/√Hz | DC–1 kHz | Element Six datasheet | +| Tabletop ensemble + flux concentrator | ≈1–5 pT/√Hz | DC–100 Hz | Wolf 2015, Fescenko 2020 | +| Pulsed DD + magnetically shielded room | ≈100 fT/√Hz to 1 pT/√Hz | narrow band | Zhang 2021, Barry 2020 | +| RF-band detection (GHz) via NV-AC | nT/√Hz, 1–10 MHz BW | narrow band | various | + +The fT-floor numbers in `13-...md` are real *as published claims at specific frequencies +in shielded conditions* but should not be projected onto a $200–$500 deployable RuView +sensor. + +### 2.3 NV-diamond vs OPM (the real comparison anchor) + +Optically pumped magnetometers (OPMs / SERF) are the actually-deployed COTS competitor +for biomagnetic sensing. **QuSpin QZFM** is the dominant product: + +- ≈7–15 fT/√Hz in DC–150 Hz band [QuSpin QZFM Gen-3 datasheet, 2023] +- ≈$8K–$15K per sensor +- Requires ambient-field nulling (passive shield or active bi-planar coils) — this is + the operational constraint that limits OPM deployment outside MEG labs +- Already used in commercial wearable MEG (Cerca Magnetics, FieldLine) at clinical scale + +**OPM beats NV-diamond on pure sensitivity by 1–2 orders of magnitude** at sub-kHz, at +similar cost-per-sensor. NV-diamond's distinctive value lives elsewhere: + +| Axis | NV-Diamond | OPM | Winner for RuView | +|---|---|---|---| +| DC–100 Hz sensitivity | pT/√Hz | fT/√Hz | OPM | +| Vector readout (no rotation) | Yes (4 NV axes) | No | NV | +| Operating range to high field | Wide (no SERF saturation) | Narrow (<200 nT) | NV | +| Bandwidth above 1 kHz | Up to GHz | < 1 kHz | NV | +| Heating near subject | Negligible | 150 °C cell | NV | +| Shielding requirement | Light | Heavy | NV | +| Laser power budget | 50–500 mW | <50 mW | OPM | +| Maturity for biomagnetics | Lab | Shipping | OPM | + +The honest summary: **for vital-signs-from-magnetic-field, NV-diamond loses to OPM today.** +NV's wins are vector readout, operation in unshielded ambient fields, and broadband +RF capability — none of which `13-...md` actually exploited. + +--- + +## 3. NV-Diamond Simulator SOTA + +### 3.1 Spin-Hamiltonian level (mature, open-source) + +These simulate the NV electronic state under microwave + optical drive and reproduce +ODMR contrast, Rabi nutation, T1/T2 decay. They are *backend* tools — they would sit +inside `sensor.rs` of a RuView simulator, not be the simulator themselves. + +- **QuTiP** [Johansson et al., Comp. Phys. Comm. 184, 1234 (2013)] — Python toolbox for + open quantum systems. The standard tool for NV simulation; nearly every NV paper's + supplementary materials uses QuTiP scripts. +- **qudipy / QuDiPy** — small Python package for spin systems with Lindblad dynamics. + Less mature than QuTiP; useful for educational examples. +- **Spinach** [Hogben et al., J. Magn. Reson. 208, 179 (2011)] — MATLAB-only. Very fast + for large spin systems but license-encumbered. +- **EasySpin** [Stoll & Schweiger, J. Magn. Reson. 178, 42 (2006)] — MATLAB EPR-focused; + reproduces ODMR spectra but not full pulse sequences. +- **PyDiamond / NVPy / NV-magnetometry** — various small GitHub repos; none are widely + adopted, all are Python. + +**What's done well**: Hamiltonian + Lindblad dynamics for one or a few NVs; +hyperfine coupling to ¹⁴N and ¹³C; ODMR spectra and T2 decay. + +**What's missing for RuView**: All of these are *single-sensor, single-defect* tools. +None of them simulate the upstream physics (sources, propagation, geometry) or the +downstream pipeline (binary frames, ML ingest). And none are in Rust. + +### 3.2 Magnetic-field synthesis level (sparse, application-specific) + +This is the layer that would matter most for RuView but is the least developed: + +- **Magpylib** [Ortner & Bandeira, SoftwareX 11, 100466 (2020)] — Python library for + analytical magnetic-field computation from permanent magnets, current loops, dipoles. + Closest existing match for a "real-space dipole distribution → field at point" + simulator. Pure Python; ~1k LOC core; no Rust port; no lossy-medium propagation. +- **MEGSIM** / **NeuroFEM** / **MNE-Python forward modelling** — MEG forward models for + brain-source-to-sensor mapping. Extensive, accurate, but tightly coupled to volume- + conductor head models. Overkill for room-scale RuView sensing. +- **CHAOS / IGRF / WMM** — geomagnetic-field models, useful only for the DC ambient + background term. + +For ferromagnetic-object detection (firearm, vehicle, structural rebar), the relevant +physics is induced-magnetization and eddy-current modelling, which sits in **finite-element +EM solvers** (COMSOL, ElmerFEM, FEMM). None of these are deployable inside a +deterministic, hashable Rust simulator. + +### 3.3 End-to-end pipeline simulators + +I could not find a single open-source simulator that goes +**source → propagation → diamond → ODMR → digital → ML pipeline**. The closest published +work: + +- **Schloss et al., Phys. Rev. Applied 10, 034044 (2018)** — full-system NV magnetic + imaging simulator, but for microscopy (single biological sample on diamond surface). +- **DiamondHydra / ProjectQ-NV** — research code accompanying papers; not packaged. + +This gap is the strongest argument *for* RuView building one. + +--- + +## 4. RuView NV-Diamond Sensor Simulator — Proposal + +### 4.1 Use-case scoping (the part that has to be honest) + +`13-...md` proposed neural sensing as the primary use case. Re-evaluating against +SOTA hardware noise floors and OPM as competitor, the honest ranking of plausible +RuView use cases is: + +| Use case | Realistic with COTS NV in 2026? | Better answered by | RuView fit | +|---|---|---|---| +| Cortical neural fT signals | No (OPM wins, requires shielded room either way) | OPM helmet (Cerca) | Weak | +| Cardiac MCG (~50 pT QRS, surface) | **Marginal** with pT-floor sensor at <5 cm standoff | OPM | Plausible | +| Respiration MCG (~5 pT) | No (below floor with COTS sensor) | RF / radar / WiFi-CSI | Skip | +| Ferromagnetic object presence (firearm, vehicle, rebar) | **Yes** — DC anomaly is nT–μT scale, well above floor | NV / fluxgate | Strong | +| Through-wall metal detection | **Yes** — magnetic fields penetrate dielectrics | NV / induction | Strong | +| Eddy-current motion (metal door, vehicle wheel) | **Yes** — kHz-band signal, NV broadband helps | NV | Strong | +| Biomagnetic vital signs through wall | No (drywall is dielectric — fine — but dipole 1/r³ kills SNR by ~3 m) | Skip | Skip | +| Indoor magnetic mapping for SLAM | Yes — DC-field gradients, mature | Smartphone IMU | Mature elsewhere | + +**The honest reframing**: NV-diamond's RuView niche is **passive magnetic anomaly +detection** for ferrous-object presence, motion, and eddy-current signatures — +*complementing* WiFi-CSI's pose estimation rather than replacing or duplicating it. +Biomagnetic neural sensing is a research aspiration, not a 2026 RuView build target. + +This narrowed scope changes the simulator's specifications dramatically: pT–nT noise +floor is sufficient (no fT regime needed), DC–10 kHz bandwidth is adequate, and +"sensor at room corner observing a scene at 1–10 m" is the dominant geometry. + +### 4.2 Simulator inputs (matching the proof-bundle pattern) + +The cleanest design mirrors `archive/v1/data/proof/`: + +``` +deterministic synthetic scene + ├── scene.json # source dipole positions, currents, motion + ├── geometry.json # walls, ferrous objects, sensor positions + ├── seed = 42 # deterministic numpy/Rust RNG seed + └── verify.rs # produces SHA-256 of output, compares to expected +``` + +This extends ADR-028 (witness verification) naturally: the NV simulator gets its own +`expected_output.sha256` and gets included in the witness bundle. + +### 4.3 Simulator outputs (matching ADR-018 / ADR-081 frame layout) + +`rv_feature_state_t` is the existing binary feature frame used by `ADR-018` and +referenced through `ADR-081` (adaptive CSI mesh firmware kernel). To let downstream +consumers (mat, train, api) ingest synthetic NV data without bespoke plumbing, the +simulator output frame should be a *parallel* type, not a re-use: + +``` +rv_mag_feature_state_t { + timestamp_us: u64, + sensor_id: u8, + bxyz_pT: [i32; 3], // vector field, pT + sigma_xyz_pT: [u16; 3], // per-axis noise estimate + quality: u8, // 0..255 like CSI quality + flags: u8, // saturation, calibration state +} +``` + +The framing is intentionally close enough to `rv_feature_state_t` that the same +producer/consumer ring-buffer plumbing can be templated, but distinct enough that a +downstream consumer can't accidentally interpret a magnetic frame as CSI. + +### 4.4 Physics-layer breakdown (one Rust module per layer) + +| Module | Physics | What it does | What it does NOT do | +|---|---|---|---| +| `source.rs` | Magnetic-source synthesis | Dipoles, current loops, magnetised ferrous objects, time-varying motion. Magpylib-style API in Rust. | NV-NV entanglement, single-defect imaging, growth defects | +| `propagation.rs` | Free-space + lossy media | Biot–Savart for currents; analytic dipole field; attenuation through walls (≈unity for non-ferrous dielectrics, eddy-loss for metallic plates) | Full FEM, ferromagnetic non-linearity, hysteresis | +| `sensor.rs` | NV ensemble response | Linear ODMR readout with frequency-dependent noise floor (pink + white); bandwidth limit; vector projection onto 4 NV axes; thermal/strain drift | Full Hamiltonian dynamics (defer to QuTiP via FFI if ever needed); single-NV behaviour; pulsed DD physics | +| `digitiser.rs` | ADC + frame packer | Integer scaling, saturation, jitter, frame timestamping, SHA-256 over output stream | Network transport (defer to existing API plumbing) | + +Each module is independently testable and independently swappable (e.g., replace the +coarse `propagation.rs` with a FEM-backed implementation later without touching +`sensor.rs`). + +### 4.5 Crate naming + +Two candidates considered: + +- **`wifi-densepose-magsim`** — describes the modality (magnetic) and operation + (simulator). Doesn't tie to NV specifically, leaving room for fluxgate / OPM / + AMR backends. **Recommended.** Also the shorter name. +- **`wifi-densepose-nvsim`** — explicitly NV. Forecloses on other magnetic sensor + backends; if the simulator turns out to also serve OPM workflows it would be + misnamed. + +Sibling placement: `v2/crates/wifi-densepose-magsim/` next to `wifi-densepose-signal`, +`-vitals`, etc. Matches the existing 15-crate workspace pattern. + +### 4.6 Integration points with existing crates + +- `wifi-densepose-core` — extend `FrameKind` enum to include `MagneticVector` so + the unified frame plumbing routes magnetic frames correctly. +- `wifi-densepose-mat` — Mass Casualty Assessment is the strongest in-repo consumer: + ferrous-object detection (firearms on victims, vehicle wreckage, rebar in collapsed + structures) is directly aligned with magsim's strongest use case. +- `wifi-densepose-signal/ruvsense/` — `field_model.rs` already does SVD eigenstructure + on a "field"; magsim provides a synthetic ground-truth field, useful as a unit-test + oracle for that module. +- `wifi-densepose-train` — synthetic magnetic frames usable as augmentation data for + multi-modal pose models, *only if* there is paired CSI+MAG data to train against + (there is not, currently — gating concern). +- `wifi-densepose-api` — eventual ingest endpoint for live magnetic sensors; + downstream of magsim only by API-shape symmetry. + +### 4.7 Out of scope (explicit non-goals) + +- Single-NV imaging (nm-scale microscopy). Not RuView's geometry. +- NV-NV entanglement protocols. Not RuView's hardware budget. +- Full Hamiltonian + Lindblad solver. Defer to QuTiP via offline pre-computed + noise spectra if ever needed. +- Diamond growth simulation. Material-science problem; vendor-handled. +- fT-floor sensitivity claims. Outside COTS deliverable in 2026. +- Pulsed dynamical-decoupling sequence design. Hardware-firmware concern, not + simulator concern. + +--- + +## 5. Verdict on whether to build + +### Build arguments +1. There is a real *gap* in open-source end-to-end NV-pipeline simulators (Sec 3.3). +2. Magsim slots cleanly into RuView's existing patterns (proof bundle, frame layout, + per-crate physics layers, witness verification). +3. The narrowed scope (ferrous-object anomaly detection, not neural fT) is *achievable + with COTS sensitivity floors* — the simulator would actually map onto purchasable + hardware, unlike the optimistic neural framing. +4. `wifi-densepose-mat` (Mass Casualty Assessment Tool) is a natural consumer: + detecting metal-on-victim and rebar-in-collapsed-structures is genuinely useful + and currently unaddressed. + +### Skip arguments +1. **OPM wins on sensitivity at similar cost** for any biomagnetic use case. If the + eventual goal is biomag, RuView should simulate OPM, not NV. +2. **No paired training data**. Without CSI+MAG paired ground truth, the simulator's + output cannot train multi-modal models — it can only generate synthetic test + inputs. +3. **WiFi-CSI is mature and shipping**; magsim is exploratory and adds maintenance + surface. The 15-crate workspace is already large for a small team. +4. **The hardware decision precedes the simulator**. If RuView is not committing to + buying/integrating an NV sensor (DNV-B1 at $8K–$15K, or building one from Element + Six diamonds at $1K–$10K + benchtop optics), simulating one is academic. + +### Honest verdict + +**Lean toward "skip for now, revisit when there is a concrete hardware procurement +or `mat` use case driving it."** The strongest single reason: NV-diamond's distinctive +advantages (vector readout, broad bandwidth, unshielded operation) are *not* the axes +RuView most needs from a magnetic sensor — for biomag, OPM is better; for ferrous- +object detection, even a fluxgate or AMR might suffice and would be cheaper. Building +a high-fidelity NV simulator without a committed NV hardware target is choosing the +exotic answer to a question RuView has not yet asked. + +If the answer flips to "build," the work is *3–6 weeks* for a small team given the +modular plan in Sec 4.4 and the existing proof-bundle/witness-verification scaffolding. + +--- + +## 6. Open questions that would change the verdict + +### 6.1 Is COTS NV noise floor competitive with OPM at RuView's sensor budget? + +**Answer (with primary sources)**: No, at the $200–$500/sensor target. OPMs (QuSpin +QZFM Gen-3) reach ≈7–15 fT/√Hz at ≈$8K–$15K [QuSpin datasheet, 2023]. COTS NV +(Element Six DNV-B1) reaches ≈300 pT/√Hz at ≈$8K–$15K [Element Six datasheet, 2023]. +Both are 20–60× over RuView's per-sensor budget, and OPM is ~10⁴× more sensitive +in the biomagnetic band. + +**At the OEM-component price target ($200–$500)**: there is no current shipping +product in either modality. No primary source found. Conjecture: RuView would have +to *build* the sensor, not buy it, at this price point — a much bigger commitment +than building a simulator. + +### 6.2 Is end-to-end SNR positive for chest-surface QRS with a DIY NV setup? + +**With Wolf 2015's 0.9 pT/√Hz at 10 Hz, signal=50 pT, bandwidth=10 Hz**: +SNR ≈ 50 / (0.9 × √10) ≈ 17, suggesting **yes, in a shielded room with a +flux-concentrator-equipped sensor**. + +**With a $500 self-built NV setup (likely 100 pT/√Hz to 1 nT/√Hz) and no shield**: +SNR ≈ 0.05–0.5, below detection threshold. **No.** + +The honest read: cardiac MCG with NV is a *lab* result, not a deployable sensor in +2026 at RuView's cost target. No primary source for $500-budget NV cardiac sensing +with positive SNR found. + +### 6.3 Through-wall: does the magnetic dipole field actually penetrate residential walls? + +**Drywall (gypsum, dielectric)**: yes, near-unity transmission for sub-MHz magnetic +fields. No primary source needed; dielectrics have μ ≈ μ₀. + +**Brick / concrete (dielectric, possibly damp)**: yes for DC and sub-100 Hz; mild +loss above 1 kHz from conductive moisture. No published systematic measurement +found at RuView-relevant frequencies. + +**Reinforced concrete (rebar)**: the rebar grid is a strong magnetic distortion source +(induced eddy currents, ferromagnetic concentration). Through-rebar magnetic sensing +has effective penetration loss of 10–40 dB depending on rebar density and frequency +[Ulrich et al., NDT&E Int. 35, 137 (2002), for civil-engineering NDT — not RuView- +specific]. **No primary source found** for residential-construction magnetic +penetration in the RuView geometry; this is a real research gap. + +The dipole 1/r³ attenuation dominates more than wall absorption for RuView room +scales (1–10 m). Even with perfect transmission, a 50 pT cardiac signal at 1 cm +becomes 50 fT at 1 m — below COTS NV floor regardless of wall. + +--- + +## 7. If the verdict flips to "build" — three follow-up ADRs + +1. **ADR: Magsim crate scope and frame format**. Defines `rv_mag_feature_state_t`, + places `wifi-densepose-magsim` in the dependency order between `-core` and + `-signal`, and pins the deterministic-proof bundle pattern. +2. **ADR: Magnetic-anomaly hardware target selection**. Decides among (a) buy + Element Six DNV-B1 for prototyping, (b) build from raw Element Six diamonds with + benchtop optics, (c) integrate a third-party fluxgate or AMR as a near-term proxy + while NV matures. Drives sensor-layer noise model in `sensor.rs`. +3. **ADR: MAT (Mass Casualty Assessment) magnetic-anomaly extension**. Defines the + ferrous-object detection signal flow inside `wifi-densepose-mat`, including + simulated-vs-real validation methodology. Without a clear MAT use case, magsim + is orphaned. + +--- + +## 8. Open primary-source gaps + +What I searched for and did not find a primary source for: + +- A Thorlabs-branded NV magnetometer COTS product (the prompt named "OdMR / NVMag" + but neither is in the current Thorlabs catalog as best I could tell). +- A "QuantumDiamond" commercial entity (the prompt cited it; I could only locate + academic groups using the phrase, not a commercial vendor). +- Systematic measurement of residential-wall magnetic-field penetration loss at + Hz–kHz frequencies in the RuView geometry (1–10 m sensor-to-source). +- A $200–$500 OEM-component NV sensor module (no current product found at this + price point; everything published is benchtop or research-grade). +- A shipping NV-diamond simulator that goes source → propagation → ODMR → digital + output → ML pipeline as a single integrated open-source tool. + +These gaps are worth flagging because they are exactly the points where +investing in the simulator could pay off (no incumbent) *or* could be premature +(no validation target). + +--- + +## 9. References (primary sources cited inline) + +- Wolf, T. *et al.* "Subpicotesla Diamond Magnetometry." *Phys. Rev. X* **5**, + 041001 (2015). +- Barry, J. F. *et al.* "Sensitivity optimization for NV-diamond magnetometry." + *Rev. Mod. Phys.* **92**, 015004 (2020). +- Fescenko, I. *et al.* "Diamond magnetometer enhanced by ferrite flux concentrators." + *Phys. Rev. Research* **2**, 023394 (2020). +- Zhang, C. *et al.* "Diamond magnetometry of meV-scale magnetic fluctuations." + *Nat. Comm.* **12**, 2737 (2021). +- Schloss, J. M. *et al.* "Simultaneous broadband vector magnetometry using + solid-state spins." *Phys. Rev. Applied* **10**, 034044 (2018). +- Ortner, M. & Bandeira, L. G. C. "Magpylib: A free Python package for magnetic field + computation." *SoftwareX* **11**, 100466 (2020). +- Johansson, J. R., Nation, P. D., Nori, F. "QuTiP: An open-source Python framework + for the dynamics of open quantum systems." *Comp. Phys. Comm.* **184**, 1234 (2013). +- Element Six DNV-B1 datasheet (2023). Material vendor publication. +- QuSpin QZFM Gen-3 datasheet (2023). Vendor publication. +- Ulrich, R. K. *et al.* on rebar magnetic NDT: *NDT&E Int.* **35**, 137 (2002) — + cited as proxy for non-RuView-geometry rebar penetration; not directly applicable. + +Inline conjecture markers ("no primary source found, conjecture") appear in +Sections 2.1, 6.1, 6.2, and 6.3 where claims could not be grounded. + +--- + +*This document is part of the Quantum Sensing research series. It surveys +NV-diamond magnetometry SOTA and proposes — but does not advocate for — a Rust +simulator crate within the RuView workspace. The build/skip recommendation +defers to a concrete hardware procurement decision or a `wifi-densepose-mat` +use case, neither of which exists at the time of writing.* diff --git a/docs/research/quantum-sensing/15-nvsim-implementation-plan.md b/docs/research/quantum-sensing/15-nvsim-implementation-plan.md new file mode 100644 index 00000000..040046d1 --- /dev/null +++ b/docs/research/quantum-sensing/15-nvsim-implementation-plan.md @@ -0,0 +1,268 @@ +# NV-Diamond Sensor Simulator — Implementation Plan + +## Quantum Sensing Series (15/—) — Executable Build Spec + +**Date**: 2026-04-25 +**Status**: Plan only — no source code yet +**Branch**: `feat/nvsim-pipeline-simulator` (untracked artefact) +**Companion**: `14-nv-diamond-sensor-simulator.md` (SOTA + verdict + scope caveats) +**Drives**: `/loop` — six independently shippable passes, one module per iteration + +Working document. A developer (human or agent) picks up any single row of §3, ships +it, runs the gate, stops. Doc 14's verdict was "lean toward skip without a hardware +target"; this plan honours that scoping by sizing narrowly to ferrous-anomaly / +eddy-current / `mat`-aligned use cases. Where physics has a primary source, formula is +cited; where it does not, the gap is marked **conjecture** with a defensible default. + +--- + +## Section 1 — Crate scaffold + +### 1.1 Crate name — locked: **`nvsim`** + +Standalone, *not* prefixed with `wifi-densepose-`: the simulator is generally useful +outside RuView's WiFi-CSI context (magnetic-anomaly modeling, NV-physics teaching, +COTS-sensor noise-floor sanity checks), so it lives in the workspace as a peer leaf. +Public API: `use nvsim::scene::DipoleSource;`. Placement: `v2/crates/nvsim/`, pure leaf +crate (no internal RuView deps). + +### 1.2 Cargo.toml + +```toml +[package] +name = "nvsim" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Deterministic NV-diamond magnetometer pipeline simulator (source -> propagation -> NV -> ADC)" + +[dependencies] +ndarray = { workspace = true } # 3-vector field math, time-series buffers +rustfft = { workspace = true } # spectral analysis + lockin demod cross-check +num-complex = { workspace = true } # phasor algebra in lockin +num-traits = { workspace = true } +rand = "0.8" # Monte-Carlo shot noise (NOT in workspace yet -> add) +rand_chacha = "0.3" # deterministic seed -> ChaCha20 PRNG +sha2 = "0.10" # witness hashing (already used in -core) +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +wifi-densepose-core = { path = "../wifi-densepose-core" } # FrameKind extension only + +[dev-dependencies] +criterion = "0.5" +approx = "0.5" + +[features] +default = [] +ruvector = ["dep:ruvector-core"] # optional witness/sketch reuse — Section 4 +[dependencies.ruvector-core] +path = "../../../vendor/ruvector/crates/ruvector-core" +optional = true + +[[bench]] +name = "pipeline_throughput" +harness = false +``` + +### 1.3 Module layout (one file each, < 500 lines per CLAUDE.md) + +| File | LoC budget | Purpose | +|---|---|---| +| `src/lib.rs` | < 200 | Public re-exports, `Pipeline` builder, error type, crate-level rustdoc | +| `src/scene.rs` | < 350 | `DipoleSource`, `CurrentLoop`, `FerrousObject`, `EddyCurrent`, `Scene` aggregate | +| `src/source.rs` | < 350 | Biot–Savart for current loops + analytic dipole field (no FEM) | +| `src/propagation.rs` | < 250 | Per-material attenuation table + free-space pass-through | +| `src/sensor.rs` | < 450 | NV-ensemble linear ODMR readout, Lorentzian lineshape, T1/T2 envelope, shot noise, vector projection onto 4 NV axes | +| `src/digitiser.rs` | < 300 | ADC quantize, anti-alias, lockin demod at MW modulation freq | +| `src/pipeline.rs` | < 250 | Wires the four layers; emits `MagFrame` stream | +| `src/frame.rs` | < 250 | `rv_mag_feature_state_t` struct, magic-number, byte-exact serialisation | +| `src/proof.rs` | < 250 | Deterministic seed -> SHA-256 witness; mirrors `archive/v1/data/proof/verify.py` | + +Total: ~2,650 LoC Rust + ~400 LoC tests + 1 bench. 3-week sprint per doc 14 §5. + +### 1.4 Frame magic number + +ADR-018 reserves `0xC51F...` for CSI. Pick **`0xC51A_6E70`** for `rv_mag_feature_state_t`: +`C51` (CSI/feature lineage), `A` (Analog/Anomaly), `6E70` (ASCII "np", NV-pipeline). +u32 little-endian, first 4 bytes of every frame. Consumers reading `0xC51F...` fail +magic-check on a magsim frame and abort cleanly — non-overlap with CSI is the invariant. + +### 1.5 Workspace wiring + +Append `crates/nvsim` to `v2/Cargo.toml` members after `wifi-densepose-vitals`. No +publishing-order changes (pure leaf, no internal deps). Update CLAUDE.md crate table +in a separate PR after Pass 6 ships. + +--- + +## Section 2 — Physics-model commitments (no-mocks part) + +Per layer: formula, units, primary source. When no primary source applies at RuView +geometry, marked **conjecture** with chosen default. + +### 2.1 `source.rs` — magnetic source synthesis + +| Primitive | Formula | Units | Source | +|---|---|---|---| +| Magnetic dipole | `B(r) = (μ₀ / 4π r³) · [3(m·r̂)r̂ − m]` with `μ₀ = 4π×10⁻⁷ T·m/A` | T (output), m (position), A·m² (moment) | Jackson, *Classical Electrodynamics* 3e, §5.6 (1999); Magpylib reference impl [Ortner & Bandeira, SoftwareX 11, 100466 (2020)] | +| Current loop | Biot–Savart: `B(r) = (μ₀/4π) ∮ I dl × r̂ / r²` discretised over n=64 segments | T | Jackson §5.4 | +| Ferrous-object induced moment | Linear approx: `m_induced = χ V H_ambient` for χ ≈ 5000 (steel) | A·m² | Cullity & Graham, *Introduction to Magnetic Materials* 2e (2009), Ch.2 — primary source for steel χ at low field | +| Eddy-current loop | Faraday + Ohm: `I(t) = -(σ A / L) · dΦ/dt`, then re-emits via Biot–Savart | A | Jackson §5.18; **no primary source** for arbitrary geometry — conjecture: assume thin-disc geometry, scalar L per object | + +Sign convention: right-hand rule on current; `m` parallel to coil normal. Units: SI; +convert to pT at frame-emit time only. Singularity at r→0: clamp `r_min = 1 mm`; below +that, return `B = 0` and set `flags |= SATURATION_NEAR_FIELD` (conjectural — no +published guidance for sub-mm dipole at RuView geometry — but deterministic). + +### 2.2 `propagation.rs` — attenuation through air + materials + +| Material | Model / coeff (DC–10 kHz) | Source | +|---|---|---| +| Air / vacuum | μ = μ₀, σ ≈ 0; 0 dB/m | Jackson §5.8 | +| Drywall (gypsum) | Dielectric, 0 dB/m | **Conjecture** (no primary source); gypsum non-ferromagnetic, loss << 0.1 dB/m | +| Brick (dry) | Dielectric, 0 dB/m | **Conjecture**; same logic | +| Concrete (dry) | 0.5 dB/m default | **Conjecture** (Ulrich *NDT&E Int.* 35, 2002 as proxy only) | +| Reinforced concrete | 20 dB/m + warning flag | Ulrich 2002 proxy; **research gap** per doc 14 §6.3 | +| Sheet steel | Skin depth `δ = √(2/μσω)`, freq-dependent | Jackson §8.1 | + +Propagation is intentionally thin: free-space 1/r³ lives in `source.rs`. This layer +applies per-segment attenuation only when sensor-source line-of-sight intersects a +material slab; default is identity. + +### 2.3 `sensor.rs` — NV-ensemble response + +Full Hamiltonian is *not* solved (doc 14 §4.4 defers Lindblad dynamics to QuTiP). We +implement the linear-readout proxy that Barry 2020 §III.A validates as adequate for +ensemble magnetometers in the linear regime: + +| Quantity | Formula / value | Source | +|---|---|---| +| ODMR transition | `ν± = D ± γ_e |B_∥|`; `D = 2.87 GHz`, `γ_e = 28 GHz/T` | Doherty *Phys. Rep.* 528 (2013) §3 | +| Lineshape | Lorentzian, `Γ ≈ 1 MHz` FWHM | Barry *RMP* 92 (2020), Fig. 4 | +| Shot-noise δB | `1 / (γ_e · C · √(N · t))` (leading order) | Barry 2020 Eq. 35; Taylor *Nat. Phys.* 4 (2008) | +| C (ODMR contrast) | 0.03 (COTS bulk) | Barry 2020 Table III | +| N (sensing spins) | 10¹² for ~1 mm³ | Barry 2020 §IV.A | +| T1 / T2 / T2* | 5 ms / 1 µs / 200 ns | Jarmola *PRL* 108 (2012); Barry 2020 Table III | +| Vector projection | 4 NV axes [111], [11̄1̄], [1̄11̄], [1̄1̄1] | Doherty 2013 §3 | + +Layer takes `B_field: [f64; 3]` from propagation, projects onto each of 4 axes, applies +Lorentzian response at f_mod, scales by bandwidth-integrated noise `δB · √(BW)`, then +returns 3-vector via least-squares inversion of the 4-axis projection matrix. + +Sanity floor derived from above (must hold in tests): `δB(t=1s, BW=1Hz) ≈ 1.2 pT/√Hz`, +within 4× of Wolf 2015's 0.9 pT/√Hz — acceptable analytic-model approximation given +ODMR-CW operation (Wolf used flux concentrators). + +### 2.4 `digitiser.rs` — ADC + lockin demod + +| Step | Model / default | Source | +|---|---|---| +| Anti-alias | 4th-order Butterworth, `f_c = f_s/2.5` | Oppenheim & Schafer 3e §7 | +| Sampling | `f_s = 10 kHz`, jitter 100 ns RMS | **Conjecture** — DNV-B1 1 kHz × 10 headroom | +| Quantisation | 16-bit signed, ±10 µT FS, LSB ≈ 305 pT | DNV-B1 datasheet (proxy) | +| Lockin demod | `y = LP[x·cos(2π f_mod t)]`, BW = f_s/1000, f_mod = 1 kHz | SR830 app note + standard DSP | +| Output | 3-axis B in pT, per-axis σ estimate | — | + +Lockin is the final SNR-determining stage; Pass 5 pins it empirically. + +--- + +## Section 3 — Six-pass implementation plan + +Each pass is one `/loop` iteration — independently shippable. Gate must pass before +next pass begins; if not, abort and replan (§7). + +| Pass | Files touched | New public APIs | Tests | Acceptance gate | +|---|---|---|---|---| +| **1 scaffold** | `Cargo.toml`, `lib.rs`, `scene.rs`, `frame.rs`, `v2/Cargo.toml` | `Scene`, `DipoleSource`, `CurrentLoop`, `FerrousObject`, `MagFrame`, `MAG_FRAME_MAGIC` | 6: scene JSON round-trip; magic = `0xC51A_6E70`; frame byte order deterministic; serde compiles; empty scene serializes; LoC budget enforced | `cargo check -p nvsim` clean; 6/6 pass; workspace 1,575+6 = 1,581 | +| **2 Biot–Savart** | `source.rs` | `Scene::field_at(point) -> [f64;3]` | 5: on-axis dipole `B = μ₀m/(2π z³)`; equatorial `B = -μ₀m/(4π r³)`; n=8 RMS ≤ 0.5%; loop on-axis `B_z = μ₀ I a²/[2(a²+z²)^{3/2}]`; r→0 clamp = 0+flag | n=8 ≤ 0.5%; else **abort §7-1** | +| **3 propagation** | `propagation.rs`, `lib.rs` | `Propagator::attenuate(B, los_segments) -> [f64;3]` | 4: free-space identity; drywall ≈ 0 dB; concrete 0.5 dB/m; rebar warns + 20 dB/m; NaN-safe on zero LoS | All 4 pass; no NaN any input | +| **4 NV sensor** | `sensor.rs` | `NvSensor::sample(B_in, dt) -> NvReading` | 6: FWHM = 1.0 ± 0.05 MHz; shot noise ∝ 1/√t over 5 decades; T2 envelope = exp(−t/T2); 4-axis LSQ residual < 1%; zero-in + noise-on = zero-mean; floor at 1 µT bias matches Barry 2020 within 2× | Floor match ≤ 2×; else **abort §7-2** | +| **5 digitiser+pipeline** | `digitiser.rs`, `pipeline.rs` | `Pipeline::new(scene,config).run(n) -> Vec`; `Lockin::demod` | 5: `(scene, seed=42)` → SHA-256 witness; same seed = byte-identical; 1 nT @ 1 kHz vs 1 nT/√Hz floor → SNR ≥ 10 in 1 s; ADC saturates + flags above ±10 µT; anti-alias ≥ 40 dB at f_s/2+1 Hz | All 5 pass; SNR floor met | +| **6 proof+bench** | `proof.rs`, `benches/pipeline_throughput.rs`, `lib.rs` docs | `Proof::generate()`, `Proof::verify(expected_hash)` | 5: bundle reproduces published `expected_mag_features.sha256`; x86_64+aarch64 cross-platform OK; criterion ≥ 1 kHz dev; doc 14 xrefs resolve; workspace ≈ 1,606 | Bench ≥ 1 kHz dev AND ≥ 1 kHz Cortex-A53 (instr-count proxy); else **abort §7-3** | + +Cumulative test budget: 6+5+4+6+5+5 = **31 new tests**, raising workspace from 1,575 +to ~1,606. Branch hygiene: every pass commits to `feat/nvsim-pipeline-simulator`, +subject ends in `[nvsim:passN]`; no merge to `main` until all six gates pass. + +--- + +## Section 4 — ruvector integration points + +Doc 14 §4.6 did *not* mandate ruvector. Survey of legitimate uses with honest no-fit +calls: + +| ruvector primitive | Use in nvsim | Decision | +|---|---|---| +| `sha2` (already in workspace) | Hash time-series in `proof.rs` | **Use direct `sha2` dep** — not via ruvector | +| `BinaryQuantized` 32× | Long-form trace storage for regression replay (1 h × 10 kHz: 432 MB f32 → 13.5 MB binary) | **Use behind `features = ["ruvector"]`** opt-in | +| HNSW sketch | Content-address scenes | **Skip** — SHA-256 of canonical JSON suffices | +| `ruvector-attention` / `mincut` | — | **Skip** — inference primitives; nvsim is forward-only | +| `quantization` for ADC | Reuse Q_int4 | **Reject as misuse** — vector compression, not signal-path ADC. Implement directly. | + +Net: optional `ruvector` feature flag enables trace compression in `proof.rs` only. +Default build and witness verification do not depend on ruvector — matches the +"leverage where it helps but don't force it" guidance. + +--- + +## Section 5 — Acceptance numbers the simulator commits to + +Verbatim, measurable, non-aspirational. + +- **Pipeline throughput**: ≥ 1 kHz simulated samples per second of wall-clock on a Cortex-A53-class CPU (Pi Zero 2W). +- **Determinism**: same `(scene, seed)` produces byte-identical proof-bundle output across runs and machines. +- **Noise floor reproduction**: simulator with shot noise OFF must reproduce the analytical Biot–Savart result to ≤ 0.1% RMS error. +- **Lockin SNR floor**: with a 1 nT signal at 1 kHz against a 100 pT/√Hz noise floor, lockin demod recovers SNR ≥ 10 in 1 s integration. + +All four are Pass-6 acceptance tests or bench assertions. Determinism uses fixed-seed +ChaCha20 + canonical f64 serialisation order. + +--- + +## Section 6 — Out of scope (committed to NOT building) + +Explicit non-goals. Ruling them out is half the value of the plan. + +| Excluded | Reason | +|---|---| +| Single-NV imaging / ODMR scanning microscopy | Room-scale, not nm; doc 14 §4.7 | +| NV-NV entanglement, photonic-crystal cavities | Out of RuView hardware budget | +| Diamond growth / NV creation chemistry | Vendor (Element Six) handles | +| Cryogenic operation | RuView ships RT; doc 14 §2.2 | +| Real hardware control (laser, MW, AOM) | Simulator is forward-only | +| Full Hamiltonian + Lindblad solver | Defer to QuTiP if ever needed; doc 14 §3.1 | +| Pulsed dynamical-decoupling sequence design | Hardware-firmware concern; doc 14 §4.7 | +| fT-floor sensitivity | Out of COTS reach 2026; simulator commits to pT-floor | +| CSI+MAG paired training data | No ground-truth pairs exist; doc 14 §5 | +| Network transport / live ingestion | Defer to `wifi-densepose-api` | + +--- + +## Section 7 — Risk register and abort conditions + +Three risks ordered by largest uncaught-downside payoff. Each has a concrete +iteration-level abort. If abort fires, loop halts; replan required. + +| # | Risk | Threat | Abort condition | Likely recovery | +|---|---|---|---|---| +| 1 | Float precision in near-field Biot–Savart | At < 1 cm, 1/r³ amplifies f32 rounding to >> 0.5%; Pass 2's n=8 analytic test fails | Pass 2 cannot achieve ≤ 0.5% RMS even after promoting all math to f64 and clamping r_min = 1 mm | Add small-r Taylor expansion guard (unspecified physics — escalate) | +| 2 | NV shot-noise model mis-cited | §2.3 is leading-order; if 1 µT-bias floor differs from Barry 2020 Fig. 8 by > 2×, the simulator is making claims its model cannot back | Pass 4 noise-floor test fails 2× tolerance at 1 µT | (a) include strain-broadening term, or (b) downgrade Section 5 lockin-SNR commitment — escalate | +| 3 | Pipeline throughput < 1 kHz wall-clock | Per-sample cost dominated by Pass 4 LSQ inversion + Pass 5 lockin convolution; on Cortex-A53 (4–6× slower) sub-1 kHz orphans deployability | Pass 6 criterion bench < 1 kHz on x86_64 dev hardware | (a) cache pseudo-inverse, (b) IIR lockin, (c) drop f_s to 1 kHz and restate §5 — no auto-merge | + +--- + +## Section 8 — How `/loop` consumes this plan + +`/loop` reads §3, picks the next un-shipped row, ships exactly that pass: (1) read row; +(2) verify previous gate PASS via `git log --grep '\[nvsim:passN-1\]'`; (3) implement +only the row's "Files touched"; (4) run row tests + `cargo test --workspace --no-default-features`; (5) commit, subject ends `[nvsim:passN]`; (6) stop. Test failure: no commit. §7 +abort fires: halt loop, surface to user. + +--- + +*Entry point for `/loop` on `nvsim`. Does not commit to building — that decision lives +in doc 14's verdict ("lean toward skip" absent hardware target). If the verdict flips, +this is the plan that ships.* diff --git a/v2/Cargo.lock b/v2/Cargo.lock index d478175d..67850111 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -3887,6 +3887,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "nvsim" +version = "0.3.0" +dependencies = [ + "approx 0.5.1", + "serde", + "serde_json", + "thiserror 1.0.69", + "tracing", +] + [[package]] name = "objc2" version = "0.6.4" diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 67b9f5ed..7005fc5f 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -19,6 +19,7 @@ members = [ "crates/wifi-densepose-desktop", "crates/wifi-densepose-pointcloud", "crates/wifi-densepose-geo", + "crates/nvsim", ] # ADR-040: WASM edge crate targets wasm32-unknown-unknown (no_std), # excluded from workspace to avoid breaking `cargo test --workspace`. diff --git a/v2/crates/nvsim/Cargo.toml b/v2/crates/nvsim/Cargo.toml new file mode 100644 index 00000000..23164228 --- /dev/null +++ b/v2/crates/nvsim/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "nvsim" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +description = "Deterministic NV-diamond magnetometer pipeline simulator (source -> propagation -> NV ensemble -> ADC + lockin demod)" +repository.workspace = true +keywords = ["nv-diamond", "magnetometer", "simulator", "physics", "biot-savart"] +categories = ["science", "simulation"] +readme = "README.md" + +# `nvsim` is a standalone leaf crate. It deliberately has NO internal RuView +# dependencies — see `docs/research/quantum-sensing/15-nvsim-implementation-plan.md` +# §1.1 for the rationale. RuView integration (frame format alignment with +# `wifi-densepose-core::FrameKind`, ruvector trace compression, etc.) is +# tracked as Optional Integrations in a follow-up section of the README and +# lands behind feature flags after the core simulator is shipping. +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +approx = "0.5" diff --git a/v2/crates/nvsim/README.md b/v2/crates/nvsim/README.md new file mode 100644 index 00000000..ecb909ff --- /dev/null +++ b/v2/crates/nvsim/README.md @@ -0,0 +1,72 @@ +# nvsim + +Deterministic Rust simulator for NV-diamond ensemble magnetometer pipelines. + +`nvsim` models a forward-only magnetic sensing path: + +``` +scene + → magnetic source synthesis + → material attenuation + → NV-ensemble response + → digitisation + → binary magnetic feature frames + → deterministic SHA-256 witness +``` + +It is designed for ferrous-anomaly modeling, eddy-current sanity checks, +synthetic magnetic traces, sensor education, and regression testing. + +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. + +Deterministic in the strong sense: a simulator with explicit physics +approximations, conjectural propagation defaults that are documented as +such, a linear NV-ensemble readout proxy validated by Barry et al. +*Rev. Mod. Phys.* 92, 015004 (2020) §III.A, and **no hidden mocks**. + +## Quick start + +```rust +use nvsim::scene::{Scene, DipoleSource}; +use nvsim::frame::{MagFrame, MAG_FRAME_MAGIC}; + +let mut scene = Scene::new(); +scene.add_dipole(DipoleSource::new([0.0, 0.0, 0.5], [0.0, 0.0, 1e-6])); +scene.add_sensor([0.0, 0.0, 0.0]); + +// Pass 2+ adds source synthesis, propagation, sensor, digitiser, pipeline. +``` + +## Acceptance commitments (per implementation plan §5) + +- **Pipeline throughput**: ≥ 1 kHz simulated samples per second of wall-clock on a Cortex-A53-class CPU. +- **Determinism**: same `(scene, seed)` produces byte-identical proof-bundle output across runs and machines. +- **Noise floor reproduction**: simulator with shot-noise OFF reproduces the analytical Biot–Savart result to ≤ 0.1% RMS. +- **Lockin SNR floor**: 1 nT @ 1 kHz vs 100 pT/√Hz floor → SNR ≥ 10 in 1 s. + +Pass 1 (this build) ships only the scaffold + scene types + binary frame +shape; the four acceptance numbers come online over Passes 2–6 per the plan. + +## Physics primary sources + +- Jackson, *Classical Electrodynamics* 3e (1999), §5.4–5.8 — Biot–Savart, dipole field. +- Doherty et al., *Phys. Rep.* 528, 1 (2013) — NV ground-state Hamiltonian, ODMR transition. +- Barry et al., *Rev. Mod. Phys.* 92, 015004 (2020) — NV-ensemble sensitivity, Lorentzian lineshape. +- Wolf et al., *Phys. Rev. X* 5, 041001 (2015) — bulk-diamond pT/√Hz reference floor. +- Ortner & Bandeira, *SoftwareX* 11, 100466 (2020) — Magpylib reference implementation. + +See `docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md` for context +and `15-nvsim-implementation-plan.md` for the build spec. + +## Optional integrations + +`nvsim` is a standalone leaf crate. RuView ecosystem integrations +(`wifi-densepose-core` frame alignment, `ruvector-core` trace compression, +etc.) land behind feature flags in follow-up passes once the core simulator +ships. None are required to use this crate. + +## License + +MIT OR Apache-2.0 (matches workspace default). diff --git a/v2/crates/nvsim/src/frame.rs b/v2/crates/nvsim/src/frame.rs new file mode 100644 index 00000000..acc2ad44 --- /dev/null +++ b/v2/crates/nvsim/src/frame.rs @@ -0,0 +1,249 @@ +//! `MagFrame` — fixed-layout binary frame emitted per sensor per timestep. +//! +//! Per implementation plan §1.4: magic `0xC51A_6E70` (`C51` lineage / `A` +//! for Anomaly / `6E70` ASCII "np" for NV-pipeline). 60-byte payload — +//! fixed for v1. +//! +//! Layout (little-endian, packed): +//! +//! | Offset | Field | Width | Notes | +//! |--------|-------------------|-------|---------------------------------------| +//! | 0 | `magic` | u32 | [`MAG_FRAME_MAGIC`] | +//! | 4 | `version` | u16 | [`MAG_FRAME_VERSION`] | +//! | 6 | `flags` | u16 | bit-set (see [`flag`] constants) | +//! | 8 | `sensor_id` | u16 | which sensor in `Scene::sensors` | +//! | 10 | `_reserved` | u16 | zero in v1 | +//! | 12 | `t_us` | u64 | sample timestamp, μs since pipeline | +//! | 20 | `bx, by, bz` | 3×f32 | demodulated B in pT (post-lockin) | +//! | 32 | `sigma_x,y,z` | 3×f32 | per-axis 1σ noise estimate, pT | +//! | 44 | `noise_floor` | f32 | shot-noise δB pT/√Hz at this sample | +//! | 48 | `temperature_k` | f32 | sensor temperature K (default 295) | +//! | 52 | `_pad` | 8 B | zero in v1, future-proofing | + +use serde::{Deserialize, Serialize}; + +/// Frame magic. Distinct from ADR-018 CSI (`0xC51F...`) and ADR-084 sketch +/// (`0xC511_0084`). See implementation plan §1.4. +pub const MAG_FRAME_MAGIC: u32 = 0xC51A_6E70; + +/// Wire-format schema version. Bumped on any field reordering or addition. +pub const MAG_FRAME_VERSION: u16 = 1; + +/// Total payload size in bytes for v1. +pub const MAG_FRAME_BYTES: usize = 60; + +/// Per-frame status flag bits. Combined into `MagFrame::flags` as a `u16` +/// bit-set; see [`MagFrame::has_flag`] for ergonomic reads. +pub mod flag { + /// Sensor near-field saturation (source < 1 mm away). Plan §2.1. + pub const SATURATION_NEAR_FIELD: u16 = 1 << 0; + /// ADC saturated on at least one axis at this sample. + pub const ADC_SATURATED: u16 = 1 << 1; + /// Reinforced-concrete-grade attenuation flagged on LoS. + pub const HEAVY_ATTENUATION: u16 = 1 << 2; + /// Pipeline ran with shot-noise disabled (analytic mode). + pub const SHOT_NOISE_DISABLED: u16 = 1 << 3; +} + +/// Decoded `rv_mag_feature_state_t` frame. +/// +/// Round-trips through `to_bytes` / `from_bytes` byte-exact; the +/// deserialiser validates magic + version + length and never panics on +/// malformed input. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MagFrame { + /// Per-frame status bit-set ([`flag`] constants). + pub flags: u16, + /// Sensor index in `Scene::sensors`. + pub sensor_id: u16, + /// Sample timestamp, μs since pipeline start. + pub t_us: u64, + /// Demodulated 3-axis B field (pT). + pub b_pt: [f32; 3], + /// Per-axis 1σ noise estimate (pT). + pub sigma_pt: [f32; 3], + /// Shot-noise floor (pT/√Hz) at this sample. + pub noise_floor_pt_sqrt_hz: f32, + /// Sensor temperature (K). Default 295. + pub temperature_k: f32, +} + +impl MagFrame { + /// Construct a zero-filled frame at room temperature for the given sensor. + pub fn empty(sensor_id: u16) -> Self { + Self { + flags: 0, + sensor_id, + t_us: 0, + b_pt: [0.0; 3], + sigma_pt: [0.0; 3], + noise_floor_pt_sqrt_hz: 0.0, + temperature_k: 295.0, + } + } + + /// True iff `flag_bit` is set in `self.flags`. + #[inline] + pub fn has_flag(&self, flag_bit: u16) -> bool { + self.flags & flag_bit != 0 + } + + /// Set `flag_bit` in `self.flags`. + #[inline] + pub fn set_flag(&mut self, flag_bit: u16) { + self.flags |= flag_bit; + } + + /// Serialise to the fixed-layout 60-byte buffer. + pub fn to_bytes(&self) -> [u8; MAG_FRAME_BYTES] { + let mut buf = [0u8; MAG_FRAME_BYTES]; + buf[0..4].copy_from_slice(&MAG_FRAME_MAGIC.to_le_bytes()); + buf[4..6].copy_from_slice(&MAG_FRAME_VERSION.to_le_bytes()); + buf[6..8].copy_from_slice(&self.flags.to_le_bytes()); + buf[8..10].copy_from_slice(&self.sensor_id.to_le_bytes()); + // [10..12] reserved, stays zero. + buf[12..20].copy_from_slice(&self.t_us.to_le_bytes()); + buf[20..24].copy_from_slice(&self.b_pt[0].to_le_bytes()); + buf[24..28].copy_from_slice(&self.b_pt[1].to_le_bytes()); + buf[28..32].copy_from_slice(&self.b_pt[2].to_le_bytes()); + buf[32..36].copy_from_slice(&self.sigma_pt[0].to_le_bytes()); + buf[36..40].copy_from_slice(&self.sigma_pt[1].to_le_bytes()); + buf[40..44].copy_from_slice(&self.sigma_pt[2].to_le_bytes()); + buf[44..48].copy_from_slice(&self.noise_floor_pt_sqrt_hz.to_le_bytes()); + buf[48..52].copy_from_slice(&self.temperature_k.to_le_bytes()); + // [52..60] padding stays zero. + buf + } + + /// Deserialise from a byte buffer. Validates magic, version, and + /// length; rejects any payload that doesn't match v1's exact 60-byte + /// shape with a typed [`crate::NvsimError`]. + pub fn from_bytes(buf: &[u8]) -> Result { + if buf.len() != MAG_FRAME_BYTES { + return Err(crate::NvsimError::FrameLengthMismatch { + got: buf.len(), + expected: MAG_FRAME_BYTES, + }); + } + let magic = u32::from_le_bytes(buf[0..4].try_into().expect("4-byte slice")); + if magic != MAG_FRAME_MAGIC { + return Err(crate::NvsimError::MagicMismatch { + got: magic, + expected: MAG_FRAME_MAGIC, + }); + } + let version = u16::from_le_bytes(buf[4..6].try_into().expect("2-byte slice")); + if version != MAG_FRAME_VERSION { + return Err(crate::NvsimError::UnsupportedVersion { + got: version, + supported: MAG_FRAME_VERSION, + }); + } + let flags = u16::from_le_bytes(buf[6..8].try_into().expect("2-byte slice")); + let sensor_id = u16::from_le_bytes(buf[8..10].try_into().expect("2-byte slice")); + let t_us = u64::from_le_bytes(buf[12..20].try_into().expect("8-byte slice")); + let bx = f32::from_le_bytes(buf[20..24].try_into().expect("4-byte slice")); + let by = f32::from_le_bytes(buf[24..28].try_into().expect("4-byte slice")); + let bz = f32::from_le_bytes(buf[28..32].try_into().expect("4-byte slice")); + let sx = f32::from_le_bytes(buf[32..36].try_into().expect("4-byte slice")); + let sy = f32::from_le_bytes(buf[36..40].try_into().expect("4-byte slice")); + let sz = f32::from_le_bytes(buf[40..44].try_into().expect("4-byte slice")); + let noise_floor = f32::from_le_bytes(buf[44..48].try_into().expect("4-byte slice")); + let temperature = f32::from_le_bytes(buf[48..52].try_into().expect("4-byte slice")); + Ok(Self { + flags, + sensor_id, + t_us, + b_pt: [bx, by, bz], + sigma_pt: [sx, sy, sz], + noise_floor_pt_sqrt_hz: noise_floor, + temperature_k: temperature, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn magic_is_locked_to_documented_value() { + // Plan §1.4 commits to 0xC51A_6E70. Any change must update the plan. + assert_eq!(MAG_FRAME_MAGIC, 0xC51A_6E70); + } + + #[test] + fn frame_round_trip_byte_exact() { + let mut f = MagFrame::empty(7); + f.set_flag(flag::ADC_SATURATED); + f.set_flag(flag::SHOT_NOISE_DISABLED); + f.t_us = 123_456_789; + f.b_pt = [1.5, -2.5, 3.5]; + f.sigma_pt = [0.1, 0.2, 0.3]; + f.noise_floor_pt_sqrt_hz = 100.0; + f.temperature_k = 295.0; + + let bytes = f.to_bytes(); + assert_eq!(bytes.len(), MAG_FRAME_BYTES); + let f2 = MagFrame::from_bytes(&bytes).unwrap(); + assert_eq!(f, f2); + assert!(f2.has_flag(flag::ADC_SATURATED)); + assert!(f2.has_flag(flag::SHOT_NOISE_DISABLED)); + assert!(!f2.has_flag(flag::SATURATION_NEAR_FIELD)); + } + + #[test] + fn frame_size_is_fixed_60_bytes() { + let f = MagFrame::empty(0); + assert_eq!(f.to_bytes().len(), 60); + } + + #[test] + fn frame_rejects_short_buffer() { + let err = MagFrame::from_bytes(&[0u8; 10]).unwrap_err(); + assert!(matches!(err, crate::NvsimError::FrameLengthMismatch { .. })); + } + + #[test] + fn frame_rejects_bad_magic() { + let mut bytes = MagFrame::empty(0).to_bytes(); + bytes[0..4].copy_from_slice(&0xDEAD_BEEF_u32.to_le_bytes()); + let err = MagFrame::from_bytes(&bytes).unwrap_err(); + assert!(matches!(err, crate::NvsimError::MagicMismatch { .. })); + } + + #[test] + fn frame_rejects_unsupported_version() { + let mut bytes = MagFrame::empty(0).to_bytes(); + bytes[4..6].copy_from_slice(&99_u16.to_le_bytes()); + let err = MagFrame::from_bytes(&bytes).unwrap_err(); + assert!(matches!(err, crate::NvsimError::UnsupportedVersion { got: 99, .. })); + } + + #[test] + fn frame_byte_order_is_deterministic() { + // Identical input must produce identical bytes — no allocator + // randomisation, no hashmap iteration order, no time-of-day field. + let f = MagFrame { + flags: 0, + sensor_id: 42, + t_us: 999, + b_pt: [1.0, 2.0, 3.0], + sigma_pt: [0.1, 0.2, 0.3], + noise_floor_pt_sqrt_hz: 50.0, + temperature_k: 295.0, + }; + let bytes_a = f.to_bytes(); + let bytes_b = f.to_bytes(); + assert_eq!(bytes_a, bytes_b); + } + + #[test] + fn flag_helpers_set_and_check() { + let mut f = MagFrame::empty(0); + assert!(!f.has_flag(flag::ADC_SATURATED)); + f.set_flag(flag::ADC_SATURATED); + assert!(f.has_flag(flag::ADC_SATURATED)); + assert!(!f.has_flag(flag::HEAVY_ATTENUATION)); + } +} diff --git a/v2/crates/nvsim/src/lib.rs b/v2/crates/nvsim/src/lib.rs new file mode 100644 index 00000000..33bea2b3 --- /dev/null +++ b/v2/crates/nvsim/src/lib.rs @@ -0,0 +1,82 @@ +//! NV-diamond magnetometer pipeline simulator — deterministic, no hidden mocks. +//! +//! `nvsim` is a standalone leaf crate. It models a forward-only magnetic +//! sensing path — scene → source synthesis → material attenuation → NV +//! ensemble → digitiser → binary frames + SHA-256 witness — using explicit +//! physics approximations validated against published primary sources. +//! +//! 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. +//! +//! # Implementation plan +//! +//! See `docs/research/quantum-sensing/15-nvsim-implementation-plan.md` for +//! the six-pass build spec. This release ships **Pass 1 only**: crate +//! scaffold, [`scene`] types, and the [`frame::MagFrame`] binary record. +//! +//! # Pass 1 surface +//! +//! - [`scene::Scene`], [`scene::DipoleSource`], [`scene::CurrentLoop`], +//! [`scene::FerrousObject`], [`scene::EddyCurrent`] +//! - [`frame::MagFrame`] + [`frame::MAG_FRAME_MAGIC`] (`0xC51A_6E70`) +//! - [`NvsimError`] — top-level error type for parse / serialisation failures +//! +//! Subsequent passes add `source`, `propagation`, `sensor`, `digitiser`, +//! `pipeline`, and `proof` modules. + +#![warn(missing_docs)] + +pub mod frame; +pub mod scene; + +pub use frame::{MagFrame, MAG_FRAME_MAGIC, MAG_FRAME_VERSION}; +pub use scene::{CurrentLoop, DipoleSource, EddyCurrent, FerrousObject, Scene}; + +/// Top-level simulator error type. +#[derive(Debug, thiserror::Error)] +pub enum NvsimError { + /// JSON serialisation / parsing failed for a scene or frame. + #[error("serde error: {0}")] + Serde(#[from] serde_json::Error), + + /// Magic-number mismatch on frame parse. + #[error("magic mismatch: got 0x{got:08X}, expected 0x{expected:08X}")] + MagicMismatch { + /// Magic value received. + got: u32, + /// Magic value expected. + expected: u32, + }, + + /// Frame buffer length disagrees with the fixed v1 layout. + #[error("frame length mismatch: got {got} bytes, expected {expected}")] + FrameLengthMismatch { + /// Bytes received. + got: usize, + /// Bytes expected for this version. + expected: usize, + }, + + /// Frame version is not supported by this build. + #[error("unsupported frame version: got {got}, this build supports {supported}")] + UnsupportedVersion { + /// Version received. + got: u16, + /// Highest version this build understands. + supported: u16, + }, + + /// A configuration value is out of the supported range. + #[error("invalid config: {0}")] + InvalidConfig(String), +} + +/// Permeability of free space (T·m/A). Jackson 3e §5.6. +pub const MU_0: f64 = 4.0 * std::f64::consts::PI * 1.0e-7; + +/// NV electronic gyromagnetic ratio (Hz/T). Doherty 2013 §3. +pub const GAMMA_E: f64 = 28.0e9; + +/// NV zero-field-splitting transition (Hz). Doherty 2013 §3. +pub const D_GS: f64 = 2.87e9; diff --git a/v2/crates/nvsim/src/scene.rs b/v2/crates/nvsim/src/scene.rs new file mode 100644 index 00000000..d7fa3908 --- /dev/null +++ b/v2/crates/nvsim/src/scene.rs @@ -0,0 +1,219 @@ +//! Scene types — ground-truth magnetic sources and ferrous-object distortion. +//! +//! Per `docs/research/quantum-sensing/15-nvsim-implementation-plan.md` §1.3 +//! and §2.1. All coordinates SI (metres, A·m², A); all moments are 3-vectors +//! in the simulator's global frame. Sign convention: right-hand rule. + +use serde::{Deserialize, Serialize}; + +/// 3-vector position / moment / direction. SI units. +pub type Vec3 = [f64; 3]; + +/// A point magnetic dipole in SI units. The dominant primitive — used for +/// far-field approximations of permanent magnets, current loops at distance, +/// and the linearised induced moment of ferrous objects. +/// +/// Field at `r` (relative to dipole): +/// `B = (μ₀ / 4π r³) · [3(m·r̂)r̂ − m]` (Jackson 3e §5.6). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DipoleSource { + /// Position in metres. + pub position: Vec3, + /// Magnetic moment in A·m². + pub moment: Vec3, +} + +impl DipoleSource { + /// Construct a dipole source. + pub const fn new(position: Vec3, moment: Vec3) -> Self { + Self { position, moment } + } +} + +/// A planar circular current loop, discretised at sample time into `n_segments` +/// straight segments for numerical Biot–Savart integration. The loop's normal +/// vector follows the right-hand rule on `current` (positive current produces +/// a moment along `+normal`). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CurrentLoop { + /// Centre of the loop (m). + pub centre: Vec3, + /// Unit normal vector (right-hand rule on current). + pub normal: Vec3, + /// Loop radius (m). + pub radius: f64, + /// Steady-state current (A). + pub current: f64, + /// Number of straight-segment chords for Biot–Savart integration. Default 64. + #[serde(default = "default_segments")] + pub n_segments: u32, +} + +const fn default_segments() -> u32 { + 64 +} + +impl CurrentLoop { + /// Construct a loop with the default 64-segment discretisation. + pub fn new(centre: Vec3, normal: Vec3, radius: f64, current: f64) -> Self { + Self { + centre, + normal, + radius, + current, + n_segments: default_segments(), + } + } +} + +/// A ferrous (high-χ) object that picks up a linearly-induced moment from the +/// ambient field and re-radiates as a dipole. Linear approximation — +/// `m_induced = χ · V · H_ambient` — valid in low-field, unsaturated regime +/// (Cullity & Graham 2e §2). For RuView geometry this is the dominant +/// "metallic-object detection" signal. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FerrousObject { + /// Centre of mass / centroid (m). + pub position: Vec3, + /// Volume (m³). + pub volume: f64, + /// Magnetic susceptibility (dimensionless). 5000 ≈ low-carbon steel. + pub susceptibility: f64, +} + +impl FerrousObject { + /// Construct a steel-default ferrous object (χ ≈ 5000). + pub fn steel(position: Vec3, volume: f64) -> Self { + Self { + position, + volume, + susceptibility: 5000.0, + } + } +} + +/// A simple eddy-current loop — a planar conductor that generates an opposing +/// dipole moment per Faraday's law when the ambient flux changes. Faraday + +/// Ohm: `I(t) = -(σ A / L) · dΦ/dt`. Geometry simplified to "thin disc with +/// scalar inductance" — see plan §2.1: no primary source for arbitrary +/// geometry, so this primitive is intentionally approximate. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EddyCurrent { + /// Centre of the disc (m). + pub position: Vec3, + /// Disc area (m²). + pub area: f64, + /// Conductivity (S/m). Copper ≈ 5.96e7. + pub conductivity: f64, + /// Disc inductance (H). Caller-supplied scalar. + pub inductance: f64, + /// Disc-normal unit vector. + pub normal: Vec3, +} + +/// Aggregate ground-truth scene — a list of every magnetic primitive plus a +/// list of sensor positions where the simulator should sample the field. +/// +/// `Scene` is the canonical input to [`crate::Pipeline`]. Two scenes that +/// serialise to the same JSON produce the same `(simulator, seed)` proof +/// bundle. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Scene { + /// Dipole sources (point moments). + pub dipoles: Vec, + /// Current-carrying loops. + pub loops: Vec, + /// Ferrous objects (linearly-induced dipoles). + pub ferrous: Vec, + /// Eddy-current discs (Faraday + Ohm). + pub eddy: Vec, + /// Sensor positions (one MagFrame per sensor per timestep). + pub sensors: Vec, + /// Ambient field at infinity (T) — drives ferrous induced-moment + /// computation. Zero by default. + #[serde(default)] + pub ambient_field: Vec3, +} + +impl Scene { + /// Construct an empty scene with no sources and no sensors. + pub fn new() -> Self { + Self::default() + } + + /// Append a dipole source. + pub fn add_dipole(&mut self, dipole: DipoleSource) -> &mut Self { + self.dipoles.push(dipole); + self + } + + /// Append a current loop. + pub fn add_loop(&mut self, l: CurrentLoop) -> &mut Self { + self.loops.push(l); + self + } + + /// Append a ferrous object. + pub fn add_ferrous(&mut self, ferrous: FerrousObject) -> &mut Self { + self.ferrous.push(ferrous); + self + } + + /// Append a sensor location. + pub fn add_sensor(&mut self, position: Vec3) -> &mut Self { + self.sensors.push(position); + self + } + + /// Total source count across all primitives. + pub fn n_sources(&self) -> usize { + self.dipoles.len() + self.loops.len() + self.ferrous.len() + self.eddy.len() + } + + /// Canonical JSON representation. Used by the proof bundle for content + /// addressing — two scenes with the same JSON produce the same witness. + pub fn to_canonical_json(&self) -> Result { + // serde_json::to_string is deterministic for serde-derived types when + // the underlying field order is stable, which it is here. + serde_json::to_string(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dipole_construction_round_trip_via_json() { + let d = DipoleSource::new([1.0, 2.0, 3.0], [0.1, 0.2, 0.3]); + let s = serde_json::to_string(&d).unwrap(); + let d2: DipoleSource = serde_json::from_str(&s).unwrap(); + assert_eq!(d, d2); + } + + #[test] + fn current_loop_default_n_segments_is_64() { + let l = CurrentLoop::new([0.0; 3], [0.0, 0.0, 1.0], 0.05, 1.5); + assert_eq!(l.n_segments, 64); + } + + #[test] + fn empty_scene_is_default_and_serialises() { + let s = Scene::new(); + assert_eq!(s.n_sources(), 0); + assert_eq!(s.sensors.len(), 0); + let _ = s.to_canonical_json().unwrap(); + } + + #[test] + fn scene_round_trip_via_json_preserves_all_primitives() { + let mut s = Scene::new(); + s.add_dipole(DipoleSource::new([0.0; 3], [1e-6, 0.0, 0.0])); + s.add_loop(CurrentLoop::new([0.0; 3], [0.0, 0.0, 1.0], 0.1, 0.5)); + s.add_ferrous(FerrousObject::steel([0.5; 3], 1e-3)); + s.add_sensor([1.0, 0.0, 0.0]); + let json = s.to_canonical_json().unwrap(); + let s2: Scene = serde_json::from_str(&json).unwrap(); + assert_eq!(s, s2); + } +}