Skip to main content

dreamwell_math/
lib.rs

1//! # dreamwell-math — Classical Mathematics for Simulation
2//!
3//! Pure mathematical primitives for the Dreamwell engine and research ecosystem.
4//! No engine dependencies. No allocation on hot path. Standalone usable.
5//!
6//! ## Modules
7//! - **Complex**: Complex number arithmetic (add, mul, conj, exp_i, norm)
8//! - **Spatial**: Distance metrics, Gaussian kernels, AABB extraction
9//! - **Wave**: Traveling wave, standing wave, wave packets, dispersion
10//! - **Fibonacci**: Golden ratio, phyllotaxis disc, Fibonacci sphere
11//! - **Interpolation**: Lerp, exponential decay, smoothstep
12//! - **Random**: Deterministic splitmix64 PRNG
13//!
14//! - **Linear Algebra**: Complex matrix multiply (CGEMM), tiled blocking
15//! - **Eigenvalue**: Jacobi rotation solver for Hermitian matrices
16//! - **Matrix Exponential**: Scaling-and-squaring with Padé approximant
17//!
18//! ## Clean Compute Compliance
19//! - Zero heap allocation in caller-provides-output functions
20//! - All functions are `#[inline]` where beneficial
21//! - No external dependencies
22//! - Deterministic: same inputs always produce same outputs
23
24// ══════════════════════════════════════════════════════════════
25// Submodules — Linear Algebra, Eigenvalues, Matrix Exponential
26// ══════════════════════════════════════════════════════════════
27
28pub mod eigen;
29pub mod linalg;
30pub mod matrix_exp;
31
32#[cfg(feature = "gpu-compute")]
33pub mod gpu_linalg;
34
35#[cfg(feature = "gpu-compute")]
36pub mod gpu_training;
37
38// ══════════════════════════════════════════════════════════════
39// Complex Numbers
40// ══════════════════════════════════════════════════════════════
41
42/// Complex number z = re + i·im.
43///
44/// Used as the base type for density matrices, Hamiltonians, and
45/// interference kernels in the quantum simulation layer.
46/// Arithmetic follows standard complex field rules.
47#[derive(Debug, Clone, Copy, Default, PartialEq)]
48#[repr(C)]
49pub struct Complex {
50    pub re: f32,
51    pub im: f32,
52}
53
54// SAFETY: Complex is #[repr(C)] with two f32 fields — trivially Pod and Zeroable.
55#[cfg(feature = "gpu-compute")]
56unsafe impl bytemuck::Pod for Complex {}
57#[cfg(feature = "gpu-compute")]
58unsafe impl bytemuck::Zeroable for Complex {}
59
60impl Complex {
61    pub const ZERO: Self = Self { re: 0.0, im: 0.0 };
62    pub const ONE: Self = Self { re: 1.0, im: 0.0 };
63    pub const I: Self = Self { re: 0.0, im: 1.0 };
64
65    #[inline]
66    pub fn new(re: f32, im: f32) -> Self {
67        Self { re, im }
68    }
69    #[inline]
70    pub fn from_real(r: f32) -> Self {
71        Self { re: r, im: 0.0 }
72    }
73    #[inline]
74    pub fn from_imag(i: f32) -> Self {
75        Self { re: 0.0, im: i }
76    }
77
78    /// Complex conjugate: z* = re - i·im
79    #[inline]
80    pub fn conj(self) -> Self {
81        Self {
82            re: self.re,
83            im: -self.im,
84        }
85    }
86
87    /// Squared magnitude: |z|² = re² + im²
88    #[inline]
89    pub fn norm_sq(self) -> f32 {
90        self.re * self.re + self.im * self.im
91    }
92
93    /// Magnitude: |z| = √(re² + im²)
94    #[inline]
95    pub fn norm(self) -> f32 {
96        self.norm_sq().sqrt()
97    }
98
99    /// Complex multiplication: (a+bi)(c+di) = (ac-bd) + (ad+bc)i
100    #[inline]
101    pub fn mul(self, other: Self) -> Self {
102        Self {
103            re: self.re * other.re - self.im * other.im,
104            im: self.re * other.im + self.im * other.re,
105        }
106    }
107
108    #[inline]
109    pub fn add(self, other: Self) -> Self {
110        Self {
111            re: self.re + other.re,
112            im: self.im + other.im,
113        }
114    }
115
116    #[inline]
117    pub fn sub(self, other: Self) -> Self {
118        Self {
119            re: self.re - other.re,
120            im: self.im - other.im,
121        }
122    }
123
124    #[inline]
125    pub fn scale(self, s: f32) -> Self {
126        Self {
127            re: self.re * s,
128            im: self.im * s,
129        }
130    }
131
132    /// Euler's formula: e^{iθ} = cos(θ) + i·sin(θ)
133    #[inline]
134    pub fn exp_i(theta: f32) -> Self {
135        Self {
136            re: theta.cos(),
137            im: theta.sin(),
138        }
139    }
140
141    /// Complex exponential: e^z = e^re · (cos(im) + i·sin(im))
142    #[inline]
143    pub fn exp(self) -> Self {
144        let r = self.re.exp();
145        Self {
146            re: r * self.im.cos(),
147            im: r * self.im.sin(),
148        }
149    }
150}
151
152// ══════════════════════════════════════════════════════════════
153// Spatial Mathematics
154// ══════════════════════════════════════════════════════════════
155
156/// Squared Euclidean distance between two 3D points.
157/// Avoids sqrt for comparison (cheaper than full distance).
158#[inline]
159pub fn distance_sq_3d(a: &[f32; 3], b: &[f32; 3]) -> f32 {
160    let dx = a[0] - b[0];
161    let dy = a[1] - b[1];
162    let dz = a[2] - b[2];
163    dx * dx + dy * dy + dz * dz
164}
165
166/// Euclidean distance between two 3D points.
167#[inline]
168pub fn distance_3d(a: &[f32; 3], b: &[f32; 3]) -> f32 {
169    distance_sq_3d(a, b).sqrt()
170}
171
172/// Gaussian radial basis function: exp(-d²/σ²).
173/// Returns 1.0 at center, decays to ~0 at distance 3σ.
174/// Used for spatial attention and relevance scoring.
175#[inline]
176pub fn gaussian_kernel(dist_sq: f32, sigma_sq: f32) -> f32 {
177    (-dist_sq / sigma_sq).exp()
178}
179
180/// 3D dot product.
181#[inline]
182pub fn vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
183    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
184}
185
186/// 3D vector length.
187#[inline]
188pub fn vec3_len(v: [f32; 3]) -> f32 {
189    vec3_dot(v, v).sqrt()
190}
191
192/// 3D vector scale.
193#[inline]
194pub fn vec3_scale(v: [f32; 3], s: f32) -> [f32; 3] {
195    [v[0] * s, v[1] * s, v[2] * s]
196}
197
198/// 3D cross product.
199#[inline]
200pub fn vec3_cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
201    [
202        a[1] * b[2] - a[2] * b[1],
203        a[2] * b[0] - a[0] * b[2],
204        a[0] * b[1] - a[1] * b[0],
205    ]
206}
207
208/// 3D vector normalize (returns zero vector if length < epsilon).
209#[inline]
210pub fn vec3_normalize(v: [f32; 3]) -> [f32; 3] {
211    let len = vec3_len(v);
212    if len < 1e-8 { [0.0; 3] } else { vec3_scale(v, 1.0 / len) }
213}
214
215// ══════════════════════════════════════════════════════════════
216// Wave Mathematics
217// ══════════════════════════════════════════════════════════════
218
219/// Traveling wave: y = A·sin(kx - ωt).
220/// k = wave number (2π/λ), ω = angular frequency (2πf).
221/// Propagates in +x direction.
222///
223/// Reference: d'Alembert wave equation solution (1747).
224#[inline]
225pub fn traveling_wave(x: f32, t: f32, k: f32, omega: f32, amplitude: f32) -> f32 {
226    amplitude * (k * x - omega * t).sin()
227}
228
229/// Standing wave: y = A·sin(kx)·cos(ωt).
230/// Created by superposition of two counter-propagating traveling waves.
231#[inline]
232pub fn standing_wave(x: f32, t: f32, k: f32, omega: f32, amplitude: f32) -> f32 {
233    amplitude * (k * x).sin() * (omega * t).cos()
234}
235
236/// 2D traveling wave with perpendicular tapering:
237/// y = A·sin(k·along - ωt)·cos(k·perp·taper)
238///
239/// Used by the DreamWaveController for fabric-like particle motion.
240#[inline]
241pub fn traveling_wave_2d(along: f32, perp: f32, t: f32, k: f32, omega: f32, amplitude: f32, taper: f32) -> f32 {
242    amplitude * (k * along - omega * t).sin() * (k * perp * taper).cos()
243}
244
245/// Wave number from wavelength: k = 2π/λ.
246#[inline]
247pub fn wave_number(wavelength: f32) -> f32 {
248    std::f32::consts::TAU / wavelength
249}
250
251/// Angular frequency from frequency: ω = 2πf.
252#[inline]
253pub fn angular_frequency(frequency: f32) -> f32 {
254    std::f32::consts::TAU * frequency
255}
256
257/// Linear dispersion relation: ω = c·k (non-dispersive medium).
258#[inline]
259pub fn dispersion_linear(k: f32, speed: f32) -> f32 {
260    speed * k
261}
262
263/// Gaussian wave packet: ψ(x,t) = exp(-(x-x0-v_g·t)²/(4σ²)) · exp(i(k0·x - ω0·t))
264/// Returns the real part (amplitude envelope × carrier).
265#[inline]
266pub fn wave_packet_gaussian(x: f32, t: f32, k0: f32, sigma: f32, group_velocity: f32) -> f32 {
267    let center = group_velocity * t;
268    let envelope = (-(x - center).powi(2) / (4.0 * sigma * sigma)).exp();
269    let carrier = (k0 * x - dispersion_linear(k0, group_velocity) * t).cos();
270    envelope * carrier
271}
272
273// ══════════════════════════════════════════════════════════════
274// Fibonacci / Golden Ratio
275// ══════════════════════════════════════════════════════════════
276
277/// The golden ratio φ = (1 + √5) / 2 ≈ 1.6180339887.
278#[inline]
279pub fn golden_ratio() -> f32 {
280    (1.0 + 5.0f32.sqrt()) / 2.0
281}
282
283/// The golden angle θ = 2π/φ² ≈ 2.3999632297 radians ≈ 137.508°.
284/// Used in phyllotaxis for optimal packing on discs and spheres.
285///
286/// Reference: Vogel, H. (1979). "A better model for sunflower phyllotaxis."
287#[inline]
288pub fn golden_angle() -> f32 {
289    let phi = golden_ratio();
290    std::f32::consts::TAU / (phi * phi)
291}
292
293/// Fibonacci sphere: N points uniformly distributed on a unit sphere
294/// using the golden ratio spiral.
295///
296/// Returns Vec of [x, y, z] unit vectors.
297/// Deterministic: same count always produces the same distribution.
298pub fn fibonacci_sphere(count: u32) -> Vec<[f32; 3]> {
299    let phi = golden_ratio();
300    let angle_inc = std::f32::consts::TAU * phi;
301    let mut points = Vec::with_capacity(count as usize);
302    for i in 0..count {
303        let t = (i as f32 + 0.5) / count as f32;
304        let polar = (1.0 - 2.0 * t).acos();
305        let azimuth = angle_inc * i as f32;
306        points.push([polar.sin() * azimuth.cos(), polar.cos(), polar.sin() * azimuth.sin()]);
307    }
308    points
309}
310
311/// Phyllotaxis disc: N points on a flat disc with golden angle spacing.
312/// Radius grows as √(index) for uniform area density.
313///
314/// `rotation`: additional rotation offset (radians), e.g. for animation.
315pub fn phyllotaxis_disc(count: u32, radius: f32, rotation: f32) -> Vec<[f32; 3]> {
316    let ga = golden_angle();
317    let mut points = Vec::with_capacity(count as usize);
318    for i in 0..count {
319        let frac = (i as f32 + 0.5) / count as f32;
320        let r = frac.sqrt() * radius;
321        let theta = i as f32 * ga + rotation;
322        points.push([r * theta.cos(), 0.0, r * theta.sin()]);
323    }
324    points
325}
326
327// ══════════════════════════════════════════════════════════════
328// Interpolation
329// ══════════════════════════════════════════════════════════════
330
331/// Linear interpolation: a + (b - a) · t.
332#[inline]
333pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
334    a + (b - a) * t
335}
336
337/// 3D linear interpolation.
338#[inline]
339pub fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
340    [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)]
341}
342
343/// Exponential decay toward target: current + (target - current) · (1 - e^{-rate·dt}).
344///
345/// This is the discrete-time solution to the first-order ODE dx/dt = -rate·(x - target).
346/// Also known as the Lindblad decoherence kernel when applied to off-diagonal elements.
347///
348/// Properties:
349/// - rate = 0: no change
350/// - rate -> infinity: instant snap to target
351/// - Typical game rates: 2.0 (gentle, ~1.5s), 8.0 (snappy, ~0.25s)
352#[inline]
353pub fn exp_decay(current: f32, target: f32, rate: f32, dt: f32) -> f32 {
354    current + (target - current) * (1.0 - (-rate * dt).exp())
355}
356
357/// Smoothstep: Hermite interpolation between edge0 and edge1.
358/// Returns 0 below edge0, 1 above edge1, smooth curve between.
359#[inline]
360pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
361    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
362    t * t * (3.0 - 2.0 * t)
363}
364
365// ══════════════════════════════════════════════════════════════
366// Deterministic Random
367// ══════════════════════════════════════════════════════════════
368
369/// Splitmix64: deterministic PRNG. Returns (value, next_state).
370///
371/// Used throughout the engine for reproducible benchmarks, seed-based
372/// procedural generation, and deterministic Born rule measurement.
373///
374/// Reference: Vigna, S. (2015). "An experimental exploration of Marsaglia's
375/// xorshift generators, scrambled."
376#[inline]
377pub fn splitmix64(state: u64) -> (u64, u64) {
378    let s = state.wrapping_add(0x9E3779B97F4A7C15);
379    let mut z = s;
380    z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
381    z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
382    z = z ^ (z >> 31);
383    (z, s)
384}
385
386/// Splitmix64 -> f32 in [0, 1). Deterministic.
387#[inline]
388pub fn splitmix64_f32(state: u64) -> (f32, u64) {
389    let (val, next) = splitmix64(state);
390    ((val >> 40) as f32 / (1u64 << 24) as f32, next)
391}
392
393// ══════════════════════════════════════════════════════════════
394// Atomic Operations (for parallel solvers)
395// ══════════════════════════════════════════════════════════════
396
397/// Atomic f32 addition via compare-exchange on AtomicU32.
398///
399/// Used by the Parallel Jacobi solver for lock-free impulse accumulation
400/// across rayon thread boundaries. Zero allocation. Wait-free under low contention.
401#[inline]
402pub fn atomic_f32_add(atom: &std::sync::atomic::AtomicU32, val: f32) {
403    use std::sync::atomic::Ordering;
404    loop {
405        let bits = atom.load(Ordering::Relaxed);
406        let current = f32::from_bits(bits);
407        let new = current + val;
408        match atom.compare_exchange_weak(bits, new.to_bits(), Ordering::Relaxed, Ordering::Relaxed) {
409            Ok(_) => break,
410            Err(_) => {} // CAS retry on contention
411        }
412    }
413}
414
415// ══════════════════════════════════════════════════════════════
416// Spatial Hashing & Broadphase Primitives
417// ══════════════════════════════════════════════════════════════
418
419/// Forward half-neighborhood offsets for broadphase pair generation.
420/// 14 cells: self + 13 lexicographically forward neighbors.
421/// Produces zero-duplicate pairs by construction (Causal Spatial Mask).
422///
423/// This is the spatial analog of the causal attention mask in transformers:
424/// only compute the upper triangle of the spatial relevance matrix.
425///
426/// Reference: Allen & Tildesley (1987), half-neighbor Verlet list.
427/// Dreamwell innovation: applied to hash-grid broadphase (novel for game engines).
428pub const HALF_NEIGHBORHOOD: [(i32, i32, i32); 14] = [
429    (0, 0, 0), // self (j > i guard)
430    (0, 0, 1),
431    (0, 1, -1),
432    (0, 1, 0),
433    (0, 1, 1),
434    (1, -1, -1),
435    (1, -1, 0),
436    (1, -1, 1),
437    (1, 0, -1),
438    (1, 0, 0),
439    (1, 0, 1),
440    (1, 1, -1),
441    (1, 1, 0),
442    (1, 1, 1),
443];
444
445/// FNV-1a hash of a 3D integer cell coordinate.
446/// Used for spatial hash grid broadphase. Deterministic, fast, low collision rate.
447#[inline]
448pub fn fnv1a_3d(ix: i32, iy: i32, iz: i32) -> u64 {
449    let mut h: u64 = 0xcbf29ce484222325;
450    for byte in ix
451        .to_le_bytes()
452        .iter()
453        .chain(iy.to_le_bytes().iter())
454        .chain(iz.to_le_bytes().iter())
455    {
456        h ^= *byte as u64;
457        h = h.wrapping_mul(0x100000001b3);
458    }
459    h
460}
461
462/// Adiabatic force distribution: returns the fractional force to apply
463/// on the current step within an adiabatic window.
464///
465/// Based on the Quantum Adiabatic Theorem (Born & Fock, 1928):
466/// slow Hamiltonian changes keep the system near its ground state.
467/// Applied to game physics: spreading impulses over N steps preserves
468/// broadphase temporal coherence and eliminates p99 frame spikes.
469///
470/// Returns `force / window_size` if within the window, 0.0 otherwise.
471#[inline]
472pub fn adiabatic_fraction(step: u32, interval: u32, window: u32) -> f32 {
473    let phase = step % interval;
474    if step > 0 && phase < window {
475        1.0 / window as f32
476    } else {
477        0.0
478    }
479}
480
481// ══════════════════════════════════════════════════════════════
482// Physics Primitives (extracted from Parallel Jacobi solver)
483// ══════════════════════════════════════════════════════════════
484
485/// Contact impulse magnitude (sequential impulse formula).
486/// j = -(1 + e) * v_along_normal / inv_mass_sum
487///
488/// Reference: Catto, E. (2005). "Iterative Dynamics with Temporal Coherence." GDC.
489#[inline]
490pub fn contact_impulse(v_along_normal: f32, restitution: f32, inv_mass_sum: f32) -> f32 {
491    if inv_mass_sum < 1e-8 {
492        return 0.0;
493    }
494    -(1.0 + restitution) * v_along_normal / inv_mass_sum
495}
496
497/// Baumgarte positional correction for penetration resolution.
498/// correction = max(0, depth - slop) * pct / inv_mass_sum
499///
500/// Prevents sinking by directly adjusting positions after impulse resolution.
501/// Standard technique in game physics (Baumgarte stabilization, 1972).
502#[inline]
503pub fn baumgarte_correction(depth: f32, slop: f32, pct: f32, inv_mass_sum: f32) -> f32 {
504    if inv_mass_sum < 1e-8 {
505        return 0.0;
506    }
507    (depth - slop).max(0.0) * pct / inv_mass_sum
508}
509
510/// Coulomb friction clamp: limits tangential impulse to mu * |normal impulse|.
511/// jt_clamped = clamp(jt, -|j_normal| * mu, |j_normal| * mu)
512#[inline]
513pub fn coulomb_friction_clamp(jt: f32, j_normal_abs: f32, mu: f32) -> f32 {
514    jt.clamp(-j_normal_abs * mu, j_normal_abs * mu)
515}
516
517/// Union-find root with path compression. O(alpha(n)) amortized.
518/// Used for island splitting in the Parallel Jacobi solver.
519pub fn union_find_root(parent: &mut [usize], mut x: usize) -> usize {
520    while parent[x] != x {
521        parent[x] = parent[parent[x]]; // path halving
522        x = parent[x];
523    }
524    x
525}
526
527/// Union-find merge by rank. O(alpha(n)) amortized.
528pub fn union_find_merge(parent: &mut [usize], rank: &mut [u8], a: usize, b: usize) {
529    let ra = union_find_root(parent, a);
530    let rb = union_find_root(parent, b);
531    if ra == rb {
532        return;
533    }
534    if rank[ra] < rank[rb] {
535        parent[ra] = rb;
536    } else if rank[ra] > rank[rb] {
537        parent[rb] = ra;
538    } else {
539        parent[rb] = ra;
540        rank[ra] += 1;
541    }
542}
543
544// ══════════════════════════════════════════════════════════════
545// Additional Interpolation & Easing
546// ══════════════════════════════════════════════════════════════
547
548/// Quaternion spherical linear interpolation (slerp).
549/// Interpolates between two unit quaternions [x, y, z, w] along the great arc.
550pub fn slerp(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
551    let mut dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
552    let mut b = b;
553    if dot < 0.0 {
554        // shortest path
555        b = [-b[0], -b[1], -b[2], -b[3]];
556        dot = -dot;
557    }
558    if dot > 0.9995 {
559        // near-parallel: lerp fallback
560        let mut r = [0.0f32; 4];
561        for i in 0..4 {
562            r[i] = a[i] + (b[i] - a[i]) * t;
563        }
564        let len = (r[0] * r[0] + r[1] * r[1] + r[2] * r[2] + r[3] * r[3]).sqrt();
565        if len > 1e-8 {
566            for i in 0..4 {
567                r[i] /= len;
568            }
569        }
570        return r;
571    }
572    let theta = dot.clamp(-1.0, 1.0).acos();
573    let sin_theta = theta.sin();
574    let wa = ((1.0 - t) * theta).sin() / sin_theta;
575    let wb = (t * theta).sin() / sin_theta;
576    [
577        a[0] * wa + b[0] * wb,
578        a[1] * wa + b[1] * wb,
579        a[2] * wa + b[2] * wb,
580        a[3] * wa + b[3] * wb,
581    ]
582}
583
584/// Range remapping: maps value from [in_min, in_max] to [out_min, out_max].
585#[inline]
586pub fn remap(value: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
587    let t = (value - in_min) / (in_max - in_min);
588    out_min + (out_max - out_min) * t
589}
590
591/// Cubic ease-in-out: smooth acceleration and deceleration.
592/// t in [0, 1]. Returns smooth curve from 0 to 1.
593#[inline]
594pub fn ease_in_out_cubic(t: f32) -> f32 {
595    if t < 0.5 {
596        4.0 * t * t * t
597    } else {
598        1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
599    }
600}
601
602/// Saturating clamp to [0, 1].
603#[inline]
604pub fn clamp01(v: f32) -> f32 {
605    v.clamp(0.0, 1.0)
606}
607
608// ══════════════════════════════════════════════════════════════
609// Tests
610// ══════════════════════════════════════════════════════════════
611
612#[cfg(test)]
613mod tests {
614    use super::*;
615
616    #[test]
617    fn complex_arithmetic() {
618        let a = Complex::new(1.0, 2.0);
619        let b = Complex::new(3.0, -1.0);
620        let c = a.mul(b); // (1+2i)(3-i) = 3-i+6i-2i² = 5+5i
621        assert!((c.re - 5.0).abs() < 1e-6);
622        assert!((c.im - 5.0).abs() < 1e-6);
623    }
624
625    #[test]
626    fn complex_euler() {
627        let z = Complex::exp_i(std::f32::consts::PI); // e^{iπ} = -1
628        assert!((z.re - (-1.0)).abs() < 1e-5);
629        assert!(z.im.abs() < 1e-5);
630    }
631
632    #[test]
633    fn complex_conjugate() {
634        let z = Complex::new(3.0, 4.0);
635        let zc = z.conj();
636        assert_eq!(zc.re, 3.0);
637        assert_eq!(zc.im, -4.0);
638        assert!((z.mul(zc).im).abs() < 1e-6); // z·z* is real
639    }
640
641    #[test]
642    fn distance_sq() {
643        let a = [1.0, 2.0, 3.0];
644        let b = [4.0, 6.0, 3.0];
645        assert!((distance_sq_3d(&a, &b) - 25.0).abs() < 1e-6); // 9+16+0
646    }
647
648    #[test]
649    fn gaussian_at_center() {
650        assert!((gaussian_kernel(0.0, 1.0) - 1.0).abs() < 1e-6);
651    }
652
653    #[test]
654    fn gaussian_decays() {
655        let near = gaussian_kernel(1.0, 4.0);
656        let far = gaussian_kernel(9.0, 4.0);
657        assert!(near > far);
658    }
659
660    #[test]
661    fn traveling_wave_oscillates() {
662        let y0 = traveling_wave(0.0, 0.0, 1.0, 1.0, 1.0);
663        let y1 = traveling_wave(std::f32::consts::PI, 0.0, 1.0, 1.0, 1.0);
664        assert!(y0.abs() < 1e-6); // sin(0) = 0
665        assert!(y1.abs() < 1e-6); // sin(π) ≈ 0
666    }
667
668    #[test]
669    fn wave_number_from_wavelength() {
670        let k = wave_number(1.0);
671        assert!((k - std::f32::consts::TAU).abs() < 1e-5);
672    }
673
674    #[test]
675    fn golden_ratio_value() {
676        assert!((golden_ratio() - 1.6180339).abs() < 1e-5);
677    }
678
679    #[test]
680    fn golden_angle_value() {
681        assert!((golden_angle() - 2.3999632).abs() < 1e-4);
682    }
683
684    #[test]
685    fn fibonacci_sphere_count() {
686        let pts = fibonacci_sphere(128);
687        assert_eq!(pts.len(), 128);
688        for p in &pts {
689            let len = vec3_len(*p);
690            assert!((len - 1.0).abs() < 0.01);
691        }
692    }
693
694    #[test]
695    fn phyllotaxis_disc_count() {
696        let pts = phyllotaxis_disc(64, 1.0, 0.0);
697        assert_eq!(pts.len(), 64);
698    }
699
700    #[test]
701    fn lerp_endpoints() {
702        assert_eq!(lerp(0.0, 10.0, 0.0), 0.0);
703        assert_eq!(lerp(0.0, 10.0, 1.0), 10.0);
704        assert!((lerp(0.0, 10.0, 0.5) - 5.0).abs() < 1e-6);
705    }
706
707    #[test]
708    fn exp_decay_converges() {
709        let mut val = 0.0f32;
710        for _ in 0..100 {
711            val = exp_decay(val, 1.0, 8.0, 1.0 / 60.0);
712        }
713        assert!((val - 1.0).abs() < 0.01);
714    }
715
716    #[test]
717    fn smoothstep_boundaries() {
718        assert!((smoothstep(0.0, 1.0, -0.5)).abs() < 1e-6);
719        assert!((smoothstep(0.0, 1.0, 1.5) - 1.0).abs() < 1e-6);
720        assert!((smoothstep(0.0, 1.0, 0.5) - 0.5).abs() < 0.1); // ~0.5 at midpoint
721    }
722
723    #[test]
724    fn splitmix64_deterministic() {
725        let (a, _) = splitmix64(42);
726        let (b, _) = splitmix64(42);
727        assert_eq!(a, b);
728    }
729
730    #[test]
731    fn splitmix64_f32_range() {
732        let (val, _) = splitmix64_f32(42);
733        assert!(val >= 0.0 && val < 1.0);
734    }
735
736    #[test]
737    fn vec3_operations() {
738        let a = [1.0, 0.0, 0.0];
739        let b = [0.0, 1.0, 0.0];
740        let cross = vec3_cross(a, b);
741        assert!((cross[2] - 1.0).abs() < 1e-6); // x × y = z
742    }
743
744    #[test]
745    fn half_neighborhood_14_cells() {
746        assert_eq!(HALF_NEIGHBORHOOD.len(), 14);
747    }
748
749    #[test]
750    fn fnv1a_deterministic() {
751        assert_eq!(fnv1a_3d(1, 2, 3), fnv1a_3d(1, 2, 3));
752        assert_ne!(fnv1a_3d(1, 2, 3), fnv1a_3d(3, 2, 1));
753    }
754
755    #[test]
756    fn adiabatic_in_window() {
757        assert!((adiabatic_fraction(1, 60, 10) - 0.1).abs() < 1e-6);
758        assert_eq!(adiabatic_fraction(15, 60, 10), 0.0); // outside window
759    }
760
761    #[test]
762    fn contact_impulse_separating() {
763        // Bodies separating (v_along_n > 0): impulse should be negative (or zero for game use)
764        let j = contact_impulse(1.0, 0.5, 2.0);
765        assert!(j < 0.0); // -(1+0.5)*1.0/2.0 = -0.75
766    }
767
768    #[test]
769    fn union_find_basic() {
770        let mut parent: Vec<usize> = (0..5).collect();
771        let mut rank = vec![0u8; 5];
772        union_find_merge(&mut parent, &mut rank, 0, 1);
773        union_find_merge(&mut parent, &mut rank, 2, 3);
774        union_find_merge(&mut parent, &mut rank, 1, 3);
775        assert_eq!(union_find_root(&mut parent, 0), union_find_root(&mut parent, 3));
776    }
777
778    #[test]
779    fn slerp_endpoints() {
780        let a = [0.0, 0.0, 0.0, 1.0]; // identity quaternion
781        let b = [0.0, 1.0, 0.0, 0.0]; // 180° around Y
782        let mid = slerp(a, b, 0.0);
783        assert!((mid[3] - 1.0).abs() < 0.01); // at t=0, should be near a
784    }
785
786    #[test]
787    fn remap_basic() {
788        assert!((remap(5.0, 0.0, 10.0, 0.0, 100.0) - 50.0).abs() < 1e-6);
789    }
790
791    #[test]
792    fn ease_cubic_endpoints() {
793        assert!(ease_in_out_cubic(0.0).abs() < 1e-6);
794        assert!((ease_in_out_cubic(1.0) - 1.0).abs() < 1e-6);
795    }
796
797    #[test]
798    fn clamp01_bounds() {
799        assert_eq!(clamp01(-0.5), 0.0);
800        assert_eq!(clamp01(1.5), 1.0);
801        assert!((clamp01(0.5) - 0.5).abs() < 1e-6);
802    }
803}