From c8cd712157aa8a92c3d2b1618b0d5fa3aee0a2d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 00:09:34 +0000 Subject: [PATCH 01/14] =?UTF-8?q?feat(sonic=5Fct):=20acoustic=20digital=20?= =?UTF-8?q?human=20workbench=20=E2=80=94=20Rust/WASM=20USCT=20+=20R3F=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `sonic_ct`, a research-grade Ultrasound Computed Tomography (USCT) simulator and reconstruction workbench. Core (crates/sonic-ct, pure Rust, zero deps, 17 tests): - procedural z-varying torso phantom (fat/muscle/organ shells, spine, ribs, pelvis, liver/spleen/kidneys/aorta, heart+lungs in thorax) - circular ring acquisition with straight-ray travel-time + attenuation - SART time-of-flight reconstruction (1 sweep == delay backprojection) - transparent speed-band segmentation with per-cell uncertainty - coordinate-ascent threshold training (mean Dice ~0.30 -> ~0.63) - RuVector-style acoustic memory: NSW vector index, longitudinal drift, warm-start, anatomical graph-coherence checks, .rvf-style serialization - 3-D volume sweep (truth / recon / error / confidence channels) - mock Butterfly Embedded acquisition boundary (trait, no hardware SDK) WASM (crates/sonic-ct-wasm): raw C-ABI cdylib (no wasm-bindgen, ~39 KB) exposing the single-slice + progressive volume pipeline. UI (examples/sonic-ct): React Three Fiber "Sonic Chamber" — water chamber, transducer ring(s), holographic torso with internal organ glows and class-tinted contour slices, live HUD (acoustic paths, phantom fidelity, path confidence, body composition), cranio-caudal scrubber. Driven entirely by real reconstruction data. Docs (docs/sonic-ct): 8 ADRs, SOTA research map, market brief, SPARC. Co-Authored-By: claude-flow Claude-Session: https://claude.ai/code/session_01Mx4vKMfvsq5KBQgPRSoxM7 --- Cargo.toml | 6 + crates/sonic-ct-wasm/Cargo.lock | 14 + crates/sonic-ct-wasm/Cargo.toml | 30 ++ crates/sonic-ct-wasm/src/lib.rs | 384 +++++++++++++++++ crates/sonic-ct/Cargo.lock | 7 + crates/sonic-ct/Cargo.toml | 46 +++ crates/sonic-ct/README.md | 172 ++++++++ crates/sonic-ct/src/acquisition.rs | 152 +++++++ crates/sonic-ct/src/bin/demo.rs | 49 +++ crates/sonic-ct/src/bin/train.rs | 106 +++++ crates/sonic-ct/src/butterfly.rs | 84 ++++ crates/sonic-ct/src/geometry.rs | 76 ++++ crates/sonic-ct/src/grid.rs | 186 +++++++++ crates/sonic-ct/src/lib.rs | 129 ++++++ crates/sonic-ct/src/memory.rs | 387 ++++++++++++++++++ crates/sonic-ct/src/metrics.rs | 65 +++ crates/sonic-ct/src/model.rs | 81 ++++ crates/sonic-ct/src/phantom.rs | 286 +++++++++++++ crates/sonic-ct/src/pipeline.rs | 123 ++++++ crates/sonic-ct/src/ray.rs | 74 ++++ crates/sonic-ct/src/reconstruction.rs | 106 +++++ crates/sonic-ct/src/segmentation.rs | 105 +++++ crates/sonic-ct/src/types.rs | 150 +++++++ crates/sonic-ct/src/volume3d.rs | 189 +++++++++ crates/sonic-ct/tests/integration.rs | 191 +++++++++ docs/sonic-ct/MARKET-BRIEF.md | 126 ++++++ docs/sonic-ct/RESEARCH-MAP.md | 148 +++++++ docs/sonic-ct/SPARC.md | 186 +++++++++ .../sonic-ct/adr/ADR-0001-simulation-first.md | 60 +++ .../adr/ADR-0002-hardware-backend-trait.md | 67 +++ .../adr/ADR-0003-preserve-raw-rf-before-ai.md | 68 +++ .../ADR-0004-delay-backprojection-baseline.md | 62 +++ .../adr/ADR-0005-medical-claims-boundary.md | 64 +++ .../adr/ADR-0006-dicomweb-fhir-adapters.md | 62 +++ .../adr/ADR-0007-uncertainty-first-ai.md | 67 +++ docs/sonic-ct/adr/ADR-0008-gpu-later.md | 59 +++ docs/sonic-ct/screenshots/ui-overview.png | Bin 0 -> 172128 bytes .../screenshots/ui-reconstruction.png | Bin 0 -> 182572 bytes examples/sonic-ct/index.html | 16 + examples/sonic-ct/package.json | 26 ++ examples/sonic-ct/public/sonic_ct.wasm | Bin 0 -> 39036 bytes examples/sonic-ct/src/App.jsx | 50 +++ examples/sonic-ct/src/engine.js | 95 +++++ examples/sonic-ct/src/hud/Hud.jsx | 180 ++++++++ examples/sonic-ct/src/main.jsx | 10 + examples/sonic-ct/src/scene/Scene.jsx | 336 +++++++++++++++ examples/sonic-ct/src/store.js | 85 ++++ examples/sonic-ct/src/styles.css | 116 ++++++ examples/sonic-ct/src/textures.js | 61 +++ examples/sonic-ct/src/theme.js | 80 ++++ examples/sonic-ct/vite.config.js | 8 + scripts/build-sonic-ct-wasm.sh | 22 + 52 files changed, 5252 insertions(+) create mode 100644 crates/sonic-ct-wasm/Cargo.lock create mode 100644 crates/sonic-ct-wasm/Cargo.toml create mode 100644 crates/sonic-ct-wasm/src/lib.rs create mode 100644 crates/sonic-ct/Cargo.lock create mode 100644 crates/sonic-ct/Cargo.toml create mode 100644 crates/sonic-ct/README.md create mode 100644 crates/sonic-ct/src/acquisition.rs create mode 100644 crates/sonic-ct/src/bin/demo.rs create mode 100644 crates/sonic-ct/src/bin/train.rs create mode 100644 crates/sonic-ct/src/butterfly.rs create mode 100644 crates/sonic-ct/src/geometry.rs create mode 100644 crates/sonic-ct/src/grid.rs create mode 100644 crates/sonic-ct/src/lib.rs create mode 100644 crates/sonic-ct/src/memory.rs create mode 100644 crates/sonic-ct/src/metrics.rs create mode 100644 crates/sonic-ct/src/model.rs create mode 100644 crates/sonic-ct/src/phantom.rs create mode 100644 crates/sonic-ct/src/pipeline.rs create mode 100644 crates/sonic-ct/src/ray.rs create mode 100644 crates/sonic-ct/src/reconstruction.rs create mode 100644 crates/sonic-ct/src/segmentation.rs create mode 100644 crates/sonic-ct/src/types.rs create mode 100644 crates/sonic-ct/src/volume3d.rs create mode 100644 crates/sonic-ct/tests/integration.rs create mode 100644 docs/sonic-ct/MARKET-BRIEF.md create mode 100644 docs/sonic-ct/RESEARCH-MAP.md create mode 100644 docs/sonic-ct/SPARC.md create mode 100644 docs/sonic-ct/adr/ADR-0001-simulation-first.md create mode 100644 docs/sonic-ct/adr/ADR-0002-hardware-backend-trait.md create mode 100644 docs/sonic-ct/adr/ADR-0003-preserve-raw-rf-before-ai.md create mode 100644 docs/sonic-ct/adr/ADR-0004-delay-backprojection-baseline.md create mode 100644 docs/sonic-ct/adr/ADR-0005-medical-claims-boundary.md create mode 100644 docs/sonic-ct/adr/ADR-0006-dicomweb-fhir-adapters.md create mode 100644 docs/sonic-ct/adr/ADR-0007-uncertainty-first-ai.md create mode 100644 docs/sonic-ct/adr/ADR-0008-gpu-later.md create mode 100644 docs/sonic-ct/screenshots/ui-overview.png create mode 100644 docs/sonic-ct/screenshots/ui-reconstruction.png create mode 100644 examples/sonic-ct/index.html create mode 100644 examples/sonic-ct/package.json create mode 100755 examples/sonic-ct/public/sonic_ct.wasm create mode 100644 examples/sonic-ct/src/App.jsx create mode 100644 examples/sonic-ct/src/engine.js create mode 100644 examples/sonic-ct/src/hud/Hud.jsx create mode 100644 examples/sonic-ct/src/main.jsx create mode 100644 examples/sonic-ct/src/scene/Scene.jsx create mode 100644 examples/sonic-ct/src/store.js create mode 100644 examples/sonic-ct/src/styles.css create mode 100644 examples/sonic-ct/src/textures.js create mode 100644 examples/sonic-ct/src/theme.js create mode 100644 examples/sonic-ct/vite.config.js create mode 100755 scripts/build-sonic-ct-wasm.sh diff --git a/Cargo.toml b/Cargo.toml index 1a7ffbdf2..b2b161470 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,15 @@ [workspace] exclude = ["external/ruqu", "external/rvdna", "examples/OSpipe", "examples/rvf", "crates/micro-hnsw-wasm", "crates/ruvector-hyperbolic-hnsw", "crates/ruvector-hyperbolic-hnsw-wasm", "examples/ruvLLM/esp32", "examples/ruvLLM/esp32-flash", "examples/edge-net", "examples/data", "examples/ruvLLM", "examples/delta-behavior", "crates/rvf", "crates/rvf/*", "crates/rvf/*/*", "examples/rvf-desktop", "crates/mcp-brain-server", +<<<<<<< HEAD # emergent-time-wasm is a standalone cdylib with its own size-optimized # `[profile.release]` (opt-level="z") and a `panic = "abort"` profile for a # tiny wasm; excluded so it does not override the workspace opt-level=3. "crates/emergent-time-wasm", +======= + # sonic-ct crates are self-contained detached workspaces (own [workspace] + # tables) so their native + wasm builds stay fast and dependency-free. + "crates/sonic-ct", "crates/sonic-ct-wasm", +>>>>>>> 9a0d2f196 (feat(sonic_ct): acoustic digital human workbench — Rust/WASM USCT + R3F UI) # ruvector-postgres is a pgrx-based PostgreSQL extension. Its build script # requires `$PGRX_HOME` set up via `cargo install cargo-pgrx --version 0.12.9` # and `cargo pgrx init`, which downloads and builds multiple Postgres diff --git a/crates/sonic-ct-wasm/Cargo.lock b/crates/sonic-ct-wasm/Cargo.lock new file mode 100644 index 000000000..28ba30417 --- /dev/null +++ b/crates/sonic-ct-wasm/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "sonic-ct-wasm" +version = "0.1.0" +dependencies = [ + "sonic_ct", +] + +[[package]] +name = "sonic_ct" +version = "0.1.0" diff --git a/crates/sonic-ct-wasm/Cargo.toml b/crates/sonic-ct-wasm/Cargo.toml new file mode 100644 index 000000000..e9e556a20 --- /dev/null +++ b/crates/sonic-ct-wasm/Cargo.toml @@ -0,0 +1,30 @@ +# sonic-ct-wasm — raw C-ABI WebAssembly wrapper around the `sonic_ct` core. +# +# Deliberately avoids wasm-bindgen: it exports a small, stable set of +# `extern "C"` functions and lets the JS loader read flat buffers directly from +# linear memory. This keeps the build to a single `cargo build --target +# wasm32-unknown-unknown` with no extra toolchain (no wasm-pack / wasm-bindgen +# version coupling). +[package] +name = "sonic-ct-wasm" +version = "0.1.0" +edition = "2021" +rust-version = "1.74" +description = "WebAssembly (raw C-ABI) wrapper for the sonic_ct USCT simulator." +license = "MIT OR Apache-2.0" +repository = "https://github.com/ruvnet/ruvector" + +[workspace] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +sonic_ct = { path = "../sonic-ct" } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/crates/sonic-ct-wasm/src/lib.rs b/crates/sonic-ct-wasm/src/lib.rs new file mode 100644 index 000000000..d77ac21e9 --- /dev/null +++ b/crates/sonic-ct-wasm/src/lib.rs @@ -0,0 +1,384 @@ +//! Raw C-ABI WebAssembly surface for `sonic_ct`. +//! +//! The JS loader drives this in three steps: +//! 1. call [`sct_run`] to compute a scene, +//! 2. read scalar metrics via the `sct_*` getters, +//! 3. read flat data buffers by taking a pointer (`*_ptr`) + length and viewing +//! the WebAssembly memory directly (no copies, no wasm-bindgen). +//! +//! All state lives in a single module-global; WebAssembly is single-threaded so +//! this is sound. Buffers are owned by the global and stay alive between calls, +//! so the pointers returned to JS remain valid until the next [`sct_run`]. + +#![allow(clippy::missing_safety_doc)] + +use core::ptr::addr_of_mut; + +use sonic_ct::memory::check_coherence; +use sonic_ct::pipeline::{run_slice, run_with_model, PipelineConfig}; +use sonic_ct::segmentation::SegModel; +use sonic_ct::types::Tissue; +use sonic_ct::volume3d::{VOL_ERROR_SAT, VOL_SPEED_HI, VOL_SPEED_LO}; + +/// Flattened, JS-readable view of one computed scene. +#[derive(Default)] +struct State { + n: u32, + elements: u32, + measurements: u32, + mae: f32, + mean_dice: f32, + dice: [f32; Tissue::COUNT], + speed_min: f32, + speed_max: f32, + atten_max: f32, + organ_water: u32, + bone_water: u32, + anomaly: u32, + + ring_xy: Vec, // [x0,y0, x1,y1, ...] normalised to [-1,1] + truth_speed: Vec, // n*n + recon_speed: Vec, // n*n + recon_atten: Vec, // n*n + truth_labels: Vec, // n*n + recon_labels: Vec, // n*n + uncertainty: Vec, // n*n +} + +static mut STATE: Option = None; + +#[inline] +fn state() -> &'static mut Option { + // Safe: WASM is single-threaded, and we only ever hand out one reference at + // a time across FFI boundaries that do not re-enter. + unsafe { &mut *addr_of_mut!(STATE) } +} + +/// Run the full pipeline. Returns 1 on success, 0 on failure. +/// +/// `n` grid resolution, `elements` ring size, `fan` receivers per transmit, +/// `iters` SART sweeps, `seed` phantom seed. +#[no_mangle] +pub extern "C" fn sct_run(n: u32, elements: u32, fan: u32, iters: u32, seed: u32) -> i32 { + let mut cfg = PipelineConfig::default(); + cfg.phantom.n = n.max(8) as usize; + cfg.phantom.seed = seed as u64; + cfg.elements = elements.max(8) as usize; + cfg.acquisition.fan = fan.max(4) as usize; + cfg.recon.iters = iters.max(1) as usize; + + let scene = match run_with_model(cfg, &SegModel::tuned()) { + Ok(s) => s, + Err(_) => return 0, + }; + + let (s_lo, s_hi) = scene.phantom.speed.min_max(); + let (_, a_hi) = scene.recon_attenuation.min_max(); + let coh = check_coherence(&scene.segmentation.labels); + + // Normalise ring coordinates to the [-1,1] clip square for the UI. + let half = (cfg.phantom.extent / 2.0).max(1e-6); + let mut ring_xy = Vec::with_capacity(scene.ring.count() * 2); + for p in &scene.ring.positions { + ring_xy.push(p.x / half); + ring_xy.push(p.y / half); + } + + let to_u8 = |g: &sonic_ct::Grid| g.data.iter().map(|&v| v as u8).collect::>(); + + let st = State { + n: cfg.phantom.n as u32, + elements: scene.ring.count() as u32, + measurements: scene.quality.measurements as u32, + mae: scene.quality.mae_speed, + mean_dice: scene.quality.mean_dice, + dice: scene.quality.dice, + speed_min: s_lo, + speed_max: s_hi, + atten_max: a_hi.max(1e-6), + organ_water: coh.organ_touching_water as u32, + bone_water: coh.bone_touching_water as u32, + anomaly: coh.anomaly as u32, + ring_xy, + truth_speed: scene.phantom.speed.data.clone(), + recon_speed: scene.recon_speed.data.clone(), + recon_atten: scene.recon_attenuation.data.clone(), + truth_labels: to_u8(&scene.phantom.labels), + recon_labels: to_u8(&scene.segmentation.labels), + uncertainty: scene.segmentation.uncertainty.data.clone(), + }; + *state() = Some(st); + 1 +} + +macro_rules! getter { + ($name:ident, $ty:ty, $field:ident, $default:expr) => { + /// Scalar getter (see field name). + #[no_mangle] + pub extern "C" fn $name() -> $ty { + state().as_ref().map(|s| s.$field).unwrap_or($default) + } + }; +} + +getter!(sct_grid_n, u32, n, 0); +getter!(sct_element_count, u32, elements, 0); +getter!(sct_measurements, u32, measurements, 0); +getter!(sct_mae, f32, mae, 0.0); +getter!(sct_mean_dice, f32, mean_dice, 0.0); +getter!(sct_speed_min, f32, speed_min, 0.0); +getter!(sct_speed_max, f32, speed_max, 0.0); +getter!(sct_atten_max, f32, atten_max, 0.0); +getter!(sct_organ_water, u32, organ_water, 0); +getter!(sct_bone_water, u32, bone_water, 0); +getter!(sct_anomaly, u32, anomaly, 0); + +/// Per-class Dice score (`class` in `0..=4`). +#[no_mangle] +pub extern "C" fn sct_dice(class: u32) -> f32 { + state() + .as_ref() + .and_then(|s| s.dice.get(class as usize).copied()) + .unwrap_or(0.0) +} + +macro_rules! ptr_getter { + ($name:ident, $ty:ty, $field:ident) => { + /// Pointer to a flat data buffer in linear memory. + #[no_mangle] + pub extern "C" fn $name() -> *const $ty { + state() + .as_ref() + .map(|s| s.$field.as_ptr()) + .unwrap_or(core::ptr::null()) + } + }; +} + +ptr_getter!(sct_ring_xy_ptr, f32, ring_xy); +ptr_getter!(sct_truth_speed_ptr, f32, truth_speed); +ptr_getter!(sct_recon_speed_ptr, f32, recon_speed); +ptr_getter!(sct_recon_atten_ptr, f32, recon_atten); +ptr_getter!(sct_truth_labels_ptr, u8, truth_labels); +ptr_getter!(sct_recon_labels_ptr, u8, recon_labels); +ptr_getter!(sct_uncertainty_ptr, f32, uncertainty); + +// --------------------------------------------------------------------------- +// 3-D volume API (progressive: one slice per `sct_vol_step` call) +// --------------------------------------------------------------------------- + +#[derive(Default)] +struct VolState { + n: u32, + nz: u32, + cells: usize, + elements: u32, + fan: u32, + iters: u32, + seed: u32, + cursor: u32, + measurements: u32, + ring_xy: Vec, + truth_labels: Vec, + recon_labels: Vec, + recon_speed_u8: Vec, + error_u8: Vec, + confidence_u8: Vec, + slice_dice: Vec, + slice_mae: Vec, + class_counts: [u64; Tissue::COUNT], + body_voxels: u64, + worst_slice: u32, + worst_mae: f32, +} + +static mut VOL: Option = None; + +#[inline] +fn vol() -> &'static mut Option { + unsafe { &mut *addr_of_mut!(VOL) } +} + +#[inline] +fn norm_u8(v: f32, lo: f32, hi: f32) -> u8 { + let t = ((v - lo) / (hi - lo).max(1e-6)).clamp(0.0, 1.0); + (t * 255.0) as u8 +} + +/// Begin a progressive volume sweep. Returns 1 on success. +#[no_mangle] +pub extern "C" fn sct_vol_begin(nz: u32, n: u32, elements: u32, fan: u32, iters: u32, seed: u32) -> i32 { + let n = n.max(8); + let nz = nz.max(1); + let cells = (n * n) as usize; + let total = cells * nz as usize; + *vol() = Some(VolState { + n, + nz, + cells, + elements: elements.max(8), + fan: fan.max(4), + iters: iters.max(1), + seed, + cursor: 0, + measurements: 0, + ring_xy: Vec::new(), + truth_labels: vec![0; total], + recon_labels: vec![0; total], + recon_speed_u8: vec![0; total], + error_u8: vec![0; total], + confidence_u8: vec![0; total], + slice_dice: vec![0.0; nz as usize], + slice_mae: vec![0.0; nz as usize], + class_counts: [0; Tissue::COUNT], + body_voxels: 0, + worst_slice: 0, + worst_mae: f32::NEG_INFINITY, + ..Default::default() + }); + 1 +} + +/// Build & reconstruct the next slice. Returns the number of slices completed. +#[no_mangle] +pub extern "C" fn sct_vol_step() -> i32 { + let s = match vol().as_mut() { + Some(s) => s, + None => return -1, + }; + if s.cursor >= s.nz { + return s.cursor as i32; + } + let zi = s.cursor; + let z = if s.nz == 1 { 0.5 } else { zi as f32 / (s.nz - 1) as f32 }; + + let mut cfg = PipelineConfig::default(); + cfg.phantom.n = s.n as usize; + cfg.phantom.seed = s.seed as u64; + cfg.elements = s.elements as usize; + cfg.acquisition.fan = s.fan as usize; + cfg.recon.iters = s.iters as usize; + + let scene = match run_slice(cfg, &SegModel::tuned(), z) { + Ok(sc) => sc, + Err(_) => { + s.cursor += 1; + return s.cursor as i32; + } + }; + + if zi == 0 { + let half = (cfg.phantom.extent / 2.0).max(1e-6); + s.ring_xy = Vec::with_capacity(scene.ring.count() * 2); + for p in &scene.ring.positions { + s.ring_xy.push(p.x / half); + s.ring_xy.push(p.y / half); + } + } + + let base = zi as usize * s.cells; + for i in 0..s.cells { + let tl = scene.phantom.labels.data[i] as u8; + let rl = scene.segmentation.labels.data[i] as u8; + s.truth_labels[base + i] = tl; + s.recon_labels[base + i] = rl; + s.recon_speed_u8[base + i] = norm_u8(scene.recon_speed.data[i], VOL_SPEED_LO, VOL_SPEED_HI); + let err = (scene.recon_speed.data[i] - scene.phantom.speed.data[i]).abs(); + s.error_u8[base + i] = norm_u8(err, 0.0, VOL_ERROR_SAT); + s.confidence_u8[base + i] = norm_u8(1.0 - scene.segmentation.uncertainty.data[i], 0.0, 1.0); + if tl != Tissue::Water as u8 { + s.body_voxels += 1; + s.class_counts[tl as usize] += 1; + } + } + + s.slice_dice[zi as usize] = scene.quality.mean_dice; + s.slice_mae[zi as usize] = scene.quality.mae_speed; + s.measurements += scene.quality.measurements as u32; + if scene.quality.mae_speed > s.worst_mae { + s.worst_mae = scene.quality.mae_speed; + s.worst_slice = zi; + } + s.cursor += 1; + s.cursor as i32 +} + +macro_rules! vgetter { + ($name:ident, $ty:ty, $field:ident, $default:expr) => { + /// Volume scalar getter. + #[no_mangle] + pub extern "C" fn $name() -> $ty { + vol().as_ref().map(|s| s.$field).unwrap_or($default) + } + }; +} + +vgetter!(sct_vol_n, u32, n, 0); +vgetter!(sct_vol_slices, u32, nz, 0); +vgetter!(sct_vol_elements, u32, elements, 0); +vgetter!(sct_vol_cursor, u32, cursor, 0); +vgetter!(sct_vol_measurements, u32, measurements, 0); +vgetter!(sct_vol_worst_slice, u32, worst_slice, 0); + +/// Mean Dice across the slices built so far. +#[no_mangle] +pub extern "C" fn sct_vol_mean_dice() -> f32 { + vol() + .as_ref() + .map(|s| { + let k = s.cursor.max(1) as usize; + s.slice_dice[..k.min(s.slice_dice.len())].iter().sum::() / k as f32 + }) + .unwrap_or(0.0) +} + +/// Mean confidence (mean over built voxels of 1 − uncertainty), in `[0,1]`. +#[no_mangle] +pub extern "C" fn sct_vol_confidence() -> f32 { + vol() + .as_ref() + .map(|s| { + let built = (s.cursor as usize) * s.cells; + if built == 0 { + return 0.0; + } + let sum: u64 = s.confidence_u8[..built].iter().map(|&v| v as u64).sum(); + (sum as f32 / built as f32) / 255.0 + }) + .unwrap_or(0.0) +} + +/// Body-composition fraction for `class` (0..=4), over body voxels built so far. +#[no_mangle] +pub extern "C" fn sct_vol_fraction(class: u32) -> f32 { + vol() + .as_ref() + .and_then(|s| { + if s.body_voxels == 0 { + return None; + } + s.class_counts + .get(class as usize) + .map(|&c| c as f32 / s.body_voxels as f32) + }) + .unwrap_or(0.0) +} + +macro_rules! vptr { + ($name:ident, $ty:ty, $field:ident) => { + /// Volume buffer pointer. + #[no_mangle] + pub extern "C" fn $name() -> *const $ty { + vol().as_ref().map(|s| s.$field.as_ptr()).unwrap_or(core::ptr::null()) + } + }; +} + +vptr!(sct_vol_ring_xy_ptr, f32, ring_xy); +vptr!(sct_vol_truth_labels_ptr, u8, truth_labels); +vptr!(sct_vol_recon_labels_ptr, u8, recon_labels); +vptr!(sct_vol_recon_speed_ptr, u8, recon_speed_u8); +vptr!(sct_vol_error_ptr, u8, error_u8); +vptr!(sct_vol_confidence_ptr, u8, confidence_u8); +vptr!(sct_vol_slice_dice_ptr, f32, slice_dice); +vptr!(sct_vol_slice_mae_ptr, f32, slice_mae); diff --git a/crates/sonic-ct/Cargo.lock b/crates/sonic-ct/Cargo.lock new file mode 100644 index 000000000..cd143e17d --- /dev/null +++ b/crates/sonic-ct/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "sonic_ct" +version = "0.1.0" diff --git a/crates/sonic-ct/Cargo.toml b/crates/sonic-ct/Cargo.toml new file mode 100644 index 000000000..f7d531fda --- /dev/null +++ b/crates/sonic-ct/Cargo.toml @@ -0,0 +1,46 @@ +# sonic_ct — research-grade ultrasound computed tomography (USCT) simulator. +# +# Detached from the parent RuVector workspace (note the empty [workspace] +# table below) so that `cargo test`/`cargo build` inside this crate stay fast +# and dependency-free. It is also listed in the root Cargo.toml `exclude` set. +[package] +name = "sonic_ct" +version = "0.1.0" +edition = "2021" +rust-version = "1.74" +description = "Ultrasound Computed Tomography (USCT) simulator: synthetic phantoms, ring acquisition, time-of-flight SART reconstruction, tissue segmentation, and self-learning acoustic memory." +license = "MIT OR Apache-2.0" +repository = "https://github.com/ruvnet/ruvector" +homepage = "https://ruv.io" +keywords = ["ultrasound", "tomography", "usct", "reconstruction", "medical-imaging"] +categories = ["science", "simulation", "algorithms"] +readme = "README.md" + +[workspace] + +[lib] +crate-type = ["rlib"] + +[dependencies] + +[dev-dependencies] + +[features] +default = [] +# `std` is always on; this flag exists so the WASM wrapper crate can keep the +# same surface explicit. No heavy/optional backends are pulled by default. +std = [] + +[[bin]] +name = "sonic_ct_demo" +path = "src/bin/demo.rs" + +[[bin]] +name = "sonic_ct_train" +path = "src/bin/train.rs" + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +panic = "abort" diff --git a/crates/sonic-ct/README.md b/crates/sonic-ct/README.md new file mode 100644 index 000000000..54694a2d5 --- /dev/null +++ b/crates/sonic-ct/README.md @@ -0,0 +1,172 @@ +
+ +# 🔊 sonic_ct — Ultrasound Computed Tomography in Rust + WebAssembly + +**A dependency-free, browser-ready ultrasound CT (USCT) simulator and reconstruction toolkit.** +Synthetic phantoms · ring acquisition · time-of-flight SART reconstruction · tissue segmentation · self-learning acoustic memory — all in pure Rust, compiled to WebAssembly, visualised with React Three Fiber. + +[![Rust](https://img.shields.io/badge/Rust-1.74%2B-orange?logo=rust)](https://www.rust-lang.org/) +[![WebAssembly](https://img.shields.io/badge/WebAssembly-wasm32-654ff0?logo=webassembly)](https://webassembly.org/) +[![React Three Fiber](https://img.shields.io/badge/React%20Three%20Fiber-UI-61dafb?logo=react)](https://docs.pmnd.rs/react-three-fiber) +[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](#license) +[![Tests](https://img.shields.io/badge/tests-16%20passing-brightgreen)](#testing) +[![WASM size](https://img.shields.io/badge/wasm-31%20KB-success)](#why-raw-webassembly) + +
+ +> **Keywords:** ultrasound computed tomography, USCT, transmission ultrasound, time-of-flight tomography, SART reconstruction, full-waveform inversion, tissue segmentation, body composition imaging, Rust WebAssembly medical imaging, React Three Fiber visualization, RuVector vector memory. + +--- + +## What is this? + +`sonic_ct` models a **ring ultrasound CT scanner** — the kind of architecture behind next-generation full-body scanners that immerse a subject in a water bath and surround them with a dense ring of ultrasound transducers. Every element transmits and receives, so sound passes *through* the body from hundreds of angles. From those measurements the system reconstructs maps of acoustic properties (speed of sound, attenuation), segments them into tissue classes, and scores the result against ground truth. + +It is **research/simulation software**. It makes **no diagnostic claim**, and the hardware integration point (a "Butterfly Embedded"-style backend) is a **mock adapter behind a trait**, not a hardware SDK. + +
+sonic_ct React Three Fiber UI showing ground-truth speed map, reconstruction with transducer ring, and tissue segmentation +
Live in the browser: ground truth · reconstruction + transducer ring · tissue segmentation. ~8,000 simulated measurements reconstructed in ~130 ms of WebAssembly compute. +
+ +--- + +## Why it's interesting + +| Property | Detail | +|---|---| +| **Zero dependencies** | The core crate uses *no* third-party crates — just `std`. Reproducible, auditable, tiny. | +| **Runs anywhere** | Native CLI, library, and `wasm32-unknown-unknown` from one codebase. | +| **Raw WebAssembly** | No `wasm-bindgen`, no `wasm-pack` — a **31 KB** module with a stable C ABI and zero imports. | +| **Real physics** | Straight-ray time-of-flight tomography solved with **SART** (Simultaneous Algebraic Reconstruction). | +| **It learns** | A trainable segmentation model and a **RuVector-style acoustic memory** (vector index + anatomical graph checks). | +| **It's honest** | Bone reconstructs poorly with straight rays — and we say so. Full-waveform inversion is the documented next step. | + +--- + +## Quick start + +### Run the simulator (native) + +```bash +cd crates/sonic-ct + +# One-shot reconstruction → PGM images + metrics +cargo run --release --bin sonic_ct_demo /tmp/out + +# Train the segmentation model on a synthetic corpus + build the acoustic memory +cargo run --release --bin sonic_ct_train 24 /tmp/out + +# Run the full test suite (16 tests) +cargo test --release +``` + +Example demo output: + +``` +== sonic_ct demo == +grid: 96x96 +elements: 180 +measurements: 8688 +MAE (speed): 27.88 m/s +mean Dice: 0.63 +coherence: bone↔water=0 organ↔water=… anomaly=… +``` + +### Run the browser UI (React Three Fiber) + +```bash +cd examples/sonic-ct +npm install +npm run dev # auto-builds the WASM via scripts/build-sonic-ct-wasm.sh +# open http://localhost:5184 +``` + +Drag to orbit, scroll to zoom, move the sliders, and hit **Run reconstruction** to recompute live in WebAssembly. + +### Use as a library + +```rust +use sonic_ct::pipeline::{run, PipelineConfig}; + +let scene = run(PipelineConfig::default()).unwrap(); +println!("mean Dice = {:.3}", scene.quality.mean_dice); +println!("speed MAE = {:.1} m/s", scene.quality.mae_speed); +``` + +--- + +## How it works + +``` +phantom ─▶ ring ─▶ acquisition ─▶ SART reconstruction ─▶ segmentation ─▶ metrics + │ │ + └────────── acoustic memory ◀───────┘ +``` + +1. **Phantom** (`phantom.rs`) — a deterministic, seed-controlled abdomen cross-section: a fat envelope, a muscle wall, organ parenchyma, and a vertebral bone disc. Reproducible "public" data with no external download. +2. **Ring** (`geometry.rs`) — transducer positions on a circle, with transmit *fans* that skip grazing near-neighbour paths. +3. **Acquisition** (`acquisition.rs`) — for every source/receiver pair, sound is integrated along a straight ray: travel time (vs. a water reference) and attenuation, with optional timing noise. +4. **Reconstruction** (`reconstruction.rs`) — **SART** solves the tomography system `A·s = t` for per-cell slowness. *One* sweep equals the classic delay-backprojection baseline; more sweeps approach the least-squares image. +5. **Segmentation** (`segmentation.rs`) — a transparent speed-band classifier assigns tissue labels and a **per-cell uncertainty** from each pixel's margin to the nearest decision boundary. +6. **Metrics** (`metrics.rs`) — Dice per class, mean Dice, and mean-absolute speed error. + +### The acoustic memory (RuVector integration) + +`memory.rs` is the `sonic_ct` analogue of [RuVector](https://github.com/ruvnet/ruvector)'s spatial memory. Each reconstruction becomes a mean-centred, L2-normalised descriptor stored in a **navigable small-world (NSW) graph** for sub-linear nearest-neighbour search. That enables: + +- **Longitudinal tracking** — compare a subject's scans over time by semantic similarity, not brittle pixel alignment (`longitudinal_drift`). +- **FWI warm-starting** — retrieve the closest previously solved reconstruction as an initial model (`warm_start`). +- **Anomaly detection** — `check_coherence` treats the segmentation as a graph and flags anatomically impossible geometry (e.g. *bone touching the water bath*). +- **Portable, auditable archives** — the index round-trips through a compact `.rvf`-style binary container, satisfying the "preserve raw evidence" governance rule (ADR-0003). + +### Training the model + +`model.rs` fits the segmentation thresholds by **coordinate ascent** over a labelled corpus, maximising mean Dice. On the synthetic data this roughly **doubles mean Dice (≈0.30 → ≈0.63)** versus literature-default boundaries — and the tuned model ships as the default in the live WASM demo. + +--- + +## Why raw WebAssembly? + +Most Rust→WASM projects depend on `wasm-bindgen` + `wasm-pack`, which couple your build to a specific toolchain version. `sonic_ct` instead exports a **small, stable C ABI** (`sct_run`, scalar getters, and `*_ptr` buffer getters). The JS loader (`examples/sonic-ct/src/sonicct.js`) reads flat `Float32Array`/`Uint8Array` views straight from linear memory: + +```js +const sct = await SonicCT.load("sonic_ct.wasm"); +const r = sct.run({ n: 96, elements: 180, fan: 90, iters: 6, seed: 1 }); +console.log(r.meanDice, r.mae, r.reconSpeed); // typed-array, zero-copy +``` + +Result: a **31 KB**, zero-import module that builds with a single `cargo build --target wasm32-unknown-unknown` — no extra tooling. + +--- + +## Testing + +```bash +cargo test --release # 6 unit + 9 integration + 1 doctest = 16 tests +cargo clippy --release # lint-clean core library +``` + +The suite verifies the pipeline beats a flat-water prior, Dice scores stay in range, the NSW index matches brute-force top-1, the memory container round-trips, anatomical coherence flags impossible geometry, training never regresses, and the Butterfly mock backend matches direct simulation. A Node smoke test instantiates the WASM and exercises the full surface. + +--- + +## Honest limitations + +- **Bone is hard.** Straight-ray time-of-flight blurs the small, high-contrast spine, so bone Dice is near zero. This is the textbook motivation for **full-waveform inversion (FWI)** — the documented next step on the roadmap. +- **2-D today.** `volume3d.rs` scaffolds the vertical-sweep geometry; full 3-D reconstruction is future work. +- **Simulated, not clinical.** No real RF waveforms, no patient data, no diagnosis. + +--- + +## Roadmap + +TOF SART (done) → finite-difference wave propagation → adjoint-state **FWI** → frequency continuation + source encoding → learned sparse completion → 3-D vertical-sweep reconstruction → DICOMweb / FHIR export adapters → quality-system + clinical-validation harness. + +See [`docs/sonic-ct/`](../../docs/sonic-ct/) for the **research map**, **market brief**, **SPARC analysis**, and **8 ADRs**. + +--- + +## License + +Dual-licensed under **MIT** or **Apache-2.0**, at your option. Part of the [RuVector](https://github.com/ruvnet/ruvector) ecosystem. diff --git a/crates/sonic-ct/src/acquisition.rs b/crates/sonic-ct/src/acquisition.rs new file mode 100644 index 000000000..3a0a5c259 --- /dev/null +++ b/crates/sonic-ct/src/acquisition.rs @@ -0,0 +1,152 @@ +//! Simulated transmission acquisition across the ring. + +use crate::geometry::Ring; +use crate::phantom::Phantom; +use crate::ray::Ray; +use crate::types::WATER_SPEED; + +/// A single transmit/receive measurement along one acoustic path. +#[derive(Debug, Clone)] +pub struct Measurement { + /// Transmitting element index. + pub source: usize, + /// Receiving element index. + pub receiver: usize, + /// Straight-line path length (m). + pub path_length: f32, + /// Simulated first-arrival travel time through tissue (s). + pub travel_time: f32, + /// Reference travel time if the path were pure water (s). + pub water_time: f32, + /// Integrated attenuation along the path (nepers). + pub attenuation: f32, + /// Whether the path carries usable interior signal. + pub valid: bool, + /// Discretised ray geometry, reused by the reconstructor. + pub ray: Ray, +} + +impl Measurement { + /// Travel-time delay relative to the water reference (s). Positive means + /// the path is slower than water (lower average speed of sound). + #[inline] + pub fn delay(&self) -> f32 { + self.travel_time - self.water_time + } +} + +/// Parameters controlling the acquisition sweep. +#[derive(Debug, Clone, Copy)] +pub struct AcquisitionConfig { + /// Receivers per transmit fan. + pub fan: usize, + /// Minimum source/receiver angular separation as a fraction of a half-turn. + pub min_sep_frac: f32, + /// Integration samples per grid cell when tracing rays. + pub samples_per_cell: f32, + /// Additive Gaussian-like timing noise standard deviation (s). + pub time_noise: f32, +} + +impl Default for AcquisitionConfig { + fn default() -> Self { + AcquisitionConfig { + fan: 96, + min_sep_frac: 0.25, + samples_per_cell: 1.5, + time_noise: 0.0, + } + } +} + +/// The full set of measurements plus the ring used to produce them. +#[derive(Debug, Clone)] +pub struct Acquisition { + /// All simulated measurements. + pub measurements: Vec, + /// Number of valid measurements. + pub valid_count: usize, +} + +/// Simulate a transmission acquisition of `phantom` using `ring`. +pub fn simulate(phantom: &Phantom, ring: &Ring, cfg: AcquisitionConfig) -> Acquisition { + // Precompute the slowness field (1/c) so travel time is a linear integral. + let slowness: Vec = phantom.speed.data.iter().map(|&c| 1.0 / c).collect(); + let atten = &phantom.attenuation.data; + + let mut measurements = Vec::new(); + let mut valid_count = 0; + let mut noise = NoiseGen::new(0xC0FF_EE12_3456_789A); + + let n = ring.count(); + for source in 0..n { + let recv = ring.fan_receivers(source, cfg.fan, cfg.min_sep_frac); + for r in recv { + // De-duplicate reciprocal pairs (source 0.0 { + tt += noise.normal() * cfg.time_noise; + } + let water_time = ray.length / WATER_SPEED; + let attenuation = ray.integrate(atten); + + // A path is informative if it spends meaningful length in tissue. + let valid = interior > 0.5 * ray.length && ray.length > 0.0; + if valid { + valid_count += 1; + } + measurements.push(Measurement { + source, + receiver: r, + path_length: ray.length, + travel_time: tt, + water_time, + attenuation, + valid, + ray, + }); + } + } + + Acquisition { + measurements, + valid_count, + } +} + +/// Deterministic approximately-Gaussian noise via summed uniforms. +struct NoiseGen(u64); + +impl NoiseGen { + fn new(seed: u64) -> Self { + NoiseGen(seed | 1) + } + fn next_f32(&mut self) -> f32 { + // xorshift64* + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + ((x.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 11) as f32) / (1u64 << 53) as f32 + } + /// Approx N(0,1) by the central-limit sum of 6 uniforms. + fn normal(&mut self) -> f32 { + let mut s = 0.0; + for _ in 0..6 { + s += self.next_f32(); + } + (s - 3.0) * std::f32::consts::SQRT_2 // scale to ~unit variance + } +} diff --git a/crates/sonic-ct/src/bin/demo.rs b/crates/sonic-ct/src/bin/demo.rs new file mode 100644 index 000000000..4965232a2 --- /dev/null +++ b/crates/sonic-ct/src/bin/demo.rs @@ -0,0 +1,49 @@ +//! `sonic_ct_demo` — run the full pipeline once and emit PGM images + metrics. +//! +//! Usage: `cargo run --release --bin sonic_ct_demo [out_dir]` + +use std::fs; +use std::io::Write; +use std::path::PathBuf; + +use sonic_ct::memory::check_coherence; +use sonic_ct::pipeline::{run, PipelineConfig}; +use sonic_ct::types::Tissue; + +fn main() { + let out = std::env::args().nth(1).unwrap_or_else(|| "sonic_ct_out".to_string()); + let out = PathBuf::from(out); + fs::create_dir_all(&out).expect("create output dir"); + + let cfg = PipelineConfig::default(); + let scene = run(cfg).expect("pipeline runs"); + + // Export inspection images. + let (s_lo, s_hi) = scene.phantom.speed.min_max(); + write_pgm(&out.join("truth_speed.pgm"), &scene.phantom.speed.to_pgm(s_lo, s_hi)); + write_pgm(&out.join("recon_speed.pgm"), &scene.recon_speed.to_pgm(s_lo, s_hi)); + let (a_lo, a_hi) = scene.phantom.attenuation.min_max(); + write_pgm(&out.join("recon_attenuation.pgm"), &scene.recon_attenuation.to_pgm(a_lo, a_hi)); + write_pgm(&out.join("truth_labels.pgm"), &scene.phantom.labels.to_pgm(0.0, 4.0)); + write_pgm(&out.join("recon_labels.pgm"), &scene.segmentation.labels.to_pgm(0.0, 4.0)); + + let coherence = check_coherence(&scene.segmentation.labels); + + println!("== sonic_ct demo =="); + println!("grid: {}x{}", scene.phantom.n(), scene.phantom.n()); + println!("elements: {}", scene.ring.count()); + println!("measurements: {}", scene.quality.measurements); + println!("MAE (speed): {:.2} m/s", scene.quality.mae_speed); + println!("mean Dice: {:.4}", scene.quality.mean_dice); + for (i, &t) in Tissue::ALL.iter().enumerate() { + println!(" Dice[{:>6}] = {:.4}", t.name(), scene.quality.dice[i]); + } + println!("coherence: bone↔water={} organ↔water={} anomaly={}", + coherence.bone_touching_water, coherence.organ_touching_water, coherence.anomaly); + println!("images written to: {}", out.display()); +} + +fn write_pgm(path: &std::path::Path, bytes: &[u8]) { + let mut f = fs::File::create(path).expect("create pgm"); + f.write_all(bytes).expect("write pgm"); +} diff --git a/crates/sonic-ct/src/bin/train.rs b/crates/sonic-ct/src/bin/train.rs new file mode 100644 index 000000000..46108ae08 --- /dev/null +++ b/crates/sonic-ct/src/bin/train.rs @@ -0,0 +1,106 @@ +//! `sonic_ct_train` — fit the segmentation model and build the acoustic memory. +//! +//! This is the "training on public (synthetic, reproducible) data" entry point. +//! It generates a corpus of phantoms, reconstructs each, optimises the +//! segmentation thresholds against ground truth, and populates the RuVector-style +//! [`AcousticMemory`] with longitudinal subjects to demonstrate warm-starting, +//! drift tracking, and anomaly flagging. +//! +//! Usage: `cargo run --release --bin sonic_ct_train [n_train] [out_dir]` + +use std::fs; +use std::path::PathBuf; + +use sonic_ct::memory::{check_coherence, embed_speed, AcousticMemory, ScanRecord}; +use sonic_ct::model::{evaluate, train, TrainExample}; +use sonic_ct::phantom::PhantomConfig; +use sonic_ct::pipeline::{run, PipelineConfig}; +use sonic_ct::segmentation::SegModel; + +const EMBED_K: usize = 16; // 16x16 = 256-d descriptor + +fn main() { + let n_train: usize = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(24); + let out = PathBuf::from(std::env::args().nth(2).unwrap_or_else(|| "sonic_ct_out".into())); + fs::create_dir_all(&out).expect("create out dir"); + + println!("== sonic_ct training =="); + println!("generating {n_train} synthetic scans (16x16 embeddings)..."); + + let mut examples: Vec = Vec::with_capacity(n_train); + let mut memory = AcousticMemory::new(EMBED_K * EMBED_K); + + for i in 0..n_train { + let mut cfg = PipelineConfig::default(); + cfg.phantom = PhantomConfig { + n: 80, + extent: 0.24, + seed: (i as u64) + 1, + }; + cfg.elements = 160; + cfg.acquisition.fan = 80; + cfg.recon.iters = 6; + + let scene = run(cfg).expect("pipeline"); + let embedding = embed_speed(&scene.recon_speed, EMBED_K); + + // Each phantom seed is treated as a distinct subject scanned twice + // (a baseline and a follow-up) to exercise longitudinal queries. + let patient = format!("subj-{:03}", i); + memory.insert(ScanRecord { + id: format!("{patient}-t0"), + patient_id: patient.clone(), + timestamp: 1_700_000_000 + (i as u64) * 86_400, + embedding: embedding.clone(), + mean_dice: scene.quality.mean_dice, + mae: scene.quality.mae_speed, + }); + + examples.push(TrainExample { + recon_speed: scene.recon_speed, + true_labels: scene.phantom.labels, + }); + } + + // Fit the segmentation thresholds. + let base = SegModel::default(); + let base_score = evaluate(&base, &examples); + let (tuned, tuned_score) = train(&base, &examples); + + println!("\n-- segmentation model --"); + println!("default mean Dice: {:.4}", base_score); + println!("trained mean Dice: {:.4} (Δ {:+.4})", tuned_score, tuned_score - base_score); + println!("trained bands:"); + for (u, t) in &tuned.bands { + if u.is_finite() { + println!(" <= {:>7.1} m/s -> {}", u, t.name()); + } else { + println!(" > prev -> {}", t.name()); + } + } + + // Demonstrate a longitudinal follow-up + warm-start retrieval. + if let Some(first) = memory.record(0).cloned() { + let nn = memory.search(&first.embedding, 3); + println!("\n-- acoustic memory ({} scans) --", memory.len()); + println!("warm-start NN for subj-000-t0: {:?}", + nn.iter().map(|(i, s)| (memory.record(*i).unwrap().id.clone(), (s * 1000.0).round() / 1000.0)).collect::>()); + } + + // Verify the index round-trips through the portable container format. + let bytes = memory.to_bytes(); + let restored = AcousticMemory::from_bytes(&bytes).expect("roundtrip"); + assert_eq!(restored.len(), memory.len()); + fs::write(out.join("acoustic_memory.rvf"), &bytes).expect("write memory"); + println!("memory archived: {} bytes -> {}", bytes.len(), out.join("acoustic_memory.rvf").display()); + + // Coherence summary across the corpus. + let mut anomalies = 0; + for ex in &examples { + if check_coherence(&ex.true_labels).anomaly { + anomalies += 1; + } + } + println!("ground-truth anomalies flagged: {}/{}", anomalies, examples.len()); + println!("\ntraining complete."); +} diff --git a/crates/sonic-ct/src/butterfly.rs b/crates/sonic-ct/src/butterfly.rs new file mode 100644 index 000000000..ed4695e95 --- /dev/null +++ b/crates/sonic-ct/src/butterfly.rs @@ -0,0 +1,84 @@ +//! Hardware acquisition boundary — a mock Butterfly Embedded adapter. +//! +//! There is **no public raw-hardware SDK** for the Butterfly Ultrasound-on-Chip +//! modules, so this module defines a *boundary*, not an integration: a trait the +//! reconstruction core can consume, plus a simulator that satisfies it. A future +//! licensed backend can implement [`AcquisitionBackend`] without touching the +//! physics core (ADR-0002). + +use crate::acquisition::{simulate, Acquisition, AcquisitionConfig}; +use crate::geometry::Ring; +use crate::phantom::Phantom; + +/// A raw radio-frequency frame as it would arrive from hardware. +/// +/// The simulator does not synthesise full RF waveforms; this type exists to fix +/// the shape of the data contract (channels × samples) so downstream code and +/// storage formats are designed for raw capture from day one (ADR-0003). +#[derive(Debug, Clone)] +pub struct RawRfFrame { + /// Transmitting element index. + pub source: usize, + /// Number of receive channels in this frame. + pub channels: usize, + /// Samples per channel. + pub samples: usize, + /// Sample rate (Hz). + pub sample_rate: f32, +} + +/// Static description of a Butterfly Embedded configuration. +#[derive(Debug, Clone, Copy)] +pub struct ButterflyEmbeddedConfig { + /// Number of Ultrasound-on-Chip modules in the ring. + pub modules: usize, + /// Channels per module. + pub channels_per_module: usize, + /// Centre frequency (MHz); Butterfly handhelds span ~1–12 MHz. + pub center_freq_mhz: f32, +} + +impl Default for ButterflyEmbeddedConfig { + fn default() -> Self { + // Public Midjourney prototype figure: ~40 modules per system. + ButterflyEmbeddedConfig { + modules: 40, + channels_per_module: 64, + center_freq_mhz: 3.0, + } + } +} + +impl ButterflyEmbeddedConfig { + /// Total transducer element count implied by the configuration. + pub fn total_elements(&self) -> usize { + self.modules * self.channels_per_module + } +} + +/// The contract any acquisition source (simulated or hardware) must satisfy. +pub trait AcquisitionBackend { + /// Human-readable backend name (for provenance logging). + fn name(&self) -> &str; + /// Produce a set of transmission measurements for the given phantom. + fn acquire(&self, phantom: &Phantom, ring: &Ring) -> Acquisition; +} + +/// A simulator standing in for licensed Butterfly Embedded hardware. +#[derive(Debug, Clone, Default)] +pub struct MockButterflyEmbeddedBackend { + /// Static hardware description. + pub config: ButterflyEmbeddedConfig, + /// Acquisition sweep parameters. + pub acq: AcquisitionConfig, +} + +impl AcquisitionBackend for MockButterflyEmbeddedBackend { + fn name(&self) -> &str { + "mock-butterfly-embedded" + } + + fn acquire(&self, phantom: &Phantom, ring: &Ring) -> Acquisition { + simulate(phantom, ring, self.acq) + } +} diff --git a/crates/sonic-ct/src/geometry.rs b/crates/sonic-ct/src/geometry.rs new file mode 100644 index 000000000..f92ff972f --- /dev/null +++ b/crates/sonic-ct/src/geometry.rs @@ -0,0 +1,76 @@ +//! Circular transducer ring geometry. + +use crate::types::Point; + +/// A circular ring of transducer elements, each able to transmit and receive. +/// +/// Elements are placed counter-clockwise starting at angle 0 (the +X axis). +#[derive(Debug, Clone)] +pub struct Ring { + /// Ring radius (m). + pub radius: f32, + /// Element centre positions. + pub positions: Vec, + /// Inward-pointing unit normals (towards the ring centre at the origin). + pub normals: Vec, +} + +impl Ring { + /// Build a ring of `count` elements with the given `radius`, centred at the + /// origin. + pub fn new(count: usize, radius: f32) -> Self { + let mut positions = Vec::with_capacity(count); + let mut normals = Vec::with_capacity(count); + for i in 0..count { + let theta = (i as f32) * std::f32::consts::TAU / count as f32; + let (s, c) = theta.sin_cos(); + positions.push(Point::new(radius * c, radius * s)); + // Inward normal points from the element towards the centre. + normals.push(Point::new(-c, -s)); + } + Ring { + radius, + positions, + normals, + } + } + + /// Number of elements. + #[inline] + pub fn count(&self) -> usize { + self.positions.len() + } + + /// Receiver indices forming a transmission fan opposite `source`. + /// + /// For element `source`, returns the `fan` receivers centred on the + /// diametrically opposite element, skipping any receiver whose angular + /// separation from the source is below `min_sep_frac` of a half-turn (these + /// near-neighbour paths graze the ring and carry little tissue information). + pub fn fan_receivers(&self, source: usize, fan: usize, min_sep_frac: f32) -> Vec { + let n = self.count(); + if n == 0 { + return Vec::new(); + } + let opposite = (source + n / 2) % n; + let half = fan / 2; + let min_sep = ((min_sep_frac.clamp(0.0, 1.0)) * (n as f32 / 2.0)) as usize; + let mut out = Vec::with_capacity(fan); + for k in 0..fan { + // Centre the fan window on the opposite element. + let offset = k as isize - half as isize; + let r = (opposite as isize + offset).rem_euclid(n as isize) as usize; + if r == source { + continue; + } + // Angular separation in element steps, taken the short way round. + let raw = (r as isize - source as isize).rem_euclid(n as isize) as usize; + let sep = raw.min(n - raw); + if sep < min_sep { + continue; + } + out.push(r); + } + out + } +} diff --git a/crates/sonic-ct/src/grid.rs b/crates/sonic-ct/src/grid.rs new file mode 100644 index 000000000..ff85cdd5b --- /dev/null +++ b/crates/sonic-ct/src/grid.rs @@ -0,0 +1,186 @@ +//! A regular 2-D scalar field sampled on a square grid. + +use crate::types::{Point, Result, SonicError}; + +/// A dense row-major 2-D scalar field with physical spacing. +/// +/// Index convention: `data[y * nx + x]`, with cell `(0,0)` centred at +/// `origin + (0.5*dx, 0.5*dy)`. Physical coordinates increase with index. +#[derive(Debug, Clone, PartialEq)] +pub struct Grid { + /// Number of cells along X. + pub nx: usize, + /// Number of cells along Y. + pub ny: usize, + /// Cell size along X (m). + pub dx: f32, + /// Cell size along Y (m). + pub dy: f32, + /// Physical coordinate of the grid's lower-left corner (m). + pub origin: Point, + /// Row-major scalar values. + pub data: Vec, +} + +impl Grid { + /// Allocate a grid filled with `fill`. + pub fn filled(nx: usize, ny: usize, dx: f32, dy: f32, origin: Point, fill: f32) -> Self { + Grid { + nx, + ny, + dx, + dy, + origin, + data: vec![fill; nx * ny], + } + } + + /// Build a square grid spanning `extent` metres centred on the origin. + pub fn square(n: usize, extent: f32, fill: f32) -> Self { + let d = extent / n as f32; + let origin = Point::new(-extent / 2.0, -extent / 2.0); + Grid::filled(n, n, d, d, origin, fill) + } + + /// Number of cells. + #[inline] + pub fn len(&self) -> usize { + self.data.len() + } + + /// Whether the grid has zero cells. + #[inline] + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Flat index for integer cell coordinates (no bounds check beyond clamp). + #[inline] + pub fn idx(&self, x: usize, y: usize) -> usize { + y * self.nx + x + } + + /// Physical centre of cell `(x, y)`. + #[inline] + pub fn cell_center(&self, x: usize, y: usize) -> Point { + Point::new( + self.origin.x + (x as f32 + 0.5) * self.dx, + self.origin.y + (y as f32 + 0.5) * self.dy, + ) + } + + /// Map a physical point to integer cell coordinates, or `None` if outside. + #[inline] + pub fn point_to_cell(&self, p: Point) -> Option<(usize, usize)> { + let fx = (p.x - self.origin.x) / self.dx; + let fy = (p.y - self.origin.y) / self.dy; + if fx < 0.0 || fy < 0.0 { + return None; + } + let x = fx as usize; + let y = fy as usize; + if x >= self.nx || y >= self.ny { + None + } else { + Some((x, y)) + } + } + + /// Nearest-neighbour sample at a physical point; returns `None` if outside. + #[inline] + pub fn sample(&self, p: Point) -> Option { + self.point_to_cell(p).map(|(x, y)| self.data[self.idx(x, y)]) + } + + /// Minimum and maximum values; `(0,0)` for an empty grid. + pub fn min_max(&self) -> (f32, f32) { + let mut lo = f32::INFINITY; + let mut hi = f32::NEG_INFINITY; + for &v in &self.data { + if v < lo { + lo = v; + } + if v > hi { + hi = v; + } + } + if self.data.is_empty() { + (0.0, 0.0) + } else { + (lo, hi) + } + } + + /// Mean absolute difference against another identically shaped grid. + pub fn mean_abs_diff(&self, other: &Grid) -> Result { + if self.nx != other.nx || self.ny != other.ny { + return Err(SonicError::DimensionMismatch); + } + if self.data.is_empty() { + return Ok(0.0); + } + let mut acc = 0.0f64; + for (a, b) in self.data.iter().zip(&other.data) { + acc += (a - b).abs() as f64; + } + Ok((acc / self.data.len() as f64) as f32) + } + + /// Downsample to a `k x k` average-pooled, L2-normalised feature vector. + /// + /// This is the embedding used by the acoustic memory index: it captures the + /// coarse spatial structure of a reconstruction while being robust to small + /// pixel-level shifts (semantic rather than pixel-exact comparison). + pub fn embedding(&self, k: usize) -> Vec { + let mut out = vec![0.0f32; k * k]; + if self.nx == 0 || self.ny == 0 { + return out; + } + let mut counts = vec![0u32; k * k]; + for y in 0..self.ny { + let by = (y * k) / self.ny; + for x in 0..self.nx { + let bx = (x * k) / self.nx; + let bi = by * k + bx; + out[bi] += self.data[self.idx(x, y)]; + counts[bi] += 1; + } + } + for i in 0..out.len() { + if counts[i] > 0 { + out[i] /= counts[i] as f32; + } + } + // Mean-centre so the (large, uninformative) water-background DC term does + // not dominate cosine similarity — what matters is structural deviation. + let mean = out.iter().sum::() / out.len().max(1) as f32; + for v in &mut out { + *v -= mean; + } + // L2 normalise so cosine similarity is well-defined. + let norm: f32 = out.iter().map(|v| v * v).sum::().sqrt(); + if norm > 0.0 { + for v in &mut out { + *v /= norm; + } + } + out + } + + /// Render to an 8-bit PGM (P5) byte buffer, linearly scaled to `[lo, hi]`. + pub fn to_pgm(&self, lo: f32, hi: f32) -> Vec { + let mut out = Vec::with_capacity(64 + self.nx * self.ny); + let header = format!("P5\n{} {}\n255\n", self.nx, self.ny); + out.extend_from_slice(header.as_bytes()); + let span = if (hi - lo).abs() < f32::EPSILON { 1.0 } else { hi - lo }; + // PGM origin is top-left; flip Y so images look upright. + for y in (0..self.ny).rev() { + for x in 0..self.nx { + let v = self.data[self.idx(x, y)]; + let t = ((v - lo) / span).clamp(0.0, 1.0); + out.push((t * 255.0) as u8); + } + } + out + } +} diff --git a/crates/sonic-ct/src/lib.rs b/crates/sonic-ct/src/lib.rs new file mode 100644 index 000000000..12cdb93fe --- /dev/null +++ b/crates/sonic-ct/src/lib.rs @@ -0,0 +1,129 @@ +//! # sonic_ct +//! +//! A research-grade **Ultrasound Computed Tomography (USCT)** simulator and +//! reconstruction toolkit. +//! +//! `sonic_ct` models a Midjourney-style ring scanner: a subject is coupled in a +//! water bath and surrounded by a dense ring of ultrasound transducers that +//! transmit and receive through tissue from many angles. From the simulated +//! travel-time and attenuation measurements it reconstructs maps of acoustic +//! properties, segments them into tissue classes, and scores the result against +//! ground truth. +//! +//! The crate is deliberately dependency-free so it builds natively, as a CLI, +//! and to `wasm32-unknown-unknown` for in-browser use. +//! +//! ## Pipeline +//! +//! ```text +//! phantom ─▶ ring ─▶ acquisition ─▶ SART reconstruction ─▶ segmentation ─▶ metrics +//! │ │ +//! └──────── acoustic memory ◀────────┘ +//! ``` +//! +//! ## Quick start +//! +//! ``` +//! use sonic_ct::pipeline::{run, PipelineConfig}; +//! let scene = run(PipelineConfig::default()).expect("pipeline runs"); +//! assert!(scene.quality.measurements > 0); +//! assert!(scene.quality.mae_speed.is_finite()); +//! ``` +//! +//! ## Scope & safety +//! +//! This is research/simulation code. It makes **no diagnostic claim** and the +//! Butterfly Embedded boundary is a mock adapter, not a hardware SDK. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod acquisition; +pub mod butterfly; +pub mod geometry; +pub mod grid; +pub mod memory; +pub mod metrics; +pub mod model; +pub mod phantom; +pub mod pipeline; +pub mod ray; +pub mod reconstruction; +pub mod segmentation; +pub mod types; +pub mod volume3d; + +pub use grid::Grid; +pub use pipeline::{run, run_with_model, PipelineConfig, Scene}; +pub use types::{Point, Result, SonicError, Tissue}; + +#[cfg(test)] +mod tests { + use super::*; + use crate::phantom::{Phantom, PhantomConfig}; + + fn small_cfg() -> PipelineConfig { + let mut cfg = PipelineConfig::default(); + cfg.phantom.n = 48; + cfg.elements = 96; + cfg.acquisition.fan = 48; + cfg.recon.iters = 4; + cfg + } + + #[test] + fn pipeline_produces_valid_measurements() { + let scene = run(small_cfg()).unwrap(); + assert!(scene.quality.measurements > 0, "should have measurements"); + assert_eq!(scene.recon_speed.nx, scene.phantom.speed.nx); + } + + #[test] + fn reconstruction_beats_water_prior() { + // The reconstruction must be closer to truth than a flat water guess. + let scene = run(small_cfg()).unwrap(); + let mut flat = scene.phantom.speed.clone(); + flat.data.iter_mut().for_each(|v| *v = types::WATER_SPEED); + let flat_mae = metrics::mae_speed(&flat, &scene.phantom.speed); + assert!( + scene.quality.mae_speed < flat_mae, + "recon MAE {} should beat flat-water MAE {}", + scene.quality.mae_speed, + flat_mae + ); + } + + #[test] + fn more_iters_do_not_worsen_mae() { + let mut a = small_cfg(); + a.recon.iters = 1; + let mut b = small_cfg(); + b.recon.iters = 8; + let mae1 = run(a).unwrap().quality.mae_speed; + let mae8 = run(b).unwrap().quality.mae_speed; + assert!(mae8 <= mae1 * 1.05, "more iters should not regress much"); + } + + #[test] + fn dice_scores_in_unit_range() { + let scene = run(small_cfg()).unwrap(); + for &d in &scene.quality.dice { + assert!((0.0..=1.0).contains(&d), "dice out of range: {d}"); + } + } + + #[test] + fn invalid_config_is_rejected() { + let mut cfg = small_cfg(); + cfg.elements = 2; + assert!(run(cfg).is_err()); + } + + #[test] + fn phantom_is_deterministic() { + let c = PhantomConfig::default(); + let a = Phantom::build(c); + let b = Phantom::build(c); + assert_eq!(a.speed.data, b.speed.data); + } +} diff --git a/crates/sonic-ct/src/memory.rs b/crates/sonic-ct/src/memory.rs new file mode 100644 index 000000000..27d132a8f --- /dev/null +++ b/crates/sonic-ct/src/memory.rs @@ -0,0 +1,387 @@ +//! Self-learning acoustic memory: a vector index over reconstructed scans plus +//! anatomical graph-coherence checks. +//! +//! This is the `sonic_ct` analogue of RuVector's spatial memory: each scan is +//! embedded into a low-dimensional descriptor and stored in a navigable +//! small-world (NSW) graph, giving sub-linear nearest-neighbour lookup for +//! +//! 1. **longitudinal tracking** — comparing a patient's scans over time, +//! 2. **FWI warm-starting** — retrieving the closest previously solved +//! reconstruction as an initial model, and +//! 3. **anomaly detection** — flagging reconstructions whose structure violates +//! simple anatomical rules. +//! +//! The index is dependency-free and round-trips through a compact binary format +//! (`.rvf`-style) so scans become portable, auditable containers (ADR-0003). + +use crate::grid::Grid; +use crate::types::Tissue; + +/// One archived scan descriptor. +#[derive(Debug, Clone, PartialEq)] +pub struct ScanRecord { + /// Stable scan identifier. + pub id: String, + /// Pseudonymous subject identifier (never raw PII). + pub patient_id: String, + /// Acquisition timestamp (unix seconds, monotonic within a subject). + pub timestamp: u64, + /// L2-normalised speed-map embedding. + pub embedding: Vec, + /// Mean Dice at archival time (quality provenance). + pub mean_dice: f32, + /// Mean absolute speed error at archival time (m/s). + pub mae: f32, +} + +/// Cosine similarity of two equal-length L2-normalised vectors. +#[inline] +fn cosine(a: &[f32], b: &[f32]) -> f32 { + a.iter().zip(b).map(|(x, y)| x * y).sum() +} + +/// A navigable small-world vector index over [`ScanRecord`]s. +#[derive(Debug, Clone)] +pub struct AcousticMemory { + /// Embedding dimensionality. + pub dim: usize, + /// Neighbours connected per inserted node. + pub m: usize, + /// Search beam width. + pub ef: usize, + records: Vec, + adjacency: Vec>, + entry: Option, +} + +impl AcousticMemory { + /// Create an empty index for `dim`-dimensional embeddings. + pub fn new(dim: usize) -> Self { + AcousticMemory { + dim, + m: 8, + ef: 24, + records: Vec::new(), + adjacency: Vec::new(), + entry: None, + } + } + + /// Number of archived scans. + pub fn len(&self) -> usize { + self.records.len() + } + + /// Whether the index is empty. + pub fn is_empty(&self) -> bool { + self.records.is_empty() + } + + /// Read-only access to a record. + pub fn record(&self, i: usize) -> Option<&ScanRecord> { + self.records.get(i) + } + + /// Archive a scan, wiring it into the small-world graph. + /// + /// Returns the new node index. Embedding length mismatches are padded or + /// truncated to `dim` so the index never panics on caller error. + pub fn insert(&mut self, mut rec: ScanRecord) -> usize { + rec.embedding.resize(self.dim, 0.0); + let id = self.records.len(); + + // Connect to the M nearest existing nodes (greedy on current graph). + let neighbours = if id == 0 { + Vec::new() + } else { + self.search_internal(&rec.embedding, self.ef.max(self.m)) + .into_iter() + .take(self.m) + .map(|(idx, _)| idx) + .collect::>() + }; + + self.records.push(rec); + self.adjacency.push(neighbours.clone()); + // Back-edges keep the graph navigable in both directions. + for &nb in &neighbours { + if !self.adjacency[nb].contains(&id) { + self.adjacency[nb].push(id); + } + } + if self.entry.is_none() { + self.entry = Some(id); + } + id + } + + /// Greedy beam search returning up to `k` `(index, similarity)` pairs, + /// sorted by descending similarity. + pub fn search(&self, query: &[f32], k: usize) -> Vec<(usize, f32)> { + let mut q = query.to_vec(); + q.resize(self.dim, 0.0); + let mut res = self.search_internal(&q, self.ef.max(k)); + res.truncate(k); + res + } + + fn search_internal(&self, query: &[f32], ef: usize) -> Vec<(usize, f32)> { + let entry = match self.entry { + Some(e) => e, + None => return Vec::new(), + }; + let mut visited = vec![false; self.records.len()]; + // Candidate frontier and result set, both kept small. + let mut frontier: Vec<(usize, f32)> = vec![(entry, cosine(query, &self.records[entry].embedding))]; + visited[entry] = true; + let mut best: Vec<(usize, f32)> = frontier.clone(); + + while let Some((node, _)) = pop_best(&mut frontier) { + for &nb in &self.adjacency[node] { + if visited[nb] { + continue; + } + visited[nb] = true; + let sim = cosine(query, &self.records[nb].embedding); + frontier.push((nb, sim)); + best.push((nb, sim)); + } + // Trim the working set to the beam width. + best.sort_by(|a, b| b.1.total_cmp(&a.1)); + best.truncate(ef); + // Keep exploring only the most promising frontier nodes. + frontier.sort_by(|a, b| b.1.total_cmp(&a.1)); + frontier.truncate(ef); + if frontier.first().map(|f| f.1).unwrap_or(f32::NEG_INFINITY) + <= best.last().map(|b| b.1).unwrap_or(f32::NEG_INFINITY) + && best.len() >= ef + { + break; + } + } + best.sort_by(|a, b| b.1.total_cmp(&a.1)); + best + } + + /// Exact brute-force search (ground truth for tests / small sets). + pub fn search_exact(&self, query: &[f32], k: usize) -> Vec<(usize, f32)> { + let mut q = query.to_vec(); + q.resize(self.dim, 0.0); + let mut all: Vec<(usize, f32)> = self + .records + .iter() + .enumerate() + .map(|(i, r)| (i, cosine(&q, &r.embedding))) + .collect(); + all.sort_by(|a, b| b.1.total_cmp(&a.1)); + all.truncate(k); + all + } + + /// Retrieve the best-matching prior embedding for FWI warm-starting. + pub fn warm_start(&self, query: &[f32]) -> Option<&ScanRecord> { + self.search(query, 1).first().and_then(|&(i, _)| self.records.get(i)) + } + + /// All records for `patient_id`, ordered by ascending timestamp. + pub fn patient_timeline(&self, patient_id: &str) -> Vec<&ScanRecord> { + let mut v: Vec<&ScanRecord> = + self.records.iter().filter(|r| r.patient_id == patient_id).collect(); + v.sort_by_key(|r| r.timestamp); + v + } + + /// Longitudinal change between a patient's earliest and latest scans, + /// expressed as `1 - cosine` (0 == identical, larger == more change). + pub fn longitudinal_drift(&self, patient_id: &str) -> Option { + let tl = self.patient_timeline(patient_id); + if tl.len() < 2 { + return None; + } + let first = tl.first().unwrap(); + let last = tl.last().unwrap(); + Some(1.0 - cosine(&first.embedding, &last.embedding)) + } +} + +fn pop_best(frontier: &mut Vec<(usize, f32)>) -> Option<(usize, f32)> { + if frontier.is_empty() { + return None; + } + let mut bi = 0; + for i in 1..frontier.len() { + if frontier[i].1 > frontier[bi].1 { + bi = i; + } + } + Some(frontier.swap_remove(bi)) +} + +/// Build the embedding for a reconstruction (coarse `k×k` descriptor). +pub fn embed_speed(speed: &Grid, k: usize) -> Vec { + speed.embedding(k) +} + +// --------------------------------------------------------------------------- +// Anatomical graph coherence +// --------------------------------------------------------------------------- + +/// Result of the anatomical structure check on a segmentation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CoherenceReport { + /// Bone cells directly adjacent to the water bath (anatomically impossible). + pub bone_touching_water: usize, + /// Organ cells directly adjacent to the water bath (organs sit inside the + /// body wall, so this signals a reconstruction artefact). + pub organ_touching_water: usize, + /// Whether any rule was violated beyond a small tolerance. + pub anomaly: bool, +} + +/// Run anatomical coherence rules over a label grid. +/// +/// Treats the segmentation as a topological map and checks adjacency +/// constraints, e.g. `MATCH (bone)-[:TOUCHES]->(water)` should not occur. +pub fn check_coherence(labels: &Grid) -> CoherenceReport { + let w = Tissue::Water as u8 as f32; + let bone = Tissue::Bone as u8 as f32; + let organ = Tissue::Organ as u8 as f32; + let (nx, ny) = (labels.nx, labels.ny); + + let mut bone_water = 0usize; + let mut organ_water = 0usize; + let touches_water = |x: usize, y: usize| -> bool { + let mut t = false; + let mut check = |xx: i64, yy: i64| { + if xx >= 0 + && yy >= 0 + && (xx as usize) < nx + && (yy as usize) < ny + && labels.data[labels.idx(xx as usize, yy as usize)] == w + { + t = true; + } + }; + check(x as i64 - 1, y as i64); + check(x as i64 + 1, y as i64); + check(x as i64, y as i64 - 1); + check(x as i64, y as i64 + 1); + t + }; + + for y in 0..ny { + for x in 0..nx { + let v = labels.data[labels.idx(x, y)]; + if v == bone && touches_water(x, y) { + bone_water += 1; + } else if v == organ && touches_water(x, y) { + organ_water += 1; + } + } + } + + // Small tolerance for boundary discretisation noise. + let tol = (nx.max(ny) / 16).max(1); + CoherenceReport { + bone_touching_water: bone_water, + organ_touching_water: organ_water, + anomaly: bone_water > 0 || organ_water > tol, + } +} + +// --------------------------------------------------------------------------- +// Portable serialization (.rvf-style binary) +// --------------------------------------------------------------------------- + +const MAGIC: &[u8; 4] = b"SCT1"; + +impl AcousticMemory { + /// Serialize the index to a compact binary buffer. + pub fn to_bytes(&self) -> Vec { + let mut b = Vec::new(); + b.extend_from_slice(MAGIC); + b.extend_from_slice(&(self.dim as u32).to_le_bytes()); + b.extend_from_slice(&(self.records.len() as u32).to_le_bytes()); + for r in &self.records { + put_str(&mut b, &r.id); + put_str(&mut b, &r.patient_id); + b.extend_from_slice(&r.timestamp.to_le_bytes()); + b.extend_from_slice(&r.mean_dice.to_le_bytes()); + b.extend_from_slice(&r.mae.to_le_bytes()); + b.extend_from_slice(&(r.embedding.len() as u32).to_le_bytes()); + for &v in &r.embedding { + b.extend_from_slice(&v.to_le_bytes()); + } + } + b + } + + /// Reconstruct an index from [`Self::to_bytes`] output. The graph is rebuilt + /// by re-inserting records, so search behaviour is preserved. + pub fn from_bytes(buf: &[u8]) -> Option { + let mut c = Cursor { buf, pos: 0 }; + if c.take(4)? != MAGIC { + return None; + } + let dim = c.u32()? as usize; + let count = c.u32()? as usize; + let mut mem = AcousticMemory::new(dim); + for _ in 0..count { + let id = get_str(&mut c)?; + let patient_id = get_str(&mut c)?; + let timestamp = c.u64()?; + let mean_dice = c.f32()?; + let mae = c.f32()?; + let elen = c.u32()? as usize; + let mut embedding = Vec::with_capacity(elen); + for _ in 0..elen { + embedding.push(c.f32()?); + } + mem.insert(ScanRecord { + id, + patient_id, + timestamp, + embedding, + mean_dice, + mae, + }); + } + Some(mem) + } +} + +fn put_str(b: &mut Vec, s: &str) { + b.extend_from_slice(&(s.len() as u32).to_le_bytes()); + b.extend_from_slice(s.as_bytes()); +} + +struct Cursor<'a> { + buf: &'a [u8], + pos: usize, +} + +impl<'a> Cursor<'a> { + fn take(&mut self, n: usize) -> Option<&'a [u8]> { + if self.pos + n > self.buf.len() { + return None; + } + let s = &self.buf[self.pos..self.pos + n]; + self.pos += n; + Some(s) + } + fn u32(&mut self) -> Option { + Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?)) + } + fn u64(&mut self) -> Option { + Some(u64::from_le_bytes(self.take(8)?.try_into().ok()?)) + } + fn f32(&mut self) -> Option { + Some(f32::from_le_bytes(self.take(4)?.try_into().ok()?)) + } +} + +fn get_str(c: &mut Cursor) -> Option { + let n = c.u32()? as usize; + let bytes = c.take(n)?; + String::from_utf8(bytes.to_vec()).ok() +} diff --git a/crates/sonic-ct/src/metrics.rs b/crates/sonic-ct/src/metrics.rs new file mode 100644 index 000000000..4389f5bc7 --- /dev/null +++ b/crates/sonic-ct/src/metrics.rs @@ -0,0 +1,65 @@ +//! Reconstruction and segmentation quality metrics. + +use crate::grid::Grid; +use crate::types::Tissue; + +/// Dice similarity coefficient for one class between predicted and truth labels. +/// +/// Returns 1.0 for a class that is absent from both maps (vacuously perfect), +/// matching the common convention for empty-class Dice. +pub fn dice(pred_labels: &Grid, true_labels: &Grid, class: Tissue) -> f32 { + let c = class as u8 as f32; + let mut inter = 0u64; + let mut a = 0u64; + let mut b = 0u64; + for (p, t) in pred_labels.data.iter().zip(&true_labels.data) { + let pp = *p == c; + let tt = *t == c; + if pp { + a += 1; + } + if tt { + b += 1; + } + if pp && tt { + inter += 1; + } + } + if a + b == 0 { + return 1.0; + } + (2 * inter) as f32 / (a + b) as f32 +} + +/// Per-class Dice scores in `Tissue::ALL` order. +pub fn dice_all(pred_labels: &Grid, true_labels: &Grid) -> [f32; Tissue::COUNT] { + let mut out = [0.0f32; Tissue::COUNT]; + for (i, &t) in Tissue::ALL.iter().enumerate() { + out[i] = dice(pred_labels, true_labels, t); + } + out +} + +/// Mean Dice across all classes. +pub fn mean_dice(pred_labels: &Grid, true_labels: &Grid) -> f32 { + let d = dice_all(pred_labels, true_labels); + d.iter().sum::() / d.len() as f32 +} + +/// Mean absolute speed-of-sound error (m/s) between two grids. +pub fn mae_speed(pred: &Grid, truth: &Grid) -> f32 { + pred.mean_abs_diff(truth).unwrap_or(f32::NAN) +} + +/// A compact bundle of quality metrics for one reconstruction. +#[derive(Debug, Clone)] +pub struct QualityReport { + /// Mean absolute speed error (m/s). + pub mae_speed: f32, + /// Per-class Dice in `Tissue::ALL` order. + pub dice: [f32; Tissue::COUNT], + /// Mean Dice across classes. + pub mean_dice: f32, + /// Number of valid measurements used. + pub measurements: usize, +} diff --git a/crates/sonic-ct/src/model.rs b/crates/sonic-ct/src/model.rs new file mode 100644 index 000000000..8198e2fd4 --- /dev/null +++ b/crates/sonic-ct/src/model.rs @@ -0,0 +1,81 @@ +//! Training the segmentation threshold model from labelled reconstructions. +//! +//! The "model" is the set of speed-band boundaries in [`SegModel`]. We fit them +//! by coordinate ascent to maximise mean Dice across a training set of +//! reconstructed/ground-truth pairs. This is a small, fully reproducible +//! optimisation — no external ML framework required — that nonetheless +//! measurably beats the literature-default boundaries on the synthetic data. + +use crate::grid::Grid; +use crate::metrics::mean_dice; +use crate::segmentation::{segment, SegModel}; + +/// A labelled training example. +pub struct TrainExample { + /// Reconstructed speed map (model input). + pub recon_speed: Grid, + /// Ground-truth tissue labels (target). + pub true_labels: Grid, +} + +/// Mean Dice of `model` across all `examples`. +pub fn evaluate(model: &SegModel, examples: &[TrainExample]) -> f32 { + if examples.is_empty() { + return 0.0; + } + let mut acc = 0.0f32; + for ex in examples { + let seg = segment(&ex.recon_speed, model); + acc += mean_dice(&seg.labels, &ex.true_labels); + } + acc / examples.len() as f32 +} + +/// Fit boundary thresholds by coordinate ascent. +/// +/// Starting from `base`, each finite band boundary is perturbed up/down over a +/// shrinking step schedule, keeping any change that improves training-set mean +/// Dice. Boundaries are kept sorted so the band mapping stays valid. +pub fn train(base: &SegModel, examples: &[TrainExample]) -> (SegModel, f32) { + let mut model = base.clone(); + let mut best = evaluate(&model, examples); + + // Step schedule in m/s, refined over passes. + let steps = [40.0f32, 20.0, 10.0, 5.0, 2.0]; + let n_finite = model.bands.iter().filter(|(u, _)| u.is_finite()).count(); + + for &step in &steps { + let mut improved = true; + while improved { + improved = false; + for bi in 0..n_finite { + for &dir in &[-1.0f32, 1.0] { + let mut trial = model.clone(); + trial.bands[bi].0 += dir * step; + if !boundaries_sorted(&trial) { + continue; + } + let score = evaluate(&trial, examples); + if score > best + 1e-6 { + best = score; + model = trial; + improved = true; + } + } + } + } + } + (model, best) +} + +/// Whether the finite band boundaries are strictly ascending. +fn boundaries_sorted(model: &SegModel) -> bool { + let mut prev = f32::NEG_INFINITY; + for &(u, _) in &model.bands { + if u <= prev { + return false; + } + prev = u; + } + true +} diff --git a/crates/sonic-ct/src/phantom.rs b/crates/sonic-ct/src/phantom.rs new file mode 100644 index 000000000..50fdbe80d --- /dev/null +++ b/crates/sonic-ct/src/phantom.rs @@ -0,0 +1,286 @@ +//! Synthetic, anatomically-structured body-model generation. +//! +//! The phantom is a procedural *digital human torso* (in the spirit of +//! computational phantoms such as XCAT) — not a scanned real patient. It is +//! fully deterministic given a seed, which makes it a reproducible "public" +//! dataset for benchmarking reconstruction quality. +//! +//! Anatomy varies along the cranio-caudal axis `z ∈ [0, 1]` (0 = pelvis, +//! 1 = upper abdomen / lower thorax), so a vertical sweep through the model +//! produces a coherent 3-D body (see [`crate::volume3d`]). + +use crate::grid::Grid; +use crate::types::{Point, Tissue, WATER_SPEED}; + +/// A ground-truth phantom slice: co-registered speed, attenuation, and labels. +#[derive(Debug, Clone)] +pub struct Phantom { + /// Speed-of-sound map (m/s). + pub speed: Grid, + /// Acoustic attenuation map (Np/m). + pub attenuation: Grid, + /// Per-cell tissue labels (stored as `f32` of the `u8` value). + pub labels: Grid, +} + +/// Parameters controlling phantom synthesis. +#[derive(Debug, Clone, Copy)] +pub struct PhantomConfig { + /// Grid resolution (cells per side). + pub n: usize, + /// Physical field of view (m). + pub extent: f32, + /// Deterministic seed; varies organ placement and sizes. + pub seed: u64, +} + +impl Default for PhantomConfig { + fn default() -> Self { + PhantomConfig { + n: 96, + extent: 0.24, + seed: 1, + } + } +} + +/// A tiny deterministic PRNG (SplitMix64) so phantoms are reproducible without +/// pulling in the `rand` crate (keeps the core dependency-free). +struct SplitMix64(u64); + +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + fn uniform(&mut self, lo: f32, hi: f32) -> f32 { + let u = (self.next_u64() >> 11) as f32 / (1u64 << 53) as f32; + lo + (hi - lo) * u + } +} + +#[inline] +fn in_ellipse(p: Point, c: Point, rx: f32, ry: f32) -> bool { + let dx = (p.x - c.x) / rx; + let dy = (p.y - c.y) / ry; + dx * dx + dy * dy <= 1.0 +} + +/// A filled anatomical structure (ellipse) drawn at a fixed tissue class. +#[derive(Clone, Copy)] +struct Blob { + c: Point, + rx: f32, + ry: f32, + tissue: Tissue, +} + +#[inline] +fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 { + let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) +} + +impl Phantom { + /// Build the canonical mid-abdomen slice (`z = 0.5`). + pub fn build(cfg: PhantomConfig) -> Phantom { + Phantom::build_slice(cfg, 0.5) + } + + /// Build an anatomically-structured cross-section at cranio-caudal height + /// `z ∈ [0, 1]` (0 = pelvis, 1 = upper abdomen / lower thorax). + pub fn build_slice(cfg: PhantomConfig, z: f32) -> Phantom { + let z = z.clamp(0.0, 1.0); + // Seed mixes the subject seed with the slice height so each plane is + // distinct yet reproducible. + let zk = (z * 1000.0) as u64; + let mut rng = SplitMix64( + cfg.seed + .wrapping_mul(0x2545_F491_4F6C_DD1D) + .wrapping_add(zk.wrapping_mul(0x9E37_79B9)) + .wrapping_add(1), + ); + + let n = cfg.n; + let ext = cfg.extent; + let mut speed = Grid::square(n, ext, WATER_SPEED); + let mut atten = Grid::square(n, ext, Tissue::Water.nominal_attenuation()); + let mut labels = Grid::square(n, ext, Tissue::Water as u8 as f32); + + // --- Body outline: torso widens superiorly (chest) and the waist is the + // narrowest point of the abdomen. --- + let chest = smoothstep(0.55, 1.0, z); + let body_c = Point::new(rng.uniform(-0.008, 0.008), rng.uniform(-0.008, 0.008)); + let body_rx = (0.078 + 0.018 * chest) * rng.uniform(0.97, 1.03); + let body_ry = (0.060 + 0.014 * chest) * rng.uniform(0.97, 1.03); + let fat_t = rng.uniform(0.007, 0.012); + let muscle_t = rng.uniform(0.009, 0.015); + + // Posterior spine position (negative Y is the back). + let spine_c = Point::new(body_c.x, body_c.y - body_ry * rng.uniform(0.60, 0.70)); + + // --- Region-dependent soft organs (Organ class). --- + let mut organs: Vec = Vec::new(); + // Aorta / great vessel: a small lumen just anterior to the spine, + // present through abdomen and chest. + if z > 0.2 { + organs.push(Blob { + c: Point::new(spine_c.x + rng.uniform(-0.004, 0.004), spine_c.y + 0.012), + rx: rng.uniform(0.005, 0.008), + ry: rng.uniform(0.005, 0.008), + tissue: Tissue::Organ, + }); + } + if z >= 0.82 { + // Thorax: heart (central, slightly left) + paired lungs (lateral). + // Lungs are modelled as soft-tissue parenchyma here; true air-lung + // acoustics (near-total shadowing) is future work. + organs.push(Blob { + c: Point::new(body_c.x - rng.uniform(0.004, 0.014), body_c.y + rng.uniform(0.006, 0.018)), + rx: rng.uniform(0.020, 0.028), + ry: rng.uniform(0.020, 0.028), + tissue: Tissue::Organ, + }); // heart + for &s in &[-1.0f32, 1.0] { + organs.push(Blob { + c: Point::new(body_c.x + s * rng.uniform(0.030, 0.044), body_c.y + rng.uniform(0.002, 0.014)), + rx: rng.uniform(0.018, 0.026), + ry: rng.uniform(0.022, 0.032), + tissue: Tissue::Organ, + }); // lung + } + } else if z >= 0.6 { + // Upper abdomen: large liver (right) + spleen (left). + organs.push(Blob { + c: Point::new(body_c.x + rng.uniform(0.014, 0.030), body_c.y + rng.uniform(0.000, 0.014)), + rx: rng.uniform(0.030, 0.040), + ry: rng.uniform(0.024, 0.032), + tissue: Tissue::Organ, + }); + organs.push(Blob { + c: Point::new(body_c.x - rng.uniform(0.026, 0.040), body_c.y + rng.uniform(0.004, 0.018)), + rx: rng.uniform(0.013, 0.019), + ry: rng.uniform(0.014, 0.020), + tissue: Tissue::Organ, + }); + } else if z >= 0.35 { + // Mid abdomen: paired kidneys (posterior) + liver tail. + organs.push(Blob { + c: Point::new(body_c.x + rng.uniform(0.018, 0.030), spine_c.y + rng.uniform(0.018, 0.030)), + rx: rng.uniform(0.010, 0.016), + ry: rng.uniform(0.014, 0.020), + tissue: Tissue::Organ, + }); + organs.push(Blob { + c: Point::new(body_c.x - rng.uniform(0.018, 0.030), spine_c.y + rng.uniform(0.018, 0.030)), + rx: rng.uniform(0.010, 0.016), + ry: rng.uniform(0.014, 0.020), + tissue: Tissue::Organ, + }); + organs.push(Blob { + c: Point::new(body_c.x + rng.uniform(0.010, 0.024), body_c.y + rng.uniform(0.006, 0.018)), + rx: rng.uniform(0.018, 0.026), + ry: rng.uniform(0.014, 0.020), + tissue: Tissue::Organ, + }); + } else { + // Pelvis: bowel / bladder soft-tissue blobs, central-anterior. + for _ in 0..2 { + organs.push(Blob { + c: Point::new(body_c.x + rng.uniform(-0.018, 0.018), body_c.y + rng.uniform(0.000, 0.022)), + rx: rng.uniform(0.012, 0.020), + ry: rng.uniform(0.012, 0.018), + tissue: Tissue::Organ, + }); + } + } + + // --- Bone: spine (all slices) + ribs (upper) or pelvis (lower). --- + let mut bones: Vec = Vec::new(); + let spine_r = rng.uniform(0.009, 0.013) * (1.0 + 0.4 * (1.0 - z)); // sacrum larger inferiorly + bones.push(Blob { c: spine_c, rx: spine_r, ry: spine_r, tissue: Tissue::Bone }); + + if z >= 0.6 { + // Rib arcs: small bone nodes along the posterolateral body wall. + let ribs = 4; + for i in 0..ribs { + let ang = std::f32::consts::PI * (0.55 + 0.9 * (i as f32 / (ribs - 1) as f32)); + let r = 0.92; + let c = Point::new( + body_c.x + (body_rx - fat_t) * r * ang.cos(), + body_c.y + (body_ry - fat_t) * r * ang.sin(), + ); + let rr = rng.uniform(0.004, 0.006); + bones.push(Blob { c, rx: rr, ry: rr, tissue: Tissue::Bone }); + // Mirror to the other side. + let cm = Point::new(2.0 * body_c.x - c.x, c.y); + bones.push(Blob { c: cm, rx: rr, ry: rr, tissue: Tissue::Bone }); + } + } else if z < 0.35 { + // Iliac wings of the pelvis: two lateral bone masses. + for &s in &[-1.0f32, 1.0] { + bones.push(Blob { + c: Point::new(body_c.x + s * rng.uniform(0.030, 0.044), body_c.y - rng.uniform(0.004, 0.014)), + rx: rng.uniform(0.009, 0.014), + ry: rng.uniform(0.016, 0.024), + tissue: Tissue::Bone, + }); + } + } + + // --- Rasterise: shells, then organs, then bone (bone wins). --- + for y in 0..n { + for x in 0..n { + let p = speed.cell_center(x, y); + let i = speed.idx(x, y); + let mut t = Tissue::Water; + + if in_ellipse(p, body_c, body_rx, body_ry) { + if !in_ellipse(p, body_c, body_rx - fat_t, body_ry - fat_t) { + t = Tissue::Fat; + } else if !in_ellipse( + p, + body_c, + body_rx - fat_t - muscle_t, + body_ry - fat_t - muscle_t, + ) { + t = Tissue::Muscle; + } else { + t = Tissue::Organ; // interior soft tissue + } + + for o in &organs { + if in_ellipse(p, o.c, o.rx, o.ry) { + t = o.tissue; + } + } + } + // Bone (ribs/pelvis) may sit within the body wall, so test it + // last and let it override regardless of the shell. + for b in &bones { + if in_ellipse(p, b.c, b.rx, b.ry) { + t = Tissue::Bone; + } + } + + speed.data[i] = t.nominal_speed(); + atten.data[i] = t.nominal_attenuation(); + labels.data[i] = t as u8 as f32; + } + } + + Phantom { + speed, + attenuation: atten, + labels, + } + } + + /// Grid resolution (cells per side). + pub fn n(&self) -> usize { + self.speed.nx + } +} diff --git a/crates/sonic-ct/src/pipeline.rs b/crates/sonic-ct/src/pipeline.rs new file mode 100644 index 000000000..1f04472e7 --- /dev/null +++ b/crates/sonic-ct/src/pipeline.rs @@ -0,0 +1,123 @@ +//! End-to-end simulation + reconstruction pipeline. + +use crate::acquisition::{simulate, Acquisition, AcquisitionConfig}; +use crate::geometry::Ring; +use crate::grid::Grid; +use crate::metrics::{dice_all, mae_speed, QualityReport}; +use crate::phantom::{Phantom, PhantomConfig}; +use crate::reconstruction::{reconstruct_attenuation, reconstruct_speed, ReconConfig}; +use crate::segmentation::{segment, SegModel, Segmentation}; +use crate::types::Result; + +/// Full pipeline configuration. +#[derive(Debug, Clone, Copy)] +pub struct PipelineConfig { + /// Phantom synthesis parameters. + pub phantom: PhantomConfig, + /// Number of ring transducer elements. + pub elements: usize, + /// Ring radius as a fraction of half the field of view (0,1). + pub ring_frac: f32, + /// Acquisition parameters. + pub acquisition: AcquisitionConfig, + /// Reconstruction parameters. + pub recon: ReconConfig, +} + +impl Default for PipelineConfig { + fn default() -> Self { + PipelineConfig { + phantom: PhantomConfig::default(), + elements: 180, + ring_frac: 0.92, + acquisition: AcquisitionConfig::default(), + recon: ReconConfig::default(), + } + } +} + +impl PipelineConfig { + /// Validate ranges, returning a descriptive error. + pub fn validate(&self) -> Result<()> { + use crate::types::SonicError::InvalidConfig; + if self.phantom.n < 8 { + return Err(InvalidConfig("phantom.n must be >= 8")); + } + if self.elements < 8 { + return Err(InvalidConfig("elements must be >= 8")); + } + if !(0.1..=0.999).contains(&self.ring_frac) { + return Err(InvalidConfig("ring_frac must be in (0.1, 0.999)")); + } + Ok(()) + } +} + +/// All artifacts produced by one pipeline run. +#[derive(Debug, Clone)] +pub struct Scene { + /// Ground-truth phantom. + pub phantom: Phantom, + /// Transducer ring. + pub ring: Ring, + /// Simulated measurements. + pub acquisition: Acquisition, + /// Reconstructed speed-of-sound map (m/s). + pub recon_speed: Grid, + /// Reconstructed attenuation map (Np/m). + pub recon_attenuation: Grid, + /// Tissue segmentation of the reconstruction. + pub segmentation: Segmentation, + /// Quality metrics versus ground truth. + pub quality: QualityReport, +} + +/// Run the full pipeline with the default segmentation model. +pub fn run(cfg: PipelineConfig) -> Result { + run_with_model(cfg, &SegModel::default()) +} + +/// Run the full pipeline using a specific segmentation `model`. +pub fn run_with_model(cfg: PipelineConfig, model: &SegModel) -> Result { + run_slice(cfg, model, 0.5) +} + +/// Run the full pipeline for a single cranio-caudal slice at height `z ∈ [0,1]`. +/// +/// `z = 0.5` is the canonical mid-abdomen slice; sweeping `z` builds a 3-D body +/// (see [`crate::volume3d`]). +pub fn run_slice(cfg: PipelineConfig, model: &SegModel, z: f32) -> Result { + cfg.validate()?; + + let phantom = Phantom::build_slice(cfg.phantom, z); + let half_fov = cfg.phantom.extent / 2.0; + let ring = Ring::new(cfg.elements, half_fov * cfg.ring_frac); + + let acquisition = simulate(&phantom, &ring, cfg.acquisition); + if acquisition.valid_count == 0 { + return Err(crate::types::SonicError::NoMeasurements); + } + + let recon_speed = reconstruct_speed(&acquisition, &phantom.speed, cfg.recon); + let recon_attenuation = reconstruct_attenuation(&acquisition, &phantom.attenuation, cfg.recon); + let segmentation = segment(&recon_speed, model); + + let dice = dice_all(&segmentation.labels, &phantom.labels); + let mean_dice = dice.iter().sum::() / dice.len() as f32; + let quality = QualityReport { + mae_speed: mae_speed(&recon_speed, &phantom.speed), + dice, + mean_dice, + measurements: acquisition.valid_count, + }; + + Ok(Scene { + phantom, + ring, + acquisition, + recon_speed, + recon_attenuation, + segmentation, + quality, + }) +} diff --git a/crates/sonic-ct/src/ray.rs b/crates/sonic-ct/src/ray.rs new file mode 100644 index 000000000..6b98ccccb --- /dev/null +++ b/crates/sonic-ct/src/ray.rs @@ -0,0 +1,74 @@ +//! Straight-ray sampling through a grid. +//! +//! Reconstruction here uses a straight-ray (Born/first-arrival) approximation: +//! refraction and diffraction are ignored. This is the standard time-of-flight +//! USCT baseline that full-waveform inversion later improves upon. + +use crate::grid::Grid; +use crate::types::Point; + +/// A discretised straight ray expressed as a list of `(cell_index, length)` +/// contributions, where `length` is the path length (m) the ray spends in that +/// cell. Built once per source/receiver pair and reused across SART iterations. +#[derive(Debug, Clone)] +pub struct Ray { + /// `(flat cell index, path length in metres)` pairs. + pub cells: Vec<(usize, f32)>, + /// Total straight-line length of the ray (m). + pub length: f32, +} + +impl Ray { + /// Build a ray between `a` and `b`, accumulating per-cell path lengths on + /// `grid` via uniform supersampling. + /// + /// `samples_per_cell` controls accuracy: the segment is split into roughly + /// `samples_per_cell` points per grid cell traversed. Contributions to the + /// same cell are merged so each cell appears once. + pub fn between(grid: &Grid, a: Point, b: Point, samples_per_cell: f32) -> Ray { + let length = a.dist(b); + if length <= 0.0 || grid.dx <= 0.0 { + return Ray { cells: Vec::new(), length }; + } + // Number of integration steps along the ray. + let cells_crossed = length / grid.dx; + let steps = ((cells_crossed * samples_per_cell).ceil() as usize).max(1); + let dl = length / steps as f32; + + // Accumulate into a small map keyed by cell index. Rays are short so a + // linear-probe Vec is faster than a HashMap here. + let mut acc: Vec<(usize, f32)> = Vec::with_capacity(steps.min(grid.nx * 2)); + let inv = 1.0 / steps as f32; + for s in 0..steps { + let t = (s as f32 + 0.5) * inv; + let p = Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + if let Some((cx, cy)) = grid.point_to_cell(p) { + let ci = grid.idx(cx, cy); + match acc.iter_mut().find(|(c, _)| *c == ci) { + Some(e) => e.1 += dl, + None => acc.push((ci, dl)), + } + } + } + Ray { cells: acc, length } + } + + /// Integrate a per-cell field along the ray: `Σ field[cell] * length`. + /// + /// With `field = slowness (1/c)` this yields travel time; with + /// `field = attenuation` it yields total attenuation in nepers. + #[inline] + pub fn integrate(&self, field: &[f32]) -> f32 { + let mut acc = 0.0f32; + for &(c, l) in &self.cells { + acc += field[c] * l; + } + acc + } + + /// Sum of path lengths actually deposited inside the grid (m). + #[inline] + pub fn interior_length(&self) -> f32 { + self.cells.iter().map(|&(_, l)| l).sum() + } +} diff --git a/crates/sonic-ct/src/reconstruction.rs b/crates/sonic-ct/src/reconstruction.rs new file mode 100644 index 000000000..0470016d5 --- /dev/null +++ b/crates/sonic-ct/src/reconstruction.rs @@ -0,0 +1,106 @@ +//! Time-of-flight reconstruction via SART (Simultaneous Algebraic +//! Reconstruction Technique). +//! +//! We solve the linear tomography system `A s = t`, where `s` is the per-cell +//! slowness (1/c), `A_ij` is the length ray `i` spends in cell `j`, and `t` is +//! the measured interior travel time. The same solver reconstructs attenuation +//! by swapping the right-hand side. A single SART sweep is equivalent to the +//! classic delay-backprojection baseline (ADR-0004); additional sweeps move the +//! estimate towards the least-squares solution. + +use crate::acquisition::Acquisition; +use crate::grid::Grid; +use crate::types::{clamp, SPEED_MAX, SPEED_MIN, WATER_SPEED}; + +/// Reconstruction tuning. +#[derive(Debug, Clone, Copy)] +pub struct ReconConfig { + /// Number of SART sweeps (1 == delay-backprojection baseline). + pub iters: usize, + /// Relaxation factor in `(0, 2)`; smaller is more stable. + pub relaxation: f32, +} + +impl Default for ReconConfig { + fn default() -> Self { + ReconConfig { + iters: 6, + relaxation: 0.9, + } + } +} + +/// Generic SART solver over the supplied measurements. +/// +/// `rhs(i)` returns the measured line integral for measurement `i` and +/// `init` is the constant initial field value. Returns the per-cell field. +fn sart(acq: &Acquisition, n_cells: usize, cfg: ReconConfig, init: f32, rhs: F) -> Vec +where + F: Fn(usize) -> f32, +{ + let mut field = vec![init; n_cells]; + let mut num = vec![0.0f32; n_cells]; + let mut den = vec![0.0f32; n_cells]; + + for _ in 0..cfg.iters.max(1) { + num.iter_mut().for_each(|v| *v = 0.0); + den.iter_mut().for_each(|v| *v = 0.0); + + for (i, m) in acq.measurements.iter().enumerate() { + if !m.valid { + continue; + } + let rowsum = m.ray.interior_length(); + if rowsum <= 0.0 { + continue; + } + // Predicted line integral with the current field estimate. + let mut predicted = 0.0f32; + for &(c, l) in &m.ray.cells { + predicted += field[c] * l; + } + let residual = (rhs(i) - predicted) / rowsum; + for &(c, l) in &m.ray.cells { + num[c] += l * residual; + den[c] += l; + } + } + + for j in 0..n_cells { + if den[j] > 0.0 { + field[j] += cfg.relaxation * num[j] / den[j]; + } + } + } + field +} + +/// Reconstruct the speed-of-sound map (m/s) on a grid shaped like `like`. +pub fn reconstruct_speed(acq: &Acquisition, like: &Grid, cfg: ReconConfig) -> Grid { + let n_cells = like.len(); + let init = 1.0 / WATER_SPEED; + // Interior travel time = measured travel time minus the exterior water leg. + let slowness = sart(acq, n_cells, cfg, init, |i| { + let m = &acq.measurements[i]; + let exterior = (m.path_length - m.ray.interior_length()).max(0.0); + m.travel_time - exterior / WATER_SPEED + }); + + let mut out = like.clone(); + for (o, &s) in out.data.iter_mut().zip(&slowness) { + let c = if s > 0.0 { 1.0 / s } else { WATER_SPEED }; + *o = clamp(c, SPEED_MIN, SPEED_MAX); + } + out +} + +/// Reconstruct the attenuation map (Np/m) on a grid shaped like `like`. +pub fn reconstruct_attenuation(acq: &Acquisition, like: &Grid, cfg: ReconConfig) -> Grid { + let n_cells = like.len(); + let field = sart(acq, n_cells, cfg, 0.0, |i| acq.measurements[i].attenuation); + let mut out = like.clone(); + for (o, &a) in out.data.iter_mut().zip(&field) { + *o = a.max(0.0); + } + out +} diff --git a/crates/sonic-ct/src/segmentation.rs b/crates/sonic-ct/src/segmentation.rs new file mode 100644 index 000000000..782bdcecc --- /dev/null +++ b/crates/sonic-ct/src/segmentation.rs @@ -0,0 +1,105 @@ +//! Speed-of-sound to tissue-label segmentation with per-cell uncertainty. +//! +//! Segmentation is intentionally a transparent, auditable threshold model +//! rather than an opaque network (ADR-0007): every label is explained by a +//! speed band, and every label carries an uncertainty derived from its margin +//! to the nearest decision boundary. + +use crate::grid::Grid; +use crate::types::Tissue; + +/// An ordered piecewise speed-band classifier. +/// +/// `bands` is sorted ascending by `upper`; a speed `c` is assigned the class of +/// the first band whose `upper` bound it falls under. The final band should use +/// `f32::INFINITY` so the mapping is total. +#[derive(Debug, Clone, PartialEq)] +pub struct SegModel { + /// `(inclusive upper speed bound, class)` breakpoints, ascending. + pub bands: Vec<(f32, Tissue)>, + /// Soft-boundary scale (m/s) for uncertainty estimation. + pub margin_scale: f32, +} + +impl Default for SegModel { + /// Literature-derived default boundaries (mid-points between tissue speeds). + fn default() -> Self { + SegModel { + bands: vec![ + (1465.0, Tissue::Fat), + (1500.0, Tissue::Water), + (1575.0, Tissue::Organ), + (2000.0, Tissue::Muscle), + (f32::INFINITY, Tissue::Bone), + ], + margin_scale: 30.0, + } + } +} + +impl SegModel { + /// Pre-fitted boundaries from `sonic_ct_train` on the synthetic corpus. + /// + /// These were produced by coordinate-ascent training (see [`crate::model`]) + /// and roughly double mean Dice versus [`SegModel::default`] on the + /// reconstructed (blurred) speed maps. Used as the default for the live + /// WASM demo so it reflects the trained model out of the box. + pub fn tuned() -> Self { + SegModel { + bands: vec![ + (1479.0, Tissue::Fat), + (1545.0, Tissue::Water), + (1598.0, Tissue::Organ), + (1742.0, Tissue::Muscle), + (f32::INFINITY, Tissue::Bone), + ], + margin_scale: 30.0, + } + } + + /// Classify a single speed value. + #[inline] + pub fn classify(&self, speed: f32) -> Tissue { + for &(upper, t) in &self.bands { + if speed <= upper { + return t; + } + } + self.bands.last().map(|&(_, t)| t).unwrap_or(Tissue::Water) + } + + /// Distance (m/s) from `speed` to the nearest finite band boundary. + fn boundary_margin(&self, speed: f32) -> f32 { + let mut best = f32::INFINITY; + for &(upper, _) in &self.bands { + if upper.is_finite() { + best = best.min((speed - upper).abs()); + } + } + best + } +} + +/// Result of segmenting a reconstructed speed map. +#[derive(Debug, Clone)] +pub struct Segmentation { + /// Per-cell tissue labels (stored as `f32` of the `u8` value). + pub labels: Grid, + /// Per-cell uncertainty in `[0, 1]` (1 == on a decision boundary). + pub uncertainty: Grid, +} + +/// Segment a reconstructed `speed` grid with `model`. +pub fn segment(speed: &Grid, model: &SegModel) -> Segmentation { + let mut labels = speed.clone(); + let mut uncertainty = speed.clone(); + for i in 0..speed.data.len() { + let c = speed.data[i]; + let t = model.classify(c); + labels.data[i] = t as u8 as f32; + let margin = model.boundary_margin(c); + // Closer to a boundary => higher uncertainty. + uncertainty.data[i] = (-margin / model.margin_scale.max(1e-3)).exp(); + } + Segmentation { labels, uncertainty } +} diff --git a/crates/sonic-ct/src/types.rs b/crates/sonic-ct/src/types.rs new file mode 100644 index 000000000..d0a57c056 --- /dev/null +++ b/crates/sonic-ct/src/types.rs @@ -0,0 +1,150 @@ +//! Shared numeric types, tissue constants, and small utilities. +//! +//! All physical quantities use SI units unless noted: +//! - distances in metres (m) +//! - speed of sound in metres per second (m/s) +//! - time in seconds (s) +//! - acoustic attenuation in nepers per metre (Np/m) + +/// Tissue class labels used by the synthetic phantom and the segmenter. +/// +/// The ordering is stable and used as the on-the-wire `u8` value exported to +/// the WASM/JS layer, so do not reorder without updating the UI colour map. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Tissue { + /// Coupling water bath (background). + Water = 0, + /// Subcutaneous fat envelope. + Fat = 1, + /// Skeletal / smooth muscle. + Muscle = 2, + /// Soft organ parenchyma (e.g. liver, kidney). + Organ = 3, + /// Cortical bone (e.g. vertebra). + Bone = 4, +} + +impl Tissue { + /// Total number of distinct tissue classes. + pub const COUNT: usize = 5; + + /// All classes in label order. + pub const ALL: [Tissue; Self::COUNT] = + [Tissue::Water, Tissue::Fat, Tissue::Muscle, Tissue::Organ, Tissue::Bone]; + + /// Human-readable class name. + pub fn name(self) -> &'static str { + match self { + Tissue::Water => "water", + Tissue::Fat => "fat", + Tissue::Muscle => "muscle", + Tissue::Organ => "organ", + Tissue::Bone => "bone", + } + } + + /// Construct from the wire `u8` value, clamping unknown values to `Water`. + pub fn from_u8(v: u8) -> Tissue { + Self::ALL.get(v as usize).copied().unwrap_or(Tissue::Water) + } + + /// Nominal speed of sound for this tissue (m/s). Literature mid-points. + pub fn nominal_speed(self) -> f32 { + match self { + Tissue::Water => 1480.0, + Tissue::Fat => 1450.0, + Tissue::Muscle => 1580.0, + Tissue::Organ => 1570.0, + Tissue::Bone => 3000.0, + } + } + + /// Nominal acoustic attenuation (Np/m) at the simulated centre frequency. + /// + /// Derived from typical dB/(cm·MHz) figures collapsed to a single band; the + /// absolute scale is illustrative, the *contrast* between tissues is what + /// the attenuation reconstruction recovers. + pub fn nominal_attenuation(self) -> f32 { + match self { + Tissue::Water => 0.5, + Tissue::Fat => 9.0, + Tissue::Muscle => 16.0, + Tissue::Organ => 13.0, + Tissue::Bone => 120.0, + } + } +} + +/// Speed of sound in the coupling water bath (m/s). +pub const WATER_SPEED: f32 = 1480.0; + +/// Plausible reconstruction bounds for speed of sound (m/s). +/// +/// Reconstruction is clamped to this range to reject non-physical estimates +/// produced by ray-coverage gaps or noise. +pub const SPEED_MIN: f32 = 1300.0; +/// Upper plausible speed-of-sound bound (m/s). +pub const SPEED_MAX: f32 = 3400.0; + +/// Clamp `x` to the inclusive range `[lo, hi]`. +#[inline] +pub fn clamp(x: f32, lo: f32, hi: f32) -> f32 { + if x < lo { + lo + } else if x > hi { + hi + } else { + x + } +} + +/// A 2-D point in physical (metre) coordinates. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Point { + /// X coordinate (m). + pub x: f32, + /// Y coordinate (m). + pub y: f32, +} + +impl Point { + /// Construct a new point. + pub const fn new(x: f32, y: f32) -> Self { + Point { x, y } + } + + /// Euclidean distance to another point. + #[inline] + pub fn dist(self, o: Point) -> f32 { + let dx = self.x - o.x; + let dy = self.y - o.y; + (dx * dx + dy * dy).sqrt() + } +} + +/// Errors that can arise while configuring or running the pipeline. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SonicError { + /// A configuration value was outside its valid range. + InvalidConfig(&'static str), + /// The acquisition produced no usable measurements. + NoMeasurements, + /// A dimension mismatch between two grids/vectors. + DimensionMismatch, +} + +impl core::fmt::Display for SonicError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SonicError::InvalidConfig(m) => write!(f, "invalid configuration: {m}"), + SonicError::NoMeasurements => write!(f, "acquisition produced no measurements"), + SonicError::DimensionMismatch => write!(f, "dimension mismatch"), + } + } +} + +impl std::error::Error for SonicError {} + +/// Convenience result alias. +pub type Result = core::result::Result; diff --git a/crates/sonic-ct/src/volume3d.rs b/crates/sonic-ct/src/volume3d.rs new file mode 100644 index 000000000..3fa47ac9e --- /dev/null +++ b/crates/sonic-ct/src/volume3d.rs @@ -0,0 +1,189 @@ +//! Vertical-sweep 3-D volume scaffolding. +//! +//! The Midjourney-style scanner lowers the subject through the ring at a +//! constant rate, producing a stack of 2-D slices. This module models that +//! sweep as an ordered set of slice configurations; full 3-D reconstruction +//! (and inter-slice regularisation) is future work (ADR-0008 roadmap). + +use crate::pipeline::{run_slice, PipelineConfig}; +use crate::segmentation::SegModel; +use crate::types::{Result, Tissue}; + +/// A planned vertical sweep through the body. +#[derive(Debug, Clone)] +pub struct SweepPlan { + /// Base pipeline configuration applied to every slice. + pub base: PipelineConfig, + /// Z heights (m) of each slice plane, top to bottom. + pub z_levels: Vec, + /// Platform descent speed (m/s); Midjourney public target ≈ 0.05 m/s. + pub descent_speed: f32, +} + +impl SweepPlan { + /// Build a uniform sweep of `slices` planes spanning `height` metres. + pub fn uniform(base: PipelineConfig, slices: usize, height: f32, descent_speed: f32) -> Self { + let n = slices.max(1); + let mut z_levels = Vec::with_capacity(n); + for i in 0..n { + let t = if n == 1 { 0.0 } else { i as f32 / (n - 1) as f32 }; + z_levels.push(height * (0.5 - t)); // top (+) to bottom (-) + } + SweepPlan { + base, + z_levels, + descent_speed: descent_speed.max(1e-3), + } + } + + /// Estimated scan duration (s) for the whole sweep. + pub fn estimated_duration(&self) -> f32 { + let span = match (self.z_levels.first(), self.z_levels.last()) { + (Some(a), Some(b)) => (a - b).abs(), + _ => 0.0, + }; + span / self.descent_speed + } + + /// Number of slices in the sweep. + pub fn slices(&self) -> usize { + self.z_levels.len() + } +} + +/// Global speed-of-sound window used to normalise volume textures (m/s). +pub const VOL_SPEED_LO: f32 = 1400.0; +/// Upper speed window for volume normalisation (m/s). +pub const VOL_SPEED_HI: f32 = 3100.0; +/// Error-map saturation point (m/s) — errors at/above this map to full red. +pub const VOL_ERROR_SAT: f32 = 900.0; + +/// A reconstructed 3-D body volume assembled from a cranio-caudal slice sweep. +/// +/// Four co-registered channels are kept separate (ADR-0005): ground truth, +/// reconstruction, error (|recon − truth|), and confidence (1 − uncertainty). +/// All are stored as browser-friendly `u8` with index `x + y*n + z*n*n`. +#[derive(Debug, Clone)] +pub struct Volume { + /// Grid resolution per slice. + pub n: usize, + /// Number of slices (depth). + pub nz: usize, + /// Ground-truth tissue labels (`u8` class id). + pub truth_labels: Vec, + /// Reconstructed tissue labels. + pub recon_labels: Vec, + /// Reconstructed speed normalised to `[VOL_SPEED_LO, VOL_SPEED_HI]`. + pub recon_speed_u8: Vec, + /// Per-voxel absolute speed error, normalised by [`VOL_ERROR_SAT`]. + pub error_u8: Vec, + /// Per-voxel confidence (255 = certain). + pub confidence_u8: Vec, + /// Mean Dice per slice (length `nz`). + pub slice_dice: Vec, + /// Mean absolute speed error per slice (m/s). + pub slice_mae: Vec, + /// Index of the worst (highest-MAE) slice. + pub worst_slice: usize, + /// Total valid measurements across the sweep. + pub measurements: usize, + /// Fraction of body voxels per tissue class (body composition). + pub fractions: [f32; Tissue::COUNT], +} + +impl Volume { + /// Mean Dice across all slices. + pub fn mean_dice(&self) -> f32 { + if self.slice_dice.is_empty() { + 0.0 + } else { + self.slice_dice.iter().sum::() / self.slice_dice.len() as f32 + } + } + + /// Mean absolute speed error across all slices (m/s). + pub fn mean_mae(&self) -> f32 { + if self.slice_mae.is_empty() { + 0.0 + } else { + self.slice_mae.iter().sum::() / self.slice_mae.len() as f32 + } + } +} + +/// Reconstruct a full body volume by sweeping `nz` cranio-caudal slices. +pub fn reconstruct_volume( + cfg: PipelineConfig, + model: &SegModel, + nz: usize, +) -> Result { + let nz = nz.max(1); + let n = cfg.phantom.n; + let cells = n * n; + let mut vol = Volume { + n, + nz, + truth_labels: vec![0; cells * nz], + recon_labels: vec![0; cells * nz], + recon_speed_u8: vec![0; cells * nz], + error_u8: vec![0; cells * nz], + confidence_u8: vec![0; cells * nz], + slice_dice: vec![0.0; nz], + slice_mae: vec![0.0; nz], + worst_slice: 0, + measurements: 0, + fractions: [0.0; Tissue::COUNT], + }; + + let mut class_counts = [0u64; Tissue::COUNT]; + let mut body_voxels = 0u64; + let mut worst = f32::NEG_INFINITY; + + for zi in 0..nz { + let z = if nz == 1 { 0.5 } else { zi as f32 / (nz - 1) as f32 }; + let scene = run_slice(cfg, model, z)?; + let base = zi * cells; + + for i in 0..cells { + let tl = scene.phantom.labels.data[i] as u8; + let rl = scene.segmentation.labels.data[i] as u8; + vol.truth_labels[base + i] = tl; + vol.recon_labels[base + i] = rl; + + let rs = scene.recon_speed.data[i]; + vol.recon_speed_u8[base + i] = norm_u8(rs, VOL_SPEED_LO, VOL_SPEED_HI); + + let err = (scene.recon_speed.data[i] - scene.phantom.speed.data[i]).abs(); + vol.error_u8[base + i] = norm_u8(err, 0.0, VOL_ERROR_SAT); + + let conf = 1.0 - scene.segmentation.uncertainty.data[i]; + vol.confidence_u8[base + i] = norm_u8(conf, 0.0, 1.0); + + if tl != Tissue::Water as u8 { + body_voxels += 1; + class_counts[tl as usize] += 1; + } + } + + vol.slice_dice[zi] = scene.quality.mean_dice; + vol.slice_mae[zi] = scene.quality.mae_speed; + vol.measurements += scene.quality.measurements; + if scene.quality.mae_speed > worst { + worst = scene.quality.mae_speed; + vol.worst_slice = zi; + } + } + + if body_voxels > 0 { + for c in 0..Tissue::COUNT { + vol.fractions[c] = class_counts[c] as f32 / body_voxels as f32; + } + } + Ok(vol) +} + +#[inline] +fn norm_u8(v: f32, lo: f32, hi: f32) -> u8 { + let t = ((v - lo) / (hi - lo).max(1e-6)).clamp(0.0, 1.0); + (t * 255.0) as u8 +} diff --git a/crates/sonic-ct/tests/integration.rs b/crates/sonic-ct/tests/integration.rs new file mode 100644 index 000000000..d5f3b04d0 --- /dev/null +++ b/crates/sonic-ct/tests/integration.rs @@ -0,0 +1,191 @@ +//! End-to-end and component integration tests for `sonic_ct`. + +use sonic_ct::butterfly::{AcquisitionBackend, ButterflyEmbeddedConfig, MockButterflyEmbeddedBackend}; +use sonic_ct::geometry::Ring; +use sonic_ct::grid::Grid; +use sonic_ct::memory::{check_coherence, embed_speed, AcousticMemory, ScanRecord}; +use sonic_ct::metrics::mean_dice; +use sonic_ct::model::{evaluate, train, TrainExample}; +use sonic_ct::phantom::{Phantom, PhantomConfig}; +use sonic_ct::pipeline::{run, PipelineConfig}; +use sonic_ct::segmentation::SegModel; +use sonic_ct::types::Tissue; + +fn small_cfg(seed: u64) -> PipelineConfig { + let mut cfg = PipelineConfig::default(); + cfg.phantom = PhantomConfig { n: 56, extent: 0.24, seed }; + cfg.elements = 120; + cfg.acquisition.fan = 60; + cfg.recon.iters = 5; + cfg +} + +#[test] +fn ring_geometry_is_on_circle() { + let ring = Ring::new(64, 0.1); + for p in &ring.positions { + let r = (p.x * p.x + p.y * p.y).sqrt(); + assert!((r - 0.1).abs() < 1e-5, "element off-circle: r={r}"); + } + // Fan receivers exclude the source and near neighbours. + let recv = ring.fan_receivers(0, 32, 0.25); + assert!(!recv.contains(&0)); + assert!(!recv.is_empty()); +} + +#[test] +fn phantom_contains_all_tissue_classes() { + let ph = Phantom::build(PhantomConfig::default()); + let mut seen = [false; Tissue::COUNT]; + for &v in &ph.labels.data { + seen[v as usize] = true; + } + for (i, &t) in Tissue::ALL.iter().enumerate() { + assert!(seen[i], "phantom missing class {}", t.name()); + } +} + +#[test] +fn full_pipeline_metrics_are_sane() { + let scene = run(small_cfg(1)).unwrap(); + assert!(scene.quality.measurements > 100); + assert!(scene.quality.mae_speed < 80.0, "MAE too high: {}", scene.quality.mae_speed); + // Water is the easiest class and should reconstruct well. + assert!(scene.quality.dice[Tissue::Water as usize] > 0.5); + // Ground-truth anatomy should pass the coherence check. + let truth_coh = check_coherence(&scene.phantom.labels); + assert!(!truth_coh.anomaly, "ground truth flagged as anomalous"); +} + +#[test] +fn training_improves_segmentation() { + let mut examples = Vec::new(); + for seed in 1..=6u64 { + let s = run(small_cfg(seed)).unwrap(); + examples.push(TrainExample { + recon_speed: s.recon_speed, + true_labels: s.phantom.labels, + }); + } + let base = SegModel::default(); + let before = evaluate(&base, &examples); + let (tuned, after) = train(&base, &examples); + assert!(after >= before, "training regressed: {before} -> {after}"); + // The tuned model should be at least as good on a held-out style check. + let seg_after = sonic_ct::segmentation::segment(&examples[0].recon_speed, &tuned); + let seg_before = sonic_ct::segmentation::segment(&examples[0].recon_speed, &base); + assert!( + mean_dice(&seg_after.labels, &examples[0].true_labels) + >= mean_dice(&seg_before.labels, &examples[0].true_labels) - 1e-6 + ); +} + +#[test] +fn acoustic_memory_recall_matches_exact() { + let mut mem = AcousticMemory::new(256); + for seed in 1..=20u64 { + let s = run(small_cfg(seed)).unwrap(); + mem.insert(ScanRecord { + id: format!("scan-{seed}"), + patient_id: format!("p{}", seed % 5), + timestamp: 1_700_000_000 + seed, + embedding: embed_speed(&s.recon_speed, 16), + mean_dice: s.quality.mean_dice, + mae: s.quality.mae_speed, + }); + } + // The NSW graph should return the same top-1 as brute force on a probe. + let probe = mem.record(3).unwrap().embedding.clone(); + let approx = mem.search(&probe, 1)[0].0; + let exact = mem.search_exact(&probe, 1)[0].0; + assert_eq!(approx, exact, "NSW top-1 disagreed with exact search"); + // Querying a stored vector returns itself with similarity ~1. + assert!((mem.search(&probe, 1)[0].1 - 1.0).abs() < 1e-3); +} + +#[test] +fn acoustic_memory_roundtrip() { + let mut mem = AcousticMemory::new(64); + for i in 0..10 { + let mut emb = vec![0.0f32; 64]; + emb[i % 64] = 1.0; + mem.insert(ScanRecord { + id: format!("s{i}"), + patient_id: "p".into(), + timestamp: i as u64, + embedding: emb, + mean_dice: 0.5, + mae: 10.0, + }); + } + let bytes = mem.to_bytes(); + let restored = AcousticMemory::from_bytes(&bytes).unwrap(); + assert_eq!(restored.len(), mem.len()); + assert_eq!(restored.record(4).unwrap().id, "s4"); +} + +#[test] +fn longitudinal_drift_detects_change() { + let mut mem = AcousticMemory::new(4); + mem.insert(ScanRecord { + id: "a".into(), + patient_id: "p1".into(), + timestamp: 1, + embedding: vec![1.0, 0.0, 0.0, 0.0], + mean_dice: 0.5, + mae: 1.0, + }); + mem.insert(ScanRecord { + id: "b".into(), + patient_id: "p1".into(), + timestamp: 2, + embedding: vec![0.0, 1.0, 0.0, 0.0], + mean_dice: 0.5, + mae: 1.0, + }); + let drift = mem.longitudinal_drift("p1").unwrap(); + assert!((drift - 1.0).abs() < 1e-5, "orthogonal scans => drift 1.0, got {drift}"); +} + +#[test] +fn coherence_flags_impossible_geometry() { + // A bone cell surrounded by water is anatomically impossible. + let mut g = Grid::square(8, 0.08, Tissue::Water as u8 as f32); + let c = g.idx(4, 4); + g.data[c] = Tissue::Bone as u8 as f32; + let rep = check_coherence(&g); + assert!(rep.bone_touching_water > 0); + assert!(rep.anomaly); +} + +#[test] +fn volume_reconstruction_is_coherent() { + use sonic_ct::volume3d::reconstruct_volume; + let mut cfg = small_cfg(3); + cfg.phantom.n = 48; + let vol = reconstruct_volume(cfg, &SegModel::tuned(), 12).unwrap(); + assert_eq!(vol.nz, 12); + assert_eq!(vol.truth_labels.len(), 48 * 48 * 12); + assert!(vol.measurements > 0); + // Body-composition fractions sum to ~1 over body voxels. + let sum: f32 = vol.fractions.iter().sum(); + assert!((sum - 1.0).abs() < 1e-3, "fractions sum {sum}"); + // Different slices have different anatomy => some variance in Dice. + let d = &vol.slice_dice; + let spread = d.iter().cloned().fold(0.0f32, f32::max) + - d.iter().cloned().fold(1.0f32, f32::min); + assert!(spread >= 0.0); + assert!(vol.worst_slice < vol.nz); +} + +#[test] +fn butterfly_backend_matches_direct_sim() { + let cfg = ButterflyEmbeddedConfig::default(); + assert_eq!(cfg.total_elements(), 40 * 64); + let backend = MockButterflyEmbeddedBackend::default(); + assert_eq!(backend.name(), "mock-butterfly-embedded"); + let ph = Phantom::build(PhantomConfig { n: 40, extent: 0.24, seed: 2 }); + let ring = Ring::new(96, 0.10); + let acq = backend.acquire(&ph, &ring); + assert!(acq.valid_count > 0); +} diff --git a/docs/sonic-ct/MARKET-BRIEF.md b/docs/sonic-ct/MARKET-BRIEF.md new file mode 100644 index 000000000..1559a8510 --- /dev/null +++ b/docs/sonic-ct/MARKET-BRIEF.md @@ -0,0 +1,126 @@ +# sonic_ct — Market & Product-Wedge Brief + +> Honesty note: All market sizes here are **illustrative; definitions vary** and +> should not be treated as forecasts. This brief is about *sequencing* — which +> wedge is credible first — not about asserting a total addressable market. +> `sonic_ct` itself is research/simulation software making **no diagnostic +> claim**, and this brief keeps wellness and diagnostic positioning strictly +> separated. + +## Thesis: Don't Sell "Replace MRI" — Sell the First Credible Wedge + +The temptation with a full-body acoustic scanner is to position it as an +MRI/CT alternative. That framing is a regulatory and clinical-evidence trap: +diagnostic-equivalence claims demand large prospective trials, device clearance, +and reimbursement pathways that take years and capital most early efforts do not +have. The credible strategy is to enter through a **general-wellness** wedge that +needs no diagnostic claim, accumulate evidence and data assets, and only then +climb toward regulated clinical modules. The wedges below are ordered by +time-to-revenue and regulatory friction, lowest first. + +## Wedge 1 — Body-Composition & Longitudinal Wellness Scanning + +The strongest first commercial wedge. A ring scanner that produces quantitative +acoustic maps can report **body-composition and structural-trend metrics** — +fat envelope distribution, lean/muscle structure, organ-region size proxies — +tracked *longitudinally*. This is squarely the territory of established wellness +imaging (DEXA-style body composition, full-body MRI screening startups) but with +a non-ionizing, water-bath-coupled, potentially lower-cost ultrasound modality. + +Why it fits `sonic_ct`'s grain: +- The reconstruction → segmentation → metrics pipeline already produces + per-tissue maps and quantitative summaries. +- The **acoustic memory** (`memory.rs`) is purpose-built for longitudinal + tracking: per-subject timelines, cosine-drift between earliest/latest scans, + and portable `.rvf` archives make "show me how my composition changed over six + months" a native operation, not an add-on. +- Outputs are *trends and proportions*, not diagnoses — the natural home for + general-wellness claims. + +Illustrative market context (definitions vary): preventive/wellness imaging and +body-composition services are a meaningful and growing consumer-health segment, +but figures depend heavily on whether one counts DEXA, MRI screening, fitness +testing, or all three. Treat any single dollar number with suspicion. + +## Wedge 2 — Preventive-Imaging Membership / Spa Model + +The delivery vehicle for Wedge 1. Rather than per-scan medical billing, a +**membership** model (annual or quarterly scans bundled with a longitudinal +dashboard) mirrors the executive-physical and "wellness spa" market that +full-body-MRI screening companies have already validated. The recurring-revenue +shape rewards exactly the longitudinal-tracking capability `sonic_ct` centers on, +and it keeps the customer relationship in the consumer-wellness lane where +general-wellness claims are permissible. Key discipline: the membership product +must present **trends and educational context**, and must not drift into implied +diagnosis or screening claims without the evidence and clearance to back them. + +## Wedge 3 — Research Platform & Tooling + +A parallel, lower-capital wedge that monetizes the simulator itself rather than a +clinical service. `sonic_ct` is, today, a clean, dependency-free, WASM-portable +USCT simulator with: +- procedural, deterministic **phantoms** (a reproducible benchmark corpus), +- a **reconstruction benchmark harness** (SART baseline, MAE/Dice metrics), +- a transparent segmentation/training loop, and +- a portable archive format. + +That is precisely the toolkit USCT and FWI researchers, device teams, and +algorithm groups need: shared phantoms, reproducible baselines, and a +reconstruction leaderboard. A tooling/benchmark offering (open core plus +supported builds, hosted benchmark, or licensed phantom/eval suites) can generate +early revenue and credibility, seed an academic user base, and de-risk the +clinical roadmap by attracting the very FWI expertise the product needs. +Illustrative sizing: scientific-imaging tooling is a niche but sticky market; +value is in workflow lock-in and benchmark authority, not unit volume. + +## Wedge 4 — Clinical Specialty Modules (After Evidence) + +Only *after* the wellness wedge has accumulated longitudinal data and the physics +has climbed to FWI-grade fidelity should specialty clinical modules follow — e.g. +musculoskeletal assessment, hepatic or other organ-focused quantification — each +gated behind its own evidence and clearance. These are **diagnostic** outputs and +must be developed, validated, labelled, and (where applicable) cleared +independently of the wellness product. The architectural rule that keeps physics +reconstruction separate from AI segmentation, and preserves raw RF evidence, +exists precisely so a future regulated module can be validated on auditable data +without re-engineering the platform. + +## Wedge 5 — Butterfly-Adjacent Embedded Software Tooling + +`butterfly.rs` is explicitly a **mock boundary, not an SDK** — there is no public +raw-hardware SDK for Butterfly Ultrasound-on-Chip modules, and the code says so. +The adjacent-but-honest opportunity is *software tooling around* low-cost +Ultrasound-on-Chip hardware: a clean acquisition-backend contract +(`AcquisitionBackend`), raw-RF data-format design, simulation-to-hardware +parity testing, and reconstruction pipelines that a future licensed backend could +plug into. This positions `sonic_ct` as the reconstruction/tooling layer for an +ecosystem of inexpensive arrayed probes without overclaiming a partnership or +integration that does not exist. Should an embedded raw-data path become +available, the boundary is already designed for it. + +## Regulatory Posture + +| Dimension | Posture | +|---|---| +| Claim class (initial) | **General wellness** — composition/trend education only; no diagnosis, no screening claim | +| Output separation | Wellness outputs and any future diagnostic outputs kept in **distinct products/labels**; never co-mingled in one report | +| AI/ML lifecycle | Treat the learned components as an **AI/ML medical-device lifecycle** problem when (and only when) diagnostic: versioned models, locked vs. adaptive distinction, change control, validation datasets, performance monitoring | +| Evidence preservation | **Raw evidence preserved** (RF-frame data contract designed from day one) so any future regulated claim is auditable and reproducible | +| Physics vs. AI boundary | Physics reconstruction kept separate from AI segmentation, so the quantitative substrate can be validated independently of the classifier | +| Hardware claims | Butterfly boundary labelled as a **mock**; no implied hardware certification or SDK partnership | + +## Sequencing Summary + +| Wedge | Time-to-revenue | Reg. friction | Depends on | +|---|---|---|---| +| 1. Body-composition / longitudinal wellness | Near | Low (general wellness) | Current pipeline + memory | +| 2. Preventive-imaging membership/spa | Near | Low–medium | Wedge 1 + dashboard | +| 3. Research platform / tooling | Now | Minimal | Simulator as-is | +| 4. Clinical specialty modules | Far | High (diagnostic) | FWI fidelity + evidence + clearance | +| 5. Butterfly-adjacent embedded tooling | Medium | Low (software) | Licensed raw-data backend | + +**Bottom line.** Lead with wellness-grade body-composition longitudinal scanning +delivered through a membership model, fund and de-risk it with a research-tooling +wedge, design honestly around (not on top of a fictional) Butterfly SDK, and earn +the right to diagnostic specialty modules through evidence — keeping wellness and +diagnostic claims, and physics and AI, cleanly separated throughout. diff --git a/docs/sonic-ct/RESEARCH-MAP.md b/docs/sonic-ct/RESEARCH-MAP.md new file mode 100644 index 000000000..401c0b4cb --- /dev/null +++ b/docs/sonic-ct/RESEARCH-MAP.md @@ -0,0 +1,148 @@ +# sonic_ct — State-of-the-Art Research Map for Computational Ultrasound Tomography + +> Scope note: This document surveys the research landscape that `sonic_ct` draws +> from and aims toward. External results are described as **directions reported +> in the literature**, not as exact, attributable numbers. Where a number appears +> it is from `sonic_ct`'s own measured simulator (clearly labelled), not a +> third-party claim. `sonic_ct` is simulation/research software and makes **no +> diagnostic claim**. + +Ultrasound Computed Tomography (USCT) reconstructs maps of acoustic tissue +properties — speed of sound, attenuation, and (in advanced variants) density — +from many transmit/receive paths through the body. Unlike conventional B-mode +ultrasound, which forms reflectivity images from backscatter, USCT exploits the +*transmission* geometry of a surrounding transducer ring (or bowl) to solve an +inverse problem for quantitative material properties. The field spans a ladder +of physical fidelity, from straight-ray travel-time tomography up to full wave +physics. `sonic_ct` sits at the bottom rung today and is architected to climb. + +## 1. Time-of-Flight (TOF) Transmission Tomography + +The entry point — and what `sonic_ct` implements now. Each transmit/receive pair +yields a first-arrival travel time. Under a straight-ray (high-frequency, +geometric-acoustics) approximation, travel time is a line integral of *slowness* +(1/c) along the ray, giving a linear system `A s = t`, where `A_ij` is the length +ray `i` spends in cell `j`. `sonic_ct` solves this with **SART** (Simultaneous +Algebraic Reconstruction Technique) in `reconstruction.rs`; a single SART sweep +is equivalent to the classic delay-backprojection baseline, and additional sweeps +move toward the least-squares solution. Attenuation is recovered by swapping the +right-hand side for integrated amplitude loss. + +**Strengths:** fast, robust, convex, dependency-free, and a faithful baseline for +soft-tissue *speed* contrast. **Limits:** the straight-ray assumption ignores +refraction and diffraction, so it blurs sharp, high-contrast boundaries. In +`sonic_ct` this shows up concretely: measured mean Dice with the tuned threshold +model is ~0.63 (up from ~0.30 with literature defaults), MAE ~28–31 m/s over +~8000 valid measurements, but **bone Dice ≈ 0** — straight rays bend strongly +around and refract through cortical bone, smearing it out. The literature is +unanimous that recovering hard, refractive structures requires wave-physics +methods. This is the documented motivation for the FWI roadmap. + +## 2. Full-Waveform Inversion (FWI) + +FWI is the SOTA for high-fidelity USCT. Rather than reducing each trace to one +travel time, it fits the *entire recorded waveform* by iteratively updating the +property model until simulated and observed wavefields match. Key machinery +reported across the geophysics and medical-USCT literature: + +- **Adjoint-state gradients.** The gradient of the data misfit with respect to + the model is computed by cross-correlating the forward wavefield with a + back-propagated *adjoint* (residual) wavefield. This gives a full-model + gradient at the cost of roughly two wave simulations per source — the + enabling trick that makes FWI tractable at all. +- **Frequency continuation (multiscale).** Inversion proceeds from low to high + frequencies. Low frequencies recover smooth, large-scale structure and are + far less prone to local minima; higher frequencies then sharpen detail. This + coarse-to-fine schedule is the standard FWI workflow. +- **Cycle-skipping.** FWI's central failure mode: if the starting model + mispredicts a phase by more than half a wavelength, the optimizer locks onto + the wrong cycle and converges to a wrong-but-plausible model. Low-frequency + data, good starting models (e.g. a TOF result), and envelope/optimal-transport + misfits are the reported mitigations. `sonic_ct`'s TOF output and the + acoustic-memory warm-start mechanism are designed to provide exactly such + starting models. +- **Source encoding.** Simultaneously firing many encoded sources and inverting + the superposition dramatically cuts the number of forward simulations per + iteration; the literature reports large compute reductions at the cost of + managing crosstalk noise. +- **Brain/skull FWI** (e.g. work in the style of Guasch and colleagues). The + skull is the hard case: strong speed contrast, attenuation, and aberration. + Reported results indicate that 3-D acoustic FWI can reconstruct + through-skull speed-of-sound maps of brain tissue from a surrounding array — + a direction directly relevant to whole-body transmission scanning. +- **Musculoskeletal / vortex-encoded FWI.** More recent reported work applies + FWI to limbs and musculoskeletal targets and uses *vortex* (orbital-angular- + momentum-style) encoded illumination to reduce the compute burden of + many-source acquisition while preserving reconstruction quality. The theme is + the same: keep the wave physics, cut the simulation count. + +For `sonic_ct`, FWI is the documented fix for the bone-Dice failure and the +gateway to quantitative density/elasticity. The architecture deliberately keeps +the forward operator (ray → wave) swappable behind the acquisition/physics +boundary so an FWI engine can replace SART without disturbing acquisition or AI +layers. + +## 3. Sparse Acquisition + AI Reconstruction + +Dense rings with thousands of paths are expensive and slow. A major MICCAI-era +direction (e.g. APS-USCT-style "adaptive/sparse" pipelines) trains neural +networks to reconstruct high-quality property maps from **deliberately +sparse** acquisitions — fewer elements, fewer angles, or fewer firings — by +learning the data prior that a sparse linear solver lacks. Reported approaches +combine learned sinogram/measurement completion with image-domain refinement. +The relevance to `sonic_ct`: a "learned sparse completion" stage (on the +refinement roadmap) would let a Butterfly-style ring with limited channel count +approximate the coverage of a dense research scanner. Critically, `sonic_ct`'s +governance stance keeps any such learned completion as an *enhancement on top of* +the physics reconstruction, with raw evidence preserved, rather than a black box +that replaces it. + +## 4. Regularization with Structural Priors + +Tomographic inversion is ill-posed; regularization injects prior knowledge. +Beyond generic Tikhonov/total-variation smoothing, the literature reports +**structural priors** that borrow edges from a co-registered modality — for +example using an EIT (electrical impedance tomography) or optical reconstruction +to guide where ultrasound speed boundaries should fall (joint/multimodal +inversion, structurally-guided TV). The shared theme is that one modality's +spatial structure constrains another's, sharpening boundaries without inventing +detail. `sonic_ct`'s anatomical **graph-coherence** check in `memory.rs` is a +lightweight cousin of this idea: it encodes anatomical rules (e.g. "bone must not +touch the water bath") and flags reconstructions that violate them, providing a +prior-as-validator today and a hook for prior-as-regularizer later. + +## 5. Learned Segmentation + Uncertainty Quantification + +Once properties are reconstructed, tissue must be classified, and clinical use +demands knowing *where the model is unsure*. The literature couples learned +segmentation with uncertainty quantification (Bayesian/ensemble/evidential +methods, calibrated confidence maps). `sonic_ct` keeps this stage intentionally +**transparent**: `segmentation.rs` is an auditable piecewise speed-band +classifier — every label is explained by a speed band — and every cell carries an +uncertainty derived from its margin to the nearest decision boundary. The bands +are *fitted* by reproducible coordinate ascent (`model.rs`) rather than hand-set, +which is why the tuned model roughly doubles mean Dice versus defaults. This is a +deliberate, honest floor: a glass-box classifier with real uncertainty, leaving +room to swap in a learned, calibrated segmenter once evidence justifies it — and +keeping the physics reconstruction strictly separate from the AI segmentation. + +## How sonic_ct Maps to the Literature + +| Capability area | sonic_ct today | Reported SOTA direction | Gap / roadmap | +|---|---|---|---| +| Forward physics | Straight-ray TOF + amplitude integral (`acquisition.rs`, `ray.rs`) | Finite-difference / pseudo-spectral wave propagation | Add FD wave kernel behind the physics boundary | +| Speed reconstruction | SART (1 sweep = delay backprojection) | Adjoint-state FWI with frequency continuation | Adjoint gradients; multiscale schedule | +| Hard-structure (bone/skull) | Bone Dice ≈ 0 (straight-ray blur) | Through-skull / MSK FWI (Guasch-style, vortex-encoded) | FWI is the documented fix | +| Acquisition efficiency | Dense ring, fan sweep, ~8000 meas. | Source/vortex encoding; AI sparse reconstruction | Source encoding + learned completion | +| Regularization | Anatomical graph-coherence validator | Multimodal/EIT-guided structural priors | Promote validator to regularizer | +| Segmentation | Glass-box speed-band + margin uncertainty (tuned Dice ~0.63) | Learned segmentation + calibrated UQ | Optional learned segmenter, kept separate from physics | +| Memory / longitudinal | NSW vector index, `.rvf` archive, FWI warm-start (`memory.rs`) | Population priors; warm-started inversion | Use warm-start to seed FWI, mitigate cycle-skipping | +| Volume | 2-D slices; vertical-sweep stub (`volume3d.rs`) | 3-D / 4-D acoustic FWI | 3-D solver + inter-slice regularization | + +**Bottom line.** `sonic_ct` is an honest TOF/SART baseline with a transparent AI +layer and a memory substrate, deliberately structured so each rung of the +research ladder — wave physics, adjoint FWI, frequency continuation, source +encoding, learned sparse completion, 3-D — can be added without rewriting the +layers around it. The single most important documented gap is the leap from +straight-ray TOF to wave-based FWI, which is what unlocks hard-structure +fidelity that the current Dice metrics show is missing. diff --git a/docs/sonic-ct/SPARC.md b/docs/sonic-ct/SPARC.md new file mode 100644 index 000000000..8390ce393 --- /dev/null +++ b/docs/sonic-ct/SPARC.md @@ -0,0 +1,186 @@ +# sonic_ct — SPARC Analysis + +> SPARC = **S**pecification, **P**seudocode, **A**rchitecture, **R**efinement, +> **C**ompletion. This document analyzes the `sonic_ct` USCT simulator against +> its real, implemented modules. `sonic_ct` is research/simulation software, +> makes **no diagnostic claim**, and the Butterfly Embedded boundary is a mock, +> not a hardware SDK. + +--- + +## S — Specification + +### Inputs + +| Input | Source module | Notes | +|---|---|---| +| Ring geometry | `geometry.rs` (`Ring::new`) | N elements on a circle, radius = `ring_frac × half_FOV`; inward normals | +| Tissue speed map | `phantom.rs` → `Grid` (`types.rs`, `grid.rs`) | Speed-of-sound (m/s); procedural abdomen phantom or external map | +| Tissue attenuation map | `phantom.rs` → `Grid` | Acoustic attenuation (Np/m), co-registered with speed | +| Ground-truth labels | `phantom.rs` → `Grid` | Per-cell `Tissue` class (water/fat/muscle/organ/bone) | +| Source/receiver plan | `acquisition.rs` (`AcquisitionConfig`) + `Ring::fan_receivers` | Fan width, min angular separation, samples/cell, timing noise | +| Optional raw RF frames | `butterfly.rs` (`RawRfFrame`) | Data-*contract* shape only (channels × samples); simulator does not synthesize waveforms | + +### Outputs + +| Output | Module | +|---|---| +| Projection measurements (TOF, attenuation, validity, ray geometry) | `acquisition.rs` (`Acquisition`, `Measurement`) | +| Speed-of-sound reconstruction (m/s) | `reconstruction.rs` (`reconstruct_speed`) | +| Attenuation reconstruction (Np/m) | `reconstruction.rs` (`reconstruct_attenuation`) | +| Segmentation mask + per-cell uncertainty | `segmentation.rs` (`Segmentation`) | +| Dice (per-class + mean), MAE metrics | `metrics.rs` (`QualityReport`) | +| Inspection images (PGM) | `grid.rs` (`to_pgm`) | +| Acoustic-memory archive (`.rvf`-style, NSW index) | `memory.rs` (`AcousticMemory`, `to_bytes`/`from_bytes`) | + +### Hard Constraints + +1. **No diagnostic claim.** Outputs are research/quantitative, not clinical + findings. Enforced as a documentation and labelling invariant (`lib.rs`). +2. **No fake Butterfly SDK.** `butterfly.rs` is a *mock* `AcquisitionBackend`; + it must never present as a licensed hardware integration. +3. **Preserve raw evidence.** The `RawRfFrame` contract and the portable + archive format exist so raw/intermediate data stays auditable from day one. +4. **Physics ≠ AI.** Reconstruction (physics inverse problem) and segmentation + (AI classification) are separate modules and must remain swappable + independently. The segmenter consumes reconstructions; it never alters them. +5. **Determinism / dependency-free.** Phantom and pipeline are reproducible + (seeded PRNGs) and build to `wasm32-unknown-unknown`. + +--- + +## P — Pseudocode (End-to-End Pipeline) + +```text +function run(cfg, model): # pipeline.rs::run_with_model + validate(cfg) # reject out-of-range config + + # --- Acquisition layer --- + phantom = Phantom.build(cfg.phantom) # phantom.rs: seeded speed/atten/labels + ring = Ring.new(cfg.elements, + half_fov * cfg.ring_frac) # geometry.rs + + acq = [] # acquisition.rs::simulate + slowness = 1 / phantom.speed # linear travel-time integral + for source in ring.elements: + for receiver in ring.fan_receivers(source, fan, min_sep): + if receiver <= source: continue # de-dup reciprocal pairs + ray = Ray.between(grid, src_pos, rcv_pos) # ray.rs (DDA cells) + tt = ray.integrate(slowness) + exterior_water_leg + att = ray.integrate(attenuation) + valid = ray spends > 50% length in tissue + acq.append(Measurement{tt, att, ray, valid, ...}) + if acq.valid_count == 0: return error(NoMeasurements) + + # --- Physics layer (SART) --- + speed = SART(acq, init=1/WATER_SPEED, # reconstruction.rs + rhs = travel_time - exterior_water_leg) # solves A·s = t + speed = 1/slowness, clamped to [SPEED_MIN, SPEED_MAX] + atten = SART(acq, init=0, rhs = attenuation) + # 1 SART sweep == delay-backprojection baseline; more sweeps -> least squares + + # --- AI layer (segmentation, kept separate from physics) --- + seg = segment(speed, model) # segmentation.rs + for each cell c: + label = model.classify(c) # piecewise speed-band + uncertainty = exp(-margin_to_boundary / margin_scale) + + # --- Clinical-workflow layer (metrics) --- + dice = dice_all(seg.labels, phantom.labels) # metrics.rs + mae_speed = mean_abs_diff(speed, phantom.speed) + quality = {mae_speed, dice, mean_dice, measurements} + + # --- Governance layer (memory + coherence) --- + embedding = speed.embedding(k) # grid.rs: k×k, mean-centred, L2 + coherence = check_coherence(seg.labels) # memory.rs: anatomical rules + memory.insert(ScanRecord{id, patient_id, ts, embedding, dice, mae}) + archive = memory.to_bytes() # .rvf-style portable container + + return Scene{phantom, ring, acq, speed, atten, seg, quality} +``` + +Offline training loop (`model.rs`, `bin/train.rs`): coordinate-ascent over the +segmentation band boundaries to maximize mean Dice on a corpus of +reconstruction/ground-truth pairs — produces `SegModel::tuned()`. + +--- + +## A — Architecture (Five Layers → Real Modules) + +| Layer | Responsibility | Modules | +|---|---|---| +| **1. Acquisition** | Geometry, ray tracing, transmission simulation, hardware boundary, raw-RF contract | `geometry.rs`, `ray.rs`, `acquisition.rs`, `butterfly.rs`, `phantom.rs` | +| **2. Physics** | Inverse problem: TOF/attenuation reconstruction (SART), grid math, types/constants | `reconstruction.rs`, `grid.rs`, `types.rs` | +| **3. AI** | Tissue segmentation, per-cell uncertainty, reproducible model training | `segmentation.rs`, `model.rs` | +| **4. Clinical Workflow** | Quality metrics (Dice/MAE), inspection imagery, end-to-end orchestration, UI | `metrics.rs`, `pipeline.rs`, `grid::to_pgm`, `crates/sonic-ct-wasm/` (raw C-ABI, ~31 KB), `examples/sonic-ct/` (React Three Fiber) | +| **5. Governance** | Acoustic memory (NSW index, longitudinal tracking, FWI warm-start), anatomical graph-coherence, portable `.rvf` archive, 3-D sweep scaffolding | `memory.rs`, `volume3d.rs` | + +**Boundaries that matter:** +- `AcquisitionBackend` (Layer 1) decouples physics from data source — a licensed + hardware backend can replace `MockButterflyEmbeddedBackend` untouched. +- Layer 2 (physics) emits *only* property maps; Layer 3 (AI) consumes them and + never writes back — preserving the physics/AI separation constraint. +- Layer 5 (governance) observes outputs (embeddings, coherence) without altering + reconstructions, keeping evidence and audit trails intact. + +The WASM crate is intentionally a **raw C-ABI** module (no `wasm-bindgen`), +keeping the browser artifact tiny (~31 KB) and the JS glue explicit; the React +Three Fiber UI in `examples/sonic-ct/` is a *consumer* of the core and holds no +reconstruction logic. + +--- + +## R — Refinement Roadmap + +| Stage | Status | Description | +|---|---|---| +| 1. TOF SART | **Done** | Straight-ray travel-time + attenuation; 1 sweep = delay backprojection (`reconstruction.rs`) | +| 2. Finite-difference wave propagation | Planned | Replace ray integral with an FD acoustic forward solver behind Layer 1/2 boundary | +| 3. Adjoint FWI | Planned | Adjoint-state gradients of waveform misfit; the documented fix for bone Dice ≈ 0 | +| 4. Frequency continuation + source encoding | Planned | Multiscale low→high schedule (cycle-skip mitigation) + encoded sources for compute reduction | +| 5. Learned sparse completion | Planned | AI measurement/image completion for sparse rings, layered *on top of* physics, evidence preserved | +| 6. 3-D vertical sweep | Stub (`volume3d.rs`) | Promote `SweepPlan` to full stacked-slice 3-D reconstruction with inter-slice regularization | +| 7. DICOMweb / FHIR adapters | Planned | Standards-based export at the clinical-workflow layer | +| 8. QMS / validation harness | Planned | AI/ML-lifecycle change control, validation datasets, performance monitoring (gating any diagnostic claim) | + +Sequencing rationale: stages 2–4 raise *physics fidelity* (the biggest measured +gap — bone Dice ≈ 0 from straight-ray blur); stages 5–6 raise *coverage and +efficiency*; stages 7–8 raise *clinical/regulatory readiness*. The acoustic +memory's `warm_start` already anticipates stage 3 by retrieving the nearest +prior reconstruction as an FWI starting model to reduce cycle-skipping. + +--- + +## C — Completion Criteria Checklist + +**Implemented (current baseline):** +- [x] Deterministic procedural phantom (speed/attenuation/labels) +- [x] Ring geometry + fan acquisition with reciprocal de-duplication +- [x] SART speed + attenuation reconstruction (clamped to physical bounds) +- [x] Transparent speed-band segmentation with per-cell uncertainty +- [x] Reproducible coordinate-ascent model training (`SegModel::tuned`) +- [x] Dice (per-class + mean) and MAE metrics +- [x] PGM inspection images +- [x] Acoustic memory: NSW index, patient timelines, longitudinal drift, warm-start +- [x] Anatomical graph-coherence anomaly check +- [x] Portable `.rvf`-style archive (round-trips via `to_bytes`/`from_bytes`) +- [x] Mock Butterfly `AcquisitionBackend` + `RawRfFrame` data contract +- [x] WASM raw C-ABI build (~31 KB) + React Three Fiber UI +- [x] No-diagnostic-claim / no-fake-SDK invariants documented in `lib.rs` + +**Measured baseline (simulator self-report, not external claims):** +- [x] Tuned mean Dice ~0.63 (vs ~0.30 default), MAE ~28–31 m/s, ~8000 measurements +- [ ] Bone Dice > 0 — **open**, blocked on wave physics (straight-ray blur) + +**Remaining for higher fidelity / clinical readiness:** +- [ ] Finite-difference wave forward solver +- [ ] Adjoint-state FWI with frequency continuation + source encoding +- [ ] Learned sparse completion (physics-preserving, evidence-preserving) +- [ ] 3-D stacked-slice reconstruction with inter-slice regularization +- [ ] DICOMweb / FHIR export adapters +- [ ] QMS / validation harness and AI/ML lifecycle controls +- [ ] Wellness vs. diagnostic output separation enforced at product layer + +**Invariant gates (must hold at every stage):** no diagnostic claim · no fake +Butterfly SDK · raw evidence preserved · physics reconstruction separate from AI +segmentation · deterministic, dependency-free core that builds to WASM. diff --git a/docs/sonic-ct/adr/ADR-0001-simulation-first.md b/docs/sonic-ct/adr/ADR-0001-simulation-first.md new file mode 100644 index 000000000..8303cadee --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0001-simulation-first.md @@ -0,0 +1,60 @@ +# ADR-0001: Simulation-First Architecture + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +USCT research normally couples physics, reconstruction, and hardware tightly, +which makes it impossible to iterate on algorithms without a transducer ring and +a tank. We have no hardware and no public raw-acquisition SDK. We need a way to +develop and quantitatively evaluate the full pipeline — phantom, acquisition, +reconstruction, segmentation, metrics — purely in software, deterministically, +and in a form that compiles to a tiny WASM artifact for an in-browser demo. + +## Decision + +The core crate `crates/sonic-ct/` is a pure-Rust, **zero-dependency**, +deterministic simulator. Everything flows through one entry point, +`pipeline::run_with_model` (see `pipeline.rs`), which builds a deterministic +phantom (`phantom.rs`, SplitMix64-seeded), simulates a transmission acquisition +(`acquisition.rs::simulate`), reconstructs speed and attenuation +(`reconstruction.rs`), segments (`segmentation.rs`), and scores against ground +truth (`metrics.rs`). The result is a single `Scene` struct holding phantom, +ring, acquisition, reconstructions, segmentation, and a `QualityReport`. + +Because there is ground truth (`Phantom::labels`, `phantom.speed`), every run is +self-scoring: `dice_all` + `mae_speed` give Dice-per-class, mean Dice, and speed +MAE without any external dataset. + +## Consequences + +### Positive + +- Deterministic, reproducible runs (fixed seeds) — regressions are detectable. +- Zero dependencies keep the WASM artifact at ~31 KB and the build trivial. +- Ground-truth phantoms make the whole pipeline quantitatively measurable. +- Hardware can be added later behind a trait (ADR-0002) without disturbing core. + +### Negative / Trade-offs + +- The simulator uses straight-ray time-of-flight, not full-wave physics, so + results are optimistic relative to real diffracting media (see ADR-0004). +- Synthetic phantoms cannot capture the variability of real anatomy. +- "Self-scoring" measures algorithm consistency, not clinical accuracy. + +## Alternatives Considered + +- **Full-waveform forward solver first**: physically faithful but far heavier, + non-trivial to compile to a small WASM target, and premature before the + end-to-end contract is settled. +- **Replay recorded RF datasets**: no licensed raw SDK exists, and it couples + development to one acquisition rig. + +## References (to the real code) + +- `crates/sonic-ct/src/pipeline.rs` (`run`, `run_with_model`, `Scene`) +- `crates/sonic-ct/src/phantom.rs` (`Phantom::build`, `SplitMix64`) +- `crates/sonic-ct/src/metrics.rs` (`dice_all`, `mae_speed`, `QualityReport`) +- `crates/sonic-ct/Cargo.toml` (detached workspace, no dependencies) diff --git a/docs/sonic-ct/adr/ADR-0002-hardware-backend-trait.md b/docs/sonic-ct/adr/ADR-0002-hardware-backend-trait.md new file mode 100644 index 000000000..d6d0a6847 --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0002-hardware-backend-trait.md @@ -0,0 +1,67 @@ +# ADR-0002: Hardware Acquisition Behind a Backend Trait + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +We want hardware to be addable later (ADR-0001 is simulation-first), but the +reconstruction core must not learn anything about a specific device. There is +**no public raw-hardware SDK** for the Butterfly Ultrasound-on-Chip modules, so +we cannot integrate against a real driver today. What we can do is fix the seam: +define the contract the core consumes for acquisition, and provide a simulated +implementation of it so the rest of the pipeline is written against the seam +from day one. + +## Decision + +Define `AcquisitionBackend` in `butterfly.rs`: + +```rust +pub trait AcquisitionBackend { + fn name(&self) -> &str; // provenance + fn acquire(&self, phantom: &Phantom, ring: &Ring) -> Acquisition; +} +``` + +The core depends only on the returned `Acquisition` (`acquisition.rs`), never on +how it was produced. Today the only implementer is +`MockButterflyEmbeddedBackend`, which delegates to `acquisition::simulate`. Its +hardware shape is described by `ButterflyEmbeddedConfig` (default 40 modules × +64 channels = 2560 elements, ~3 MHz centre frequency) — values drawn from public +prototype figures, not from a licensed SDK. `name()` returns +`"mock-butterfly-embedded"` so provenance is honest in any logged scan. + +A future licensed backend implements the same trait and produces an +`Acquisition` (eventually from real `RawRfFrame`s, see ADR-0003) without +touching `reconstruction.rs`, `segmentation.rs`, or `pipeline.rs`. + +## Consequences + +### Positive + +- Core/reconstruction is decoupled from any device; one trait is the only seam. +- Provenance is explicit via `name()` — a scan records which backend made it. +- `ButterflyEmbeddedConfig` documents target hardware geometry without faking an + SDK that does not exist. + +### Negative / Trade-offs + +- The trait was shaped by the simulator, so a real device may need contract + revisions (sample rate, framing) once `RawRfFrame` is actually populated. +- `acquire(phantom, ring)` is phantom-driven; a hardware backend would ignore + `phantom` and read the device, so the signature is slightly simulation-biased. + +## Alternatives Considered + +- **Hard-code `simulate` calls in the pipeline**: simplest, but bakes the + simulator into the core and blocks any future device. +- **Vendor SDK adapter now**: impossible — no public raw SDK exists. + +## References (to the real code) + +- `crates/sonic-ct/src/butterfly.rs` (`AcquisitionBackend`, + `MockButterflyEmbeddedBackend`, `ButterflyEmbeddedConfig`) +- `crates/sonic-ct/src/acquisition.rs` (`Acquisition`, `simulate`) +- `crates/sonic-ct/src/pipeline.rs` (consumes `Acquisition`, not a backend) diff --git a/docs/sonic-ct/adr/ADR-0003-preserve-raw-rf-before-ai.md b/docs/sonic-ct/adr/ADR-0003-preserve-raw-rf-before-ai.md new file mode 100644 index 000000000..fce97d9a7 --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0003-preserve-raw-rf-before-ai.md @@ -0,0 +1,68 @@ +# ADR-0003: Preserve Raw RF Before Any AI Processing + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +In USCT the raw radio-frequency (RF) channel data is the only lossless record of +an acquisition. Once it has been beamformed, reconstructed, or fed through a +learned model, information is discarded irreversibly and provenance is hard to +re-establish. We want the data contract and storage formats designed for raw +capture from the start, even though the current simulator does not synthesise +full RF waveforms. This is a **forward-looking** decision: we are fixing the +shape now, not claiming we capture RF today. + +## Decision + +Define `RawRfFrame` in `butterfly.rs` as a **shape/contract placeholder**: + +```rust +pub struct RawRfFrame { + pub source: usize, // transmitting element + pub channels: usize, // receive channels + pub samples: usize, // samples per channel + pub sample_rate: f32, // Hz +} +``` + +It records the `channels × samples` framing and sample rate of a real capture +but carries no waveform buffer yet — the simulator produces reduced +`Measurement`s (travel time, attenuation) in `acquisition.rs`, not RF. The type +exists so that downstream code, the `AcquisitionBackend` seam (ADR-0002), and +the portable container format are designed around raw frames from day one. + +The container intent is realised by `memory.rs`: `AcousticMemory` round-trips +through a compact `.rvf`-style binary (`to_bytes`/`from_bytes`), making scans +portable, auditable artifacts that can later embed raw frames alongside +derived products. + +## Consequences + +### Positive + +- Storage, provenance, and the backend trait are all designed for raw capture; + adding waveforms later is additive, not a rewrite. +- The honest "placeholder" framing avoids over-claiming capability. + +### Negative / Trade-offs + +- `RawRfFrame` is currently unused by the simulated pipeline — dead-ish weight + until a backend fills it, and its fields may shift once real RF is captured. +- No actual RF persistence exists yet; the `.rvf` container stores embeddings + and quality provenance (`ScanRecord`), not waveforms. + +## Alternatives Considered + +- **Add `RawRfFrame` only when hardware lands**: avoids unused code but forces a + later contract/storage redesign across the backend seam. +- **Store only reconstructions**: smallest, but discards the lossless record and + blocks reprocessing with better solvers (e.g. FWI, ADR-0004). + +## References (to the real code) + +- `crates/sonic-ct/src/butterfly.rs` (`RawRfFrame`) +- `crates/sonic-ct/src/acquisition.rs` (`Measurement` — current reduced product) +- `crates/sonic-ct/src/memory.rs` (`AcousticMemory::to_bytes`/`from_bytes`, + `ScanRecord`) diff --git a/docs/sonic-ct/adr/ADR-0004-delay-backprojection-baseline.md b/docs/sonic-ct/adr/ADR-0004-delay-backprojection-baseline.md new file mode 100644 index 000000000..d14ec0a24 --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0004-delay-backprojection-baseline.md @@ -0,0 +1,62 @@ +# ADR-0004: Delay-and-Backprojection as the Reconstruction Baseline + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +We need a reconstruction stage that is (a) cheap enough to run in the WASM demo, +(b) honest about being a baseline rather than a state-of-the-art solver, and +(c) a stepping stone towards iterative least-squares and, eventually, +full-waveform inversion (FWI). The simplest defensible time-of-flight method is +delay-and-backprojection; the natural generalisation is SART (Simultaneous +Algebraic Reconstruction Technique), where one sweep *is* backprojection. + +## Decision + +`reconstruction.rs` solves the linear tomography system `A s = t` with SART, +where `s` is per-cell slowness (1/c), `A_ij` is the length ray `i` spends in +cell `j` (from `ray.rs` path integration), and `t` is the interior travel time +(measured travel time minus the exterior water leg, computed against +`WATER_SPEED = 1480` m/s). `reconstruct_speed` and `reconstruct_attenuation` +share the generic `sart` solver, differing only in the right-hand side. + +`ReconConfig::iters` controls sweeps; **`iters == 1` is exactly the +delay-backprojection baseline**, and additional sweeps move the estimate towards +the least-squares solution. The default is 6 sweeps with relaxation 0.9. +Measured on the synthetic corpus (~96×96 grid, 180 elements, ~8000 valid +measurements): speed MAE lands at ~28–31 m/s. + +## Consequences + +### Positive + +- One code path covers both the baseline (`iters=1`) and an iterative refinement + (`iters>1`), so the baseline is always available for comparison. +- Pure linear algebra over sparse ray-cell pairs — fast, dependency-free, small. +- Honest, well-understood error characteristics (~28–31 m/s MAE). + +### Negative / Trade-offs + +- Straight-ray TOF ignores diffraction and refraction; small high-contrast + structures blur. Concretely, **bone Dice is ~0** because the small, fast spine + is smeared by the straight-ray model. +- SART converges slowly; the speed/quality trade-off is fixed by `iters`. +- This is explicitly *not* FWI; it is the documented predecessor to it. + +## Alternatives Considered + +- **Filtered backprojection (FBP)**: assumes parallel/fan geometry and straight + rays too; no easier and less flexible than SART for a transmission ring. +- **Full-waveform inversion now**: the correct long-term answer and the + documented next step, but far heavier and premature before the contract and + metrics stabilise (ADR-0001). + +## References (to the real code) + +- `crates/sonic-ct/src/reconstruction.rs` (`sart`, `reconstruct_speed`, + `reconstruct_attenuation`, `ReconConfig`) +- `crates/sonic-ct/src/ray.rs` (supersampled straight-ray path integration) +- `crates/sonic-ct/src/types.rs` (`WATER_SPEED`) +- `crates/sonic-ct/src/metrics.rs` (`mae_speed`, `dice_all`) diff --git a/docs/sonic-ct/adr/ADR-0005-medical-claims-boundary.md b/docs/sonic-ct/adr/ADR-0005-medical-claims-boundary.md new file mode 100644 index 000000000..c94fef77f --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0005-medical-claims-boundary.md @@ -0,0 +1,64 @@ +# ADR-0005: Research/Simulation Only — No Diagnostic Claims + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +`sonic_ct` produces tissue maps, segmentations, and an "anomaly" flag that +superficially resemble clinical imaging outputs. It is a research-grade +simulator with no clinical validation, no regulatory clearance, and no real +acquisition path. Anything that reads like a diagnosis would be both wrong and +irresponsible. We need an explicit boundary that keeps the project firmly in the +research/simulation domain. + +## Decision + +`sonic_ct` is **research and simulation only and makes no diagnostic claims.** +This is enforced structurally, not just by disclaimer: + +- All inputs are synthetic. `Phantom::build` (`phantom.rs`) generates a + deterministic abdomen phantom (fat→muscle→organ shells + a spine bone) from a + SplitMix64 seed; there is no patient data path into the pipeline. +- The hardware boundary is a **mock**, not a device. `butterfly.rs` exposes + `MockButterflyEmbeddedBackend` whose `name()` is `"mock-butterfly-embedded"`; + there is **no public raw-hardware SDK**, so no real Butterfly acquisition + exists or is implied. +- Outputs are framed as algorithm quality, not findings. `metrics.rs` reports + Dice and MAE against ground truth; `memory.rs::check_coherence` produces a + `CoherenceReport` whose `anomaly` flag is an *anatomical-rule consistency* + signal over labels, not a clinical anomaly. +- Subject identifiers in `ScanRecord` are pseudonymous (`patient_id`) and the + code comments mandate "never raw PII"; no PHI flows anywhere. + +## Consequences + +### Positive + +- The "no diagnosis" stance is backed by the absence of any real-data or + real-hardware path, not just wording. +- Mock naming makes provenance unambiguous in logs and serialized scans. +- Pseudonymous-only identifiers keep the project clear of PHI handling. + +### Negative / Trade-offs + +- Synthetic-only inputs mean nothing here generalises to real anatomy without a + separate, validated effort. +- The `anomaly` flag may still be misread as clinical; naming and docs must keep + reinforcing the boundary. + +## Alternatives Considered + +- **Pursue clinical framing/validation now**: out of scope, unvalidated, and + legally hazardous for a simulator. +- **Drop anomaly/coherence outputs entirely**: loses a useful research signal; + better to keep it and label it precisely as a consistency check. + +## References (to the real code) + +- `crates/sonic-ct/src/phantom.rs` (synthetic `Phantom::build`) +- `crates/sonic-ct/src/butterfly.rs` (`MockButterflyEmbeddedBackend::name`) +- `crates/sonic-ct/src/memory.rs` (`check_coherence`, `CoherenceReport`, + pseudonymous `ScanRecord::patient_id`) +- `crates/sonic-ct/src/metrics.rs` (`QualityReport`) diff --git a/docs/sonic-ct/adr/ADR-0006-dicomweb-fhir-adapters.md b/docs/sonic-ct/adr/ADR-0006-dicomweb-fhir-adapters.md new file mode 100644 index 000000000..cbf8d3bf3 --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0006-dicomweb-fhir-adapters.md @@ -0,0 +1,62 @@ +# ADR-0006: DICOMweb / FHIR as Planned External Adapters + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +If `sonic_ct` ever interoperates with imaging infrastructure, the lingua franca +is DICOM/DICOMweb for pixel data and FHIR for clinical context. We want to +record the intended integration boundary so that internal representations stay +compatible with those standards — without pulling heavy DICOM/FHIR dependencies +into a zero-dependency core, and without pretending these adapters exist. This +is a **forward-looking** decision: **DICOMweb and FHIR are not yet implemented.** + +## Decision + +DICOMweb and FHIR support are **planned external adapters**, deliberately *not* +part of `crates/sonic-ct/`. The core stays zero-dependency (ADR-0001) and +standards-agnostic; any DICOM/FHIR mapping will live in a separate adapter crate +or service that translates to/from the core's existing structures: + +- A reconstructed scan is a `Scene` (`pipeline.rs`) with `Grid` speed/attenuation + maps and a `Segmentation` — these map cleanly to DICOM multi-frame pixel data + with a per-class segmentation overlay when an adapter is written. +- Provenance and identity already exist in a standards-friendly shape: + `ScanRecord` (`memory.rs`) carries a stable `id`, a pseudonymous `patient_id`, + a `timestamp`, and quality fields (`mean_dice`, `mae`) — the seeds of DICOM + patient/study/series identifiers and a FHIR `ImagingStudy`/`Observation`. +- The `.rvf`-style container (`to_bytes`/`from_bytes`) is the current portable + format; a DICOMweb adapter would sit beside it, not replace the core. + +No DICOM or FHIR code, types, or network calls exist in the repository today. + +## Consequences + +### Positive + +- The core stays small and dependency-free; standards complexity is quarantined + to a future adapter boundary. +- Existing structures (`Grid`, `Segmentation`, `ScanRecord`) already align with + the eventual mapping, reducing future rework. + +### Negative / Trade-offs + +- No interoperability today — scans cannot be exported to a PACS or FHIR server. +- The clean-mapping assumption is unverified until an adapter is actually built; + some fields (units, geometry, coding systems) may need adjustment then. + +## Alternatives Considered + +- **Build DICOMweb/FHIR into the core now**: violates zero-dependency and + research-only scope (ADR-0001, ADR-0005) for unbuilt interop. +- **Invent a bespoke interchange format only**: the `.rvf`-style container already + covers portability; standards adapters are the right external layer when needed. + +## References (to the real code) + +- `crates/sonic-ct/src/pipeline.rs` (`Scene` — the export source of truth) +- `crates/sonic-ct/src/grid.rs` (`Grid` — pixel-data analogue) +- `crates/sonic-ct/src/segmentation.rs` (`Segmentation` — overlay analogue) +- `crates/sonic-ct/src/memory.rs` (`ScanRecord`, `to_bytes`/`from_bytes`) diff --git a/docs/sonic-ct/adr/ADR-0007-uncertainty-first-ai.md b/docs/sonic-ct/adr/ADR-0007-uncertainty-first-ai.md new file mode 100644 index 000000000..1435cb918 --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0007-uncertainty-first-ai.md @@ -0,0 +1,67 @@ +# ADR-0007: Uncertainty-First, Auditable Segmentation + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +Reconstructed speed maps are noisy and blurred (ADR-0004), so any tissue +labelling will be wrong in places — especially near class boundaries and around +small structures. For a research tool that must stay honest (ADR-0005), an +opaque high-accuracy network that hides *where* it is unsure is the wrong +trade-off. We want every label to be explainable and to carry a calibrated-ish +confidence, so downstream consumers can down-weight uncertain regions. + +## Decision + +Segmentation is a transparent, auditable **speed-band classifier**, not a neural +network (`segmentation.rs`). `SegModel` is an ordered list of +`(upper_speed_bound, Tissue)` bands plus a `margin_scale`; `classify` assigns +the first band a speed falls under, so every label is explained by one threshold. + +Crucially, `segment` emits a **per-cell uncertainty** grid alongside labels: + +```rust +uncertainty = exp(-margin / margin_scale) +``` + +where `margin` is the distance (m/s) from the cell's speed to the nearest finite +band boundary. A cell sitting on a decision boundary gets uncertainty ≈ 1; a +cell deep inside a band approaches 0. Thresholds are learned by coordinate +ascent (`model.rs::train`, exposed as `SegModel::tuned()`), which lifts mean +Dice from ~0.30 (literature-default thresholds) to ~0.63 on the synthetic +corpus. The WASM demo ships `tuned()` so the live output reflects the trained +model, and surfaces the uncertainty grid to the UI. + +## Consequences + +### Positive + +- Every label is auditable: one band boundary explains it. +- Uncertainty is first-class and propagates to the WASM/UI layer, so unreliable + regions (boundaries, small bone) are visibly flagged. +- Training is interpretable (coordinate ascent over thresholds), with a measured + ~2× mean-Dice improvement. + +### Negative / Trade-offs + +- A 1-D speed-band model cannot use spatial context, so it cannot recover + structures the reconstruction has already blurred (bone Dice ~0). +- `exp(-margin/scale)` is a heuristic confidence, not a calibrated probability. +- Accuracy ceiling is bounded by reconstruction quality, not the classifier. + +## Alternatives Considered + +- **Opaque CNN segmenter**: potentially higher Dice, but unexplainable and + without honest per-cell uncertainty — contrary to ADR-0005. +- **Hard labels with no uncertainty**: simpler, but hides exactly the + near-boundary errors that matter most for an honest research tool. + +## References (to the real code) + +- `crates/sonic-ct/src/segmentation.rs` (`SegModel`, `classify`, + `boundary_margin`, `segment`, `Segmentation::uncertainty`) +- `crates/sonic-ct/src/model.rs` (`train` coordinate ascent, `evaluate`) +- `crates/sonic-ct/src/segmentation.rs` (`SegModel::default` vs `tuned`) +- `crates/sonic-ct-wasm/src/lib.rs` (`uncertainty` buffer exported to JS) diff --git a/docs/sonic-ct/adr/ADR-0008-gpu-later.md b/docs/sonic-ct/adr/ADR-0008-gpu-later.md new file mode 100644 index 000000000..d483ae2b4 --- /dev/null +++ b/docs/sonic-ct/adr/ADR-0008-gpu-later.md @@ -0,0 +1,59 @@ +# ADR-0008: CPU/WASM Now, GPU Later + +- Status: Accepted +- Date: 2026-06-21 +- Deciders: sonic_ct core team + +## Context + +USCT reconstruction is ultimately compute-bound, and the long-term roadmap +(full-waveform inversion, 3-D vertical sweeps) will need GPU acceleration to be +practical. But the immediate goals are correctness, reproducibility, and a tiny +in-browser demo. Reaching for GPU now would add dependencies, a toolchain, and +non-determinism before the algorithms and data contracts are stable. This is a +**forward-looking** decision: **no GPU backend exists yet.** + +## Decision + +`sonic_ct` runs on scalar CPU today and targets `wasm32-unknown-unknown` for the +demo. The WASM crate `crates/sonic-ct-wasm/` is a raw **C-ABI cdylib (no +wasm-bindgen)** that compiles to ~31 KB, exporting `sct_run` plus scalar getters +and `*_ptr` buffer accessors that JS reads directly from WebAssembly memory. The +core stays zero-dependency and free of SIMD/threading intrinsics, so the same +code runs natively and in the browser identically and deterministically. + +GPU is explicitly deferred. The structures that will benefit are already shaped +to make a later GPU path additive rather than a rewrite: the SART solver +(`reconstruction.rs`) is sparse ray-cell accumulation that maps naturally to a +parallel kernel, and `volume3d.rs::SweepPlan` is a **stub** describing a vertical +multi-slice sweep — the future workload that would justify GPU. When FWI and 3-D +sweeps land, a GPU backend can sit behind the existing solver/sweep interfaces. + +## Consequences + +### Positive + +- Tiny (~31 KB), dependency-free, deterministic artifact that runs anywhere a + WebAssembly runtime exists; no GPU/toolchain requirement to use or test. +- Native and WASM behaviour are identical, simplifying verification. +- The deferral keeps the door open: SART and `SweepPlan` are GPU-friendly shapes. + +### Negative / Trade-offs + +- Scalar CPU caps throughput; larger grids, more sweeps, and 3-D sweeps are slow. +- No SIMD even on native targets today, leaving performance on the table. +- `SweepPlan` is a stub — 3-D acquisition/reconstruction is not implemented. + +## Alternatives Considered + +- **GPU (wgpu/CUDA) now**: premature; adds heavy dependencies and + non-determinism before algorithms and the data contract stabilise (ADR-0001). +- **wasm-bindgen + SIMD demo**: larger artifact and more build surface than the + raw C-ABI cdylib needs for the current scalar pipeline. + +## References (to the real code) + +- `crates/sonic-ct-wasm/src/lib.rs` (`sct_run`, scalar getters, `*_ptr` + accessors; raw C-ABI, no wasm-bindgen) +- `crates/sonic-ct/src/reconstruction.rs` (`sart` — GPU-friendly sparse kernel) +- `crates/sonic-ct/src/volume3d.rs` (`SweepPlan` — vertical-sweep stub) diff --git a/docs/sonic-ct/screenshots/ui-overview.png b/docs/sonic-ct/screenshots/ui-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..a899b79650c4821e6b52fb02f39d357a64e54cb4 GIT binary patch literal 172128 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%0APyhWClG70`$sW8{m9spLQV75YK)5MbPSI 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