Skip to main content

proof_engine/math/
eval.rs

1//! MathFunction enum and evaluation.
2//!
3//! Every visual property of every Glyph can be driven by a MathFunction.
4//! There are no keyframes, no tweens — only continuous functions of time.
5
6use glam::Vec3;
7use std::f32::consts::{PI, TAU};
8use super::attractors;
9
10/// A continuous mathematical function that maps (time, input) → f32.
11///
12/// Functions are composable: `Sum`, `Product`, and `Chain` allow building
13/// arbitrarily complex behaviors from simple primitives.
14#[derive(Debug, Clone)]
15pub enum MathFunction {
16    // ── Basic ─────────────────────────────────────────────────────────────────
17    /// Always returns the same value.
18    Constant(f32),
19    /// Linear function: slope * t + offset.
20    Linear { slope: f32, offset: f32 },
21
22    // ── Oscillation ───────────────────────────────────────────────────────────
23    /// Sinusoidal oscillation.
24    Sine { amplitude: f32, frequency: f32, phase: f32 },
25    /// Cosinusoidal oscillation.
26    Cosine { amplitude: f32, frequency: f32, phase: f32 },
27    /// Triangle wave oscillation.
28    Triangle { amplitude: f32, frequency: f32, phase: f32 },
29    /// Square wave (snaps between +/- amplitude).
30    Square { amplitude: f32, frequency: f32, duty: f32 },
31
32    // ── Organic motion ────────────────────────────────────────────────────────
33    /// Lorenz attractor trajectory (x-coordinate).
34    Lorenz { sigma: f32, rho: f32, beta: f32, scale: f32 },
35    /// Perlin noise.
36    Perlin { frequency: f32, octaves: u8, amplitude: f32 },
37    /// Simplex noise.
38    Simplex { frequency: f32, amplitude: f32 },
39
40    // ── Convergence / divergence ──────────────────────────────────────────────
41    /// Exponential approach to target: target + (start - target) * e^(-rate * t).
42    Exponential { start: f32, target: f32, rate: f32 },
43    /// Logistic map iteration: x_{n+1} = r * x_n * (1 - x_n). Exhibits bifurcation.
44    LogisticMap { r: f32, x0: f32 },
45    /// Collatz sequence mapped to a float path. Bouncy convergence.
46    Collatz { seed: u64, scale: f32 },
47
48    // ── Attraction / orbiting ─────────────────────────────────────────────────
49    /// Circular or elliptical orbit around a center point.
50    Orbit { center: Vec3, radius: f32, speed: f32, eccentricity: f32 },
51    /// Outward (or inward) spiral.
52    Spiral { center: Vec3, radius_rate: f32, speed: f32 },
53    /// Golden-ratio spiral.
54    GoldenSpiral { center: Vec3, scale: f32, speed: f32 },
55    /// Lissajous figure.
56    Lissajous { a: f32, b: f32, delta: f32, scale: f32 },
57
58    // ── Strange attractor ─────────────────────────────────────────────────────
59    /// Move along a strange attractor trajectory (x-coordinate output).
60    StrangeAttractor {
61        attractor_type: crate::math::attractors::AttractorType,
62        scale: f32,
63        strength: f32,
64    },
65
66    // ── Fractal ───────────────────────────────────────────────────────────────
67    /// Mandelbrot escape-time mapped to float.
68    MandelbrotEscape { c_real: f32, c_imag: f32, scale: f32 },
69    /// Julia set escape-time mapped to float.
70    JuliaSet { c_real: f32, c_imag: f32, scale: f32 },
71
72    // ── Damping / weight ──────────────────────────────────────────────────────
73    /// Spring-damper: approaches target with optional overshoot.
74    SpringDamper { target: f32, stiffness: f32, damping: f32 },
75    /// Critically damped spring (no overshoot, fastest convergence).
76    CriticallyDamped { target: f32, speed: f32 },
77
78    // ── Special biological ────────────────────────────────────────────────────
79    /// Realistic cardiac waveform (P, QRS, T waves).
80    HeartBeat { bpm: f32, intensity: f32 },
81    /// Four-phase breathing cycle: inhale → hold → exhale → hold.
82    Breathing { rate: f32, depth: f32 },
83    /// Physical pendulum with gravity and damping.
84    Pendulum { length: f32, gravity: f32, damping: f32 },
85    /// Traveling wave.
86    Wave { wavelength: f32, speed: f32, amplitude: f32, decay: f32 },
87
88    // ── Sawtooth / pulse ──────────────────────────────────────────────────────
89    /// Sawtooth wave (rising ramp that resets to -1).
90    Sawtooth { amplitude: f32, frequency: f32, phase: f32 },
91    /// Ramp: linearly rises from 0 to amplitude over `duration`, then resets.
92    Ramp { amplitude: f32, duration: f32 },
93
94    // ── Signal functions ──────────────────────────────────────────────────────
95    /// Sinc function: sin(π·x) / (π·x). Used for signal reconstruction.
96    Sinc { frequency: f32, amplitude: f32 },
97    /// Gaussian bell curve: amplitude · e^(-((t-center)/width)²).
98    Gaussian { amplitude: f32, center: f32, width: f32 },
99    /// Two-frequency beat: interference between slightly detuned oscillators.
100    BeatFrequency { freq1: f32, freq2: f32, amplitude: f32 },
101    /// Wave packet: Gaussian envelope × carrier sine.
102    WavePacket { carrier_freq: f32, envelope_width: f32, amplitude: f32, center: f32 },
103    /// Fourier series: sum of harmonics with user-specified coefficients.
104    FourierSeries { fundamental: f32, coefficients: Vec<(f32, f32)> }, // (sin_coeff, cos_coeff)
105
106    // ── Activation / shaping ─────────────────────────────────────────────────
107    /// Sigmoid (logistic) function maps any input → (0, 1).
108    Sigmoid { steepness: f32, center: f32 },
109    /// Hyperbolic tangent: amplitude · tanh(steepness · t).
110    Tanh { amplitude: f32, steepness: f32 },
111    /// Soft-plus: amplitude · ln(1 + e^(steepness·t)) / steepness.
112    SoftPlus { amplitude: f32, steepness: f32 },
113    /// Rectified linear: max(0, slope·t + offset).
114    Relu { slope: f32, offset: f32 },
115    /// Power law: sign(t) · |t|^exponent.
116    PowerLaw { exponent: f32, scale: f32 },
117
118    // ── Chaos / dynamical systems ─────────────────────────────────────────────
119    /// Van der Pol oscillator (nonlinear oscillator with limit cycle).
120    VanDerPol { mu: f32, amplitude: f32 },
121    /// Duffing oscillator (chaotic forced nonlinear oscillator).
122    Duffing { alpha: f32, beta: f32, delta: f32, gamma: f32, omega: f32 },
123    /// Tent map iterated n times: exhibits period-doubling route to chaos.
124    TentMap { r: f32, x0: f32 },
125    /// Hénon map: chaotic 2D map (returns x-coordinate).
126    HenonMap { a: f32, b: f32, x0: f32, y0: f32 },
127    /// Rössler system x-coordinate trajectory.
128    Roessler { a: f32, b: f32, c: f32, scale: f32 },
129
130    // ── Physical simulations ──────────────────────────────────────────────────
131    /// Double pendulum: chaotic system of two connected pendulums (angle θ₁).
132    DoublePendulum { l1: f32, l2: f32, m1: f32, m2: f32, theta1_0: f32, theta2_0: f32 },
133    /// Projectile motion: height above ground (y-axis) at time t.
134    Projectile { v0: f32, angle_deg: f32, gravity: f32 },
135    /// Simple harmonic motion with optional initial displacement.
136    SimpleHarmonic { omega: f32, amplitude: f32, phase: f32, decay: f32 },
137    /// Damped sine with exact analytic form.
138    DampedSine { omega: f32, zeta: f32, amplitude: f32, phase: f32 },
139    /// Epicyclic motion: small circle of radius `r2` orbiting at radius `r1`.
140    Epicycle { r1: f32, r2: f32, omega1: f32, omega2: f32 },
141
142    // ── Statistical / noise ───────────────────────────────────────────────────
143    /// Fractional Brownian motion: multi-octave noise with Hurst exponent H.
144    FractionalBrownian { frequency: f32, octaves: u8, hurst: f32, amplitude: f32 },
145    /// Domain-warped noise: the input to the noise is itself displaced by another noise.
146    DomainWarp { frequency: f32, warp_strength: f32, octaves: u8, amplitude: f32 },
147    /// Cellular / Worley noise: distance to nearest point in a Poisson point process.
148    Cellular { frequency: f32, amplitude: f32 },
149
150    // ── Composition ───────────────────────────────────────────────────────────
151    /// Add two function outputs.
152    Sum(Box<MathFunction>, Box<MathFunction>),
153    /// Multiply two function outputs.
154    Product(Box<MathFunction>, Box<MathFunction>),
155    /// Chain: output of each function feeds as input to the next.
156    Chain(Vec<MathFunction>),
157    /// Modulate amplitude of inner function by outer function output.
158    Modulate { carrier: Box<MathFunction>, modulator: Box<MathFunction> },
159    /// Clamp the output to [min, max].
160    Clamp { inner: Box<MathFunction>, min: f32, max: f32 },
161    /// Absolute value of inner.
162    Abs(Box<MathFunction>),
163    /// Scale: multiply output of inner by `factor`.
164    Scale { inner: Box<MathFunction>, factor: f32 },
165    /// Offset: add `offset` to output of inner.
166    Offset { inner: Box<MathFunction>, offset: f32 },
167    /// Invert: negate the output of inner.
168    Invert(Box<MathFunction>),
169    /// Normalize inner to [-1, 1] over a given sample window of `t_range` seconds.
170    /// (sampled at `steps` points; expensive — use sparingly.)
171    Normalize { inner: Box<MathFunction>, t_range: f32, steps: u32 },
172    /// Delay: evaluate inner at (t - delay_seconds).
173    Delay { inner: Box<MathFunction>, delay: f32 },
174    /// Mirror: map t → |t mod (2·period) - period| creating a symmetric waveform.
175    Mirror { inner: Box<MathFunction>, period: f32 },
176}
177
178impl MathFunction {
179    /// Evaluate the function at time `t` with chained `input`.
180    pub fn evaluate(&self, t: f32, input: f32) -> f32 {
181        match self {
182            // ── Basic ─────────────────────────────────────────────────────────
183            MathFunction::Constant(v) => *v,
184            MathFunction::Linear { slope, offset } => slope * t + offset,
185
186            // ── Oscillation ───────────────────────────────────────────────────
187            MathFunction::Sine { amplitude, frequency, phase } => {
188                amplitude * (t * frequency * TAU + phase).sin()
189            }
190            MathFunction::Cosine { amplitude, frequency, phase } => {
191                amplitude * (t * frequency * TAU + phase).cos()
192            }
193            MathFunction::Triangle { amplitude, frequency, phase } => {
194                let p = (t * frequency + phase / TAU).fract();
195                amplitude * (if p < 0.5 { 4.0 * p - 1.0 } else { 3.0 - 4.0 * p })
196            }
197            MathFunction::Square { amplitude, frequency, duty } => {
198                let p = (t * frequency).fract();
199                if p < *duty { *amplitude } else { -amplitude }
200            }
201
202            // ── Organic ───────────────────────────────────────────────────────
203            MathFunction::Lorenz { sigma: _, rho: _, beta: _, scale } => {
204                // Run 40 iterations of Lorenz from a seed derived from t
205                let init = glam::Vec3::new(
206                    (t * 0.1).sin() * 10.0,
207                    (t * 0.07).cos() * 10.0,
208                    20.0 + (t * 0.05).sin() * 5.0,
209                );
210                let mut state = init;
211                for _ in 0..40 {
212                    let (next, _) = attractors::step(attractors::AttractorType::Lorenz, state, 0.01);
213                    state = next;
214                }
215                state.x * scale
216            }
217            MathFunction::Perlin { frequency, octaves, amplitude } => {
218                use crate::math::noise::fbm;
219                fbm(t * frequency, 0.0, *octaves, 0.5, 2.0) * amplitude
220            }
221            MathFunction::Simplex { frequency, amplitude } => {
222                use crate::math::noise::noise1;
223                noise1(t * frequency) * amplitude
224            }
225
226            // ── Convergence ───────────────────────────────────────────────────
227            MathFunction::Exponential { start, target, rate } => {
228                target + (start - target) * (-rate * t).exp()
229            }
230            MathFunction::LogisticMap { r, x0 } => {
231                // Iterate the logistic map `n` times where n ~ t * 60
232                let n = (t * 30.0) as u32;
233                let mut x = *x0;
234                for _ in 0..n.min(200) {
235                    x = r * x * (1.0 - x);
236                }
237                x * 2.0 - 1.0 // map [0,1] → [-1,1]
238            }
239            MathFunction::Collatz { seed, scale } => {
240                // Generate Collatz sequence from seed, use t to index into it
241                let n = (t * 10.0) as usize;
242                let seq = collatz_sequence(*seed, 200);
243                let v = seq.get(n % seq.len()).copied().unwrap_or(1) as f32;
244                // Normalize: the sequence is bounded by the highest step
245                let max = seq.iter().copied().fold(1u64, u64::max) as f32;
246                (v / max) * 2.0 * scale - scale
247            }
248
249            // ── Orbiting ──────────────────────────────────────────────────────
250            MathFunction::Orbit { radius, speed, eccentricity, .. } => {
251                let angle = t * speed;
252                let r = radius * (1.0 - eccentricity * angle.cos());
253                r * angle.cos()
254            }
255            MathFunction::Spiral { radius_rate, speed, .. } => {
256                let angle = t * speed;
257                let r = t * radius_rate;
258                r * angle.cos()
259            }
260            MathFunction::GoldenSpiral { scale, speed, .. } => {
261                let phi = 1.618_034_f32;
262                let angle = t * speed;
263                phi.powf(angle / TAU) * angle.cos() * scale
264            }
265            MathFunction::Lissajous { a, b, delta, scale } => {
266                (a * t).sin() * scale + (b * t + delta).sin() * scale * 0.5
267            }
268
269            // ── Strange attractor ──────────────────────────────────────────────
270            MathFunction::StrangeAttractor { attractor_type, scale, strength } => {
271                let init = attractors::initial_state(*attractor_type);
272                let seed_t = (t * 0.05).sin() * 5.0;
273                let mut state = glam::Vec3::new(
274                    init.x + seed_t,
275                    init.y + (t * 0.03).cos() * 5.0,
276                    init.z,
277                );
278                for _ in 0..20 {
279                    let (next, _) = attractors::step(*attractor_type, state, 0.01);
280                    state = next;
281                }
282                state.x * scale * strength
283            }
284
285            // ── Fractal ───────────────────────────────────────────────────────
286            MathFunction::MandelbrotEscape { c_real, c_imag, scale } => {
287                let (mut zr, mut zi) = (input, input * 0.5);
288                let max_iter = 64;
289                let mut iter = 0;
290                while iter < max_iter && zr * zr + zi * zi < 4.0 {
291                    let new_zr = zr * zr - zi * zi + c_real;
292                    zi = 2.0 * zr * zi + c_imag;
293                    zr = new_zr;
294                    iter += 1;
295                }
296                (iter as f32 / max_iter as f32) * 2.0 * scale - scale
297            }
298            MathFunction::JuliaSet { c_real, c_imag, scale } => {
299                let (mut zr, mut zi) = ((t * 0.1).sin() * 2.0, (t * 0.07).cos() * 2.0);
300                let max_iter = 64;
301                let mut iter = 0;
302                while iter < max_iter && zr * zr + zi * zi < 4.0 {
303                    let new_zr = zr * zr - zi * zi + c_real;
304                    zi = 2.0 * zr * zi + c_imag;
305                    zr = new_zr;
306                    iter += 1;
307                }
308                (iter as f32 / max_iter as f32) * 2.0 * scale - scale
309            }
310
311            // ── Damping ───────────────────────────────────────────────────────
312            MathFunction::SpringDamper { target, stiffness, damping } => {
313                // Analytical underdamped spring
314                let omega = stiffness.sqrt();
315                let zeta = damping / (2.0 * omega);
316                if zeta < 1.0 {
317                    let omega_d = omega * (1.0 - zeta * zeta).sqrt();
318                    let decay = (-zeta * omega * t).exp();
319                    target + (-target) * decay * ((omega_d * t).cos() + zeta / (1.0 - zeta * zeta).sqrt() * (omega_d * t).sin())
320                } else {
321                    // Overdamped: simple exponential
322                    target + (-target) * (-omega * t).exp()
323                }
324            }
325            MathFunction::CriticallyDamped { target, speed } => {
326                target + (-target) * (1.0 + speed * t) * (-speed * t).exp()
327            }
328
329            // ── Biological ────────────────────────────────────────────────────
330            MathFunction::HeartBeat { bpm, intensity } => {
331                let cycle = (t * bpm / 60.0).fract();
332                if cycle < 0.10 {
333                    intensity * 0.3 * (cycle / 0.10 * PI).sin()      // P wave
334                } else if cycle < 0.18 {
335                    0.0                                                // PR segment
336                } else if cycle < 0.28 {
337                    intensity * 2.0 * (cycle - 0.18) / 0.10          // QRS up
338                } else if cycle < 0.33 {
339                    intensity * 2.0 * (1.0 - (cycle - 0.28) / 0.05)  // QRS down
340                } else if cycle < 0.50 {
341                    intensity * 0.3 * ((cycle - 0.33) / 0.17 * PI).sin() // T wave
342                } else {
343                    0.0                                                // diastole
344                }
345            }
346            MathFunction::Breathing { rate, depth } => {
347                let cycle = (t * rate).fract();
348                if cycle < 0.40 {
349                    depth * (cycle / 0.40 * PI).sin()                 // inhale
350                } else if cycle < 0.45 {
351                    *depth                                              // hold full
352                } else if cycle < 0.85 {
353                    depth * ((cycle - 0.45) / 0.40 * PI + PI).sin() + depth // exhale
354                } else {
355                    0.0                                                // hold empty
356                }
357            }
358            MathFunction::Pendulum { length, gravity, damping } => {
359                let omega = (gravity / length).sqrt();
360                let theta0 = PI / 6.0; // 30 degree initial swing
361                let decay = (-damping * t).exp();
362                theta0 * decay * (omega * t).cos()
363            }
364            MathFunction::Wave { wavelength, speed, amplitude, decay } => {
365                let phase = t * speed / wavelength;
366                amplitude * phase.sin() * (-decay * t).exp()
367            }
368
369            // ── Sawtooth / pulse ─────────────────────────────────────────────
370            MathFunction::Sawtooth { amplitude, frequency, phase } => {
371                let p = (t * frequency + phase / TAU).fract();
372                amplitude * (2.0 * p - 1.0)
373            }
374            MathFunction::Ramp { amplitude, duration } => {
375                let p = (t / duration.max(f32::EPSILON)).fract();
376                amplitude * p
377            }
378
379            // ── Signal functions ──────────────────────────────────────────────
380            MathFunction::Sinc { frequency, amplitude } => {
381                let x = t * frequency * PI;
382                let v = if x.abs() < f32::EPSILON { 1.0 } else { x.sin() / x };
383                amplitude * v
384            }
385            MathFunction::Gaussian { amplitude, center, width } => {
386                let x = (t - center) / width.max(f32::EPSILON);
387                amplitude * (-x * x).exp()
388            }
389            MathFunction::BeatFrequency { freq1, freq2, amplitude } => {
390                amplitude * (t * freq1 * TAU).sin() * (t * freq2 * TAU).sin()
391            }
392            MathFunction::WavePacket { carrier_freq, envelope_width, amplitude, center } => {
393                let carrier  = (t * carrier_freq * TAU).sin();
394                let envelope = {
395                    let x = (t - center) / envelope_width.max(f32::EPSILON);
396                    (-x * x).exp()
397                };
398                amplitude * carrier * envelope
399            }
400            MathFunction::FourierSeries { fundamental, coefficients } => {
401                let mut v = 0.0_f32;
402                for (n, (sin_c, cos_c)) in coefficients.iter().enumerate() {
403                    let harmonic = (n as f32 + 1.0) * fundamental * TAU * t;
404                    v += sin_c * harmonic.sin() + cos_c * harmonic.cos();
405                }
406                v
407            }
408
409            // ── Activation / shaping ─────────────────────────────────────────
410            MathFunction::Sigmoid { steepness, center } => {
411                1.0 / (1.0 + (-steepness * (t - center)).exp())
412            }
413            MathFunction::Tanh { amplitude, steepness } => {
414                amplitude * (steepness * t).tanh()
415            }
416            MathFunction::SoftPlus { amplitude, steepness } => {
417                let s = steepness.max(f32::EPSILON);
418                amplitude * (1.0 + (s * t).exp()).ln() / s
419            }
420            MathFunction::Relu { slope, offset } => {
421                (slope * t + offset).max(0.0)
422            }
423            MathFunction::PowerLaw { exponent, scale } => {
424                scale * t.abs().powf(*exponent) * t.signum()
425            }
426
427            // ── Chaos / dynamical systems ──────────────────────────────────────
428            MathFunction::VanDerPol { mu, amplitude } => {
429                // Numerical integration of Van der Pol: ẍ - μ(1 - x²)ẋ + x = 0
430                let dt_inner = 0.02_f32;
431                let steps    = (t / dt_inner) as u32;
432                let mut x    = *amplitude;
433                let mut v    = 0.0_f32;
434                for _ in 0..steps.min(5000) {
435                    let a = mu * (1.0 - x * x) * v - x;
436                    v += a * dt_inner;
437                    x += v * dt_inner;
438                }
439                x.clamp(-amplitude * 3.0, amplitude * 3.0)
440            }
441            MathFunction::Duffing { alpha, beta, delta, gamma, omega } => {
442                // Forced Duffing oscillator: ẍ + δẋ + αx + βx³ = γcos(ωt)
443                let dt_inner = 0.005_f32;
444                let steps    = (t / dt_inner) as u32;
445                let mut x    = 0.5_f32;
446                let mut v    = 0.0_f32;
447                let mut s    = 0.0_f32;
448                for _ in 0..steps.min(10000) {
449                    let a = -delta * v - alpha * x - beta * x * x * x + gamma * (omega * s).cos();
450                    v += a * dt_inner;
451                    x += v * dt_inner;
452                    s += dt_inner;
453                }
454                x.clamp(-5.0, 5.0)
455            }
456            MathFunction::TentMap { r, x0 } => {
457                let n = (t * 40.0) as u32;
458                let mut x = *x0;
459                for _ in 0..n.min(500) {
460                    x = if x < 0.5 { r * x } else { r * (1.0 - x) };
461                }
462                x * 2.0 - 1.0
463            }
464            MathFunction::HenonMap { a, b, x0, y0 } => {
465                let n = (t * 30.0) as u32;
466                let mut x = *x0;
467                let mut y = *y0;
468                for _ in 0..n.min(1000) {
469                    let new_x = 1.0 - a * x * x + y;
470                    y = b * x;
471                    x = new_x;
472                }
473                x.clamp(-2.0, 2.0)
474            }
475            MathFunction::Roessler { a, b, c, scale } => {
476                let dt_inner = 0.01_f32;
477                let steps    = (t / dt_inner) as u32;
478                let (mut rx, mut ry, mut rz) = (0.1_f32, 0.0_f32, 0.0_f32);
479                for _ in 0..steps.min(5000) {
480                    let dx = -(ry + rz);
481                    let dy = rx + a * ry;
482                    let dz = b + rz * (rx - c);
483                    rx += dx * dt_inner;
484                    ry += dy * dt_inner;
485                    rz += dz * dt_inner;
486                }
487                rx * scale
488            }
489
490            // ── Physical simulations ───────────────────────────────────────────
491            MathFunction::DoublePendulum { l1, l2, m1, m2, theta1_0, theta2_0 } => {
492                // Runge-Kutta 4 integration of double pendulum equations
493                let g     = 9.81_f32;
494                let dt_rk = 0.005_f32;
495                let steps = (t / dt_rk) as u32;
496                let mut th1 = *theta1_0;
497                let mut th2 = *theta2_0;
498                let mut w1  = 0.0_f32;
499                let mut w2  = 0.0_f32;
500
501                let dp_accel = |th1: f32, th2: f32, w1: f32, w2: f32| -> (f32, f32) {
502                    let dth = th1 - th2;
503                    let denom1 = (m1 + m2) * l1 - m2 * l1 * dth.cos() * dth.cos();
504                    let denom2 = (l2 / l1) * denom1;
505                    let a1 = (m2 * l1 * w1 * w1 * dth.sin() * dth.cos()
506                             + m2 * g * th2.sin() * dth.cos()
507                             + m2 * l2 * w2 * w2 * dth.sin()
508                             - (m1 + m2) * g * th1.sin()) / denom1.max(f32::EPSILON);
509                    let a2 = (-(m1 + m2) * (l1 * w1 * w1 * dth.sin()
510                             + g * th1.sin() * dth.cos() - g * th2.sin())
511                             - m2 * l2 * w2 * w2 * dth.sin() * dth.cos()) / denom2.max(f32::EPSILON);
512                    (a1, a2)
513                };
514
515                for _ in 0..steps.min(8000) {
516                    let (a1, a2) = dp_accel(th1, th2, w1, w2);
517                    // Simple Euler (fast enough for visual use)
518                    w1 += a1 * dt_rk;
519                    w2 += a2 * dt_rk;
520                    th1 += w1 * dt_rk;
521                    th2 += w2 * dt_rk;
522                }
523                th1.clamp(-PI * 4.0, PI * 4.0)
524            }
525            MathFunction::Projectile { v0, angle_deg, gravity } => {
526                let angle = angle_deg.to_radians();
527                let vy    = v0 * angle.sin();
528                vy * t - 0.5 * gravity * t * t
529            }
530            MathFunction::SimpleHarmonic { omega, amplitude, phase, decay } => {
531                let env = if *decay > f32::EPSILON { (-decay * t).exp() } else { 1.0 };
532                amplitude * env * (omega * t + phase).sin()
533            }
534            MathFunction::DampedSine { omega, zeta, amplitude, phase } => {
535                let omega_d = omega * (1.0 - zeta * zeta).abs().sqrt();
536                let env     = (-zeta * omega * t).exp();
537                amplitude * env * (omega_d * t + phase).sin()
538            }
539            MathFunction::Epicycle { r1, r2, omega1, omega2 } => {
540                let x = r1 * (omega1 * t).cos() + r2 * (omega2 * t).cos();
541                x // y-coord is evaluate(t+0.5, .) with offset if caller wants 2D
542            }
543
544            // ── Statistical / noise ───────────────────────────────────────────
545            MathFunction::FractionalBrownian { frequency, octaves, hurst, amplitude } => {
546                use crate::math::noise::fbm;
547                // Hurst exponent controls persistence: H=0.5 → Brownian, H>0.5 → smooth
548                let persistence = 2.0_f32.powf(-*hurst);
549                fbm(t * frequency, 0.0, *octaves, persistence, 2.0) * amplitude
550            }
551            MathFunction::DomainWarp { frequency, warp_strength, octaves, amplitude } => {
552                use crate::math::noise::{fbm, noise1};
553                // Displace the domain by a noise field before sampling
554                let warp = noise1(t * frequency * 0.5) * warp_strength;
555                fbm((t + warp) * frequency, 0.0, *octaves, 0.5, 2.0) * amplitude
556            }
557            MathFunction::Cellular { frequency, amplitude } => {
558                // Approximated Worley noise: minimum distance to a grid of random points
559                let cell = (t * frequency).floor();
560                let frac = (t * frequency).fract();
561                let mut min_dist = f32::MAX;
562                for offset in [-1i32, 0, 1] {
563                    let point_cell = cell + offset as f32;
564                    // Deterministic random point within each cell
565                    let hash = (point_cell * 127.321 + 3481.12).sin().abs();
566                    let point = hash; // point position within [0, 1]
567                    let dist  = (frac - point - offset as f32).abs();
568                    min_dist = min_dist.min(dist);
569                }
570                amplitude * min_dist * 2.0
571            }
572
573            // ── Composition ───────────────────────────────────────────────────
574            MathFunction::Sum(a, b) => a.evaluate(t, input) + b.evaluate(t, input),
575            MathFunction::Product(a, b) => a.evaluate(t, input) * b.evaluate(t, input),
576            MathFunction::Chain(functions) => {
577                let mut value = input;
578                for f in functions {
579                    value = f.evaluate(t, value);
580                }
581                value
582            }
583            MathFunction::Modulate { carrier, modulator } => {
584                carrier.evaluate(t, input) * modulator.evaluate(t, input)
585            }
586            MathFunction::Clamp { inner, min, max } => {
587                inner.evaluate(t, input).clamp(*min, *max)
588            }
589            MathFunction::Abs(inner)      => inner.evaluate(t, input).abs(),
590            MathFunction::Scale  { inner, factor } => inner.evaluate(t, input) * factor,
591            MathFunction::Offset { inner, offset } => inner.evaluate(t, input) + offset,
592            MathFunction::Invert(inner)   => -inner.evaluate(t, input),
593            MathFunction::Normalize { inner, t_range, steps } => {
594                // Sample inner at `steps` points to find the value range, then normalize
595                let n   = (*steps).max(2) as usize;
596                let dt  = t_range / (n - 1) as f32;
597                let mut min_v = f32::MAX;
598                let mut max_v = f32::MIN;
599                for i in 0..n {
600                    let v = inner.evaluate(i as f32 * dt, 0.0);
601                    if v < min_v { min_v = v; }
602                    if v > max_v { max_v = v; }
603                }
604                let range = (max_v - min_v).max(f32::EPSILON);
605                let raw   = inner.evaluate(t, input);
606                (raw - min_v) / range * 2.0 - 1.0
607            }
608            MathFunction::Delay { inner, delay } => {
609                inner.evaluate((t - delay).max(0.0), input)
610            }
611            MathFunction::Mirror { inner, period } => {
612                let p   = period.max(f32::EPSILON);
613                let t2  = (t % (2.0 * p) + 2.0 * p) % (2.0 * p);
614                let t_m = if t2 < p { t2 } else { 2.0 * p - t2 };
615                inner.evaluate(t_m, input)
616            }
617        }
618    }
619
620    // ── Utility methods ────────────────────────────────────────────────────────
621
622    /// Numerical derivative dF/dt at time `t` using central differences.
623    ///
624    /// Epsilon defaults to 1e-4 — reduce for smoother but less accurate results.
625    pub fn derivative(&self, t: f32, epsilon: f32) -> f32 {
626        let hi = self.evaluate(t + epsilon, 0.0);
627        let lo = self.evaluate(t - epsilon, 0.0);
628        (hi - lo) / (2.0 * epsilon.max(f32::EPSILON))
629    }
630
631    /// Numerical integral ∫F dt over [from, to] using Simpson's rule with `steps` intervals.
632    ///
633    /// `steps` must be even (rounded up if not).
634    pub fn integrate(&self, from: f32, to: f32, steps: u32) -> f32 {
635        let n  = ((steps + 1) & !1) as usize; // ensure even
636        let h  = (to - from) / n as f32;
637        let mut sum = self.evaluate(from, 0.0) + self.evaluate(to, 0.0);
638        for i in 1..n {
639            let x = from + i as f32 * h;
640            let w = if i % 2 == 0 { 2.0 } else { 4.0 };
641            sum += w * self.evaluate(x, 0.0);
642        }
643        sum * h / 3.0
644    }
645
646    /// Sample the function at `n` uniformly spaced points over [t_start, t_end].
647    pub fn sample_range(&self, t_start: f32, t_end: f32, n: u32) -> Vec<f32> {
648        let count = n.max(2) as usize;
649        let dt    = (t_end - t_start) / (count - 1) as f32;
650        (0..count).map(|i| self.evaluate(t_start + i as f32 * dt, 0.0)).collect()
651    }
652
653    /// Find the approximate minimum and maximum output over [t_start, t_end].
654    pub fn find_range(&self, t_start: f32, t_end: f32, steps: u32) -> (f32, f32) {
655        let samples = self.sample_range(t_start, t_end, steps.max(2));
656        let min = samples.iter().cloned().fold(f32::MAX, f32::min);
657        let max = samples.iter().cloned().fold(f32::MIN, f32::max);
658        (min, max)
659    }
660
661    /// Find approximate zero-crossings in [t_start, t_end].
662    pub fn zero_crossings(&self, t_start: f32, t_end: f32, steps: u32) -> Vec<f32> {
663        let count = steps.max(2) as usize;
664        let dt    = (t_end - t_start) / (count - 1) as f32;
665        let mut crossings = Vec::new();
666        let mut prev = self.evaluate(t_start, 0.0);
667        for i in 1..count {
668            let t = t_start + i as f32 * dt;
669            let v = self.evaluate(t, 0.0);
670            if (prev < 0.0) != (v < 0.0) {
671                // Linear interpolation of crossing time
672                let frac = -prev / (v - prev).max(f32::EPSILON);
673                crossings.push(t - dt + frac * dt);
674            }
675            prev = v;
676        }
677        crossings
678    }
679
680    /// Evaluate as a 3D trajectory: x = f(t, 0), y = f(t+1, 0), z = f(t+2, 0).
681    ///
682    /// This is the default particle behavior — all three axes driven by the same
683    /// function but offset in time, producing organic non-planar motion.
684    pub fn evaluate_vec3(&self, t: f32) -> glam::Vec3 {
685        glam::Vec3::new(
686            self.evaluate(t,       0.0),
687            self.evaluate(t + 1.0, 0.0),
688            self.evaluate(t + 2.0, 0.0),
689        )
690    }
691
692    /// Evaluate as a 3D trajectory with explicit phase offsets per axis.
693    pub fn evaluate_vec3_phased(&self, t: f32, phase_x: f32, phase_y: f32, phase_z: f32) -> glam::Vec3 {
694        glam::Vec3::new(
695            self.evaluate(t + phase_x, 0.0),
696            self.evaluate(t + phase_y, 0.0),
697            self.evaluate(t + phase_z, 0.0),
698        )
699    }
700
701    /// Create a `Sum` of this function and another.
702    pub fn add(self, other: MathFunction) -> MathFunction {
703        MathFunction::Sum(Box::new(self), Box::new(other))
704    }
705
706    /// Create a `Product` of this function and another.
707    pub fn mul(self, other: MathFunction) -> MathFunction {
708        MathFunction::Product(Box::new(self), Box::new(other))
709    }
710
711    /// Scale the output by `factor`.
712    pub fn scale(self, factor: f32) -> MathFunction {
713        MathFunction::Scale { inner: Box::new(self), factor }
714    }
715
716    /// Add a constant offset to the output.
717    pub fn offset(self, offset: f32) -> MathFunction {
718        MathFunction::Offset { inner: Box::new(self), offset }
719    }
720
721    /// Clamp the output to [min, max].
722    pub fn clamp(self, min: f32, max: f32) -> MathFunction {
723        MathFunction::Clamp { inner: Box::new(self), min, max }
724    }
725
726    /// Delay the function by `seconds`.
727    pub fn delay(self, seconds: f32) -> MathFunction {
728        MathFunction::Delay { inner: Box::new(self), delay: seconds }
729    }
730
731    /// Invert (negate) the output.
732    pub fn invert(self) -> MathFunction {
733        MathFunction::Invert(Box::new(self))
734    }
735
736    /// Modulate by another function (amplitude modulation).
737    pub fn modulate(self, modulator: MathFunction) -> MathFunction {
738        MathFunction::Modulate {
739            carrier:   Box::new(self),
740            modulator: Box::new(modulator),
741        }
742    }
743}
744
745/// Generate Collatz sequence starting from n, up to max_steps.
746fn collatz_sequence(n: u64, max_steps: usize) -> Vec<u64> {
747    let mut seq = Vec::with_capacity(max_steps);
748    let mut x = n.max(1);
749    seq.push(x);
750    while x != 1 && seq.len() < max_steps {
751        x = if x % 2 == 0 { x / 2 } else { 3 * x + 1 };
752        seq.push(x);
753    }
754    seq
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760
761    #[test]
762    fn sine_at_zero() {
763        let f = MathFunction::Sine { amplitude: 1.0, frequency: 1.0, phase: 0.0 };
764        assert!((f.evaluate(0.0, 0.0)).abs() < 1e-5);
765    }
766
767    #[test]
768    fn constant_is_constant() {
769        let f = MathFunction::Constant(42.0);
770        assert_eq!(f.evaluate(0.0, 0.0), 42.0);
771        assert_eq!(f.evaluate(100.0, -999.0), 42.0);
772    }
773
774    #[test]
775    fn breathing_never_negative() {
776        let f = MathFunction::Breathing { rate: 0.25, depth: 1.0 };
777        for i in 0..100 {
778            let v = f.evaluate(i as f32 * 0.1, 0.0);
779            assert!(v >= -0.01, "breathing went negative at t={}", i as f32 * 0.1);
780        }
781    }
782
783    #[test]
784    fn chain_composes() {
785        let f = MathFunction::Chain(vec![
786            MathFunction::Constant(2.0),
787            MathFunction::Linear { slope: 1.0, offset: 0.0 }, // output of prev is input
788        ]);
789        // Constant(2.0) always outputs 2.0 regardless of input.
790        // Linear with slope=1 just passes through the input.
791        // Chain: first outputs 2.0, that becomes input to Linear → 2.0
792        // But Linear uses t not input for its own computation...
793        // Actually our Linear is slope * t + offset, input is ignored.
794        // So result = 1.0 * 0.0 + 0.0 = 0.0 for t=0.
795        let _ = f.evaluate(0.0, 0.0); // just test it doesn't panic
796    }
797}