From d5aa37ff1a609a61f0ca9a1400862365f335125d Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Tue, 14 Jul 2026 11:44:11 -0500 Subject: [PATCH 1/7] add tools/testkit --- Cargo.toml | 2 +- crates/multicalc/Cargo.toml | 1 + tools/oracle/Cargo.toml | 1 + tools/oracle/src/load.rs | 2 +- tools/oracle/src/problems.rs | 168 ++------------------------------- tools/oracle/src/schema.rs | 9 ++ tools/testkit/Cargo.toml | 19 ++++ tools/testkit/src/lib.rs | 10 ++ tools/testkit/src/problems.rs | 171 ++++++++++++++++++++++++++++++++++ tools/testkit/src/tol.rs | 157 +++++++++++++++++++++++++++++++ 10 files changed, 377 insertions(+), 163 deletions(-) create mode 100644 tools/testkit/Cargo.toml create mode 100644 tools/testkit/src/lib.rs create mode 100644 tools/testkit/src/problems.rs create mode 100644 tools/testkit/src/tol.rs diff --git a/Cargo.toml b/Cargo.toml index 42d4d18..fb42b47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/multicalc", "tools/embedded-smoke", "tools/oracle", "showcase/viz"] +members = ["crates/multicalc", "tools/embedded-smoke", "tools/oracle", "tools/testkit", "showcase/viz"] default-members = ["crates/multicalc"] resolver = "3" diff --git a/crates/multicalc/Cargo.toml b/crates/multicalc/Cargo.toml index 74f01e4..48c99de 100644 --- a/crates/multicalc/Cargo.toml +++ b/crates/multicalc/Cargo.toml @@ -20,6 +20,7 @@ libm = "0.2" rand = "0.8" criterion = "0.5" proptest = "1.11" +multicalc-testkit = { path = "../../tools/testkit" } [features] default = [] diff --git a/tools/oracle/Cargo.toml b/tools/oracle/Cargo.toml index 37358cc..c653b25 100644 --- a/tools/oracle/Cargo.toml +++ b/tools/oracle/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] multicalc = { path = "../../crates/multicalc" } +multicalc-testkit = { path = "../testkit" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/tools/oracle/src/load.rs b/tools/oracle/src/load.rs index c36179c..3ea5516 100644 --- a/tools/oracle/src/load.rs +++ b/tools/oracle/src/load.rs @@ -74,7 +74,7 @@ pub fn to_vector_f32(v: &Value) -> Vector { /// True when `got` is within `t` of `want`, using a combined absolute and /// relative bound: `|got - want| <= abs + rel * max(|got|, |want|)`. pub fn close(got: f64, want: f64, t: Tol) -> bool { - (got - want).abs() <= t.abs + t.rel * got.abs().max(want.abs()) + multicalc_testkit::tol::close(got, want, t.into()) } /// Asserts a scalar matches the expected value within `t`. diff --git a/tools/oracle/src/problems.rs b/tools/oracle/src/problems.rs index 99e23c6..f2bb610 100644 --- a/tools/oracle/src/problems.rs +++ b/tools/oracle/src/problems.rs @@ -1,141 +1,14 @@ //! Named-problem registry. //! -//! Quadrature integrands and least-squares residuals are functions, so they -//! cannot live in a JSON fixture. Instead a fixture names a problem by a stable -//! string key, and both this module and the Python generator implement that key -//! with the identical formula. Adding a problem means adding it on both sides -//! under the same key. +//! The generic integrands and least-squares residuals live in `multicalc-testkit` +//! so the host oracle and the bare-metal smoke firmware share one definition; they +//! are re-exported here so fixtures and tests still name them under +//! `multicalc_oracle::problems`. The ODE right-hand sides stay local because they +//! carry the integrator's concrete `f64`/`Vector` signature. -use multicalc::linear_algebra::Vector; -use multicalc::scalar::{Numeric, VectorFn}; - -/// Returns the `f64` integrand for a quadrature key. Panics on an unknown key. -/// -/// Gauss-Hermite folds an `e^{-x^2}` weight and Gauss-Laguerre an `e^{-x}` weight -/// around this integrand; Legendre and the iterative rules integrate it directly. -pub fn integrand_f64(key: &str) -> fn(f64) -> f64 { - match key { - "two_x" => |x| 2.0 * x, - "quartic" => |x| 4.0 * x * x * x - 3.0 * x * x, - "cube" => |x| x * x * x, - "x_squared" => |x| x * x, - "inv_1px2" => |x| 1.0 / (1.0 + x * x), - "exp_neg" => |x| (-x).exp(), - other => unreachable!("unknown integrand key {other:?}"), - } -} - -/// Returns the `f32` integrand for a quadrature key. Panics on an unknown key. -pub fn integrand_f32(key: &str) -> fn(f32) -> f32 { - match key { - "two_x" => |x| 2.0 * x, - "quartic" => |x| 4.0 * x * x * x - 3.0 * x * x, - "cube" => |x| x * x * x, - "x_squared" => |x| x * x, - "inv_1px2" => |x| 1.0 / (1.0 + x * x), - "exp_neg" => |x| (-x).exp(), - other => unreachable!("unknown integrand key {other:?}"), - } -} - -/// Rosenbrock residual `[10*(x1 - x0^2), 1 - x0]`; the minimum is `x = [1, 1]`. -pub struct Rosenbrock; - -impl VectorFn<2, 2> for Rosenbrock { - fn eval(&self, x: &[S; 2]) -> [S; 2] { - [S::from_f64(10.0) * (x[1] - x[0] * x[0]), S::ONE - x[0]] - } -} - -/// Moré-Garbow-Hillstrom trigonometric function (problem 26) in six variables. -/// Its global minimum is zero. -pub struct Trigonometric6; - -impl VectorFn<6, 6> for Trigonometric6 { - fn eval(&self, x: &[S; 6]) -> [S; 6] { - let n = S::from_f64(6.0); - let mut cos_sum = S::ZERO; - for &xj in x { - cos_sum += xj.cos(); - } - core::array::from_fn(|i| { - n - cos_sum + S::from_f64((i + 1) as f64) * (S::ONE - x[i].cos()) - x[i].sin() - }) - } -} - -// Circle-fit target: 40 points sampled exactly on the circle of center (2, -1), -// radius 3. The same formula is mirrored in the Python generator. -const CIRCLE_POINTS: usize = 40; - -fn circle_px(i: usize) -> f64 { - let angle = std::f64::consts::TAU * i as f64 / CIRCLE_POINTS as f64; - 2.0 + 3.0 * angle.cos() -} - -fn circle_py(i: usize) -> f64 { - let angle = std::f64::consts::TAU * i as f64 / CIRCLE_POINTS as f64; - -1.0 + 3.0 * angle.sin() -} - -/// Fit a circle `[cx, cy, r]` to 40 fixed points, minimizing the geometric -/// distance residual `sqrt((x-cx)^2 + (y-cy)^2) - r`. The recovered geometry is -/// center `(2, -1)`, radius `3`. -pub struct CircleFit; +pub use multicalc_testkit::problems::*; -impl VectorFn<3, CIRCLE_POINTS> for CircleFit { - fn eval(&self, p: &[S; 3]) -> [S; CIRCLE_POINTS] { - let (cx, cy, r) = (p[0], p[1], p[2]); - core::array::from_fn(|i| { - let dx = S::from_f64(circle_px(i)) - cx; - let dy = S::from_f64(circle_py(i)) - cy; - (dx * dx + dy * dy).sqrt() - r - }) - } -} - -// Gaussian-peaks target: two Gaussians [a, mu, sigma] sampled at 50 points. -const GAUSS_POINTS: usize = 50; -const GAUSS_TRUTH: [f64; 6] = [2.0, 3.0, 0.8, 1.5, 7.0, 1.2]; - -fn gauss_t(i: usize) -> f64 { - i as f64 * 10.0 / (GAUSS_POINTS as f64 - 1.0) -} - -fn gauss_y(i: usize) -> f64 { - let t = gauss_t(i); - let mut y = 0.0; - for k in 0..2 { - let a = GAUSS_TRUTH[3 * k]; - let mu = GAUSS_TRUTH[3 * k + 1]; - let sigma = GAUSS_TRUTH[3 * k + 2]; - let z = (t - mu) / sigma; - y += a * (-(z * z)).exp(); - } - y -} - -/// Fit two Gaussian peaks `[a, mu, sigma]` to a spectrum sampled at 50 points. -/// The residual is `model(p) - y`, with `y` the two-peak signal at the true -/// parameters `[2, 3, 0.8, 1.5, 7, 1.2]`. -pub struct GaussianPeaks; - -impl VectorFn<6, GAUSS_POINTS> for GaussianPeaks { - fn eval(&self, p: &[S; 6]) -> [S; GAUSS_POINTS] { - core::array::from_fn(|i| { - let t = S::from_f64(gauss_t(i)); - let mut model = S::ZERO; - for k in 0..2 { - let a = p[3 * k]; - let mu = p[3 * k + 1]; - let sigma = p[3 * k + 2]; - let z = (t - mu) / sigma; - model += a * (-(z * z)).exp(); - } - model - S::from_f64(gauss_y(i)) - }) - } -} +use multicalc::linear_algebra::Vector; // ODE right-hand sides `y' = f(t, y)`, with the integrator's exact signature so the // oracle test can pass `&fn`. Each key is mirrored in the Python generator. @@ -162,30 +35,3 @@ pub fn ode_van_der_pol_mild(_t: f64, y: &Vector<2>) -> Vector<2> { let mu = 1.0; Vector::new([y[1], mu * (1.0 - y[0] * y[0]) * y[1] - y[0]]) } - -#[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - - use super::*; - - #[test] - fn integrands_evaluate() { - assert_eq!(integrand_f64("two_x")(3.0), 6.0); - assert_eq!(integrand_f64("x_squared")(4.0), 16.0); - assert_eq!(integrand_f32("cube")(2.0), 8.0); - } - - #[test] - fn residuals_vanish_at_the_solution() { - // Each problem is a zero-residual fit at its true parameters. - let r = Rosenbrock.eval(&[1.0, 1.0]); - assert!(r.iter().all(|v: &f64| v.abs() < 1e-12)); - - let c = CircleFit.eval(&[2.0, -1.0, 3.0]); - assert!(c.iter().all(|v: &f64| v.abs() < 1e-12)); - - let g = GaussianPeaks.eval(&GAUSS_TRUTH); - assert!(g.iter().all(|v: &f64| v.abs() < 1e-12)); - } -} diff --git a/tools/oracle/src/schema.rs b/tools/oracle/src/schema.rs index 176c046..96899fa 100644 --- a/tools/oracle/src/schema.rs +++ b/tools/oracle/src/schema.rs @@ -163,6 +163,15 @@ pub struct Tol { pub rel: f64, } +impl From for multicalc_testkit::tol::Tol { + fn from(t: Tol) -> Self { + multicalc_testkit::tol::Tol { + abs: t.abs, + rel: t.rel, + } + } +} + /// Per-`/` tolerance table, e.g. `"f64/host"` or `"f32/host"`. /// Reserved targets: `host`, `aarch64`, `thumbv7em-eabi`, `thumbv7em-eabihf`, /// `thumbv6m`. v0.7 populates only the `host` entries. diff --git a/tools/testkit/Cargo.toml b/tools/testkit/Cargo.toml new file mode 100644 index 0000000..72bac45 --- /dev/null +++ b/tools/testkit/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "multicalc-testkit" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +multicalc = { path = "../../crates/multicalc", default-features = false } + +# Test-support crate: assert helpers panic by design and the structural checkers +# unwrap on known-good factorizations, so opt out of the workspace no-panic lints +# (mirrors what tests/ and embedded-smoke already do). +[lints.rust] +unsafe_code = "forbid" +[lints.clippy] +unwrap_used = "allow" +expect_used = "allow" +panic = "allow" diff --git a/tools/testkit/src/lib.rs b/tools/testkit/src/lib.rs new file mode 100644 index 0000000..dd747e3 --- /dev/null +++ b/tools/testkit/src/lib.rs @@ -0,0 +1,10 @@ +//! Shared test support: tolerance helpers, structural checkers, and the named +//! problem registry, usable from host tests and the bare-metal smoke firmware. + +#![no_std] + +#[cfg(test)] +extern crate std; + +pub mod problems; +pub mod tol; diff --git a/tools/testkit/src/problems.rs b/tools/testkit/src/problems.rs new file mode 100644 index 0000000..0f677c8 --- /dev/null +++ b/tools/testkit/src/problems.rs @@ -0,0 +1,171 @@ +//! Named-problem registry. +//! +//! Quadrature integrands and least-squares residuals are functions, so they +//! cannot live in a JSON fixture. Instead a fixture names a problem by a stable +//! string key, and both this module and the Python generator implement that key +//! with the identical formula. Adding a problem means adding it on both sides +//! under the same key. + +use multicalc::scalar::{Numeric, ScalarFnN, VectorFn}; + +/// Returns the `f64` integrand for a quadrature key. Panics on an unknown key. +/// +/// Gauss-Hermite folds an `e^{-x^2}` weight and Gauss-Laguerre an `e^{-x}` weight +/// around this integrand; Legendre and the iterative rules integrate it directly. +pub fn integrand_f64(key: &str) -> fn(f64) -> f64 { + match key { + "two_x" => |x| 2.0 * x, + "quartic" => |x| 4.0 * x * x * x - 3.0 * x * x, + "cube" => |x| x * x * x, + "x_squared" => |x| x * x, + "inv_1px2" => |x| 1.0 / (1.0 + x * x), + "exp_neg" => |x| Numeric::exp(-x), + other => unreachable!("unknown integrand key {other:?}"), + } +} + +/// Returns the `f32` integrand for a quadrature key. Panics on an unknown key. +pub fn integrand_f32(key: &str) -> fn(f32) -> f32 { + match key { + "two_x" => |x| 2.0 * x, + "quartic" => |x| 4.0 * x * x * x - 3.0 * x * x, + "cube" => |x| x * x * x, + "x_squared" => |x| x * x, + "inv_1px2" => |x| 1.0 / (1.0 + x * x), + "exp_neg" => |x| Numeric::exp(-x), + other => unreachable!("unknown integrand key {other:?}"), + } +} + +/// Transcendental `g(x, y, z) = y·sin x + x·cos y + x·y·eᶻ`. +pub struct G; + +impl ScalarFnN<3> for G { + fn eval(&self, v: &[S; 3]) -> S { + v[1] * v[0].sin() + v[0] * v[1].cos() + v[0] * v[1] * v[2].exp() + } +} + +/// Rosenbrock residual `[10*(x1 - x0^2), 1 - x0]`; the minimum is `x = [1, 1]`. +pub struct Rosenbrock; + +impl VectorFn<2, 2> for Rosenbrock { + fn eval(&self, x: &[S; 2]) -> [S; 2] { + [S::from_f64(10.0) * (x[1] - x[0] * x[0]), S::ONE - x[0]] + } +} + +/// Moré-Garbow-Hillstrom trigonometric function (problem 26) in six variables. +/// Its global minimum is zero. +pub struct Trigonometric6; + +impl VectorFn<6, 6> for Trigonometric6 { + fn eval(&self, x: &[S; 6]) -> [S; 6] { + let n = S::from_f64(6.0); + let mut cos_sum = S::ZERO; + for &xj in x { + cos_sum += xj.cos(); + } + core::array::from_fn(|i| { + n - cos_sum + S::from_f64((i + 1) as f64) * (S::ONE - x[i].cos()) - x[i].sin() + }) + } +} + +// Circle-fit target: 40 points sampled exactly on the circle of center (2, -1), +// radius 3. The same formula is mirrored in the Python generator. +const CIRCLE_POINTS: usize = 40; + +fn circle_px(i: usize) -> f64 { + let angle = core::f64::consts::TAU * i as f64 / CIRCLE_POINTS as f64; + 2.0 + 3.0 * angle.cos() +} + +fn circle_py(i: usize) -> f64 { + let angle = core::f64::consts::TAU * i as f64 / CIRCLE_POINTS as f64; + -1.0 + 3.0 * angle.sin() +} + +/// Fit a circle `[cx, cy, r]` to 40 fixed points, minimizing the geometric +/// distance residual `sqrt((x-cx)^2 + (y-cy)^2) - r`. The recovered geometry is +/// center `(2, -1)`, radius `3`. +pub struct CircleFit; + +impl VectorFn<3, CIRCLE_POINTS> for CircleFit { + fn eval(&self, p: &[S; 3]) -> [S; CIRCLE_POINTS] { + let (cx, cy, r) = (p[0], p[1], p[2]); + core::array::from_fn(|i| { + let dx = S::from_f64(circle_px(i)) - cx; + let dy = S::from_f64(circle_py(i)) - cy; + (dx * dx + dy * dy).sqrt() - r + }) + } +} + +// Gaussian-peaks target: two Gaussians [a, mu, sigma] sampled at 50 points. +const GAUSS_POINTS: usize = 50; +const GAUSS_TRUTH: [f64; 6] = [2.0, 3.0, 0.8, 1.5, 7.0, 1.2]; + +fn gauss_t(i: usize) -> f64 { + i as f64 * 10.0 / (GAUSS_POINTS as f64 - 1.0) +} + +fn gauss_y(i: usize) -> f64 { + let t = gauss_t(i); + let mut y = 0.0; + for k in 0..2 { + let a = GAUSS_TRUTH[3 * k]; + let mu = GAUSS_TRUTH[3 * k + 1]; + let sigma = GAUSS_TRUTH[3 * k + 2]; + let z = (t - mu) / sigma; + y += a * Numeric::exp(-(z * z)); + } + y +} + +/// Fit two Gaussian peaks `[a, mu, sigma]` to a spectrum sampled at 50 points. +/// The residual is `model(p) - y`, with `y` the two-peak signal at the true +/// parameters `[2, 3, 0.8, 1.5, 7, 1.2]`. +pub struct GaussianPeaks; + +impl VectorFn<6, GAUSS_POINTS> for GaussianPeaks { + fn eval(&self, p: &[S; 6]) -> [S; GAUSS_POINTS] { + core::array::from_fn(|i| { + let t = S::from_f64(gauss_t(i)); + let mut model = S::ZERO; + for k in 0..2 { + let a = p[3 * k]; + let mu = p[3 * k + 1]; + let sigma = p[3 * k + 2]; + let z = (t - mu) / sigma; + model += a * (-(z * z)).exp(); + } + model - S::from_f64(gauss_y(i)) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn integrands_evaluate() { + assert_eq!(integrand_f64("two_x")(3.0), 6.0); + assert_eq!(integrand_f64("x_squared")(4.0), 16.0); + assert_eq!(integrand_f32("cube")(2.0), 8.0); + } + + #[test] + fn residuals_vanish_at_the_solution() { + // Each problem is a zero-residual fit at its true parameters. + let r = Rosenbrock.eval(&[1.0, 1.0]); + assert!(r.iter().all(|v: &f64| v.abs() < 1e-12)); + + let c = CircleFit.eval(&[2.0, -1.0, 3.0]); + assert!(c.iter().all(|v: &f64| v.abs() < 1e-12)); + + let g = GaussianPeaks.eval(&GAUSS_TRUTH); + assert!(g.iter().all(|v: &f64| v.abs() < 1e-12)); + } +} diff --git a/tools/testkit/src/tol.rs b/tools/testkit/src/tol.rs new file mode 100644 index 0000000..2be7f73 --- /dev/null +++ b/tools/testkit/src/tol.rs @@ -0,0 +1,157 @@ +//! Closeness helpers in two contracts: an abs+rel `Tol` bound for scalars and +//! vectors, and an absolute per-entry bound for the linear-algebra structural +//! checkers. `Numeric` is in scope so `.abs()`/`.max()` resolve to `libm` and +//! compile on bare metal. + +use multicalc::linear_algebra::{Matrix, Vector}; +use multicalc::scalar::Numeric; + +/// Absolute and relative thresholds for one comparison. +#[derive(Clone, Copy, Debug)] +pub struct Tol { + pub abs: f64, + pub rel: f64, +} + +/// True when `got` is within `t` of `want`, using a combined absolute and +/// relative bound: `|got - want| <= abs + rel * max(|got|, |want|)`. +pub fn close(got: f64, want: f64, t: Tol) -> bool { + (got - want).abs() <= t.abs + t.rel * got.abs().max(want.abs()) +} + +/// Asserts a scalar matches the expected value within `t`. +pub fn assert_scalar_close(got: f64, want: f64, t: Tol) { + assert!(close(got, want, t), "got {got}, want {want}, tol {t:?}"); +} + +/// Asserts every component of a vector matches within `t`. +pub fn assert_vector_close(got: &Vector, want: &Vector, t: Tol) { + for i in 0..N { + assert!( + close(got[i], want[i], t), + "[{i}]: got {}, want {}, tol {t:?}", + got[i], + want[i] + ); + } +} + +/// Asserts two matrices agree entrywise within an absolute `tol`. +pub fn assert_matrix_close( + actual: Matrix, + expected: Matrix, + tol: T, +) { + for r in 0..R { + for c in 0..C { + assert!((actual[(r, c)] - expected[(r, c)]).abs() < tol); + } + } +} + +/// Asserts every entry of `m` is within `tol` of the identity matrix. +pub fn assert_identity(m: Matrix, tol: T) { + assert_matrix_close(m, Matrix::identity(), tol); +} + +/// Factorizes `a`, checks the factors are triangular, and that they reconstruct `P·A`. +pub fn lu_reconstructs(a: Matrix, tol: T) { + let f = a.lu().unwrap(); + let l = f.l(); + let u = f.u(); + let perm = f.permutation(); + + // L is unit lower-triangular; U is upper-triangular. + for r in 0..N { + assert_eq!(l[(r, r)], T::ONE); + for c in (r + 1)..N { + assert_eq!(l[(r, c)], T::ZERO); + } + for c in 0..r { + assert_eq!(u[(r, c)], T::ZERO); + } + } + + let pa = Matrix::::from_fn(|i, c| a[(perm[i], c)]); + assert_matrix_close(l * u, pa, tol); +} + +/// Checks the Cholesky factor is lower-triangular with a positive diagonal and reconstructs `A`. +pub fn cholesky_reconstructs(a: Matrix, tol: T) { + let l = a.cholesky().unwrap().l(); + for r in 0..N { + assert!(l[(r, r)] > T::ZERO); + for c in (r + 1)..N { + assert_eq!(l[(r, c)], T::ZERO); + } + } + assert_matrix_close(l * l.transpose(), a, tol); +} + +/// Checks the singular values are ordered and that `U·diag(σ)·Vᵀ` reconstructs `A`. +pub fn svd_reconstructs(a: Matrix, tol: T) { + let f = a.svd().unwrap(); + let (u, s, v) = (f.u(), f.singular_values(), f.v()); + + for k in 0..N { + assert!(s[k] >= T::ZERO); + if k + 1 < N { + assert!(s[k] >= s[k + 1]); + } + } + + assert_identity(u.transpose() * u, tol); + assert_identity(v.transpose() * v, tol); + + let recon = Matrix::::from_fn(|r, c| { + let mut acc = T::ZERO; + for k in 0..N { + acc += u[(r, k)] * s[k] * v[(c, k)]; + } + acc + }); + assert_matrix_close(recon, a, tol); +} + +/// Verifies the four Moore–Penrose conditions for the pseudo-inverse of `a`. +pub fn svd_moore_penrose(a: Matrix, tol: T) { + let ap = a.pseudo_inverse().unwrap(); + assert_matrix_close(a * ap * a, a, tol); + assert_matrix_close(ap * a * ap, ap, tol); + let aap = a * ap; + assert_matrix_close(aap, aap.transpose(), tol); + let apa = ap * a; + assert_matrix_close(apa, apa.transpose(), tol); +} + +fn max_abs(m: Matrix) -> f32 { + let mut max = 0.0_f32; + for r in 0..R { + for c in 0..C { + max = max.max(m[(r, c)].abs()); + } + } + max +} + +fn f32_scaled_tol(scale: f32, dim: usize) -> f32 { + 512.0 * f32::EPSILON * dim as f32 * scale.max(1.0) +} + +/// Verifies the four Moore-Penrose conditions for an f32 pseudo-inverse with +/// tolerances scaled by matrix magnitude and dimension. +pub fn svd_moore_penrose_f32(a: Matrix) { + let ap = a.pseudo_inverse().unwrap(); + + let aap_a = a * ap * a; + assert_matrix_close(aap_a, a, f32_scaled_tol(max_abs(a), M.max(N))); + + let apa_ap = ap * a * ap; + assert_matrix_close(apa_ap, ap, f32_scaled_tol(max_abs(ap), M.max(N))); + + let aap = a * ap; + assert_matrix_close(aap, aap.transpose(), f32_scaled_tol(max_abs(aap), M)); + + let apa = ap * a; + assert_matrix_close(apa, apa.transpose(), f32_scaled_tol(max_abs(apa), N)); +} From dda03af18faa2f4f94debbdaabccc2f79df42bb6 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Tue, 14 Jul 2026 11:48:13 -0500 Subject: [PATCH 2/7] modify embedded-smoke --- tools/embedded-smoke/Cargo.toml | 1 + tools/embedded-smoke/README.md | 62 +++++++++++++++--------------- tools/embedded-smoke/src/checks.rs | 29 ++++++-------- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/tools/embedded-smoke/Cargo.toml b/tools/embedded-smoke/Cargo.toml index 7a5d4a3..aa4da7d 100644 --- a/tools/embedded-smoke/Cargo.toml +++ b/tools/embedded-smoke/Cargo.toml @@ -13,6 +13,7 @@ full-smoke = [] [dependencies] multicalc = { path = "../../crates/multicalc", default-features = false } +multicalc-testkit = { path = "../testkit" } cortex-m-rt = "0.7" cortex-m-semihosting = "0.5" # With the "exit" feature a panic prints its message and exits QEMU with a failure code. diff --git a/tools/embedded-smoke/README.md b/tools/embedded-smoke/README.md index 20b04fa..0cfe7cf 100644 --- a/tools/embedded-smoke/README.md +++ b/tools/embedded-smoke/README.md @@ -13,6 +13,37 @@ ABIs under QEMU. It is a dev-only crate (`publish = false`, not in `default-members`) and is never built for a host target, and `cortex-m-rt` only links for the `thumb*` triples. +## Running + +```sh +rustup target add thumbv7em-none-eabi thumbv7em-none-eabihf thumbv6m-none-eabi +sudo apt-get install -y qemu-system-arm # provides qemu-system-arm +cargo install cargo-binutils # provides cargo size, for the gate + +cargo run -p embedded-smoke --release --target thumbv7em-none-eabi +cargo run -p embedded-smoke --release --target thumbv7em-none-eabihf +cargo run -p embedded-smoke --release --target thumbv6m-none-eabi +``` + +Aliases: `cargo smoke-eabi`, `cargo smoke-eabihf`, `cargo smoke-m0`. + +## Targets and QEMU machine + +| Target | Codegen | QEMU machine | RAM | +|-------------------------|------------------------|----------------|------| +| `thumbv7em-none-eabi` | Cortex-M4, soft-float | `netduinoplus2`| 64K | +| `thumbv7em-none-eabihf` | Cortex-M4, hard-float | `netduinoplus2`| 64K | +| `thumbv6m-none-eabi` | Cortex-M0, CAS-free | `microbit` | 16K | + +The `thumbv7em` ABIs run on `netduinoplus2` (Cortex-M4, FPU, 64K RAM, flash at +`0x08000000`). `thumbv6m` runs on `microbit` (Cortex-M0, nRF51, 16K RAM, flash +at `0x0`) — a real M0 core, so the run now asserts both RAM-size and ISA +fidelity: an oversized image or an out-of-ISA (ARMv7E-M) instruction faults just +as it would on silicon. `build.rs` picks each target's memory map. + +The runners and `rustflags` (`-Tlink.x`, `--nmagic`) live in +`.cargo/config.toml`; the per-target memory map is supplied by `build.rs`. + ## Why a separate crate, not tests inside `multicalc` These smoke tests cannot live next to the code they check. This is a full @@ -55,23 +86,6 @@ The binary runs under QEMU semihosting: > Note: `cargo run … | tee` masks the exit code unless `pipefail` is set. CI runs steps under `bash -eo pipefail`, so a panic still fails the job. -## Targets and QEMU machine - -| Target | Codegen | QEMU machine | RAM | -|-------------------------|------------------------|----------------|------| -| `thumbv7em-none-eabi` | Cortex-M4, soft-float | `netduinoplus2`| 64K | -| `thumbv7em-none-eabihf` | Cortex-M4, hard-float | `netduinoplus2`| 64K | -| `thumbv6m-none-eabi` | Cortex-M0, CAS-free | `microbit` | 16K | - -The `thumbv7em` ABIs run on `netduinoplus2` (Cortex-M4, FPU, 64K RAM, flash at -`0x08000000`). `thumbv6m` runs on `microbit` (Cortex-M0, nRF51, 16K RAM, flash -at `0x0`) — a real M0 core, so the run now asserts both RAM-size and ISA -fidelity: an oversized image or an out-of-ISA (ARMv7E-M) instruction faults just -as it would on silicon. `build.rs` picks each target's memory map. - -The runners and `rustflags` (`-Tlink.x`, `--nmagic`) live in -`.cargo/config.toml`; the per-target memory map is supplied by `build.rs`. - ## Stack high-water mark `main.rs` measures peak stack by painting free stack below the entry frame with @@ -89,20 +103,6 @@ This is a fixed-window scan below the SP, so it needs no linker symbol and is identical across machines. If a target cannot yield a stable number, drop its `STACK_HWM_BYTES` line and leave that ABI `.text`-gated only. -## Running - -```sh -rustup target add thumbv7em-none-eabi thumbv7em-none-eabihf thumbv6m-none-eabi -sudo apt-get install -y qemu-system-arm # provides qemu-system-arm -cargo install cargo-binutils # provides cargo size, for the gate - -cargo run -p embedded-smoke --release --target thumbv7em-none-eabi -cargo run -p embedded-smoke --release --target thumbv7em-none-eabihf -cargo run -p embedded-smoke --release --target thumbv6m-none-eabi -``` - -Aliases: `cargo smoke-eabi`, `cargo smoke-eabihf`, `cargo smoke-m0`. - ## Size and stack gate `ci/budgets.toml` holds per-target `.text` and stack budgets with a shared diff --git a/tools/embedded-smoke/src/checks.rs b/tools/embedded-smoke/src/checks.rs index b63fd8e..ea7c1cd 100644 --- a/tools/embedded-smoke/src/checks.rs +++ b/tools/embedded-smoke/src/checks.rs @@ -11,29 +11,18 @@ use multicalc::error::LinalgError; use multicalc::linear_algebra::Matrix; use multicalc::numerical_derivative::autodiff::{AutoDiffMulti, AutoDiffSingle}; use multicalc::numerical_derivative::derivator::DerivatorSingleVariable; -use multicalc::scalar::{Numeric, VectorFn}; +use multicalc::scalar::Numeric; use multicalc::scalar_fn; +use multicalc_testkit::problems::Rosenbrock; +use multicalc_testkit::tol::{Tol, close}; use crate::fixtures; -/// Combined absolute/relative closeness, matching `close` in -/// tools/oracle/src/load.rs: `|got - want| <= abs + rel * max(|got|, |want|)`. -fn close(got: f64, want: f64, abs: f64, rel: f64) -> bool { - (got - want).abs() <= abs + rel * got.abs().max(want.abs()) -} - /// Golden: the Rosenbrock least-squares minimizer must match the host oracle /// golden (optimization/rosenbrock). Residuals `[10 (y - x^2), 1 - x]` are zero /// at the minimum `(1, 1)`. Part of the full set only (thumbv7em). #[cfg_attr(not(feature = "full-smoke"), allow(dead_code))] pub fn lm_fit() { - struct Rosenbrock; - impl VectorFn<2, 2> for Rosenbrock { - fn eval(&self, p: &[S; 2]) -> [S; 2] { - let (x, y) = (p[0], p[1]); - [S::from_f64(10.0) * (y - x * x), S::from_f64(1.0) - x] - } - } let solver = LevenbergMarquardt::::default().with_patience(100); let report = solver .minimize(&Rosenbrock, &fixtures::ROSENBROCK_X0) @@ -42,8 +31,10 @@ pub fn lm_fit() { assert!(close( report.solution[i], fixtures::ROSENBROCK_SOLUTION[i], - fixtures::ROSENBROCK_ABS, - fixtures::ROSENBROCK_REL, + Tol { + abs: fixtures::ROSENBROCK_ABS, + rel: fixtures::ROSENBROCK_REL, + }, )); } } @@ -91,8 +82,10 @@ pub fn svd_golden() -> [f64; 3] { assert!(close( sv[i], fixtures::SVD_3X3_SINGULAR_VALUES[i], - fixtures::SVD_3X3_ABS, - fixtures::SVD_3X3_REL, + Tol { + abs: fixtures::SVD_3X3_ABS, + rel: fixtures::SVD_3X3_REL, + }, )); } [sv[0], sv[1], sv[2]] From 6a6f910f46e84dabc34b2feae96afdfc5a36f96f Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Tue, 14 Jul 2026 11:57:16 -0500 Subject: [PATCH 3/7] Consolidate tests/ into one binary --- crates/multicalc/tests/linear_algebra.rs | 23 ---- .../multicalc/tests/linear_algebra/helpers.rs | 130 ------------------ crates/multicalc/tests/ode.rs | 7 - crates/multicalc/tests/spatial.rs | 12 -- .../tests/{ => suite}/approximation.rs | 0 .../tests/{ => suite}/discretization.rs | 0 .../tests/{ => suite}/gaussian_tables.rs | 0 .../{ => suite}/linear_algebra/cholesky.rs | 8 +- .../tests/{ => suite}/linear_algebra/lu.rs | 6 +- .../{ => suite}/linear_algebra/macros.rs | 0 .../{ => suite}/linear_algebra/matrix.rs | 6 +- .../tests/suite/linear_algebra/mod.rs | 7 + .../tests/{ => suite}/linear_algebra/qr.rs | 4 +- .../tests/{ => suite}/linear_algebra/svd.rs | 13 +- .../{ => suite}/linear_algebra/vector.rs | 0 crates/multicalc/tests/suite/main.rs | 14 ++ .../tests/{ => suite}/numerical_derivative.rs | 9 +- .../{ => suite}/numerical_integration.rs | 0 crates/multicalc/tests/suite/ode/mod.rs | 2 + crates/multicalc/tests/{ => suite}/ode/rk4.rs | 0 .../multicalc/tests/{ => suite}/ode/rk45.rs | 0 .../tests/{ => suite}/optimization.rs | 13 +- .../tests/{ => suite}/root_finding.rs | 0 crates/multicalc/tests/{ => suite}/scalar.rs | 0 .../tests/{ => suite}/spatial/lie.rs | 0 crates/multicalc/tests/suite/spatial/mod.rs | 3 + .../tests/{ => suite}/spatial/quaternion.rs | 0 .../tests/{ => suite}/spatial/twist_wrench.rs | 0 crates/multicalc/tests/{ => suite}/utils.rs | 0 .../tests/{ => suite}/vector_field.rs | 0 30 files changed, 55 insertions(+), 202 deletions(-) delete mode 100644 crates/multicalc/tests/linear_algebra.rs delete mode 100644 crates/multicalc/tests/linear_algebra/helpers.rs delete mode 100644 crates/multicalc/tests/ode.rs delete mode 100644 crates/multicalc/tests/spatial.rs rename crates/multicalc/tests/{ => suite}/approximation.rs (100%) rename crates/multicalc/tests/{ => suite}/discretization.rs (100%) rename crates/multicalc/tests/{ => suite}/gaussian_tables.rs (100%) rename crates/multicalc/tests/{ => suite}/linear_algebra/cholesky.rs (93%) rename crates/multicalc/tests/{ => suite}/linear_algebra/lu.rs (96%) rename crates/multicalc/tests/{ => suite}/linear_algebra/macros.rs (100%) rename crates/multicalc/tests/{ => suite}/linear_algebra/matrix.rs (98%) create mode 100644 crates/multicalc/tests/suite/linear_algebra/mod.rs rename crates/multicalc/tests/{ => suite}/linear_algebra/qr.rs (98%) rename crates/multicalc/tests/{ => suite}/linear_algebra/svd.rs (97%) rename crates/multicalc/tests/{ => suite}/linear_algebra/vector.rs (100%) create mode 100644 crates/multicalc/tests/suite/main.rs rename crates/multicalc/tests/{ => suite}/numerical_derivative.rs (98%) rename crates/multicalc/tests/{ => suite}/numerical_integration.rs (100%) create mode 100644 crates/multicalc/tests/suite/ode/mod.rs rename crates/multicalc/tests/{ => suite}/ode/rk4.rs (100%) rename crates/multicalc/tests/{ => suite}/ode/rk45.rs (100%) rename crates/multicalc/tests/{ => suite}/optimization.rs (96%) rename crates/multicalc/tests/{ => suite}/root_finding.rs (100%) rename crates/multicalc/tests/{ => suite}/scalar.rs (100%) rename crates/multicalc/tests/{ => suite}/spatial/lie.rs (100%) create mode 100644 crates/multicalc/tests/suite/spatial/mod.rs rename crates/multicalc/tests/{ => suite}/spatial/quaternion.rs (100%) rename crates/multicalc/tests/{ => suite}/spatial/twist_wrench.rs (100%) rename crates/multicalc/tests/{ => suite}/utils.rs (100%) rename crates/multicalc/tests/{ => suite}/vector_field.rs (100%) diff --git a/crates/multicalc/tests/linear_algebra.rs b/crates/multicalc/tests/linear_algebra.rs deleted file mode 100644 index 7fe8009..0000000 --- a/crates/multicalc/tests/linear_algebra.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Linear algebra integration tests, split by topic. Shared helpers live in `helpers`; the -//! topic modules are kept in the `linear_algebra/` subdirectory so they form one test binary -//! rather than one per file. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -#[path = "linear_algebra/helpers.rs"] -mod helpers; - -#[path = "linear_algebra/cholesky.rs"] -mod cholesky; -#[path = "linear_algebra/lu.rs"] -mod lu; -#[path = "linear_algebra/macros.rs"] -mod macros; -#[path = "linear_algebra/matrix.rs"] -mod matrix; -#[path = "linear_algebra/qr.rs"] -mod qr; -#[path = "linear_algebra/svd.rs"] -mod svd; -#[path = "linear_algebra/vector.rs"] -mod vector; diff --git a/crates/multicalc/tests/linear_algebra/helpers.rs b/crates/multicalc/tests/linear_algebra/helpers.rs deleted file mode 100644 index 7100c4a..0000000 --- a/crates/multicalc/tests/linear_algebra/helpers.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Shared helpers for the linear algebra test suite. - -use multicalc::linear_algebra::Matrix; -use multicalc::scalar::Numeric; - -/// Asserts two matrices agree entrywise within `tol`. -pub(crate) fn assert_close( - actual: Matrix, - expected: Matrix, - tol: T, -) { - for r in 0..R { - for c in 0..C { - assert!((actual[(r, c)] - expected[(r, c)]).abs() < tol); - } - } -} - -/// Asserts every entry of `m` is within `tol` of the identity matrix. -pub(crate) fn assert_identity(m: Matrix, tol: T) { - assert_close(m, Matrix::identity(), tol); -} - -/// Factorizes `a`, checks the factors are triangular, and that they reconstruct `P·A`. -pub(crate) fn lu_reconstructs(a: Matrix, tol: T) { - let f = a.lu().unwrap(); - let l = f.l(); - let u = f.u(); - let perm = f.permutation(); - - // L is unit lower-triangular; U is upper-triangular. - for r in 0..N { - assert_eq!(l[(r, r)], T::ONE); - for c in (r + 1)..N { - assert_eq!(l[(r, c)], T::ZERO); - } - for c in 0..r { - assert_eq!(u[(r, c)], T::ZERO); - } - } - - let pa = Matrix::::from_fn(|i, c| a[(perm[i], c)]); - assert_close(l * u, pa, tol); -} - -/// Checks the Cholesky factor is lower-triangular with a positive diagonal and reconstructs `A`. -pub(crate) fn cholesky_reconstructs(a: Matrix, tol: T) { - let l = a.cholesky().unwrap().l(); - for r in 0..N { - assert!(l[(r, r)] > T::ZERO); - for c in (r + 1)..N { - assert_eq!(l[(r, c)], T::ZERO); - } - } - assert_close(l * l.transpose(), a, tol); -} - -/// Checks the singular values are ordered and that `U·diag(σ)·Vᵀ` reconstructs `A`. -pub(crate) fn svd_reconstructs( - a: Matrix, - tol: T, -) { - let f = a.svd().unwrap(); - let (u, s, v) = (f.u(), f.singular_values(), f.v()); - - for k in 0..N { - assert!(s[k] >= T::ZERO); - if k + 1 < N { - assert!(s[k] >= s[k + 1]); - } - } - - assert_identity(u.transpose() * u, tol); - assert_identity(v.transpose() * v, tol); - - let recon = Matrix::::from_fn(|r, c| { - let mut acc = T::ZERO; - for k in 0..N { - acc += u[(r, k)] * s[k] * v[(c, k)]; - } - acc - }); - assert_close(recon, a, tol); -} - -/// Verifies the four Moore–Penrose conditions for the pseudo-inverse of `a`. -pub(crate) fn svd_moore_penrose( - a: Matrix, - tol: T, -) { - let ap = a.pseudo_inverse().unwrap(); - assert_close(a * ap * a, a, tol); - assert_close(ap * a * ap, ap, tol); - let aap = a * ap; - assert_close(aap, aap.transpose(), tol); - let apa = ap * a; - assert_close(apa, apa.transpose(), tol); -} - -fn max_abs(m: Matrix) -> f32 { - let mut max = 0.0_f32; - for r in 0..R { - for c in 0..C { - max = max.max(m[(r, c)].abs()); - } - } - max -} - -fn f32_scaled_tol(scale: f32, dim: usize) -> f32 { - 512.0 * f32::EPSILON * dim as f32 * scale.max(1.0) -} - -/// Verifies the four Moore-Penrose conditions for an f32 pseudo-inverse with -/// tolerances scaled by matrix magnitude and dimension. -pub(crate) fn svd_moore_penrose_f32(a: Matrix) { - let ap = a.pseudo_inverse().unwrap(); - - let aap_a = a * ap * a; - assert_close(aap_a, a, f32_scaled_tol(max_abs(a), M.max(N))); - - let apa_ap = ap * a * ap; - assert_close(apa_ap, ap, f32_scaled_tol(max_abs(ap), M.max(N))); - - let aap = a * ap; - assert_close(aap, aap.transpose(), f32_scaled_tol(max_abs(aap), M)); - - let apa = ap * a; - assert_close(apa, apa.transpose(), f32_scaled_tol(max_abs(apa), N)); -} diff --git a/crates/multicalc/tests/ode.rs b/crates/multicalc/tests/ode.rs deleted file mode 100644 index 4abb24a..0000000 --- a/crates/multicalc/tests/ode.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! ODE integrator integration tests. -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -#[path = "ode/rk4.rs"] -mod rk4; -#[path = "ode/rk45.rs"] -mod rk45; diff --git a/crates/multicalc/tests/spatial.rs b/crates/multicalc/tests/spatial.rs deleted file mode 100644 index a9cc7e5..0000000 --- a/crates/multicalc/tests/spatial.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Spatial-math integration tests. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -#[path = "spatial/quaternion.rs"] -mod quaternion; - -#[path = "spatial/lie.rs"] -mod lie; - -#[path = "spatial/twist_wrench.rs"] -mod twist_wrench; diff --git a/crates/multicalc/tests/approximation.rs b/crates/multicalc/tests/suite/approximation.rs similarity index 100% rename from crates/multicalc/tests/approximation.rs rename to crates/multicalc/tests/suite/approximation.rs diff --git a/crates/multicalc/tests/discretization.rs b/crates/multicalc/tests/suite/discretization.rs similarity index 100% rename from crates/multicalc/tests/discretization.rs rename to crates/multicalc/tests/suite/discretization.rs diff --git a/crates/multicalc/tests/gaussian_tables.rs b/crates/multicalc/tests/suite/gaussian_tables.rs similarity index 100% rename from crates/multicalc/tests/gaussian_tables.rs rename to crates/multicalc/tests/suite/gaussian_tables.rs diff --git a/crates/multicalc/tests/linear_algebra/cholesky.rs b/crates/multicalc/tests/suite/linear_algebra/cholesky.rs similarity index 93% rename from crates/multicalc/tests/linear_algebra/cholesky.rs rename to crates/multicalc/tests/suite/linear_algebra/cholesky.rs index efcbbfd..100688a 100644 --- a/crates/multicalc/tests/linear_algebra/cholesky.rs +++ b/crates/multicalc/tests/suite/linear_algebra/cholesky.rs @@ -1,6 +1,6 @@ -use crate::helpers::{assert_close, assert_identity, cholesky_reconstructs}; use multicalc::error::LinalgError; use multicalc::linear_algebra::{Matrix, Vector}; +use multicalc_testkit::tol::{assert_identity, assert_matrix_close, cholesky_reconstructs}; #[test] fn cholesky_reconstructs_spd() { @@ -13,7 +13,7 @@ fn cholesky_reconstructs_spd() { [-16.0, -43.0, 98.0], ]); cholesky_reconstructs(a, 1e-12); - assert_close( + assert_matrix_close( a.cholesky().unwrap().l(), Matrix::new([[2.0, 0.0, 0.0], [6.0, 1.0, 0.0], [-8.0, 5.0, 3.0]]), 1e-12, @@ -84,7 +84,7 @@ fn cholesky_solves() { let f = s.cholesky().unwrap(); let rhs = Matrix::<2, 3>::new([[8.0, 6.0, 4.0], [8.0, 5.0, 3.0]]); let xm = f.solve_matrix(rhs); - assert_close(s * xm, rhs, 1e-12); + assert_matrix_close(s * xm, rhs, 1e-12); for c in 0..3 { let single = f.solve(rhs.column(c)); for r in 0..2 { @@ -113,5 +113,5 @@ fn cholesky_inverse_matches_lu() { let inv = a.cholesky().unwrap().inverse(); assert_identity(inv * a, 1e-12); assert_identity(a * inv, 1e-12); - assert_close(inv, a.lu().unwrap().inverse(), 1e-12); + assert_matrix_close(inv, a.lu().unwrap().inverse(), 1e-12); } diff --git a/crates/multicalc/tests/linear_algebra/lu.rs b/crates/multicalc/tests/suite/linear_algebra/lu.rs similarity index 96% rename from crates/multicalc/tests/linear_algebra/lu.rs rename to crates/multicalc/tests/suite/linear_algebra/lu.rs index 15aec19..f3c9855 100644 --- a/crates/multicalc/tests/linear_algebra/lu.rs +++ b/crates/multicalc/tests/suite/linear_algebra/lu.rs @@ -1,6 +1,6 @@ -use crate::helpers::{assert_close, assert_identity, lu_reconstructs}; use multicalc::error::LinalgError; use multicalc::linear_algebra::{Matrix, Vector}; +use multicalc_testkit::tol::{assert_identity, assert_matrix_close, lu_reconstructs}; // ----- LU decomposition (Doolittle, partial pivoting) ----- @@ -70,7 +70,7 @@ fn lu_solves() { // Multiple RHS: A·X == B, and each column agrees with a single-RHS solve. let rhs = Matrix::<3, 2>::new([[7.0, 4.0], [19.0, 10.0], [49.0, 26.0]]); let xm = f.solve_matrix(rhs); - assert_close(a * xm, rhs, 1e-12); + assert_matrix_close(a * xm, rhs, 1e-12); for c in 0..2 { let single = f.solve(rhs.column(c)); for r in 0..3 { @@ -93,7 +93,7 @@ fn lu_inverse_matches_reference_5x5() { assert!((a.lu().unwrap().determinant() - 10406.0).abs() < 1e-9); let inv = a.lu().unwrap().inverse(); - assert_close( + assert_matrix_close( inv, Matrix::new([ [ diff --git a/crates/multicalc/tests/linear_algebra/macros.rs b/crates/multicalc/tests/suite/linear_algebra/macros.rs similarity index 100% rename from crates/multicalc/tests/linear_algebra/macros.rs rename to crates/multicalc/tests/suite/linear_algebra/macros.rs diff --git a/crates/multicalc/tests/linear_algebra/matrix.rs b/crates/multicalc/tests/suite/linear_algebra/matrix.rs similarity index 98% rename from crates/multicalc/tests/linear_algebra/matrix.rs rename to crates/multicalc/tests/suite/linear_algebra/matrix.rs index 01e0f5d..6e200c4 100644 --- a/crates/multicalc/tests/linear_algebra/matrix.rs +++ b/crates/multicalc/tests/suite/linear_algebra/matrix.rs @@ -1,6 +1,6 @@ -use crate::helpers::{assert_close, assert_identity}; use multicalc::error::LinalgError; use multicalc::linear_algebra::{Matrix, Vector}; +use multicalc_testkit::tol::{assert_identity, assert_matrix_close}; // ----- matrix arithmetic, multiply, transpose ----- @@ -113,7 +113,7 @@ fn matrix_4x4_determinant_and_inverse() { assert_eq!(a.determinant(), 20.0); let inv = a.inverse().unwrap(); - assert_close( + assert_matrix_close( inv, Matrix::new([ [0.6, -0.5, 0.0, 0.1], @@ -136,7 +136,7 @@ fn matrix_4x4_determinant_and_inverse() { assert_eq!(b.determinant(), -20.0); let b_inv = b.inverse().unwrap(); - assert_close( + assert_matrix_close( b_inv, Matrix::new([ [-0.15, 0.45, -0.05, 0.25], diff --git a/crates/multicalc/tests/suite/linear_algebra/mod.rs b/crates/multicalc/tests/suite/linear_algebra/mod.rs new file mode 100644 index 0000000..d785780 --- /dev/null +++ b/crates/multicalc/tests/suite/linear_algebra/mod.rs @@ -0,0 +1,7 @@ +mod cholesky; +mod lu; +mod macros; +mod matrix; +mod qr; +mod svd; +mod vector; diff --git a/crates/multicalc/tests/linear_algebra/qr.rs b/crates/multicalc/tests/suite/linear_algebra/qr.rs similarity index 98% rename from crates/multicalc/tests/linear_algebra/qr.rs rename to crates/multicalc/tests/suite/linear_algebra/qr.rs index ceab57e..cac0678 100644 --- a/crates/multicalc/tests/linear_algebra/qr.rs +++ b/crates/multicalc/tests/suite/linear_algebra/qr.rs @@ -1,6 +1,6 @@ -use crate::helpers::{assert_close, assert_identity}; use multicalc::error::LinalgError; use multicalc::linear_algebra::{Matrix, PivotedQr, Vector}; +use multicalc_testkit::tol::{assert_identity, assert_matrix_close}; // ----- column-pivoted QR (decompose, accessors, solve) ----- @@ -177,7 +177,7 @@ fn qr_factorizes_hilbert_stably() { // The factorization stays backward-stable regardless of conditioning. assert_identity(q.transpose() * q, 1e-12); let ap = Matrix::<8, 8>::from_fn(|i, c| hilbert[(i, perm[c])]); - assert_close(q * r, ap, 1e-12); + assert_matrix_close(q * r, ap, 1e-12); // Solving is backward-stable (tiny residual) though the solution itself degrades. let x_true = [1.0; 8]; diff --git a/crates/multicalc/tests/linear_algebra/svd.rs b/crates/multicalc/tests/suite/linear_algebra/svd.rs similarity index 97% rename from crates/multicalc/tests/linear_algebra/svd.rs rename to crates/multicalc/tests/suite/linear_algebra/svd.rs index 81c7c9e..a0fd36e 100644 --- a/crates/multicalc/tests/linear_algebra/svd.rs +++ b/crates/multicalc/tests/suite/linear_algebra/svd.rs @@ -1,8 +1,9 @@ -use crate::helpers::{ - assert_close, assert_identity, svd_moore_penrose, svd_moore_penrose_f32, svd_reconstructs, -}; use multicalc::error::LinalgError; use multicalc::linear_algebra::{Matrix, Vector}; +use multicalc_testkit::tol::{ + assert_identity, assert_matrix_close, svd_moore_penrose, svd_moore_penrose_f32, + svd_reconstructs, +}; #[test] fn svd_reconstructs_various() { @@ -215,7 +216,7 @@ fn svd_kabsch_rotation_recovery() { rhat = uf * v.transpose(); } // Recovered rotation matches, is orthonormal, and has determinant +1. - assert_close(rhat, rot, 1e-9); + assert_matrix_close(rhat, rot, 1e-9); assert_identity(rhat.transpose() * rhat, 1e-12); assert!((rhat.determinant() - 1.0).abs() < 1e-9); } @@ -237,9 +238,9 @@ fn svd_redundant_jacobian_pseudo_inverse() { let jp = j.pseudo_inverse().unwrap(); // Moore–Penrose: J·J⁺·J == J and J·J⁺ symmetric. - assert_close(j * jp * j, j, 1e-9); + assert_matrix_close(j * jp * j, j, 1e-9); let jjp = j * jp; - assert_close(jjp, jjp.transpose(), 1e-12); + assert_matrix_close(jjp, jjp.transpose(), 1e-12); // Minimum-norm resolution: J⁺·v beats any other solution of J·x = v. let vtwist = Vector::<6>::from_fn(|i| i as f64 - 2.5); diff --git a/crates/multicalc/tests/linear_algebra/vector.rs b/crates/multicalc/tests/suite/linear_algebra/vector.rs similarity index 100% rename from crates/multicalc/tests/linear_algebra/vector.rs rename to crates/multicalc/tests/suite/linear_algebra/vector.rs diff --git a/crates/multicalc/tests/suite/main.rs b/crates/multicalc/tests/suite/main.rs new file mode 100644 index 0000000..efc064f --- /dev/null +++ b/crates/multicalc/tests/suite/main.rs @@ -0,0 +1,14 @@ +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +mod approximation; +mod discretization; +mod gaussian_tables; +mod linear_algebra; +mod numerical_derivative; +mod numerical_integration; +mod ode; +mod optimization; +mod root_finding; +mod scalar; +mod spatial; +mod utils; +mod vector_field; diff --git a/crates/multicalc/tests/numerical_derivative.rs b/crates/multicalc/tests/suite/numerical_derivative.rs similarity index 98% rename from crates/multicalc/tests/numerical_derivative.rs rename to crates/multicalc/tests/suite/numerical_derivative.rs index 63e8fce..6724e30 100644 --- a/crates/multicalc/tests/numerical_derivative.rs +++ b/crates/multicalc/tests/suite/numerical_derivative.rs @@ -9,6 +9,7 @@ use multicalc::numerical_derivative::jacobian::Jacobian; use multicalc::numerical_derivative::mode::*; use multicalc::scalar::{Numeric, ScalarFn, ScalarFnN, VectorFn, c}; use multicalc::{scalar_fn, scalar_fn_vec}; +use multicalc_testkit::problems::G; use proptest::prelude::*; use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner}; use std::cell::Cell; @@ -40,9 +41,7 @@ fn ad_first_partials() { #[test] fn ad_first_partials_transcendental() { // f(x, y, z) = y*sin(x) + x*cos(y) + x*y*e^z - let func = scalar_fn!(|v: &[f64; 3]| { - v[1] * v[0].sin() + v[0] * v[1].cos() + v[0] * v[1] * v[2].exp() - }); + let func = G; let d = AutoDiffMulti::default(); let point = [1.0, 2.0, 3.0]; @@ -58,9 +57,7 @@ fn ad_first_partials_transcendental() { #[test] fn ad_second_partials() { // f(x, y, z) = y*sin(x) + x*cos(y) + x*y*e^z - let func = scalar_fn!(|v: &[f64; 3]| { - v[1] * v[0].sin() + v[0] * v[1].cos() + v[0] * v[1] * v[2].exp() - }); + let func = G; let d = AutoDiffMulti::default(); let point = [1.0, 2.0, 3.0]; diff --git a/crates/multicalc/tests/numerical_integration.rs b/crates/multicalc/tests/suite/numerical_integration.rs similarity index 100% rename from crates/multicalc/tests/numerical_integration.rs rename to crates/multicalc/tests/suite/numerical_integration.rs diff --git a/crates/multicalc/tests/suite/ode/mod.rs b/crates/multicalc/tests/suite/ode/mod.rs new file mode 100644 index 0000000..c536345 --- /dev/null +++ b/crates/multicalc/tests/suite/ode/mod.rs @@ -0,0 +1,2 @@ +mod rk4; +mod rk45; diff --git a/crates/multicalc/tests/ode/rk4.rs b/crates/multicalc/tests/suite/ode/rk4.rs similarity index 100% rename from crates/multicalc/tests/ode/rk4.rs rename to crates/multicalc/tests/suite/ode/rk4.rs diff --git a/crates/multicalc/tests/ode/rk45.rs b/crates/multicalc/tests/suite/ode/rk45.rs similarity index 100% rename from crates/multicalc/tests/ode/rk45.rs rename to crates/multicalc/tests/suite/ode/rk45.rs diff --git a/crates/multicalc/tests/optimization.rs b/crates/multicalc/tests/suite/optimization.rs similarity index 96% rename from crates/multicalc/tests/optimization.rs rename to crates/multicalc/tests/suite/optimization.rs index ae35565..1ddd756 100644 --- a/crates/multicalc/tests/optimization.rs +++ b/crates/multicalc/tests/suite/optimization.rs @@ -9,12 +9,13 @@ use multicalc::optimization::{ }; use multicalc::scalar::{Numeric, VectorFn, c}; use multicalc::scalar_fn_vec; +use multicalc_testkit::problems::Rosenbrock; // ----- Levenberg-Marquardt solver ----- #[test] fn lm_solves_rosenbrock() { - let f = scalar_fn_vec!(|v: &[f64; 2]| [c(10.0) * (v[1] - v[0] * v[0]), c(1.0) - v[0]]); + let f = Rosenbrock; let report: MinimizationReport<2> = LevenbergMarquardt::::default() .minimize(&f, &[-1.2, 1.0]) .unwrap(); @@ -62,7 +63,7 @@ fn lm_fits_exponential_decay() { #[test] fn lm_solves_rosenbrock_f32() { // One residual definition drives both precisions; eval is generic over the scalar. - let f = scalar_fn_vec!(|v: &[f64; 2]| [c(10.0) * (v[1] - v[0] * v[0]), c(1.0) - v[0]]); + let f = Rosenbrock; let report = LevenbergMarquardt::>::default() .minimize(&f, &[-1.2_f32, 1.0]) .unwrap(); @@ -92,7 +93,7 @@ fn lm_reports_non_finite() { #[test] fn lm_reports_did_not_converge() { // A one-iteration budget is too small for Rosenbrock, so the solver runs out. - let f = scalar_fn_vec!(|v: &[f64; 2]| [c(10.0) * (v[1] - v[0] * v[0]), c(1.0) - v[0]]); + let f = Rosenbrock; let result = LevenbergMarquardt::::default() .with_patience(1) .minimize(&f, &[-1.2, 1.0]); @@ -205,7 +206,7 @@ fn gn_recovers_linear_least_squares() { #[test] fn gn_solves_rosenbrock() { // From a near guess, Gauss-Newton converges quadratically on this zero-residual problem. - let f = scalar_fn_vec!(|v: &[f64; 2]| [c(10.0) * (v[1] - v[0] * v[0]), c(1.0) - v[0]]); + let f = Rosenbrock; let report = GaussNewton::::default() .minimize(&f, &[0.9, 0.9]) .unwrap(); @@ -406,7 +407,7 @@ fn check_jacobian, const N: usize, const M: usize>(f: &F, x: & #[test] fn autodiff_jacobian_matches_finite_differences() { // Rosenbrock residual: a low-degree polynomial, so central differences are near exact. - let rosenbrock = scalar_fn_vec!(|v: &[f64; 2]| [c(10.0) * (v[1] - v[0] * v[0]), c(1.0) - v[0]]); + let rosenbrock = Rosenbrock; assert!(check_jacobian(&rosenbrock, &[-1.2, 1.0]) < 1e-6); // A transcendental residual exercises the sin and exp derivatives. @@ -418,7 +419,7 @@ fn autodiff_jacobian_matches_finite_differences() { fn solvers_accept_a_finite_difference_backend() { // Swap the autodiff default for a finite-difference Jacobian; both solvers still converge on // the zero-residual Rosenbrock problem. - let f = scalar_fn_vec!(|v: &[f64; 2]| [c(10.0) * (v[1] - v[0] * v[0]), c(1.0) - v[0]]); + let f = Rosenbrock; let lm = LevenbergMarquardt::from_derivator(FiniteDifferenceMulti::::default()) .minimize(&f, &[-1.2, 1.0]) diff --git a/crates/multicalc/tests/root_finding.rs b/crates/multicalc/tests/suite/root_finding.rs similarity index 100% rename from crates/multicalc/tests/root_finding.rs rename to crates/multicalc/tests/suite/root_finding.rs diff --git a/crates/multicalc/tests/scalar.rs b/crates/multicalc/tests/suite/scalar.rs similarity index 100% rename from crates/multicalc/tests/scalar.rs rename to crates/multicalc/tests/suite/scalar.rs diff --git a/crates/multicalc/tests/spatial/lie.rs b/crates/multicalc/tests/suite/spatial/lie.rs similarity index 100% rename from crates/multicalc/tests/spatial/lie.rs rename to crates/multicalc/tests/suite/spatial/lie.rs diff --git a/crates/multicalc/tests/suite/spatial/mod.rs b/crates/multicalc/tests/suite/spatial/mod.rs new file mode 100644 index 0000000..fe34c42 --- /dev/null +++ b/crates/multicalc/tests/suite/spatial/mod.rs @@ -0,0 +1,3 @@ +mod lie; +mod quaternion; +mod twist_wrench; diff --git a/crates/multicalc/tests/spatial/quaternion.rs b/crates/multicalc/tests/suite/spatial/quaternion.rs similarity index 100% rename from crates/multicalc/tests/spatial/quaternion.rs rename to crates/multicalc/tests/suite/spatial/quaternion.rs diff --git a/crates/multicalc/tests/spatial/twist_wrench.rs b/crates/multicalc/tests/suite/spatial/twist_wrench.rs similarity index 100% rename from crates/multicalc/tests/spatial/twist_wrench.rs rename to crates/multicalc/tests/suite/spatial/twist_wrench.rs diff --git a/crates/multicalc/tests/utils.rs b/crates/multicalc/tests/suite/utils.rs similarity index 100% rename from crates/multicalc/tests/utils.rs rename to crates/multicalc/tests/suite/utils.rs diff --git a/crates/multicalc/tests/vector_field.rs b/crates/multicalc/tests/suite/vector_field.rs similarity index 100% rename from crates/multicalc/tests/vector_field.rs rename to crates/multicalc/tests/suite/vector_field.rs From 301e0d595aea3e16a45176c6f2462355140e1e01 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Tue, 14 Jul 2026 12:05:07 -0500 Subject: [PATCH 4/7] initial refactor to showcase --- .github/workflows/full.yml | 20 ++++--- Cargo.toml | 2 +- demos/Cargo.toml | 52 ++++++++++++++++++ {showcase/viz => demos}/README.md | 0 .../examples/showcase}/curve_fit_live.rs | 8 +-- .../examples/showcase}/curve_fit_record.rs | 8 +-- .../examples/showcase}/fourier_ferris.rs | 12 ++-- .../examples/showcase}/gradient_marbles.rs | 12 ++-- .../examples/showcase}/ik_servo.rs | 12 ++-- .../examples/showcase}/newton_fractal.rs | 12 ++-- .../showcase}/support/ferris_outline.rs | 0 .../support/fourier_ferris_showcase.gif | Bin .../support/gradient_marbles_showcase.gif | Bin .../showcase}/support/ik_servo_showcase.gif | Bin .../support/newton_fractal_showcase.gif | Bin {showcase/viz => demos}/plot.py | 0 {showcase/viz => demos}/src/csv_sink.rs | 0 {showcase/viz => demos}/src/lib.rs | 17 +++--- {showcase/viz => demos}/src/loop_util.rs | 0 {showcase/viz => demos}/src/rerun_sink.rs | 0 {showcase/viz => demos}/src/sink.rs | 0 deny.toml | 4 +- showcase/viz/Cargo.toml | 17 ------ 23 files changed, 110 insertions(+), 66 deletions(-) create mode 100644 demos/Cargo.toml rename {showcase/viz => demos}/README.md (100%) rename {showcase/viz/examples => demos/examples/showcase}/curve_fit_live.rs (87%) rename {showcase/viz/examples => demos/examples/showcase}/curve_fit_record.rs (89%) rename {showcase/viz/examples => demos/examples/showcase}/fourier_ferris.rs (96%) rename {showcase/viz/examples => demos/examples/showcase}/gradient_marbles.rs (96%) rename {showcase/viz/examples => demos/examples/showcase}/ik_servo.rs (95%) rename {showcase/viz/examples => demos/examples/showcase}/newton_fractal.rs (95%) rename {showcase/viz/examples => demos/examples/showcase}/support/ferris_outline.rs (100%) rename {showcase/viz/examples => demos/examples/showcase}/support/fourier_ferris_showcase.gif (100%) rename {showcase/viz/examples => demos/examples/showcase}/support/gradient_marbles_showcase.gif (100%) rename {showcase/viz/examples => demos/examples/showcase}/support/ik_servo_showcase.gif (100%) rename {showcase/viz/examples => demos/examples/showcase}/support/newton_fractal_showcase.gif (100%) rename {showcase/viz => demos}/plot.py (100%) rename {showcase/viz => demos}/src/csv_sink.rs (100%) rename {showcase/viz => demos}/src/lib.rs (58%) rename {showcase/viz => demos}/src/loop_util.rs (100%) rename {showcase/viz => demos}/src/rerun_sink.rs (100%) rename {showcase/viz => demos}/src/sink.rs (100%) delete mode 100644 showcase/viz/Cargo.toml diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index 9e0ed6f..3f4568d 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -197,8 +197,8 @@ jobs: lcov.info target/llvm-cov/html - viz: - name: viz adapter (host-only, non-PR) + demos: + name: demos crate (host-only, non-PR) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -206,9 +206,15 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@v2 - - name: Build (lib + examples) - run: cargo build -p multicalc-viz --examples - - name: Clippy - run: cargo clippy -p multicalc-viz --all-targets -- -D warnings + - name: Build (lib + examples, default features) + run: cargo build -p multicalc-demos --examples + - name: Check headless (no default features) + run: cargo check -p multicalc-demos --no-default-features + - name: Clippy (default features) + run: cargo clippy -p multicalc-demos --all-targets -- -D warnings + - name: Clippy (headless) + run: cargo clippy -p multicalc-demos --all-targets --no-default-features -- -D warnings + - name: No Rerun in the headless tree + run: "! cargo tree -p multicalc-demos --no-default-features -i rerun" - name: Headless record smoke test - run: cargo test -p multicalc-viz + run: cargo test -p multicalc-demos diff --git a/Cargo.toml b/Cargo.toml index fb42b47..d5145d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/multicalc", "tools/embedded-smoke", "tools/oracle", "tools/testkit", "showcase/viz"] +members = ["crates/multicalc", "tools/embedded-smoke", "tools/oracle", "tools/testkit", "demos"] default-members = ["crates/multicalc"] resolver = "3" diff --git a/demos/Cargo.toml b/demos/Cargo.toml new file mode 100644 index 0000000..b64adee --- /dev/null +++ b/demos/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "multicalc-demos" +version.workspace = true +edition.workspace = true +# Rerun requires Rust 1.92, above the workspace 1.85 floor, so this crate sets its own. +rust-version = "1.92" +license.workspace = true +publish = false +autoexamples = false + +[features] +default = ["rerun"] +rerun = ["dep:rerun"] + +[dependencies] +multicalc = { path = "../crates/multicalc" } +# Pinned to match the paired Rerun viewer version. Only the logging SDK is needed: `spawn()` +# launches the external viewer on PATH, so the in-process `native_viewer` is not compiled in. +rerun = { version = "=0.33.1", default-features = false, features = ["sdk"], optional = true } + +[lints] +workspace = true + +[[example]] +name = "curve_fit_live" +path = "examples/showcase/curve_fit_live.rs" +required-features = ["rerun"] + +[[example]] +name = "curve_fit_record" +path = "examples/showcase/curve_fit_record.rs" +required-features = ["rerun"] + +[[example]] +name = "fourier_ferris" +path = "examples/showcase/fourier_ferris.rs" +required-features = ["rerun"] + +[[example]] +name = "gradient_marbles" +path = "examples/showcase/gradient_marbles.rs" +required-features = ["rerun"] + +[[example]] +name = "ik_servo" +path = "examples/showcase/ik_servo.rs" +required-features = ["rerun"] + +[[example]] +name = "newton_fractal" +path = "examples/showcase/newton_fractal.rs" +required-features = ["rerun"] diff --git a/showcase/viz/README.md b/demos/README.md similarity index 100% rename from showcase/viz/README.md rename to demos/README.md diff --git a/showcase/viz/examples/curve_fit_live.rs b/demos/examples/showcase/curve_fit_live.rs similarity index 87% rename from showcase/viz/examples/curve_fit_live.rs rename to demos/examples/showcase/curve_fit_live.rs index a9146d6..078fcfa 100644 --- a/showcase/viz/examples/curve_fit_live.rs +++ b/demos/examples/showcase/curve_fit_live.rs @@ -1,14 +1,14 @@ //! Streams a Levenberg-Marquardt curve fit (`y = a·e^(b·t)`) to a live Rerun viewer. //! -//! Requires the `rerun` viewer (version 0.33.1) on PATH; see showcase/viz/README.md. -//! Run with: cargo run -p multicalc-viz --example curve_fit_live +//! Requires the `rerun` viewer (version 0.33.1) on PATH; see demos/README.md. +//! Run with: cargo run -p multicalc-demos --example curve_fit_live #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use multicalc::LevenbergMarquardt; use multicalc::numerical_derivative::autodiff::AutoDiffMulti; use multicalc::scalar::{Numeric, VectorFn}; -use multicalc_viz::{RerunSink, VizError, VizSink}; +use multicalc_demos::{RerunSink, VizError, VizSink}; const A_TRUE: f64 = 100.0; const B_TRUE: f64 = -0.5; @@ -39,7 +39,7 @@ fn main() -> Result<(), VizError> { let fit = |tt: f64| a * (b * tt).exp(); // Spawns the viewer and streams data scatter, fitted curve, and residual series. - let mut rr = RerunSink::live("multicalc-viz/curve-fit")?; + let mut rr = RerunSink::live("multicalc-demos/curve-fit")?; let data_pts: Vec<[f64; 2]> = (0..M).map(|i| [t[i], y[i]]).collect(); rr.points2d("data", &data_pts)?; diff --git a/showcase/viz/examples/curve_fit_record.rs b/demos/examples/showcase/curve_fit_record.rs similarity index 89% rename from showcase/viz/examples/curve_fit_record.rs rename to demos/examples/showcase/curve_fit_record.rs index 6c69c09..0c135fd 100644 --- a/showcase/viz/examples/curve_fit_record.rs +++ b/demos/examples/showcase/curve_fit_record.rs @@ -1,13 +1,13 @@ //! Records a Levenberg-Marquardt curve fit (`y = a·e^(b·t)`) to a `.rrd` and a `.csv`. //! -//! Run with: cargo run -p multicalc-viz --example curve_fit_record +//! Run with: cargo run -p multicalc-demos --example curve_fit_record #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use multicalc::LevenbergMarquardt; use multicalc::numerical_derivative::autodiff::AutoDiffMulti; use multicalc::scalar::{Numeric, VectorFn}; -use multicalc_viz::{CsvSink, RerunSink, VizError, VizSink}; +use multicalc_demos::{CsvSink, RerunSink, VizError, VizSink}; const A_TRUE: f64 = 100.0; const B_TRUE: f64 = -0.5; @@ -42,7 +42,7 @@ fn main() -> Result<(), VizError> { let csv = dir.join("curve_fit.csv"); // Rerun recording: data scatter, dense fitted curve, per-sample residual series. - let mut rr = RerunSink::record("multicalc-viz/curve-fit", &rrd)?; + let mut rr = RerunSink::record("multicalc-demos/curve-fit", &rrd)?; let data_pts: Vec<[f64; 2]> = (0..M).map(|i| [t[i], y[i]]).collect(); rr.points2d("data", &data_pts)?; @@ -76,7 +76,7 @@ fn main() -> Result<(), VizError> { println!("wrote {} and {}", rrd.display(), csv.display()); println!( - " open the .rrd in the rerun viewer, or: python showcase/viz/plot.py {} --x t", + " open the .rrd in the rerun viewer, or: python demos/plot.py {} --x t", csv.display() ); Ok(()) diff --git a/showcase/viz/examples/fourier_ferris.rs b/demos/examples/showcase/fourier_ferris.rs similarity index 96% rename from showcase/viz/examples/fourier_ferris.rs rename to demos/examples/showcase/fourier_ferris.rs index aa7faba..053b4d5 100644 --- a/showcase/viz/examples/fourier_ferris.rs +++ b/demos/examples/showcase/fourier_ferris.rs @@ -11,16 +11,16 @@ //! percentile (not a plot), since it is the OS, not the library. The headline is that math cost and //! its headroom under the 1 ms budget. //! -//! Streams live to a Rerun viewer; see showcase/viz/README.md for the WSL setup. -//! Run with: cargo run --release -p multicalc-viz --example fourier_ferris +//! Streams live to a Rerun viewer; see demos/README.md for the WSL setup. +//! Run with: cargo run --release -p multicalc-demos --example fourier_ferris #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use multicalc::numerical_integration::gaussian_integration::GaussianSingle; use multicalc::numerical_integration::integrator::IntegratorSingleVariable; use multicalc::numerical_integration::mode::GaussianQuadratureMethod; -use multicalc_viz::loop_util::{LatencyRing, Pacer, commas}; -use multicalc_viz::{RerunSink, Rgba, VizError, VizSink}; +use multicalc_demos::loop_util::{LatencyRing, Pacer, commas}; +use multicalc_demos::{RerunSink, Rgba, VizError, VizSink}; use std::collections::VecDeque; use std::f64::consts::TAU; use std::time::Instant; @@ -187,7 +187,7 @@ fn main() -> Result<(), VizError> { if cfg!(debug_assertions) { eprintln!( "WARNING: debug build — timing numbers are meaningless. \ - Re-run with: cargo run --release -p multicalc-viz --example fourier_ferris" + Re-run with: cargo run --release -p multicalc-demos --example fourier_ferris" ); } @@ -202,7 +202,7 @@ fn main() -> Result<(), VizError> { commas(node_evals) ); - let mut rr = RerunSink::live("multicalc-viz/fourier-ferris")?; + let mut rr = RerunSink::live("multicalc-demos/fourier-ferris")?; rr.set_sequence("tick", 0); rr.series_style( "plots/coeff_error", diff --git a/showcase/viz/examples/gradient_marbles.rs b/demos/examples/showcase/gradient_marbles.rs similarity index 96% rename from showcase/viz/examples/gradient_marbles.rs rename to demos/examples/showcase/gradient_marbles.rs index 7c91529..05cf4ce 100644 --- a/showcase/viz/examples/gradient_marbles.rs +++ b/demos/examples/showcase/gradient_marbles.rs @@ -11,16 +11,16 @@ //! lateness is measured too but shown only as a hud percentile (not a plot), since it is the OS, //! not the library. The headline is that math cost and its headroom under the 1 ms budget. //! -//! Streams live to a Rerun viewer; see showcase/viz/README.md for the WSL setup. -//! Run with: cargo run --release -p multicalc-viz --example gradient_marbles +//! Streams live to a Rerun viewer; see demos/README.md for the WSL setup. +//! Run with: cargo run --release -p multicalc-demos --example gradient_marbles #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use multicalc::numerical_derivative::autodiff::AutoDiffMulti; use multicalc::numerical_derivative::jacobian::Jacobian; use multicalc::scalar::{Numeric, VectorFn}; -use multicalc_viz::loop_util::{LatencyRing, Pacer, commas}; -use multicalc_viz::{RerunSink, Rgba, VizError, VizSink}; +use multicalc_demos::loop_util::{LatencyRing, Pacer, commas}; +use multicalc_demos::{RerunSink, Rgba, VizError, VizSink}; use std::f64::consts::TAU; use std::time::Instant; @@ -162,7 +162,7 @@ fn main() -> Result<(), VizError> { if cfg!(debug_assertions) { eprintln!( "WARNING: debug build — timing numbers are meaningless. \ - Re-run with: cargo run --release -p multicalc-viz --example gradient_marbles" + Re-run with: cargo run --release -p multicalc-demos --example gradient_marbles" ); } @@ -187,7 +187,7 @@ fn main() -> Result<(), VizError> { .collect(); respawn(&mut marbles, &mut rng); - let mut rr = RerunSink::live("multicalc-viz/gradient-marbles")?; + let mut rr = RerunSink::live("multicalc-demos/gradient-marbles")?; rr.set_sequence("tick", 0); rr.series_style( "plots/ad_vs_analytic", diff --git a/showcase/viz/examples/ik_servo.rs b/demos/examples/showcase/ik_servo.rs similarity index 95% rename from showcase/viz/examples/ik_servo.rs rename to demos/examples/showcase/ik_servo.rs index 0350dc6..c27a97d 100644 --- a/showcase/viz/examples/ik_servo.rs +++ b/demos/examples/showcase/ik_servo.rs @@ -13,16 +13,16 @@ //! result. The headline is the math cost and its headroom under the 1 ms budget, not a claim of //! hard real-time on a desktop OS. //! -//! Streams live to a Rerun viewer; see showcase/viz/README.md for the WSL setup. -//! Run with: cargo run --release -p multicalc-viz --example ik_servo +//! Streams live to a Rerun viewer; see demos/README.md for the WSL setup. +//! Run with: cargo run --release -p multicalc-demos --example ik_servo #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use multicalc::LevenbergMarquardt; use multicalc::numerical_derivative::autodiff::AutoDiffMulti; use multicalc::scalar::{Numeric, VectorFn}; -use multicalc_viz::loop_util::{LatencyRing, Pacer}; -use multicalc_viz::{RerunSink, Rgba, VizError, VizSink}; +use multicalc_demos::loop_util::{LatencyRing, Pacer}; +use multicalc_demos::{RerunSink, Rgba, VizError, VizSink}; use std::collections::VecDeque; use std::f64::consts::TAU; use std::time::Instant; @@ -126,11 +126,11 @@ fn main() -> Result<(), VizError> { if cfg!(debug_assertions) { eprintln!( "WARNING: debug build — timing numbers are meaningless. \ - Re-run with: cargo run --release -p multicalc-viz --example ik_servo" + Re-run with: cargo run --release -p multicalc-demos --example ik_servo" ); } - let mut rr = RerunSink::live("multicalc-viz/ik-servo")?; + let mut rr = RerunSink::live("multicalc-demos/ik-servo")?; // Statics: stamp at tick 0 so they forward-fill across the run (see rerun-viz-gotchas). rr.set_sequence("tick", 0); diff --git a/showcase/viz/examples/newton_fractal.rs b/demos/examples/showcase/newton_fractal.rs similarity index 95% rename from showcase/viz/examples/newton_fractal.rs rename to demos/examples/showcase/newton_fractal.rs index 57157d4..defdd83 100644 --- a/showcase/viz/examples/newton_fractal.rs +++ b/demos/examples/showcase/newton_fractal.rs @@ -13,16 +13,16 @@ //! *instantaneous* `plots/solves_per_sec` — the hud headline reports the robust median over recent //! frames instead, which is the library's real rate. //! -//! Streams live to a Rerun viewer; see showcase/viz/README.md for the WSL setup. -//! Run with: cargo run --release -p multicalc-viz --example newton_fractal +//! Streams live to a Rerun viewer; see demos/README.md for the WSL setup. +//! Run with: cargo run --release -p multicalc-demos --example newton_fractal #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] use multicalc::numerical_derivative::autodiff::AutoDiffMulti; use multicalc::root_finding::NewtonSystem; use multicalc::scalar::{Numeric, VectorFn}; -use multicalc_viz::loop_util::{LatencyRing, commas}; -use multicalc_viz::{RerunSink, Rgba, VizError, VizSink}; +use multicalc_demos::loop_util::{LatencyRing, commas}; +use multicalc_demos::{RerunSink, Rgba, VizError, VizSink}; use std::f64::consts::TAU; use std::time::Instant; @@ -90,11 +90,11 @@ fn main() -> Result<(), VizError> { if cfg!(debug_assertions) { eprintln!( "WARNING: debug build — throughput numbers are meaningless. \ - Re-run with: cargo run --release -p multicalc-viz --example newton_fractal" + Re-run with: cargo run --release -p multicalc-demos --example newton_fractal" ); } - let mut rr = RerunSink::live("multicalc-viz/newton-fractal")?; + let mut rr = RerunSink::live("multicalc-demos/newton-fractal")?; rr.set_sequence("frame", 0); rr.series_style( "plots/solves_per_sec", diff --git a/showcase/viz/examples/support/ferris_outline.rs b/demos/examples/showcase/support/ferris_outline.rs similarity index 100% rename from showcase/viz/examples/support/ferris_outline.rs rename to demos/examples/showcase/support/ferris_outline.rs diff --git a/showcase/viz/examples/support/fourier_ferris_showcase.gif b/demos/examples/showcase/support/fourier_ferris_showcase.gif similarity index 100% rename from showcase/viz/examples/support/fourier_ferris_showcase.gif rename to demos/examples/showcase/support/fourier_ferris_showcase.gif diff --git a/showcase/viz/examples/support/gradient_marbles_showcase.gif b/demos/examples/showcase/support/gradient_marbles_showcase.gif similarity index 100% rename from showcase/viz/examples/support/gradient_marbles_showcase.gif rename to demos/examples/showcase/support/gradient_marbles_showcase.gif diff --git a/showcase/viz/examples/support/ik_servo_showcase.gif b/demos/examples/showcase/support/ik_servo_showcase.gif similarity index 100% rename from showcase/viz/examples/support/ik_servo_showcase.gif rename to demos/examples/showcase/support/ik_servo_showcase.gif diff --git a/showcase/viz/examples/support/newton_fractal_showcase.gif b/demos/examples/showcase/support/newton_fractal_showcase.gif similarity index 100% rename from showcase/viz/examples/support/newton_fractal_showcase.gif rename to demos/examples/showcase/support/newton_fractal_showcase.gif diff --git a/showcase/viz/plot.py b/demos/plot.py similarity index 100% rename from showcase/viz/plot.py rename to demos/plot.py diff --git a/showcase/viz/src/csv_sink.rs b/demos/src/csv_sink.rs similarity index 100% rename from showcase/viz/src/csv_sink.rs rename to demos/src/csv_sink.rs diff --git a/showcase/viz/src/lib.rs b/demos/src/lib.rs similarity index 58% rename from showcase/viz/src/lib.rs rename to demos/src/lib.rs index 00baf4b..761c3e0 100644 --- a/showcase/viz/src/lib.rs +++ b/demos/src/lib.rs @@ -1,10 +1,12 @@ -//! Thin, std-only Rerun visualization adapter for `multicalc`. +//! Std-only visualization adapter for `multicalc`. //! -//! Maps core types to Rerun archetypes behind a small [`VizSink`] trait, with a Rerun backend -//! ([`RerunSink`], live or recorded) and a CSV backend ([`CsvSink`]) for the `plot.py` fallback. -//! A satellite crate: never a dependency of the core, excluded from bare-metal builds. +//! Maps core types to a small [`VizSink`] trait, with a CSV backend ([`CsvSink`]) for the +//! `plot.py` fallback and, behind the `rerun` feature, a Rerun backend ([`RerunSink`], live or +//! recorded). With the feature off the crate builds headless, with no Rerun in the dependency +//! tree. A satellite crate: never a dependency of the core, excluded from bare-metal builds. mod csv_sink; +#[cfg(feature = "rerun")] mod rerun_sink; mod sink; @@ -13,10 +15,11 @@ pub mod loop_util; pub use csv_sink::CsvSink; pub use multicalc::scalar::Primal; +#[cfg(feature = "rerun")] pub use rerun_sink::RerunSink; pub use sink::{Rgba, VizError, VizSink, VizSinkExt}; -#[cfg(test)] +#[cfg(all(test, feature = "rerun"))] mod tests { #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -26,10 +29,10 @@ mod tests { // Headless: `save` needs no viewer, so this runs in CI. #[test] fn record_writes_nonempty_rrd() -> Result<(), VizError> { - let path = std::env::temp_dir().join("multicalc_viz_smoke.rrd"); + let path = std::env::temp_dir().join("multicalc_demos_smoke.rrd"); let _ = std::fs::remove_file(&path); - let mut sink = RerunSink::record("multicalc-viz/smoke", &path)?; + let mut sink = RerunSink::record("multicalc-demos/smoke", &path)?; sink.set_sequence("iteration", 0); sink.scalar("objective", 1.0)?; sink.points2d("data", &[[0.0, 0.0], [1.0, 1.0]])?; diff --git a/showcase/viz/src/loop_util.rs b/demos/src/loop_util.rs similarity index 100% rename from showcase/viz/src/loop_util.rs rename to demos/src/loop_util.rs diff --git a/showcase/viz/src/rerun_sink.rs b/demos/src/rerun_sink.rs similarity index 100% rename from showcase/viz/src/rerun_sink.rs rename to demos/src/rerun_sink.rs diff --git a/showcase/viz/src/sink.rs b/demos/src/sink.rs similarity index 100% rename from showcase/viz/src/sink.rs rename to demos/src/sink.rs diff --git a/deny.toml b/deny.toml index 0241e25..b19d033 100644 --- a/deny.toml +++ b/deny.toml @@ -1,9 +1,9 @@ # Supply-chain checks for cargo-deny. [graph] -# multicalc-viz is a host-only, unpublished visualization satellite; its heavy Rerun tree is not +# multicalc-demos is a host-only, unpublished visualization satellite; its heavy Rerun tree is not # part of the shipped or bare-metal graph, so it is excluded from the supply-chain audit. -exclude = ["multicalc-viz"] +exclude = ["multicalc-demos"] [advisories] # Fail on known security advisories and yanked crates. diff --git a/showcase/viz/Cargo.toml b/showcase/viz/Cargo.toml deleted file mode 100644 index 266ade9..0000000 --- a/showcase/viz/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "multicalc-viz" -version.workspace = true -edition.workspace = true -# Rerun requires Rust 1.92, above the workspace 1.85 floor, so this crate sets its own. -rust-version = "1.92" -license.workspace = true -publish = false - -[dependencies] -multicalc = { path = "../../crates/multicalc" } -# Pinned to match the paired Rerun viewer version. Only the logging SDK is needed: `spawn()` -# launches the external viewer on PATH, so the in-process `native_viewer` is not compiled in. -rerun = { version = "=0.33.1", default-features = false, features = ["sdk"] } - -[lints] -workspace = true From a67db357d6fd17a9b09ec804882486622ba55ee4 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Tue, 14 Jul 2026 12:25:04 -0500 Subject: [PATCH 5/7] move examples --- .cargo/config.toml | 2 +- .github/workflows/ci.yml | 6 +- .github/workflows/full.yml | 6 +- crates/multicalc/examples/README.md | 29 --------- demos/Cargo.toml | 63 +++++++++++++++++++ .../examples/basics}/approximation.rs | 4 +- .../examples/basics}/autodiff_scalars.rs | 2 +- .../examples/basics}/curve_fit.rs | 2 +- .../examples/basics}/differentiation.rs | 3 +- .../examples/basics}/discretization.rs | 3 +- .../examples/basics}/gaussian_integration.rs | 14 ++--- .../examples/basics}/iterative_integration.rs | 9 ++- .../examples/basics}/jacobian_hessian.rs | 8 ++- .../examples/basics}/lie_groups.rs | 3 +- .../examples/basics}/linear_algebra.rs | 11 +++- .../examples => demos/examples/basics}/ode.rs | 4 +- .../examples/basics}/optimization_solvers.rs | 2 +- .../examples/basics}/root_finding.rs | 3 +- .../examples => demos/examples/basics}/svd.rs | 3 +- .../examples/basics}/vector_field.rs | 4 +- scripts/run_examples.sh | 30 +++++++++ 21 files changed, 151 insertions(+), 60 deletions(-) delete mode 100644 crates/multicalc/examples/README.md rename {crates/multicalc/examples => demos/examples/basics}/approximation.rs (91%) rename {crates/multicalc/examples => demos/examples/basics}/autodiff_scalars.rs (95%) rename {crates/multicalc/examples => demos/examples/basics}/curve_fit.rs (95%) rename {crates/multicalc/examples => demos/examples/basics}/differentiation.rs (94%) rename {crates/multicalc/examples => demos/examples/basics}/discretization.rs (93%) rename {crates/multicalc/examples => demos/examples/basics}/gaussian_integration.rs (90%) rename {crates/multicalc/examples => demos/examples/basics}/iterative_integration.rs (91%) rename {crates/multicalc/examples => demos/examples/basics}/jacobian_hessian.rs (84%) rename {crates/multicalc/examples => demos/examples/basics}/lie_groups.rs (95%) rename {crates/multicalc/examples => demos/examples/basics}/linear_algebra.rs (91%) rename {crates/multicalc/examples => demos/examples/basics}/ode.rs (98%) rename {crates/multicalc/examples => demos/examples/basics}/optimization_solvers.rs (95%) rename {crates/multicalc/examples => demos/examples/basics}/root_finding.rs (95%) rename {crates/multicalc/examples => demos/examples/basics}/svd.rs (97%) rename {crates/multicalc/examples => demos/examples/basics}/vector_field.rs (90%) create mode 100644 scripts/run_examples.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index 2d84caa..7d0f7b5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -33,7 +33,7 @@ rustflags = [ ] [alias] -fit = "run -p multicalc --example curve_fit" +fit = "run -p multicalc-demos --example curve_fit" smoke-eabi = "run -p embedded-smoke --release --target thumbv7em-none-eabi" smoke-eabihf = "run -p embedded-smoke --release --target thumbv7em-none-eabihf" smoke-m0 = "run -p embedded-smoke --release --no-default-features --target thumbv6m-none-eabi" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c8a936..894e678 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,8 +122,10 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: Build examples - run: cargo build -p multicalc --examples + - name: Build headless demos + run: cargo build -p multicalc-demos --examples --no-default-features + - name: Run basics + run: bash scripts/run_examples.sh - name: Build benches run: cargo build -p multicalc --benches # runs a clean `cargo package` build — the same packaging the Step 15 diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index 3f4568d..0eb03ad 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -149,8 +149,10 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: Build examples - run: cargo build -p multicalc --examples + - name: Build headless demos + run: cargo build -p multicalc-demos --examples --no-default-features + - name: Run basics + run: bash scripts/run_examples.sh - name: Build benches run: cargo build -p multicalc --benches # runs a clean `cargo package` build — the same packaging the Step 15 diff --git a/crates/multicalc/examples/README.md b/crates/multicalc/examples/README.md deleted file mode 100644 index f398f04..0000000 --- a/crates/multicalc/examples/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Examples - -Runnable, self-contained examples for each module of `multicalc`. Every file has a `main` -that prints its results against the known analytic value (with the `|err|`), so you can see -the API in action and check accuracy at the same time: - -```sh -cargo run --example -``` - -Several examples also reproduce the published accuracy figures in [`benches/`](../benches) -(noted below), so the tables stay honest. - -| Example | Module(s) | What it shows | -| --- | --- | --- | -| [`differentiation`](differentiation.rs) | `numerical_derivative` | Single- and multi-variable finite-difference derivatives (orders 1-3, partials, mixed partials). Reproduces the differentiation accuracy tables in benches/calculus.md. | -| [`autodiff_scalars`](autodiff_scalars.rs) | `scalar` | Use `Dual` and `HyperDual` directly: evaluate a generic `Numeric` function and read f, f′, f″ from the result fields (no derivator). | -| [`jacobian_hessian`](jacobian_hessian.rs) | `numerical_derivative::{jacobian, hessian}` | Jacobian of a vector of functions and the Hessian of a scalar function. | -| [`iterative_integration`](iterative_integration.rs) | `numerical_integration::iterative_integration` | Boole / Simpson / Trapezoidal rules, multi-variable partial integrals, and infinite / semi-infinite limits. Reproduces the iterative-integration accuracy table in benches/calculus.md. | -| [`gaussian_integration`](gaussian_integration.rs) | `numerical_integration::gaussian_integration` | Gauss-Legendre (finite), Gauss-Hermite and Gauss-Laguerre (infinite), with the bare-integrand convention. Reproduces the Gaussian-quadrature accuracy table in benches/calculus.md. | -| [`vector_field`](vector_field.rs) | `vector_field` | Curl, divergence, line integrals and flux integrals. | -| [`approximation`](approximation.rs) | `approximation` | Linear and quadratic Taylor approximations, `predict`, and goodness-of-fit metrics. | -| [`linear_algebra`](linear_algebra.rs) | `linear_algebra` | LU and Cholesky factorizations, linear solves, and the direct 4x4 inverse under a latency + approximation-error stress test on well- and ill-conditioned inputs. Reproduces the LU / Cholesky / inverse accuracy tables in benches/linear_algebra.md. | -| [`svd`](svd.rs) | `linear_algebra::svd` | Singular value decomposition and Moore-Penrose pseudo-inverse under a robotics stress test (Kabsch rotation recovery, a redundant-arm pseudo-inverse, a near-singular Jacobian, and an overdetermined fit) with latency + approximation error. Reproduces the SVD / pseudo-inverse accuracy table in benches/linear_algebra.md. | -| [`root_finding`](root_finding.rs) | `root_finding` | Bracketed bisection, Newton with exact derivatives, damped (backtracking) Newton rescuing a far start, and a square-system Newton solve, each printed against its known root. | -| [`curve_fit`](curve_fit.rs) | `optimization` | Levenberg-Marquardt fit of `y = a·e^(b·t)` to sensor samples with exact autodiff Jacobians; prints recovered `a`, `b`, and `\|err\|`. | -| [`optimization_solvers`](optimization_solvers.rs) | `optimization` | Gauss-Newton on a well-conditioned linear residual (`y = a + b·t`); when GN is enough vs LM (`curve_fit`). | -| [`lie_groups`](lie_groups.rs) | `spatial` | SO(3)/SE(3) compose, act on a point, exp/log round trips, geodesic interpolation, and a one-`Dual` autodiff derivative pushed through `exp` ∘ `act`. | -| [`discretization`](discretization.rs) | `discretization`, `linear_algebra::expm` | ZOH on a double integrator, Van Loan process-noise discretization, the filterpy `q_discrete_white_noise` model, and a one-`Dual` derivative pushed through the matrix exponential. | diff --git a/demos/Cargo.toml b/demos/Cargo.toml index b64adee..e1dbc12 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -21,6 +21,69 @@ rerun = { version = "=0.33.1", default-features = false, features = ["sdk"], opt [lints] workspace = true +# Basics: headless, self-checking, multicalc-only. No required-features, so they +# build and run with the rerun feature off. +[[example]] +name = "approximation" +path = "examples/basics/approximation.rs" + +[[example]] +name = "autodiff_scalars" +path = "examples/basics/autodiff_scalars.rs" + +[[example]] +name = "curve_fit" +path = "examples/basics/curve_fit.rs" + +[[example]] +name = "differentiation" +path = "examples/basics/differentiation.rs" + +[[example]] +name = "discretization" +path = "examples/basics/discretization.rs" + +[[example]] +name = "gaussian_integration" +path = "examples/basics/gaussian_integration.rs" + +[[example]] +name = "iterative_integration" +path = "examples/basics/iterative_integration.rs" + +[[example]] +name = "jacobian_hessian" +path = "examples/basics/jacobian_hessian.rs" + +[[example]] +name = "lie_groups" +path = "examples/basics/lie_groups.rs" + +[[example]] +name = "linear_algebra" +path = "examples/basics/linear_algebra.rs" + +[[example]] +name = "ode" +path = "examples/basics/ode.rs" + +[[example]] +name = "optimization_solvers" +path = "examples/basics/optimization_solvers.rs" + +[[example]] +name = "root_finding" +path = "examples/basics/root_finding.rs" + +[[example]] +name = "svd" +path = "examples/basics/svd.rs" + +[[example]] +name = "vector_field" +path = "examples/basics/vector_field.rs" + +# Showcases: live Rerun demos, require the rerun feature. [[example]] name = "curve_fit_live" path = "examples/showcase/curve_fit_live.rs" diff --git a/crates/multicalc/examples/approximation.rs b/demos/examples/basics/approximation.rs similarity index 91% rename from crates/multicalc/examples/approximation.rs rename to demos/examples/basics/approximation.rs index e83c8e6..8dbc2af 100644 --- a/crates/multicalc/examples/approximation.rs +++ b/demos/examples/basics/approximation.rs @@ -1,7 +1,7 @@ //! Linear and quadratic (Taylor) approximation of a function about a point, plus //! goodness-of-fit metrics. //! -//! Run with: `cargo run --example approximation` +//! Run with: `cargo run -p multicalc-demos --example approximation` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -17,6 +17,8 @@ fn main() { let linear: LinearApproximator = LinearApproximator::default(); let model = linear.get(&f, &base).unwrap(); + // A first-order model is exact at its own expansion point. + assert!((model.predict(&base) - f.eval(&base)).abs() < 1e-9); println!("Linear model of x + y^2 + z^3 about {base:?}"); println!( diff --git a/crates/multicalc/examples/autodiff_scalars.rs b/demos/examples/basics/autodiff_scalars.rs similarity index 95% rename from crates/multicalc/examples/autodiff_scalars.rs rename to demos/examples/basics/autodiff_scalars.rs index 6e9cc9e..e3b049f 100644 --- a/crates/multicalc/examples/autodiff_scalars.rs +++ b/demos/examples/basics/autodiff_scalars.rs @@ -1,6 +1,6 @@ //! Using autodiff scalar types directly (`Dual`, `HyperDual`). //! -//! Run with: `cargo run --example autodiff_scalars` +//! Run with: `cargo run -p multicalc-demos --example autodiff_scalars` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] diff --git a/crates/multicalc/examples/curve_fit.rs b/demos/examples/basics/curve_fit.rs similarity index 95% rename from crates/multicalc/examples/curve_fit.rs rename to demos/examples/basics/curve_fit.rs index e9b02fc..243a696 100644 --- a/crates/multicalc/examples/curve_fit.rs +++ b/demos/examples/basics/curve_fit.rs @@ -1,7 +1,7 @@ //! Sensor-calibration curve fit: recover a and b in y = a·e^(b·t) from samples using //! Levenberg–Marquardt with exact autodiff Jacobians (no hand-derived derivatives), zero heap. //! -//! Run with: cargo run --example curve_fit (or: cargo fit) +//! Run with: cargo run -p multicalc-demos --example curve_fit (or: cargo fit) #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] diff --git a/crates/multicalc/examples/differentiation.rs b/demos/examples/basics/differentiation.rs similarity index 94% rename from crates/multicalc/examples/differentiation.rs rename to demos/examples/basics/differentiation.rs index aa6bec3..5101a3b 100644 --- a/crates/multicalc/examples/differentiation.rs +++ b/demos/examples/basics/differentiation.rs @@ -1,7 +1,7 @@ //! Single- and multi-variable differentiation. //! The derivative order for a partial is just the number of indices passed. //! -//! Run with: `cargo run --example differentiation` +//! Run with: `cargo run -p multicalc-demos --example differentiation` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -10,6 +10,7 @@ use multicalc::numerical_derivative::derivator::{DerivatorMultiVariable, Derivat use multicalc::scalar_fn; fn report(label: &str, value: f64, exact: f64) { + assert!((value - exact).abs() < 1e-6, "{label}: |err| too large"); println!( " {label:<18} = {value:>13.8} (exact {exact:>13.8}, |err| {:.0e})", (value - exact).abs() diff --git a/crates/multicalc/examples/discretization.rs b/demos/examples/basics/discretization.rs similarity index 93% rename from crates/multicalc/examples/discretization.rs rename to demos/examples/basics/discretization.rs index d135d03..5cc49e4 100644 --- a/crates/multicalc/examples/discretization.rs +++ b/demos/examples/basics/discretization.rs @@ -1,7 +1,7 @@ //! Discretization: zero-order hold on a double integrator, Van Loan process noise, the discrete //! white-noise model, and a one-`Dual` derivative through `expm`. //! -//! Run with: `cargo run --example discretization` +//! Run with: `cargo run -p multicalc-demos --example discretization` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -10,6 +10,7 @@ use multicalc::linear_algebra::Matrix; use multicalc::scalar::Dual; fn report(label: &str, value: f64, exact: f64) { + assert!((value - exact).abs() < 1e-9, "{label}: |err| too large"); println!( " {label:<22} = {value:>12.8} (exact {exact:>12.8}, |err| {:.0e})", (value - exact).abs() diff --git a/crates/multicalc/examples/gaussian_integration.rs b/demos/examples/basics/gaussian_integration.rs similarity index 90% rename from crates/multicalc/examples/gaussian_integration.rs rename to demos/examples/basics/gaussian_integration.rs index ba54af9..74107ac 100644 --- a/crates/multicalc/examples/gaussian_integration.rs +++ b/demos/examples/basics/gaussian_integration.rs @@ -4,7 +4,7 @@ //! are exact (to machine precision) for polynomial integrands, and lose accuracy fast on //! non-polynomial ones. //! -//! Run with: `cargo run --example gaussian_integration` +//! Run with: `cargo run -p multicalc-demos --example gaussian_integration` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -28,13 +28,11 @@ fn main() { let legendre = GaussianSingle::from_parameters(5, GaussianQuadratureMethod::GaussLegendre); println!("Gauss-Legendre (finite limits):"); // int_0^2 (4x^3 - 3x^2) dx = 8 (exact: order 5 handles degree <= 9) - report( - "int_0^2 4x^3-3x^2", - legendre - .get_single(&|x| 4.0 * x * x * x - 3.0 * x * x, &[0.0, 2.0]) - .unwrap(), - 8.0, - ); + let poly: f64 = legendre + .get_single(&|x| 4.0 * x * x * x - 3.0 * x * x, &[0.0, 2.0]) + .unwrap(); + assert!((poly - 8.0).abs() < 1e-9, "Legendre is exact for polynomials"); + report("int_0^2 4x^3-3x^2", poly, 8.0); // non-polynomial integrand: accuracy falls report( "int_0^1 (sinx-sqrtx)e^-x", diff --git a/crates/multicalc/examples/iterative_integration.rs b/demos/examples/basics/iterative_integration.rs similarity index 91% rename from crates/multicalc/examples/iterative_integration.rs rename to demos/examples/basics/iterative_integration.rs index 296573b..e7a3895 100644 --- a/crates/multicalc/examples/iterative_integration.rs +++ b/demos/examples/basics/iterative_integration.rs @@ -4,7 +4,7 @@ //! Also reproduces the iterative-integration accuracy figures in benches/calculus.md: Boole is //! the highest-order rule and most accurate, Simpson is intermediate, Trapezoidal is lowest order. //! -//! Run with: `cargo run --example iterative_integration` +//! Run with: `cargo run -p multicalc-demos --example iterative_integration` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -25,10 +25,9 @@ fn main() { // ---- single variable: int_0^2 2x dx = 4 ---- let f = |x: f64| 2.0 * x; let integrator = IterativeSingle::default(); // Boole's rule, 120 intervals - println!( - "int_0^2 2x dx = {:.8} (exact 4)", - integrator.get_single(&f, &[0.0, 2.0]).unwrap() - ); + let two_x = integrator.get_single(&f, &[0.0, 2.0]).unwrap(); + assert!((two_x - 4.0).abs() < 1e-9, "Boole is exact for a linear integrand"); + println!("int_0^2 2x dx = {two_x:.8} (exact 4)"); // ---- compare the three rules on the same integrand ---- // int_0^1 (yz x^2 e^x) dx folded three times, with y*z = 6 -> 6*(e - 2) diff --git a/crates/multicalc/examples/jacobian_hessian.rs b/demos/examples/basics/jacobian_hessian.rs similarity index 84% rename from crates/multicalc/examples/jacobian_hessian.rs rename to demos/examples/basics/jacobian_hessian.rs index f289acd..0fa0ea2 100644 --- a/crates/multicalc/examples/jacobian_hessian.rs +++ b/demos/examples/basics/jacobian_hessian.rs @@ -1,6 +1,6 @@ //! Jacobian and Hessian matrices of multi-variable functions. //! -//! Run with: `cargo run --example jacobian_hessian` +//! Run with: `cargo run -p multicalc-demos --example jacobian_hessian` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -22,6 +22,12 @@ fn main() { println!(" [{:.4}, {:.4}, {:.4}]", row[0], row[1], row[2]); } println!(" (exact [[6, 3, 2], [2, 4, 0]])"); + let exact = [[6.0, 3.0, 2.0], [2.0, 4.0, 0.0]]; + for i in 0..2 { + for j in 0..3 { + assert!((result[i][j] - exact[i][j]).abs() < 1e-9); + } + } // ---- Hessian of f(x, y) = y*sin(x) + 2*x*e^y ---- let g = scalar_fn!(|v: &[f64; 2]| v[1] * v[0].sin() + c(2.0) * v[0] * v[1].exp()); diff --git a/crates/multicalc/examples/lie_groups.rs b/demos/examples/basics/lie_groups.rs similarity index 95% rename from crates/multicalc/examples/lie_groups.rs rename to demos/examples/basics/lie_groups.rs index 4e8f2a6..c0b912d 100644 --- a/crates/multicalc/examples/lie_groups.rs +++ b/demos/examples/basics/lie_groups.rs @@ -1,7 +1,7 @@ //! SO(3) and SE(3) Lie groups: compose, act on a point, exp/log round trips, geodesic //! interpolation, and a one-`Dual` autodiff derivative through the whole composition. //! -//! Run with: `cargo run --example lie_groups` +//! Run with: `cargo run -p multicalc-demos --example lie_groups` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -12,6 +12,7 @@ use multicalc::scalar::Dual; use multicalc::spatial::{SE3, SO3}; fn report(label: &str, value: f64, exact: f64) { + assert!((value - exact).abs() < 1e-9, "{label}: |err| too large"); println!( " {label:<20} = {value:>12.8} (exact {exact:>12.8}, |err| {:.0e})", (value - exact).abs() diff --git a/crates/multicalc/examples/linear_algebra.rs b/demos/examples/basics/linear_algebra.rs similarity index 91% rename from crates/multicalc/examples/linear_algebra.rs rename to demos/examples/basics/linear_algebra.rs index e9f01c6..969435b 100644 --- a/crates/multicalc/examples/linear_algebra.rs +++ b/demos/examples/basics/linear_algebra.rs @@ -3,7 +3,7 @@ //! residual, and inverse identity error) on well- and ill-conditioned inputs. //! //! Latency is illustrative in a debug build; run with `--release` for representative numbers: -//! `cargo run --release --example linear_algebra` +//! `cargo run -p multicalc-demos --release --example linear_algebra` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -96,6 +96,15 @@ fn inverse4_report(a: Matrix<4, 4>, label: &str) { } fn main() { + // Sanity: the LU solve residual on a well-conditioned system is tiny. + { + let a = general::<4>(); + let x_true = Vector::<4>::from_fn(|i| 1.0 + i as f64); + let b = a * x_true; + let x = a.lu().unwrap().solve(b); + assert!((a * x - b).norm() < 1e-9, "LU solve residual too large"); + } + println!("LU (any invertible matrix) - decompose + solve:"); lu_report(general::<4>(), "general 4x4"); lu_report(general::<8>(), "general 8x8"); diff --git a/crates/multicalc/examples/ode.rs b/demos/examples/basics/ode.rs similarity index 98% rename from crates/multicalc/examples/ode.rs rename to demos/examples/basics/ode.rs index d3fb1f2..e3a9965 100644 --- a/crates/multicalc/examples/ode.rs +++ b/demos/examples/basics/ode.rs @@ -5,7 +5,7 @@ //! reported as the drift in a conserved quantity (energy, kinetic energy, quaternion norm). //! These figures reproduce the accuracy table in `benches/ode.md`. //! -//! Run with: `cargo run --example ode` +//! Run with: `cargo run -p multicalc-demos --example ode` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -41,6 +41,7 @@ fn harmonic_oscillator() { " y(2*pi) = [{:.12}, {:.12}] max|err| = {max_err:.2e}", yf[0], yf[1] ); + assert!(max_err < 1e-3, "RK4 should track the exact harmonic solution"); // RK45: adaptive solve to t = 2*pi, then dense-output sampling. let solver = Rk45::default().with_rtol(1e-9).with_atol(1e-12); @@ -68,6 +69,7 @@ fn harmonic_oscillator() { }) .fold(0.0_f64, f64::max); println!(" dense-output grid max|err| = {grid_err:.2e}"); + assert!(grid_err < 1e-6, "RK45 dense output should be accurate"); } // Largest drift of the invariant `inv` from its initial value over an RK4 integration. diff --git a/crates/multicalc/examples/optimization_solvers.rs b/demos/examples/basics/optimization_solvers.rs similarity index 95% rename from crates/multicalc/examples/optimization_solvers.rs rename to demos/examples/basics/optimization_solvers.rs index 92d2f80..7dff214 100644 --- a/crates/multicalc/examples/optimization_solvers.rs +++ b/demos/examples/basics/optimization_solvers.rs @@ -5,7 +5,7 @@ //! - **Levenberg-Marquardt** (see curve_fit.rs): damped / trust-region style; prefer when //! far from the solution or the Jacobian is poorly conditioned. //! -//! Run: cargo run -p multicalc --example optimization_solvers +//! Run: cargo run -p multicalc-demos --example optimization_solvers #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] diff --git a/crates/multicalc/examples/root_finding.rs b/demos/examples/basics/root_finding.rs similarity index 95% rename from crates/multicalc/examples/root_finding.rs rename to demos/examples/basics/root_finding.rs index b8167be..e18f6d6 100644 --- a/crates/multicalc/examples/root_finding.rs +++ b/demos/examples/basics/root_finding.rs @@ -1,7 +1,7 @@ //! Root finding: bracketed bisection, Newton with exact derivatives, damped Newton, and a //! square-system Newton solve. Each result prints against its known root with the `|err|`. //! -//! Run with: `cargo run --example root_finding` +//! Run with: `cargo run -p multicalc-demos --example root_finding` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -67,6 +67,7 @@ fn main() { .solve(&system, &[1.5, 0.8]) .unwrap(); let err = (r.root[0] - x_true).abs().max((r.root[1] - y_true).abs()); + assert!(err < 1e-9, "Newton system should converge to the intersection"); println!("\nNewton system x^2 + y^2 = 4 and x*y = 1"); println!(" root = [{:.12}, {:.12}]", r.root[0], r.root[1]); println!( diff --git a/crates/multicalc/examples/svd.rs b/demos/examples/basics/svd.rs similarity index 97% rename from crates/multicalc/examples/svd.rs rename to demos/examples/basics/svd.rs index be4bcb1..c81134b 100644 --- a/crates/multicalc/examples/svd.rs +++ b/demos/examples/basics/svd.rs @@ -3,7 +3,7 @@ //! recovery, a redundant-arm pseudo-inverse, a near-singular Jacobian, and an overdetermined fit. //! //! Latency is illustrative in a debug build; run with `--release` for representative numbers: -//! `cargo run --release --example svd` +//! `cargo run -p multicalc-demos --release --example svd` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -87,6 +87,7 @@ fn kabsch() { } let rot_err = max_abs(rhat, rot); let ortho_err = max_abs(rhat.transpose() * rhat, Matrix::<3, 3>::identity()); + assert!(rot_err < 1e-9 && ortho_err < 1e-9, "SVD should recover the rotation"); let label = "Kabsch 3x3"; println!(" {label:<20} {ns:>8.1} ns R-error {rot_err:.1e} orthogonality {ortho_err:.1e}"); } diff --git a/crates/multicalc/examples/vector_field.rs b/demos/examples/basics/vector_field.rs similarity index 90% rename from crates/multicalc/examples/vector_field.rs rename to demos/examples/basics/vector_field.rs index 88ced60..26ca592 100644 --- a/crates/multicalc/examples/vector_field.rs +++ b/demos/examples/basics/vector_field.rs @@ -1,6 +1,6 @@ //! Vector-field calculus: curl, divergence, line integrals and flux integrals. //! -//! Run with: `cargo run --example vector_field` +//! Run with: `cargo run -p multicalc-demos --example vector_field` #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] @@ -16,6 +16,8 @@ fn main() { let curl_2d = curl::get_2d(AutoDiffMulti::default(), &field, &point).unwrap(); let div_2d = divergence::get_2d(AutoDiffMulti::default(), &field, &point).unwrap(); + assert!((curl_2d + 2.0).abs() < 1e-9, "curl"); + assert!((div_2d - std::f64::consts::TAU).abs() < 1e-9, "divergence"); println!("field (2xy, 3cos y) at {point:?}"); println!(" curl = {curl_2d:.4} (exact -2)"); println!( diff --git a/scripts/run_examples.sh b/scripts/run_examples.sh new file mode 100644 index 0000000..48210d2 --- /dev/null +++ b/scripts/run_examples.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Build and run every headless basic demo, failing on the first nonzero exit. Basics +# take no features and self-check with asserts; showcases are skipped (they need the +# Rerun viewer and loop forever). Run from the repository root. +set -euo pipefail + +basics=( + approximation + autodiff_scalars + curve_fit + differentiation + discretization + gaussian_integration + iterative_integration + jacobian_hessian + lie_groups + linear_algebra + ode + optimization_solvers + root_finding + svd + vector_field +) + +for name in "${basics[@]}"; do + echo "== $name ==" + cargo run -p multicalc-demos --example "$name" --no-default-features +done + +echo "all ${#basics[@]} basics passed" From 0f781cbbbf3a470998767fbf306e18a6945f9d7b Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Tue, 14 Jul 2026 12:50:44 -0500 Subject: [PATCH 6/7] documentation --- CONTRIBUTING.md | 19 +++ README.md | 18 +-- crates/multicalc/README.md | 12 +- crates/multicalc/benches/README.md | 5 +- crates/multicalc/benches/linear_algebra.md | 8 +- crates/multicalc/benches/ode.md | 4 +- demos/README.md | 146 ++++++++++++--------- 7 files changed, 127 insertions(+), 85 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8473a0..a61d862 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,6 +42,25 @@ locally anyway, the setup lives in [ci/README.md](ci/README.md) (optional). - **Docs**: public APIs get a doc example; behavior notes (NaN policy, iteration budgets) live on the item. +## Where does a check go? + +The workspace has several test and demo layers. Each has one job; adding a check means picking +the layer that matches and not duplicating another's. + +| Layer | Purpose | Must not | +|---|---|---| +| doctests | one minimal runnable demo per public item | become the correctness suite | +| `src/**/test.rs` inline | white-box tests of `pub(crate)` internals only (LU/lmpar) | test public API | +| `tests/suite/` | **the** correctness suite: public API, edge cases, proptests | re-declare problems/helpers inline | +| `demos/examples/basics/` | copy-pasteable, headless, terminating demos; multicalc-only imports | exit 0 without ≥1 sanity `assert!`; touch a sink | +| `demos/examples/showcase/` | live Rerun demos; measured numbers only | panic on edge cases (errors render as demo states); hardcode a perf claim | +| `benches/` | timing; `.md` tables are labeled illustrative snapshots | present tables as verified claims | +| `tools/oracle` | cross-implementation goldens (numpy/mpmath/MINPACK) only | duplicate self-consistency tests | +| `tools/embedded-smoke` | on-target FP-path + stack/text budgets; goldens only via generated `fixtures.rs` | hand-write golden values | + +Shared problem definitions and tolerance helpers live in `tools/testkit`, so a problem is +declared once and reused across `tests/suite/`, the oracle, and embedded-smoke. + ## Releasing Releases are automated from `main`: diff --git a/README.md b/README.md index 4d9a379..e85f80e 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ derivatives, integrals, curve fitting and linear algebra; built and tested on fi hardware targets. Exercise the same code from a 64-bit server CPU down to a bare-metal microcontroller.**

- 1 kHz 3-link arm running a full Levenberg-Marquardt solve every millisecond - Morphing Newton fractal, every pixel a full Newton solve + 1 kHz 3-link arm running a full Levenberg-Marquardt solve every millisecond + Morphing Newton fractal, every pixel a full Newton solve

-*Two of four live [showcase demos](showcase/viz#showcases): a 1 kHz 3-link arm running a complete +*Two of four live [showcase demos](demos#live-showcases): a 1 kHz 3-link arm running a complete Levenberg-Marquardt solve every millisecond, and a Newton fractal at ~4 million solves/sec on one core — every number measured live.* @@ -102,9 +102,9 @@ below. - **[Full guide](crates/multicalc/README.md)**: Every feature with a runnable snippet, plus notes on `no_std`, error handling, and heap allocation. - **[API docs](https://docs.rs/multicalc)** on docs.rs. -- **[Examples](crates/multicalc/examples)**: Self-contained programs for each module. Run one - with `cargo run --example `. -- **[Live showcases](showcase/viz#showcases)**: Four animated Rerun demos — a 1 kHz IK on a 3-link arm, a +- **[Examples](demos#start-here)**: Self-contained, self-checking programs for each module in the + `demos/` crate. Run one with `cargo run -p multicalc-demos --example `. +- **[Live showcases](demos#live-showcases)**: Four animated Rerun demos — a 1 kHz IK on a 3-link arm, a Newton fractal, Fourier epicycles drawing Ferris, and gradient-driven marbles — each streaming live-measured speed and accuracy. - **[Benchmarks](crates/multicalc/benches)**: Accuracy figures and measured latency. @@ -112,9 +112,9 @@ below. ## Repository layout The published library crate lives in [`crates/multicalc`](crates/multicalc); the repository -root is a Cargo workspace. A second, dev-only crate, -[`tools/embedded-smoke`](tools/embedded-smoke), runs `multicalc` on the three bare-metal -Cortex-M targets under QEMU every PR. +root is a Cargo workspace. Runnable demos live in the dev-only [`demos/`](demos) crate (basics and +live Rerun showcases), and [`tools/embedded-smoke`](tools/embedded-smoke) runs `multicalc` on the +three bare-metal Cortex-M targets under QEMU every PR. ## Contributing diff --git a/crates/multicalc/README.md b/crates/multicalc/README.md index 5c08f9e..bf36f50 100644 --- a/crates/multicalc/README.md +++ b/crates/multicalc/README.md @@ -33,11 +33,11 @@ Jacobians and Hessians, vector-field operators, and Taylor approximation in a `n - A runnable example for every module, and a test suite covering each error path.

- 1 kHz 3-link arm running a full Levenberg-Marquardt solve every millisecond - Morphing Newton fractal, every pixel a full Newton solve + 1 kHz 3-link arm running a full Levenberg-Marquardt solve every millisecond + Morphing Newton fractal, every pixel a full Newton solve

-*Two of four live [showcase demos](showcase/viz#showcases): a 1 kHz 3-link arm running a complete +*Two of four live [showcase demos](../../demos#live-showcases): a 1 kHz 3-link arm running a complete Levenberg-Marquardt solve every millisecond, and a Newton fractal at ~4 million solves/sec on one core — every number measured live.* @@ -373,11 +373,11 @@ returns a `Vec>` of the scalar (`Vec>` by default). ## Examples -Runnable, self-contained programs for each module live in [`examples/`](./examples). See -[examples/README.md](./examples/README.md). Run one with: +Runnable, self-contained programs for each module live in the [`demos/`](../../demos) crate. See +[demos/README.md](../../demos/README.md). Run one with: ```sh -cargo run --example +cargo run -p multicalc-demos --example ``` ## Benchmarks diff --git a/crates/multicalc/benches/README.md b/crates/multicalc/benches/README.md index f0b50e3..67505fe 100644 --- a/crates/multicalc/benches/README.md +++ b/crates/multicalc/benches/README.md @@ -14,8 +14,9 @@ figures (wall-clock time per call). Accuracy vs latency: every suite doc reports latency (wall-clock time per call); all five also report accuracy (how close the result lands to the known value, or the drift of a conserved -quantity for the ODE systems). The examples in [`examples/`](../examples) reproduce those accuracy -tables, so the published figures stay honest. +quantity for the ODE systems). These docs are illustrative snapshots, not verified claims: +verified accuracy lives in the golden fixtures under [`tools/oracle`](../../../tools/oracle), and +runnable, self-checking demos live in [`demos/`](../../../demos). ## Running diff --git a/crates/multicalc/benches/linear_algebra.md b/crates/multicalc/benches/linear_algebra.md index b68a96f..10ad739 100644 --- a/crates/multicalc/benches/linear_algebra.md +++ b/crates/multicalc/benches/linear_algebra.md @@ -7,10 +7,10 @@ report wall-clock medians on the machine noted in [README.md](README.md). ## Accuracy -Measured by the [`linear_algebra`](../examples/linear_algebra.rs) and [`svd`](../examples/svd.rs) -stress-test examples on well- and ill-conditioned inputs. Unlike latency, these are deterministic -numerical errors and reproduce on any machine. Reconstruction is the entrywise error of the -factorization; the solve residual is $$\lVert Ax - b\rVert$$ for a known solution. +Measured by the [`linear_algebra`](../../../demos) and [`svd`](../../../demos) stress-test demos +on well- and ill-conditioned inputs. Unlike latency, these are deterministic numerical errors and +reproduce on any machine. Reconstruction is the entrywise error of the factorization; the solve +residual is $$\lVert Ax - b\rVert$$ for a known solution. ### LU and Cholesky (decompose + solve) diff --git a/crates/multicalc/benches/ode.md b/crates/multicalc/benches/ode.md index b02fc81..f04bb0d 100644 --- a/crates/multicalc/benches/ode.md +++ b/crates/multicalc/benches/ode.md @@ -17,8 +17,8 @@ reports wall-clock medians on the machine noted in [README.md](README.md). ## Accuracy None of these systems has a closed-form solution, so accuracy is the drift of a conserved -quantity: `max |Q(t) − Q(0)|` over the trajectory (relative, for the N-body energy). The figures -below are reproduced by [`examples/ode.rs`](../examples/ode.rs) (`cargo run --example ode`). +quantity: `max |Q(t) − Q(0)|` over the trajectory (relative, for the N-body energy). The same +systems run in the [`ode`](../../../demos) demo (`cargo run -p multicalc-demos --example ode`). | System | Invariant | RK4 drift | RK45 drift | Notes | | ------------------------------ | --------------------- | --------- | ---------- | ----------------------------------------------------------- | diff --git a/demos/README.md b/demos/README.md index 5a30552..5e7c81c 100644 --- a/demos/README.md +++ b/demos/README.md @@ -1,57 +1,62 @@ -# multicalc-viz +# multicalc-demos -A thin, std-only [Rerun](https://rerun.io) visualization adapter for `multicalc`. It maps core -types to Rerun archetypes behind a small `VizSink` trait, with a Rerun backend (`live()` or -`record(path)`) and a CSV backend for a matplotlib fallback. +Runnable demos for [`multicalc`](../crates/multicalc), in two flavors: + +- **Basics** — headless, terminating programs, one per module. Each prints its results against + the known analytic value (with the `|err|`) and self-checks with an assert. No viewer, no + feature flags; they depend only on `multicalc`. +- **Showcases** — live [Rerun](https://rerun.io) demos that render an animated scene and stream + live-measured speed and accuracy. They require the `rerun` feature (on by default) and a + version-matched viewer. This is a satellite crate: it is never a dependency of the core library, is excluded from bare-metal builds and the default `cargo test`, and its dependency tree is excluded from the workspace supply-chain audit. -## Versions - -Rerun SDK `=0.33.1` ⇄ viewer `0.33.1`. The SDK is exact-pinned; the viewer must match. - -## Viewer install (for the live example) - -`live()` spawns the external Rerun viewer found on PATH, so install it version-matched to the SDK: - -``` -cargo install rerun-cli --locked --version 0.33.1 -# or: pip install rerun-sdk==0.33.1 -# or: cargo binstall rerun-cli --version 0.33.1 -``` - -## Running the examples +## Start here -Recorded (no viewer needed; writes a `.rrd` and a `.csv` to the temp dir): +No viewer, no flags — each terminates and prints results vs the analytic value with the `|err|`: -``` -cargo run -p multicalc-viz --example curve_fit_record +```sh +cargo run -p multicalc-demos --example ``` -Open the printed `.rrd` in the viewer, or render the CSV fallback: - -``` -python showcase/viz/plot.py --x t -``` - -Live (on a normal host this spawns a local viewer; under WSL see the section below): - -``` -cargo run -p multicalc-viz --example curve_fit_live -``` - -## Showcases +| Example | Module(s) | What it shows | +| --- | --- | --- | +| `approximation` | `approximation` | Linear and quadratic (Taylor) approximations, `predict`, and goodness-of-fit metrics. | +| `autodiff_scalars` | `scalar` | Use `Dual` and `HyperDual` directly: evaluate a generic `Numeric` function and read f, f′, f″ from the result fields (no derivator). | +| `curve_fit` | `optimization` | Levenberg-Marquardt fit of `y = a·e^(b·t)` to sensor samples with exact autodiff Jacobians; prints recovered `a`, `b`, and `\|err\|`. | +| `differentiation` | `numerical_derivative` | Single- and multi-variable derivatives (orders 1-3, partials, mixed partials) by autodiff. | +| `discretization` | `discretization`, `linear_algebra::expm` | ZOH on a double integrator, Van Loan process-noise discretization, the filterpy `q_discrete_white_noise` model, and a one-`Dual` derivative through the matrix exponential. | +| `gaussian_integration` | `numerical_integration::gaussian_integration` | Gauss-Legendre (finite), Gauss-Hermite and Gauss-Laguerre (infinite), with the bare-integrand convention. | +| `iterative_integration` | `numerical_integration::iterative_integration` | Boole / Simpson / Trapezoidal rules, multi-variable partial integrals, and infinite / semi-infinite limits. | +| `jacobian_hessian` | `numerical_derivative::{jacobian, hessian}` | Jacobian of a vector of functions and the Hessian of a scalar function. | +| `lie_groups` | `spatial` | SO(3)/SE(3) compose, act on a point, exp/log round trips, geodesic interpolation, and a one-`Dual` autodiff derivative pushed through `exp` ∘ `act`. | +| `linear_algebra` | `linear_algebra` | LU and Cholesky factorizations, linear solves, and the direct 4x4 inverse under a latency + approximation-error stress test on well- and ill-conditioned inputs. | +| `ode` | `ode` | Fixed-step RK4 and adaptive RK45 on the harmonic oscillator (known solution) plus an acrobot, a tumbling quadrotor, and an outer-solar-system N-body, reporting error and conserved-quantity drift. | +| `optimization_solvers` | `optimization` | Gauss-Newton on a well-conditioned linear residual (`y = a + b·t`); when GN is enough vs LM (`curve_fit`). | +| `root_finding` | `root_finding` | Bracketed bisection, Newton with exact derivatives, damped (backtracking) Newton rescuing a far start, and a square-system Newton solve, each printed against its known root. | +| `svd` | `linear_algebra::svd` | Singular value decomposition and Moore-Penrose pseudo-inverse under a robotics stress test (Kabsch rotation recovery, a redundant-arm pseudo-inverse, a near-singular Jacobian, and an overdetermined fit) with latency + approximation error. | +| `vector_field` | `vector_field` | Curl, divergence, line integrals and flux integrals. | + +`linear_algebra` and `svd` also print per-call latency; build them `--release` for representative +numbers. + +## Live showcases Four live demos, one per core module, each an attention-grabbing animated scene that markets the -library's raw speed and accuracy. **Every number on screen is measured live** with +library's raw speed and accuracy. They need the `rerun` feature (on by default) and a +version-matched viewer already up. **Every number on screen is measured live** with `std::time::Instant` inside the demo — nothing is hardcoded. Run each with `--release` (mandatory -for the timing readouts) and the viewer already up. +for the timing readouts): + +```sh +cargo run --release -p multicalc-demos --example +``` Each demo advances its simulation on logical time (a fixed 1 ms per tick / one step per frame), -so the numbers are deterministic and reproducible. An OS scheduling spike can make a tick display late or jitter but never changes -what the demo computes. +so the numbers are deterministic and reproducible. An OS scheduling spike can make a tick display +late or jitter but never changes what the demo computes. The figures below are representative of a modern desktop core (`x86_64`, `--release`). @@ -59,45 +64,62 @@ The figures below are representative of a modern desktop core (`x86_64`, `--rele exact autodiff Jacobians, every single millisecond. **Median solve ≈ 6 µs — under 1 % of the 1 ms budget — with zero missed ticks over 120,000 solves.** - ``` - cargo run --release -p multicalc-viz --example ik_servo - ``` - - ![ik_servo — a 3-link arm running a full LM IK solve every millisecond](examples/support/ik_servo_showcase.gif) + ![ik_servo — a 3-link arm running a full LM IK solve every millisecond](examples/showcase/support/ik_servo_showcase.gif) - **`newton_fractal`** (root finding) — every pixel is a full Newton-system solve with an exact autodiff Jacobian, and the cubic's basins swirl as its roots orbit. **≈ 4 million Newton solves/sec on one core** (a 256×256 grid re-solved at ~60 fps), each converged root accurate to **≈ 5e-15**. - ``` - cargo run --release -p multicalc-viz --example newton_fractal - ``` - - ![newton_fractal — cubic basins swirling, every pixel a full Newton solve](examples/support/newton_fractal_showcase.gif) + ![newton_fractal — cubic basins swirling, every pixel a full Newton solve](examples/showcase/support/newton_fractal_showcase.gif) - **`fourier_ferris`** (integration) — Gauss-Legendre quadrature computes the Fourier coefficients of Ferris's outline; a chain of epicycles then draws the crab. **≈ 600,000 quadrature node evaluations in ≈ 8 ms** at startup, with every coefficient matching the exact closed form to **≈ 1e-15**. - ``` - cargo run --release -p multicalc-viz --example fourier_ferris - ``` - - ![fourier_ferris — an epicycle chain drawing Ferris from Fourier coefficients](examples/support/fourier_ferris_showcase.gif) + ![fourier_ferris — an epicycle chain drawing Ferris from Fourier coefficients](examples/showcase/support/fourier_ferris_showcase.gif) - **`gradient_marbles`** (autodiff) — 2,000 marbles across a 3D Himmelblau landscape, each steered by an exact autodiff gradient every millisecond. **2,000 exact gradients in under 3 µs per tick (~750,000 gradients/ms), and the autodiff-vs-analytic error is pinned at exactly 0.0** on screen. - ``` - cargo run --release -p multicalc-viz --example gradient_marbles - ``` + ![gradient_marbles — 2,000 marbles steered by exact autodiff gradients down a 3D landscape](examples/showcase/support/gradient_marbles_showcase.gif) + +`curve_fit_live` and `curve_fit_record` are two more showcase examples: the first streams a live +Levenberg-Marquardt fit, the second writes a `.rrd` (and a `.csv`) with no viewer needed. + +## Viewer setup + +### Versions + +Rerun SDK `=0.33.1` ⇄ viewer `0.33.1`. The SDK is exact-pinned; the viewer must match. - ![gradient_marbles — 2,000 marbles steered by exact autodiff gradients down a 3D landscape](examples/support/gradient_marbles_showcase.gif) +### Install (for the live showcases) + +`live()` spawns the external Rerun viewer found on PATH, so install it version-matched to the SDK: + +``` +cargo install rerun-cli --locked --version 0.33.1 +# or: pip install rerun-sdk==0.33.1 +# or: cargo binstall rerun-cli --version 0.33.1 +``` + +### Recorded output and the CSV fallback + +`curve_fit_record` needs no viewer; it writes a `.rrd` and a `.csv` to the temp dir: + +``` +cargo run -p multicalc-demos --example curve_fit_record +``` + +Open the printed `.rrd` in the viewer, or render the CSV fallback: + +``` +python demos/plot.py --x t +``` -## WSL usage (viewer on Windows) +### WSL usage (viewer on Windows) The live viewer is a GPU application; under WSL its virtualized GPU often cannot start it. Run the viewer on Windows instead (real GPU) and stream to it from WSL over gRPC. @@ -128,11 +150,11 @@ the viewer on Windows instead (real GPU) and stream to it from WSL over gRPC. rerun ``` -4. From WSL, run the live example. Under WSL it auto-detects the environment and streams to the +4. From WSL, run a live example. Under WSL it auto-detects the environment and streams to the Windows viewer over the shared localhost instead of spawning a local one: ``` - cargo run -p multicalc-viz --example curve_fit_live + cargo run -p multicalc-demos --example curve_fit_live ``` The Windows viewer from step 3 MUST already be running — under WSL the example connects to it @@ -143,5 +165,5 @@ launch the viewer bound to `0.0.0.0`, and allow inbound TCP 9876 in Windows Fire ``` export RERUN_VIZ_URL="rerun+http://$(ip route show default | awk '{print $3}'):9876/proxy" -cargo run -p multicalc-viz --example curve_fit_live +cargo run -p multicalc-demos --example curve_fit_live ``` From 73f7143015f3bf154021b9526f2db35f39c5bcc6 Mon Sep 17 00:00:00 2001 From: Anmol Kathail Date: Tue, 14 Jul 2026 12:59:43 -0500 Subject: [PATCH 7/7] formatting --- demos/examples/basics/gaussian_integration.rs | 5 ++++- demos/examples/basics/iterative_integration.rs | 5 ++++- demos/examples/basics/ode.rs | 5 ++++- demos/examples/basics/root_finding.rs | 5 ++++- demos/examples/basics/svd.rs | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/demos/examples/basics/gaussian_integration.rs b/demos/examples/basics/gaussian_integration.rs index 74107ac..1da4382 100644 --- a/demos/examples/basics/gaussian_integration.rs +++ b/demos/examples/basics/gaussian_integration.rs @@ -31,7 +31,10 @@ fn main() { let poly: f64 = legendre .get_single(&|x| 4.0 * x * x * x - 3.0 * x * x, &[0.0, 2.0]) .unwrap(); - assert!((poly - 8.0).abs() < 1e-9, "Legendre is exact for polynomials"); + assert!( + (poly - 8.0).abs() < 1e-9, + "Legendre is exact for polynomials" + ); report("int_0^2 4x^3-3x^2", poly, 8.0); // non-polynomial integrand: accuracy falls report( diff --git a/demos/examples/basics/iterative_integration.rs b/demos/examples/basics/iterative_integration.rs index e7a3895..3893efb 100644 --- a/demos/examples/basics/iterative_integration.rs +++ b/demos/examples/basics/iterative_integration.rs @@ -26,7 +26,10 @@ fn main() { let f = |x: f64| 2.0 * x; let integrator = IterativeSingle::default(); // Boole's rule, 120 intervals let two_x = integrator.get_single(&f, &[0.0, 2.0]).unwrap(); - assert!((two_x - 4.0).abs() < 1e-9, "Boole is exact for a linear integrand"); + assert!( + (two_x - 4.0).abs() < 1e-9, + "Boole is exact for a linear integrand" + ); println!("int_0^2 2x dx = {two_x:.8} (exact 4)"); // ---- compare the three rules on the same integrand ---- diff --git a/demos/examples/basics/ode.rs b/demos/examples/basics/ode.rs index e3a9965..3014ab0 100644 --- a/demos/examples/basics/ode.rs +++ b/demos/examples/basics/ode.rs @@ -41,7 +41,10 @@ fn harmonic_oscillator() { " y(2*pi) = [{:.12}, {:.12}] max|err| = {max_err:.2e}", yf[0], yf[1] ); - assert!(max_err < 1e-3, "RK4 should track the exact harmonic solution"); + assert!( + max_err < 1e-3, + "RK4 should track the exact harmonic solution" + ); // RK45: adaptive solve to t = 2*pi, then dense-output sampling. let solver = Rk45::default().with_rtol(1e-9).with_atol(1e-12); diff --git a/demos/examples/basics/root_finding.rs b/demos/examples/basics/root_finding.rs index e18f6d6..fb173a5 100644 --- a/demos/examples/basics/root_finding.rs +++ b/demos/examples/basics/root_finding.rs @@ -67,7 +67,10 @@ fn main() { .solve(&system, &[1.5, 0.8]) .unwrap(); let err = (r.root[0] - x_true).abs().max((r.root[1] - y_true).abs()); - assert!(err < 1e-9, "Newton system should converge to the intersection"); + assert!( + err < 1e-9, + "Newton system should converge to the intersection" + ); println!("\nNewton system x^2 + y^2 = 4 and x*y = 1"); println!(" root = [{:.12}, {:.12}]", r.root[0], r.root[1]); println!( diff --git a/demos/examples/basics/svd.rs b/demos/examples/basics/svd.rs index c81134b..4f57f15 100644 --- a/demos/examples/basics/svd.rs +++ b/demos/examples/basics/svd.rs @@ -87,7 +87,10 @@ fn kabsch() { } let rot_err = max_abs(rhat, rot); let ortho_err = max_abs(rhat.transpose() * rhat, Matrix::<3, 3>::identity()); - assert!(rot_err < 1e-9 && ortho_err < 1e-9, "SVD should recover the rotation"); + assert!( + rot_err < 1e-9 && ortho_err < 1e-9, + "SVD should recover the rotation" + ); let label = "Kabsch 3x3"; println!(" {label:<20} {ns:>8.1} ns R-error {rot_err:.1e} orthogonality {ortho_err:.1e}"); }