Skip to main content

proof_engine/math/
complex.rs

1//! Complex numbers, quaternions, and iterative fractal evaluation.
2//!
3//! Provides:
4//! - `Complex` — fully-featured complex number arithmetic
5//! - `Quaternion` — 3D rotation quaternion with SLERP and expmap
6//! - Fractal samplers: Mandelbrot, Julia, BurningShip, Newton, Lyapunov
7//! - Color mapping utilities for escape-time fractals
8
9use std::fmt;
10use glam::{Vec2, Vec3, Vec4};
11
12// ── Complex ───────────────────────────────────────────────────────────────────
13
14/// A complex number z = re + im·i.
15#[derive(Clone, Copy, PartialEq, Debug, Default)]
16pub struct Complex {
17    pub re: f64,
18    pub im: f64,
19}
20
21impl Complex {
22    pub const ZERO: Self = Self { re: 0.0, im: 0.0 };
23    pub const ONE:  Self = Self { re: 1.0, im: 0.0 };
24    pub const I:    Self = Self { re: 0.0, im: 1.0 };
25
26    #[inline] pub fn new(re: f64, im: f64) -> Self { Self { re, im } }
27    #[inline] pub fn from_polar(r: f64, theta: f64) -> Self {
28        Self { re: r * theta.cos(), im: r * theta.sin() }
29    }
30    #[inline] pub fn from_vec2(v: Vec2) -> Self { Self { re: v.x as f64, im: v.y as f64 } }
31    #[inline] pub fn to_vec2(self) -> Vec2 { Vec2::new(self.re as f32, self.im as f32) }
32
33    // ── Basic properties ─────────────────────────────────────────────────────
34
35    #[inline] pub fn conj(self)   -> Self { Self::new(self.re, -self.im) }
36    #[inline] pub fn norm_sq(self) -> f64 { self.re * self.re + self.im * self.im }
37    #[inline] pub fn norm(self)   -> f64 { self.norm_sq().sqrt() }
38    #[inline] pub fn arg(self)    -> f64 { self.im.atan2(self.re) }
39    #[inline] pub fn abs(self)    -> f64 { self.norm() }
40    #[inline] pub fn is_zero(self) -> bool { self.re == 0.0 && self.im == 0.0 }
41
42    /// Normalize to unit circle. Returns zero if already zero.
43    pub fn normalize(self) -> Self {
44        let n = self.norm();
45        if n < 1e-300 { Self::ZERO } else { Self::new(self.re / n, self.im / n) }
46    }
47
48    // ── Arithmetic ───────────────────────────────────────────────────────────
49
50    pub fn add(self, rhs: Self) -> Self {
51        Self::new(self.re + rhs.re, self.im + rhs.im)
52    }
53    pub fn sub(self, rhs: Self) -> Self {
54        Self::new(self.re - rhs.re, self.im - rhs.im)
55    }
56    pub fn mul(self, rhs: Self) -> Self {
57        Self::new(
58            self.re * rhs.re - self.im * rhs.im,
59            self.re * rhs.im + self.im * rhs.re,
60        )
61    }
62    pub fn div(self, rhs: Self) -> Self {
63        let d = rhs.norm_sq();
64        Self::new(
65            (self.re * rhs.re + self.im * rhs.im) / d,
66            (self.im * rhs.re - self.re * rhs.im) / d,
67        )
68    }
69    pub fn scale(self, s: f64) -> Self { Self::new(self.re * s, self.im * s) }
70    pub fn neg(self)           -> Self { Self::new(-self.re, -self.im) }
71    pub fn recip(self)         -> Self { Self::ONE.div(self) }
72
73    // ── Transcendentals ──────────────────────────────────────────────────────
74
75    pub fn exp(self) -> Self {
76        let e = self.re.exp();
77        Self::new(e * self.im.cos(), e * self.im.sin())
78    }
79
80    pub fn ln(self) -> Self {
81        Self::new(self.norm().ln(), self.arg())
82    }
83
84    pub fn sqrt(self) -> Self {
85        let r   = self.norm().sqrt();
86        let phi = self.arg() * 0.5;
87        Self::from_polar(r, phi)
88    }
89
90    /// z^w = exp(w * ln(z)) — principal branch.
91    pub fn pow(self, w: Self) -> Self {
92        if self.is_zero() { return Self::ZERO; }
93        self.ln().mul(w).exp()
94    }
95
96    /// z^n for integer exponent (fast repeated squaring).
97    pub fn powi(self, n: i32) -> Self {
98        if n == 0 { return Self::ONE; }
99        if n < 0  { return self.recip().powi(-n); }
100        let mut result = Self::ONE;
101        let mut base   = self;
102        let mut exp    = n as u32;
103        while exp > 0 {
104            if exp & 1 == 1 { result = result.mul(base); }
105            base = base.mul(base);
106            exp >>= 1;
107        }
108        result
109    }
110
111    pub fn sin(self) -> Self {
112        // sin(a+bi) = sin(a)cosh(b) + i cos(a)sinh(b)
113        Self::new(
114            self.re.sin() * self.im.cosh(),
115            self.re.cos() * self.im.sinh(),
116        )
117    }
118
119    pub fn cos(self) -> Self {
120        // cos(a+bi) = cos(a)cosh(b) - i sin(a)sinh(b)
121        Self::new(
122             self.re.cos() * self.im.cosh(),
123            -self.re.sin() * self.im.sinh(),
124        )
125    }
126
127    pub fn tan(self) -> Self {
128        self.sin().div(self.cos())
129    }
130
131    pub fn sinh(self) -> Self {
132        Self::new(self.re.sinh() * self.im.cos(), self.re.cosh() * self.im.sin())
133    }
134
135    pub fn cosh(self) -> Self {
136        Self::new(self.re.cosh() * self.im.cos(), self.re.sinh() * self.im.sin())
137    }
138
139    pub fn tanh(self) -> Self {
140        self.sinh().div(self.cosh())
141    }
142
143    pub fn asin(self) -> Self {
144        // asin(z) = -i * ln(iz + sqrt(1 - z^2))
145        let iz  = Self::I.mul(self);
146        let sq  = Self::ONE.sub(self.mul(self)).sqrt();
147        Self::I.neg().mul(iz.add(sq).ln())
148    }
149
150    pub fn acos(self) -> Self {
151        // acos(z) = -i * ln(z + i*sqrt(1 - z^2))
152        let sq  = Self::ONE.sub(self.mul(self)).sqrt();
153        Self::I.neg().mul(self.add(Self::I.mul(sq)).ln())
154    }
155
156    pub fn atan(self) -> Self {
157        // atan(z) = i/2 * ln((i+z)/(i-z))
158        let half_i = Self::new(0.0, 0.5);
159        let iz_plus  = Self::I.add(self);
160        let iz_minus = Self::I.sub(self);
161        half_i.mul(iz_plus.div(iz_minus).ln())
162    }
163
164    // ── Iteration helpers ────────────────────────────────────────────────────
165
166    /// Mandelbrot iteration: z_{n+1} = z_n^2 + c. Returns z.
167    #[inline] pub fn mandelbrot_step(self, c: Self) -> Self {
168        self.mul(self).add(c)
169    }
170
171    /// Burning ship step: z_{n+1} = (|re| + i|im|)^2 + c.
172    #[inline] pub fn burning_ship_step(self, c: Self) -> Self {
173        let abs_z = Self::new(self.re.abs(), self.im.abs());
174        abs_z.mul(abs_z).add(c)
175    }
176
177    /// Tricorn step: z_{n+1} = conj(z)^2 + c.
178    #[inline] pub fn tricorn_step(self, c: Self) -> Self {
179        self.conj().mul(self.conj()).add(c)
180    }
181}
182
183impl fmt::Display for Complex {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        if self.im >= 0.0 {
186            write!(f, "{:.4}+{:.4}i", self.re, self.im)
187        } else {
188            write!(f, "{:.4}{:.4}i", self.re, self.im)
189        }
190    }
191}
192
193impl std::ops::Add for Complex {
194    type Output = Self;
195    fn add(self, rhs: Self) -> Self { Complex::add(self, rhs) }
196}
197impl std::ops::Sub for Complex {
198    type Output = Self;
199    fn sub(self, rhs: Self) -> Self { Complex::sub(self, rhs) }
200}
201impl std::ops::Mul for Complex {
202    type Output = Self;
203    fn mul(self, rhs: Self) -> Self { Complex::mul(self, rhs) }
204}
205impl std::ops::Div for Complex {
206    type Output = Self;
207    fn div(self, rhs: Self) -> Self { Complex::div(self, rhs) }
208}
209impl std::ops::Neg for Complex {
210    type Output = Self;
211    fn neg(self) -> Self { Complex::neg(self) }
212}
213
214// ── Fractal samplers ──────────────────────────────────────────────────────────
215
216/// Result of an escape-time fractal iteration.
217#[derive(Clone, Copy, Debug)]
218pub struct EscapeResult {
219    /// Whether the orbit escaped within `max_iter`.
220    pub escaped:    bool,
221    /// Number of iterations before escape (or max_iter).
222    pub iterations: u32,
223    /// Smooth iteration count for anti-banding coloring.
224    pub smooth:     f64,
225    /// Final z magnitude before escape.
226    pub final_norm: f64,
227    /// Final z value at escape.
228    pub final_z:    Complex,
229}
230
231impl EscapeResult {
232    /// Normalized [0, 1] escape value using smooth iteration count.
233    pub fn normalized(&self, max_iter: u32) -> f64 {
234        self.smooth / max_iter as f64
235    }
236
237    /// Hue angle [0, 2π] from the orbit angle of the final z.
238    pub fn orbit_angle(&self) -> f64 {
239        self.final_z.arg()
240    }
241}
242
243// ── Mandelbrot ────────────────────────────────────────────────────────────────
244
245/// Parameters for the generalized Mandelbrot set: z → z^power + c.
246#[derive(Clone, Debug)]
247pub struct MandelbrotParams {
248    /// Exponent (2 = classic Mandelbrot).
249    pub power:      f64,
250    /// Maximum iterations.
251    pub max_iter:   u32,
252    /// Escape radius squared.
253    pub escape_r_sq: f64,
254}
255
256impl Default for MandelbrotParams {
257    fn default() -> Self {
258        Self { power: 2.0, max_iter: 256, escape_r_sq: 4.0 }
259    }
260}
261
262impl MandelbrotParams {
263    pub fn new(power: f64, max_iter: u32) -> Self {
264        Self { power, max_iter, escape_r_sq: 4.0 }
265    }
266
267    pub fn sample(&self, c: Complex) -> EscapeResult {
268        let mut z = Complex::ZERO;
269        let pw = Complex::new(self.power, 0.0);
270        for i in 0..self.max_iter {
271            z = z.pow(pw).add(c);
272            let n = z.norm_sq();
273            if n > self.escape_r_sq {
274                // Smooth (continuous) coloring — subtract log2(log2(|z|))
275                let smooth = i as f64 + 1.0 - (n.sqrt().ln().ln() / std::f64::consts::LN_2);
276                return EscapeResult {
277                    escaped: true, iterations: i,
278                    smooth, final_norm: n.sqrt(), final_z: z,
279                };
280            }
281        }
282        EscapeResult {
283            escaped: false, iterations: self.max_iter,
284            smooth: self.max_iter as f64, final_norm: z.norm(), final_z: z,
285        }
286    }
287}
288
289// ── Julia ─────────────────────────────────────────────────────────────────────
290
291/// Julia set: z → z^power + c where c is fixed.
292#[derive(Clone, Debug)]
293pub struct JuliaParams {
294    /// The constant c.
295    pub c:           Complex,
296    pub power:       f64,
297    pub max_iter:    u32,
298    pub escape_r_sq: f64,
299}
300
301impl JuliaParams {
302    pub fn new(c: Complex, max_iter: u32) -> Self {
303        Self { c, power: 2.0, max_iter, escape_r_sq: 4.0 }
304    }
305
306    /// Notable Julia sets by index (0..7).
307    pub fn preset(idx: usize) -> Self {
308        let presets = [
309            Complex::new(-0.7269, 0.1889),   // Douady rabbit
310            Complex::new(-0.4, 0.6),          // Spirals
311            Complex::new(0.285, 0.01),        // Dense spirals
312            Complex::new(-0.835, -0.2321),    // Dendrite
313            Complex::new(-0.7, 0.27015),      // Whirlpools
314            Complex::new(0.45, 0.1428),       // Cauliflower
315            Complex::new(-0.123, 0.745),      // Douady/Hubbard
316            Complex::new(0.0, 0.8),           // San Marco
317        ];
318        let c = presets[idx % presets.len()];
319        Self { c, power: 2.0, max_iter: 256, escape_r_sq: 4.0 }
320    }
321
322    pub fn sample(&self, z0: Complex) -> EscapeResult {
323        let mut z = z0;
324        let pw    = Complex::new(self.power, 0.0);
325        for i in 0..self.max_iter {
326            z = z.pow(pw).add(self.c);
327            let n = z.norm_sq();
328            if n > self.escape_r_sq {
329                let smooth = i as f64 + 1.0 - (n.sqrt().ln().ln() / std::f64::consts::LN_2);
330                return EscapeResult {
331                    escaped: true, iterations: i,
332                    smooth, final_norm: n.sqrt(), final_z: z,
333                };
334            }
335        }
336        EscapeResult {
337            escaped: false, iterations: self.max_iter,
338            smooth: self.max_iter as f64, final_norm: z.norm(), final_z: z,
339        }
340    }
341}
342
343// ── Burning Ship ──────────────────────────────────────────────────────────────
344
345pub struct BurningShip {
346    pub max_iter:    u32,
347    pub escape_r_sq: f64,
348}
349
350impl Default for BurningShip {
351    fn default() -> Self { Self { max_iter: 256, escape_r_sq: 4.0 } }
352}
353
354impl BurningShip {
355    pub fn sample(&self, c: Complex) -> EscapeResult {
356        let mut z = Complex::ZERO;
357        for i in 0..self.max_iter {
358            z = z.burning_ship_step(c);
359            let n = z.norm_sq();
360            if n > self.escape_r_sq {
361                let smooth = i as f64 + 1.0 - (n.sqrt().ln().ln() / std::f64::consts::LN_2);
362                return EscapeResult {
363                    escaped: true, iterations: i,
364                    smooth, final_norm: n.sqrt(), final_z: z,
365                };
366            }
367        }
368        EscapeResult {
369            escaped: false, iterations: self.max_iter,
370            smooth: self.max_iter as f64, final_norm: z.norm(), final_z: z,
371        }
372    }
373}
374
375// ── Newton fractal ────────────────────────────────────────────────────────────
376
377/// Newton's method fractal for a polynomial p(z)/p'(z).
378/// Converges to roots; color by which root was reached.
379pub struct NewtonFractal {
380    /// Coefficients of p(z) = coeff[0] + coeff[1]*z + ... + coeff[n]*z^n.
381    pub coeffs:   Vec<Complex>,
382    pub max_iter: u32,
383    pub tol:      f64,
384    pub relax:    Complex,  // relaxation parameter (usually 1+0i)
385}
386
387impl NewtonFractal {
388    /// z^3 - 1 = 0 (classic 3-root Newton fractal).
389    pub fn cubic_unity() -> Self {
390        Self {
391            coeffs:   vec![
392                Complex::new(-1.0, 0.0),
393                Complex::ZERO,
394                Complex::ZERO,
395                Complex::ONE,
396            ],
397            max_iter: 50,
398            tol:      1e-6,
399            relax:    Complex::ONE,
400        }
401    }
402
403    /// z^4 - 1 = 0.
404    pub fn quartic_unity() -> Self {
405        Self {
406            coeffs:   vec![
407                Complex::new(-1.0, 0.0),
408                Complex::ZERO, Complex::ZERO, Complex::ZERO,
409                Complex::ONE,
410            ],
411            max_iter: 50,
412            tol:      1e-6,
413            relax:    Complex::ONE,
414        }
415    }
416
417    /// Evaluate polynomial at z.
418    fn eval(&self, z: Complex) -> Complex {
419        let mut result = Complex::ZERO;
420        let mut zpow   = Complex::ONE;
421        for &c in &self.coeffs {
422            result = result.add(c.mul(zpow));
423            zpow   = zpow.mul(z);
424        }
425        result
426    }
427
428    /// Evaluate polynomial derivative at z.
429    fn eval_deriv(&self, z: Complex) -> Complex {
430        let mut result = Complex::ZERO;
431        let mut zpow   = Complex::ONE;
432        for (i, &c) in self.coeffs.iter().enumerate().skip(1) {
433            let coeff = c.scale(i as f64);
434            result = result.add(coeff.mul(zpow));
435            zpow   = zpow.mul(z);
436        }
437        result
438    }
439
440    /// Returns (converged_to_root_index, iterations, final_z).
441    pub fn sample(&self, z0: Complex) -> (Option<usize>, u32, Complex) {
442        // Find roots (degree 1 less than coeffs.len()-1... but we don't compute analytically)
443        // Just run Newton and let caller identify which root was hit.
444        let mut z = z0;
445        for i in 0..self.max_iter {
446            let fz  = self.eval(z);
447            let dfz = self.eval_deriv(z);
448            if dfz.norm_sq() < 1e-300 { break; }
449            let step = fz.div(dfz);
450            z = z.sub(self.relax.mul(step));
451            if step.norm() < self.tol {
452                return (None, i, z);  // converged — root index assigned by caller
453            }
454        }
455        (None, self.max_iter, z)
456    }
457}
458
459// ── Lyapunov exponent ─────────────────────────────────────────────────────────
460
461/// Computes the Lyapunov exponent for a logistic-map-like system.
462/// The sequence string alternates between two parameter values A and B.
463pub struct LyapunovFractal {
464    pub sequence:    Vec<bool>, // false = use A, true = use B
465    pub warmup:      u32,
466    pub iterations:  u32,
467}
468
469impl Default for LyapunovFractal {
470    fn default() -> Self {
471        // Classic "AB" pattern
472        Self { sequence: vec![false, true], warmup: 200, iterations: 1000 }
473    }
474}
475
476impl LyapunovFractal {
477    pub fn new(pattern: &str) -> Self {
478        let sequence = pattern.chars()
479            .filter(|c| *c == 'A' || *c == 'B')
480            .map(|c| c == 'B')
481            .collect();
482        Self { sequence, warmup: 200, iterations: 1000 }
483    }
484
485    /// Sample the Lyapunov exponent at (a, b) in [0,4]×[0,4].
486    /// Returns the exponent (negative = stable, positive = chaotic).
487    pub fn sample(&self, a: f64, b: f64) -> f64 {
488        if self.sequence.is_empty() { return 0.0; }
489        let mut x   = 0.5_f64;
490        let seq_len = self.sequence.len();
491
492        // Warmup
493        for i in 0..(self.warmup as usize) {
494            let r = if self.sequence[i % seq_len] { b } else { a };
495            x = r * x * (1.0 - x);
496        }
497
498        // Measure
499        let mut lyap = 0.0_f64;
500        for i in 0..(self.iterations as usize) {
501            let r = if self.sequence[i % seq_len] { b } else { a };
502            x = r * x * (1.0 - x);
503            let d = (r * (1.0 - 2.0 * x)).abs();
504            if d > 0.0 { lyap += d.ln(); }
505        }
506        lyap / self.iterations as f64
507    }
508}
509
510// ── Fractal color palette ─────────────────────────────────────────────────────
511
512/// Maps an EscapeResult to RGBA color.
513#[derive(Clone, Copy, Debug)]
514pub enum FractalPalette {
515    /// Classic grayscale escape time.
516    Grayscale,
517    /// Smooth gradient using a 3-cycle cosine palette.
518    Smooth { offset: f32, freq: f32 },
519    /// Orbit trap: color based on closest approach to origin.
520    OrbitTrap,
521    /// Angle coloring: hue = orbit angle.
522    AngleBased,
523    /// Ultra-fractal style: 4 stops of gradient.
524    UltraFractal,
525    /// Fire: black → red → orange → yellow → white.
526    Fire,
527    /// Electric: black → deep blue → cyan → white.
528    Electric,
529}
530
531impl FractalPalette {
532    /// Convert an EscapeResult to RGBA in [0,1].
533    pub fn color(&self, result: &EscapeResult, max_iter: u32) -> Vec4 {
534        if !result.escaped {
535            return Vec4::ZERO; // inside = black
536        }
537        let t = (result.smooth / max_iter as f64).clamp(0.0, 1.0) as f32;
538
539        match self {
540            FractalPalette::Grayscale => {
541                let v = t;
542                Vec4::new(v, v, v, 1.0)
543            }
544            FractalPalette::Smooth { offset, freq } => {
545                let r = (std::f32::consts::TAU * (t * freq + offset + 0.0)).cos() * 0.5 + 0.5;
546                let g = (std::f32::consts::TAU * (t * freq + offset + 0.333)).cos() * 0.5 + 0.5;
547                let b = (std::f32::consts::TAU * (t * freq + offset + 0.667)).cos() * 0.5 + 0.5;
548                Vec4::new(r, g, b, 1.0)
549            }
550            FractalPalette::OrbitTrap => {
551                // Color by how close orbit got to origin
552                let n = result.final_norm as f32;
553                let v = 1.0 - (n / 2.0).min(1.0);
554                Vec4::new(v * 0.9, v * 0.4, v * 1.0, 1.0)
555            }
556            FractalPalette::AngleBased => {
557                let angle = result.final_z.arg() as f32;
558                let hue   = (angle / std::f32::consts::TAU + 0.5).fract();
559                Self::hsv_to_rgb(hue, 0.8 + t * 0.2, 0.7 + t * 0.3)
560            }
561            FractalPalette::UltraFractal => {
562                // 4-stop gradient: 0=black, 0.25=teal, 0.5=gold, 0.75=white, 1=black
563                let t4 = (t * 4.0).fract();
564                let stop = (t * 4.0) as u32 % 4;
565                let (a, b) = match stop {
566                    0 => (Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.8, 0.8)),
567                    1 => (Vec3::new(0.0, 0.8, 0.8), Vec3::new(1.0, 0.85, 0.0)),
568                    2 => (Vec3::new(1.0, 0.85, 0.0), Vec3::new(1.0, 1.0, 1.0)),
569                    _ => (Vec3::new(1.0, 1.0, 1.0), Vec3::new(0.0, 0.0, 0.0)),
570                };
571                let c = a.lerp(b, t4);
572                Vec4::new(c.x, c.y, c.z, 1.0)
573            }
574            FractalPalette::Fire => {
575                if t < 0.25 {
576                    let tt = t / 0.25;
577                    Vec4::new(tt, 0.0, 0.0, 1.0)
578                } else if t < 0.5 {
579                    let tt = (t - 0.25) / 0.25;
580                    Vec4::new(1.0, tt * 0.5, 0.0, 1.0)
581                } else if t < 0.75 {
582                    let tt = (t - 0.5) / 0.25;
583                    Vec4::new(1.0, 0.5 + tt * 0.5, 0.0, 1.0)
584                } else {
585                    let tt = (t - 0.75) / 0.25;
586                    Vec4::new(1.0, 1.0, tt, 1.0)
587                }
588            }
589            FractalPalette::Electric => {
590                if t < 0.33 {
591                    let tt = t / 0.33;
592                    Vec4::new(0.0, 0.0, tt * 0.6, 1.0)
593                } else if t < 0.66 {
594                    let tt = (t - 0.33) / 0.33;
595                    Vec4::new(0.0, tt, 0.6 + tt * 0.4, 1.0)
596                } else {
597                    let tt = (t - 0.66) / 0.34;
598                    Vec4::new(tt, 1.0, 1.0, 1.0)
599                }
600            }
601        }
602    }
603
604    fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Vec4 {
605        let i = (h * 6.0) as u32;
606        let f = h * 6.0 - i as f32;
607        let p = v * (1.0 - s);
608        let q = v * (1.0 - f * s);
609        let t = v * (1.0 - (1.0 - f) * s);
610        let (r, g, b) = match i % 6 {
611            0 => (v, t, p),
612            1 => (q, v, p),
613            2 => (p, v, t),
614            3 => (p, q, v),
615            4 => (t, p, v),
616            _ => (v, p, q),
617        };
618        Vec4::new(r, g, b, 1.0)
619    }
620}
621
622// ── Quaternion ────────────────────────────────────────────────────────────────
623
624/// Unit quaternion for 3D rotation: q = w + xi + yj + zk.
625#[derive(Clone, Copy, Debug, PartialEq)]
626pub struct Quaternion {
627    pub w: f32,
628    pub x: f32,
629    pub y: f32,
630    pub z: f32,
631}
632
633impl Quaternion {
634    pub const IDENTITY: Self = Self { w: 1.0, x: 0.0, y: 0.0, z: 0.0 };
635
636    pub fn new(w: f32, x: f32, y: f32, z: f32) -> Self { Self { w, x, y, z } }
637
638    /// Create from axis-angle (axis must be normalized).
639    pub fn from_axis_angle(axis: Vec3, angle: f32) -> Self {
640        let half = angle * 0.5;
641        let s    = half.sin();
642        Self::new(half.cos(), axis.x * s, axis.y * s, axis.z * s)
643    }
644
645    /// Create from Euler angles (roll, pitch, yaw) in radians.
646    pub fn from_euler(roll: f32, pitch: f32, yaw: f32) -> Self {
647        let cr = (roll  * 0.5).cos(); let sr = (roll  * 0.5).sin();
648        let cp = (pitch * 0.5).cos(); let sp = (pitch * 0.5).sin();
649        let cy = (yaw   * 0.5).cos(); let sy = (yaw   * 0.5).sin();
650        Self::new(
651            cr * cp * cy + sr * sp * sy,
652            sr * cp * cy - cr * sp * sy,
653            cr * sp * cy + sr * cp * sy,
654            cr * cp * sy - sr * sp * cy,
655        )
656    }
657
658    pub fn norm(self) -> f32 {
659        (self.w*self.w + self.x*self.x + self.y*self.y + self.z*self.z).sqrt()
660    }
661
662    pub fn normalize(self) -> Self {
663        let n = self.norm();
664        Self::new(self.w/n, self.x/n, self.y/n, self.z/n)
665    }
666
667    pub fn conjugate(self) -> Self {
668        Self::new(self.w, -self.x, -self.y, -self.z)
669    }
670
671    pub fn inverse(self) -> Self {
672        let n2 = self.w*self.w + self.x*self.x + self.y*self.y + self.z*self.z;
673        let c  = self.conjugate();
674        Self::new(c.w/n2, c.x/n2, c.y/n2, c.z/n2)
675    }
676
677    pub fn dot(self, rhs: Self) -> f32 {
678        self.w*rhs.w + self.x*rhs.x + self.y*rhs.y + self.z*rhs.z
679    }
680
681    /// Hamilton product.
682    pub fn mul(self, rhs: Self) -> Self {
683        Self::new(
684            self.w*rhs.w - self.x*rhs.x - self.y*rhs.y - self.z*rhs.z,
685            self.w*rhs.x + self.x*rhs.w + self.y*rhs.z - self.z*rhs.y,
686            self.w*rhs.y - self.x*rhs.z + self.y*rhs.w + self.z*rhs.x,
687            self.w*rhs.z + self.x*rhs.y - self.y*rhs.x + self.z*rhs.w,
688        )
689    }
690
691    /// Rotate a 3D vector by this quaternion.
692    pub fn rotate(self, v: Vec3) -> Vec3 {
693        let q = self;
694        let qv = Vec3::new(q.x, q.y, q.z);
695        let uv = qv.cross(v);
696        let uuv = qv.cross(uv);
697        v + (uv * q.w + uuv) * 2.0
698    }
699
700    /// Spherical linear interpolation between two quaternions.
701    pub fn slerp(self, other: Self, t: f32) -> Self {
702        let mut d = self.dot(other);
703        let other = if d < 0.0 {
704            d = -d;
705            Self::new(-other.w, -other.x, -other.y, -other.z)
706        } else {
707            other
708        };
709
710        if d > 0.9995 {
711            // Very close — linear interpolation is fine
712            return Self::new(
713                self.w + t*(other.w - self.w),
714                self.x + t*(other.x - self.x),
715                self.y + t*(other.y - self.y),
716                self.z + t*(other.z - self.z),
717            ).normalize();
718        }
719
720        let theta_0 = d.acos();
721        let theta   = theta_0 * t;
722        let sin_t0  = theta_0.sin();
723        let sin_t   = theta.sin();
724        let s1 = (theta_0 - theta).sin() / sin_t0;
725        let s2 = sin_t / sin_t0;
726
727        Self::new(
728            s1 * self.w + s2 * other.w,
729            s1 * self.x + s2 * other.x,
730            s1 * self.y + s2 * other.y,
731            s1 * self.z + s2 * other.z,
732        )
733    }
734
735    /// Extract the rotation angle around the rotation axis.
736    pub fn angle(self) -> f32 {
737        2.0 * self.w.clamp(-1.0, 1.0).acos()
738    }
739
740    /// Extract the rotation axis (normalized). Returns Y if rotation is near zero.
741    pub fn axis(self) -> Vec3 {
742        let s_sq = 1.0 - self.w * self.w;
743        if s_sq < 1e-10 { return Vec3::Y; }
744        let inv_s = 1.0 / s_sq.sqrt();
745        Vec3::new(self.x * inv_s, self.y * inv_s, self.z * inv_s)
746    }
747
748    /// Exponential map: converts a rotation vector (axis * angle) to a quaternion.
749    pub fn exp_map(v: Vec3) -> Self {
750        let theta = v.length();
751        if theta < 1e-8 { return Self::IDENTITY; }
752        let half  = theta * 0.5;
753        let s     = half.sin() / theta;
754        Self::new(half.cos(), v.x * s, v.y * s, v.z * s)
755    }
756
757    /// Log map: converts a quaternion to its rotation vector (axis * angle).
758    pub fn log_map(self) -> Vec3 {
759        let v_norm = Vec3::new(self.x, self.y, self.z).length();
760        if v_norm < 1e-8 { return Vec3::ZERO; }
761        let theta = v_norm.atan2(self.w);
762        Vec3::new(self.x, self.y, self.z) * (theta / v_norm)
763    }
764
765    /// To 4x4 rotation matrix (column-major, compatible with glam).
766    pub fn to_mat4(self) -> glam::Mat4 {
767        glam::Mat4::from_quat(glam::Quat::from_xyzw(self.x, self.y, self.z, self.w))
768    }
769}
770
771impl Default for Quaternion {
772    fn default() -> Self { Self::IDENTITY }
773}
774
775impl std::ops::Mul for Quaternion {
776    type Output = Self;
777    fn mul(self, rhs: Self) -> Self { Quaternion::mul(self, rhs) }
778}
779
780// ── Tests ─────────────────────────────────────────────────────────────────────
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785
786    #[test]
787    fn complex_mul() {
788        let a = Complex::new(1.0, 2.0);
789        let b = Complex::new(3.0, 4.0);
790        let c = a * b;
791        assert!((c.re - (-5.0)).abs() < 1e-10);
792        assert!((c.im - 10.0).abs() < 1e-10);
793    }
794
795    #[test]
796    fn complex_euler_identity() {
797        // e^(iπ) + 1 = 0
798        let z = Complex::new(0.0, std::f64::consts::PI).exp();
799        assert!((z.re + 1.0).abs() < 1e-10);
800        assert!(z.im.abs() < 1e-10);
801    }
802
803    #[test]
804    fn complex_sqrt() {
805        let z = Complex::new(-1.0, 0.0).sqrt();
806        assert!(z.re.abs() < 1e-10);
807        assert!((z.im - 1.0).abs() < 1e-10);
808    }
809
810    #[test]
811    fn mandelbrot_origin_escapes_not() {
812        let params = MandelbrotParams::default();
813        let result = params.sample(Complex::ZERO);
814        assert!(!result.escaped);
815    }
816
817    #[test]
818    fn mandelbrot_far_escapes() {
819        let params = MandelbrotParams::default();
820        let result = params.sample(Complex::new(3.0, 3.0));
821        assert!(result.escaped);
822        assert!(result.iterations < 5);
823    }
824
825    #[test]
826    fn julia_samples() {
827        let j = JuliaParams::preset(0);
828        let r = j.sample(Complex::new(0.0, 0.0));
829        // Origin for Douady rabbit — should be inside (not escaped)
830        // (it's near the Julia set; result may vary)
831        let _ = r;
832    }
833
834    #[test]
835    fn quat_identity_rotates_nothing() {
836        let q = Quaternion::IDENTITY;
837        let v = Vec3::new(1.0, 2.0, 3.0);
838        let w = q.rotate(v);
839        assert!((w - v).length() < 1e-5);
840    }
841
842    #[test]
843    fn quat_axis_angle_roundtrip() {
844        let axis  = Vec3::new(0.0, 1.0, 0.0);
845        let angle = std::f32::consts::FRAC_PI_4;
846        let q     = Quaternion::from_axis_angle(axis, angle);
847        let v     = Vec3::new(1.0, 0.0, 0.0);
848        let w     = q.rotate(v);
849        // Should rotate X toward -Z by 45°
850        assert!((w.x - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-5);
851        assert!((w.z + std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-5);
852    }
853
854    #[test]
855    fn quat_slerp_halfway() {
856        let a = Quaternion::IDENTITY;
857        let b = Quaternion::from_axis_angle(Vec3::Y, std::f32::consts::PI);
858        let m = a.slerp(b, 0.5);
859        let expected_angle = std::f32::consts::FRAC_PI_2;
860        assert!((m.angle() - expected_angle).abs() < 1e-4);
861    }
862
863    #[test]
864    fn lyapunov_stable() {
865        let l = LyapunovFractal::default();
866        // r=2 should be stable (positive population growth, non-chaotic)
867        let e = l.sample(2.0, 2.0);
868        assert!(e < 0.0, "Expected negative Lyapunov exponent, got {e}");
869    }
870
871    #[test]
872    fn lyapunov_chaotic() {
873        let l = LyapunovFractal::default();
874        // r near 4 is chaotic
875        let e = l.sample(3.9, 3.9);
876        assert!(e > 0.0, "Expected positive Lyapunov exponent at r=3.9, got {e}");
877    }
878
879    #[test]
880    fn palette_maps_escaped() {
881        let result = EscapeResult {
882            escaped: true, iterations: 50, smooth: 51.3,
883            final_norm: 2.1, final_z: Complex::new(2.0, 0.5),
884        };
885        let color = FractalPalette::Fire.color(&result, 256);
886        assert!(color.w > 0.0);
887    }
888}