Skip to main content

oxiphysics_geometry/fractal_geometry/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use rand::RngExt;
6use std::collections::HashMap;
7
8/// Lacunarity computation for fractal point sets.
9///
10/// Lacunarity measures the "gappiness" or texture of a fractal. A higher
11/// lacunarity indicates more heterogeneity (more gaps).
12pub struct Lacunarity;
13impl Lacunarity {
14    /// Compute the gliding-box lacunarity of a 2D binary image (grid).
15    ///
16    /// `grid` is a 2D boolean array where `true` indicates the fractal is present.
17    /// `box_size` is the side length of the gliding box.
18    ///
19    /// Returns the lacunarity value Lambda = Var(mass) / Mean(mass)^2 + 1.
20    pub fn gliding_box(grid: &[Vec<bool>], box_size: usize) -> f64 {
21        let rows = grid.len();
22        if rows == 0 || box_size == 0 {
23            return 0.0;
24        }
25        let cols = grid[0].len();
26        if box_size > rows || box_size > cols {
27            return 0.0;
28        }
29        let mut masses = Vec::new();
30        for r in 0..=(rows - box_size) {
31            for c in 0..=(cols - box_size) {
32                let mut mass = 0u64;
33                for dr in 0..box_size {
34                    for dc in 0..box_size {
35                        if grid[r + dr][c + dc] {
36                            mass += 1;
37                        }
38                    }
39                }
40                masses.push(mass as f64);
41            }
42        }
43        if masses.is_empty() {
44            return 0.0;
45        }
46        let n = masses.len() as f64;
47        let mean = masses.iter().sum::<f64>() / n;
48        if mean.abs() < 1e-15 {
49            return 0.0;
50        }
51        let variance = masses.iter().map(|&m| (m - mean).powi(2)).sum::<f64>() / n;
52        variance / (mean * mean) + 1.0
53    }
54    /// Compute lacunarity for a point set by first rasterizing onto a grid.
55    ///
56    /// `resolution` is the grid side length. Points are mapped to grid cells.
57    pub fn from_points(points: &[Point2], resolution: usize, box_size: usize) -> f64 {
58        if points.is_empty() || resolution == 0 {
59            return 0.0;
60        }
61        let (min_x, max_x, min_y, max_y) = FractalDimension::bounding_box(points);
62        let dx = (max_x - min_x).max(1e-15);
63        let dy = (max_y - min_y).max(1e-15);
64        let mut grid = vec![vec![false; resolution]; resolution];
65        for p in points {
66            let ix = (((p.x - min_x) / dx) * (resolution - 1) as f64)
67                .round()
68                .clamp(0.0, (resolution - 1) as f64) as usize;
69            let iy = (((p.y - min_y) / dy) * (resolution - 1) as f64)
70                .round()
71                .clamp(0.0, (resolution - 1) as f64) as usize;
72            grid[iy][ix] = true;
73        }
74        Self::gliding_box(&grid, box_size)
75    }
76    /// Multi-scale lacunarity: compute lacunarity for a range of box sizes.
77    ///
78    /// Returns `(box_size, lacunarity)` pairs.
79    pub fn multiscale(grid: &[Vec<bool>], box_sizes: &[usize]) -> Vec<(usize, f64)> {
80        box_sizes
81            .iter()
82            .map(|&bs| (bs, Self::gliding_box(grid, bs)))
83            .collect()
84    }
85}
86/// Sierpinski arrowhead curve L-system preset.
87pub struct SierpinskiArrowhead;
88impl SierpinskiArrowhead {
89    /// Create the L-system for a Sierpinski arrowhead curve.
90    pub fn lsystem() -> LSystem {
91        LSystem::new(
92            vec!['F', 'G', '+', '-'],
93            vec![
94                ProductionRule::new('F', "G-F-G"),
95                ProductionRule::new('G', "F+G+F"),
96            ],
97            "F",
98        )
99    }
100    /// Theoretical dimension: log(3)/log(2) ≈ 1.585.
101    pub fn theoretical_dimension() -> f64 {
102        (3.0_f64).ln() / (2.0_f64).ln()
103    }
104    /// Generate Sierpinski arrowhead segments.
105    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
106        let ls = Self::lsystem();
107        ls.turtle_interpret(generations, step_size, 60.0)
108    }
109}
110/// An Iterated Function System consisting of affine transforms with associated probabilities.
111#[derive(Debug, Clone)]
112pub struct IteratedFunctionSystem {
113    /// The affine transforms.
114    pub transforms: Vec<AffineTransform2D>,
115    /// Probability weights for each transform (must sum to ~1.0).
116    pub probabilities: Vec<f64>,
117}
118impl IteratedFunctionSystem {
119    /// Create a new IFS with given transforms and probabilities.
120    ///
121    /// Handles empty transforms gracefully (returns empty results).
122    pub fn new(transforms: Vec<AffineTransform2D>, probabilities: Vec<f64>) -> Self {
123        if transforms.is_empty() {
124            return Self {
125                transforms: Vec::new(),
126                probabilities: Vec::new(),
127            };
128        }
129        assert_eq!(
130            transforms.len(),
131            probabilities.len(),
132            "transforms and probabilities must have equal length"
133        );
134        Self {
135            transforms,
136            probabilities,
137        }
138    }
139    /// Create an IFS with equal probabilities for all transforms.
140    pub fn with_equal_probabilities(transforms: Vec<AffineTransform2D>) -> Self {
141        let n = transforms.len();
142        assert!(n > 0, "IFS must have at least one transform");
143        let prob = 1.0 / n as f64;
144        let probabilities = vec![prob; n];
145        Self {
146            transforms,
147            probabilities,
148        }
149    }
150    /// Run the chaos game algorithm to generate attractor points.
151    ///
152    /// Starting from `start`, iterates `iterations` times selecting transforms
153    /// according to their probabilities. Returns the generated points after
154    /// discarding the first `skip` transient points.
155    pub fn chaos_game(&self, start: Point2, iterations: usize, skip: usize) -> Vec<Point2> {
156        use rand::RngExt;
157        let mut rng = rand::rng();
158        let mut point = start;
159        let mut points = Vec::with_capacity(iterations.saturating_sub(skip));
160        let cumulative = self.cumulative_distribution();
161        for i in 0..iterations {
162            let r: f64 = rng.random_range(0.0..1.0);
163            let idx = cumulative
164                .iter()
165                .position(|&c| r < c)
166                .unwrap_or(self.transforms.len() - 1);
167            point = self.transforms[idx].apply(&point);
168            if i >= skip {
169                points.push(point);
170            }
171        }
172        points
173    }
174    /// Deterministic iteration: apply all transforms to every point in the set.
175    ///
176    /// Starting from `initial_points`, applies all transforms to produce
177    /// the next generation. Repeats for `generations` steps.
178    pub fn deterministic_iterate(
179        &self,
180        initial_points: &[Point2],
181        generations: usize,
182    ) -> Vec<Point2> {
183        let mut current = initial_points.to_vec();
184        for _ in 0..generations {
185            let mut next = Vec::with_capacity(current.len() * self.transforms.len());
186            for t in &self.transforms {
187                for p in &current {
188                    next.push(t.apply(p));
189                }
190            }
191            current = next;
192        }
193        current
194    }
195    /// Number of transforms in this IFS.
196    pub fn num_transforms(&self) -> usize {
197        self.transforms.len()
198    }
199    /// Create the Barnsley Fern IFS.
200    pub fn barnsley_fern() -> Self {
201        let transforms = vec![
202            AffineTransform2D::new(0.0, 0.0, 0.0, 0.16, 0.0, 0.0),
203            AffineTransform2D::new(0.85, 0.04, -0.04, 0.85, 0.0, 1.6),
204            AffineTransform2D::new(0.20, -0.26, 0.23, 0.22, 0.0, 1.6),
205            AffineTransform2D::new(-0.15, 0.28, 0.26, 0.24, 0.0, 0.44),
206        ];
207        let probabilities = vec![0.01, 0.85, 0.07, 0.07];
208        Self::new(transforms, probabilities)
209    }
210    /// Generate `n` points using a deterministic seeded chaos game.
211    ///
212    /// Returns points as `[x, y]` arrays.
213    pub fn generate(&self, n: usize, seed: u64) -> Vec<[f64; 2]> {
214        if self.transforms.is_empty() {
215            return Vec::new();
216        }
217        let cumulative = self.cumulative_distribution();
218        let mut x = 0.0_f64;
219        let mut y = 0.0_f64;
220        let mut rng_state = seed;
221        let mut points = Vec::with_capacity(n);
222        // Simple LCG pseudo-random for reproducibility
223        for _ in 0..n {
224            rng_state = rng_state
225                .wrapping_mul(6364136223846793005)
226                .wrapping_add(1442695040888963407);
227            let r = ((rng_state >> 33) as f64) / (u32::MAX as f64);
228            let idx = cumulative
229                .iter()
230                .position(|&c| r < c)
231                .unwrap_or(self.transforms.len() - 1);
232            let t = &self.transforms[idx];
233            let nx = t.a * x + t.b * y + t.e;
234            let ny = t.c * x + t.d * y + t.f;
235            x = nx;
236            y = ny;
237            points.push([x, y]);
238        }
239        points
240    }
241    /// Build cumulative distribution from probabilities.
242    fn cumulative_distribution(&self) -> Vec<f64> {
243        let mut cum = Vec::with_capacity(self.probabilities.len());
244        let mut acc = 0.0;
245        for &p in &self.probabilities {
246            acc += p;
247            cum.push(acc);
248        }
249        cum
250    }
251}
252/// A production rule for an L-system.
253#[derive(Debug, Clone)]
254pub struct ProductionRule {
255    /// The predecessor character.
256    pub predecessor: char,
257    /// The successor string.
258    pub successor: String,
259}
260impl ProductionRule {
261    /// Create a new production rule.
262    pub fn new(predecessor: char, successor: impl Into<String>) -> Self {
263        Self {
264            predecessor,
265            successor: successor.into(),
266        }
267    }
268}
269/// The Mandelbrot set: z_{n+1} = z_n^2 + c, starting from z_0 = 0.
270///
271/// Points `c` in the complex plane are classified by the escape time
272/// of the orbit {z_n}.
273pub struct MandelbrotSet {
274    /// Maximum number of iterations before declaring a point in the set.
275    pub max_iter: u32,
276    /// Escape radius squared.
277    pub escape_radius_sq: f64,
278}
279impl MandelbrotSet {
280    /// Create a new Mandelbrot set with given parameters.
281    pub fn new(max_iter: u32, escape_radius: f64) -> Self {
282        Self {
283            max_iter,
284            escape_radius_sq: escape_radius * escape_radius,
285        }
286    }
287    /// Default Mandelbrot set with max_iter=256, escape_radius=2.
288    pub fn default_set() -> Self {
289        Self::new(256, 2.0)
290    }
291    /// Compute escape time for a complex number c = (cr, ci).
292    ///
293    /// Returns `None` if the orbit does not escape within `max_iter` iterations,
294    /// or `Some(iterations)` with the escape iteration count.
295    pub fn escape_time(&self, cr: f64, ci: f64) -> Option<u32> {
296        let mut zr = 0.0;
297        let mut zi = 0.0;
298        for i in 0..self.max_iter {
299            let zr2 = zr * zr;
300            let zi2 = zi * zi;
301            if zr2 + zi2 > self.escape_radius_sq {
302                return Some(i);
303            }
304            zi = 2.0 * zr * zi + ci;
305            zr = zr2 - zi2 + cr;
306        }
307        None
308    }
309    /// Smooth escape time using renormalization.
310    ///
311    /// Returns a continuous (smooth) iteration count for coloring.
312    pub fn smooth_escape_time(&self, cr: f64, ci: f64) -> f64 {
313        let mut zr = 0.0;
314        let mut zi = 0.0;
315        for i in 0..self.max_iter {
316            let zr2 = zr * zr;
317            let zi2 = zi * zi;
318            if zr2 + zi2 > self.escape_radius_sq {
319                let modulus = (zr2 + zi2).sqrt();
320                let mu = i as f64 - modulus.ln().ln() / (2.0_f64).ln();
321                return mu;
322            }
323            zi = 2.0 * zr * zi + ci;
324            zr = zr2 - zi2 + cr;
325        }
326        self.max_iter as f64
327    }
328    /// Check if a point is in the Mandelbrot set.
329    pub fn is_in_set(&self, cr: f64, ci: f64) -> bool {
330        self.escape_time(cr, ci).is_none()
331    }
332    /// Detect if a point is near the boundary of the set.
333    ///
334    /// A point is near the boundary if it is in the set but has a neighbor
335    /// that is not (or vice versa), within the given `delta` distance.
336    pub fn is_boundary(&self, cr: f64, ci: f64, delta: f64) -> bool {
337        let center = self.is_in_set(cr, ci);
338        let offsets = [(delta, 0.0), (-delta, 0.0), (0.0, delta), (0.0, -delta)];
339        for (dr, di) in offsets {
340            if self.is_in_set(cr + dr, ci + di) != center {
341                return true;
342            }
343        }
344        false
345    }
346    /// Compute the area of the Mandelbrot set using Monte Carlo sampling.
347    ///
348    /// Samples random points in the rectangle \[x_min, x_max\] x \[y_min, y_max\]
349    /// and estimates the area as the fraction of points in the set times the
350    /// rectangle area.
351    pub fn area_monte_carlo(
352        &self,
353        x_min: f64,
354        x_max: f64,
355        y_min: f64,
356        y_max: f64,
357        num_samples: usize,
358    ) -> f64 {
359        let mut rng = rand::rng();
360        let mut count = 0usize;
361        for _ in 0..num_samples {
362            let cr = rng.random_range(x_min..x_max);
363            let ci = rng.random_range(y_min..y_max);
364            if self.is_in_set(cr, ci) {
365                count += 1;
366            }
367        }
368        let rect_area = (x_max - x_min) * (y_max - y_min);
369        rect_area * count as f64 / num_samples as f64
370    }
371    /// Generate a grid of escape times for rendering.
372    ///
373    /// Returns a row-major `width x height` grid of escape times.
374    pub fn render_grid(
375        &self,
376        x_min: f64,
377        x_max: f64,
378        y_min: f64,
379        y_max: f64,
380        width: usize,
381        height: usize,
382    ) -> Vec<Vec<Option<u32>>> {
383        let dx = (x_max - x_min) / width as f64;
384        let dy = (y_max - y_min) / height as f64;
385        (0..height)
386            .map(|j| {
387                let ci = y_min + (j as f64 + 0.5) * dy;
388                (0..width)
389                    .map(|i| {
390                        let cr = x_min + (i as f64 + 0.5) * dx;
391                        self.escape_time(cr, ci)
392                    })
393                    .collect()
394            })
395            .collect()
396    }
397}
398/// Hilbert curve coordinate mapping (non-L-system approach).
399///
400/// Provides index-to-coordinate and coordinate-to-index conversions
401/// for Hilbert curves of order `n` (grid size `2^n x 2^n`).
402pub struct HilbertCurveMap;
403impl HilbertCurveMap {
404    /// Convert a Hilbert curve index `d` to (x, y) coordinates for a curve of order `n`.
405    pub fn d2xy(n: u32, d: u32) -> (u32, u32) {
406        let mut rx: u32;
407        let mut ry: u32;
408        let mut x = 0_u32;
409        let mut y = 0_u32;
410        let mut d = d;
411        let mut s = 1_u32;
412        while s < n {
413            rx = if (d & 2) != 0 { 1 } else { 0 };
414            ry = if (d & 1) != 0 {
415                if rx == 0 { 1 } else { 0 }
416            } else {
417                0
418            };
419            Self::rot(s, &mut x, &mut y, rx, ry);
420            x += s * rx;
421            y += s * ry;
422            d /= 4;
423            s *= 2;
424        }
425        (x, y)
426    }
427    /// Convert (x, y) coordinates to a Hilbert curve index for a curve of order `n`.
428    pub fn xy2d(n: u32, x: u32, y: u32) -> u32 {
429        let mut rx: u32;
430        let mut ry: u32;
431        let mut d = 0_u32;
432        let mut x = x;
433        let mut y = y;
434        let mut s = n / 2;
435        while s > 0 {
436            rx = if (x & s) > 0 { 1 } else { 0 };
437            ry = if (y & s) > 0 { 1 } else { 0 };
438            d += s * s * ((3 * rx) ^ ry);
439            Self::rot(s, &mut x, &mut y, rx, ry);
440            s /= 2;
441        }
442        d
443    }
444    /// Rotate/flip a quadrant.
445    fn rot(n: u32, x: &mut u32, y: &mut u32, rx: u32, ry: u32) {
446        if ry == 0 {
447            if rx == 1 {
448                *x = n.wrapping_sub(1).wrapping_sub(*x);
449                *y = n.wrapping_sub(1).wrapping_sub(*y);
450            }
451            std::mem::swap(x, y);
452        }
453    }
454    /// Generate the full Hilbert curve path as a sequence of (x, y) for order `n`.
455    ///
456    /// `n` must be a power of 2. Returns `n*n` points.
457    pub fn generate_path(n: u32) -> Vec<(u32, u32)> {
458        let total = n * n;
459        (0..total).map(|d| Self::d2xy(n, d)).collect()
460    }
461}
462/// Fractal terrain generator using the diamond-square algorithm.
463///
464/// Produces a heightmap on a (2^n + 1) x (2^n + 1) grid.
465pub struct FractalTerrain;
466impl FractalTerrain {
467    /// Generate a terrain heightmap using the diamond-square algorithm.
468    ///
469    /// `power` determines the grid size: side = 2^power + 1.
470    /// `roughness` controls how quickly the random displacement decreases (0..1 typical).
471    /// `seed_corners` sets the four corner heights `[top_left, top_right, bot_left, bot_right]`.
472    pub fn diamond_square(power: u32, roughness: f64, seed_corners: [f64; 4]) -> Vec<Vec<f64>> {
473        let size = (1 << power) + 1;
474        let mut grid = vec![vec![0.0_f64; size]; size];
475        let last = size - 1;
476        grid[0][0] = seed_corners[0];
477        grid[0][last] = seed_corners[1];
478        grid[last][0] = seed_corners[2];
479        grid[last][last] = seed_corners[3];
480        let mut rng = rand::rng();
481        let mut step = last;
482        let mut scale = 1.0_f64;
483        while step > 1 {
484            let half = step / 2;
485            let mut y = 0;
486            while y < last {
487                let mut x = 0;
488                while x < last {
489                    let avg = (grid[y][x]
490                        + grid[y][x + step]
491                        + grid[y + step][x]
492                        + grid[y + step][x + step])
493                        / 4.0;
494                    grid[y + half][x + half] = avg + rng.random_range(-scale..scale);
495                    x += step;
496                }
497                y += step;
498            }
499            let mut y = 0;
500            while y <= last {
501                let x_start = if (y / half).is_multiple_of(2) {
502                    half
503                } else {
504                    0
505                };
506                let mut x = x_start;
507                while x <= last {
508                    let mut sum = 0.0;
509                    let mut count = 0.0;
510                    if y >= half {
511                        sum += grid[y - half][x];
512                        count += 1.0;
513                    }
514                    if y + half <= last {
515                        sum += grid[y + half][x];
516                        count += 1.0;
517                    }
518                    if x >= half {
519                        sum += grid[y][x - half];
520                        count += 1.0;
521                    }
522                    if x + half <= last {
523                        sum += grid[y][x + half];
524                        count += 1.0;
525                    }
526                    grid[y][x] = sum / count + rng.random_range(-scale..scale);
527                    x += step;
528                }
529                y += half;
530            }
531            step = half;
532            scale *= roughness;
533        }
534        grid
535    }
536    /// Generate terrain using midpoint displacement (1D variant).
537    ///
538    /// Produces a heightmap profile (1D array) of size 2^power + 1.
539    /// `roughness` is the Hurst exponent factor (0 < roughness < 1).
540    pub fn midpoint_displacement_1d(
541        power: u32,
542        roughness: f64,
543        h_left: f64,
544        h_right: f64,
545    ) -> Vec<f64> {
546        let size = (1 << power) + 1;
547        let mut heights = vec![0.0_f64; size];
548        heights[0] = h_left;
549        heights[size - 1] = h_right;
550        let mut rng = rand::rng();
551        let mut step = size - 1;
552        let mut scale = 1.0_f64;
553        while step > 1 {
554            let half = step / 2;
555            let mut i = half;
556            while i < size {
557                let left = heights[i - half];
558                let right = if i + half < size {
559                    heights[i + half]
560                } else {
561                    heights[size - 1]
562                };
563                heights[i] = (left + right) / 2.0 + rng.random_range(-scale..scale);
564                i += step;
565            }
566            step = half;
567            scale *= roughness;
568        }
569        heights
570    }
571    /// Compute the RMS (root mean square) roughness of a heightmap.
572    pub fn rms_roughness(grid: &[Vec<f64>]) -> f64 {
573        let mut sum = 0.0;
574        let mut count = 0usize;
575        let mut mean = 0.0;
576        for row in grid {
577            for &h in row {
578                mean += h;
579                count += 1;
580            }
581        }
582        if count == 0 {
583            return 0.0;
584        }
585        mean /= count as f64;
586        for row in grid {
587            for &h in row {
588                sum += (h - mean).powi(2);
589            }
590        }
591        (sum / count as f64).sqrt()
592    }
593    /// Find the min and max height in a grid.
594    pub fn height_range(grid: &[Vec<f64>]) -> (f64, f64) {
595        let mut lo = f64::MAX;
596        let mut hi = f64::MIN;
597        for row in grid {
598            for &h in row {
599                if h < lo {
600                    lo = h;
601                }
602                if h > hi {
603                    hi = h;
604                }
605            }
606        }
607        (lo, hi)
608    }
609}
610/// Simple fractal mesh: triangle mesh from a fractal terrain heightmap.
611///
612/// Converts a 2D heightmap grid into a triangle mesh with vertices and triangle
613/// index lists suitable for rendering.
614pub struct FractalMesh;
615impl FractalMesh {
616    /// Convert a heightmap to a triangle mesh.
617    ///
618    /// Returns `(vertices, triangles)` where vertices are `[f64; 3]` positions
619    /// and triangles are index triples into the vertex array.
620    pub fn from_heightmap(grid: &[Vec<f64>], spacing: f64) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
621        let rows = grid.len();
622        if rows == 0 {
623            return (Vec::new(), Vec::new());
624        }
625        let cols = grid[0].len();
626        let mut vertices = Vec::with_capacity(rows * cols);
627        for (r, row) in grid.iter().enumerate() {
628            for (c, &h) in row.iter().enumerate() {
629                vertices.push([c as f64 * spacing, h, r as f64 * spacing]);
630            }
631        }
632        let mut triangles = Vec::new();
633        for r in 0..(rows - 1) {
634            for c in 0..(cols - 1) {
635                let i0 = r * cols + c;
636                let i1 = i0 + 1;
637                let i2 = i0 + cols;
638                let i3 = i2 + 1;
639                triangles.push([i0, i2, i1]);
640                triangles.push([i1, i2, i3]);
641            }
642        }
643        (vertices, triangles)
644    }
645    /// Compute the total surface area of a triangle mesh.
646    pub fn surface_area(vertices: &[[f64; 3]], triangles: &[[usize; 3]]) -> f64 {
647        let mut area = 0.0;
648        for tri in triangles {
649            let a = vertices[tri[0]];
650            let b = vertices[tri[1]];
651            let c = vertices[tri[2]];
652            let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
653            let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
654            let cross = [
655                ab[1] * ac[2] - ab[2] * ac[1],
656                ab[2] * ac[0] - ab[0] * ac[2],
657                ab[0] * ac[1] - ab[1] * ac[0],
658            ];
659            area += (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt() / 2.0;
660        }
661        area
662    }
663    /// Number of vertices and triangles expected from a heightmap of given size.
664    pub fn expected_counts(rows: usize, cols: usize) -> (usize, usize) {
665        let verts = rows * cols;
666        let tris = if rows > 1 && cols > 1 {
667            2 * (rows - 1) * (cols - 1)
668        } else {
669            0
670        };
671        (verts, tris)
672    }
673}
674/// Hilbert curve L-system preset.
675pub struct HilbertCurve;
676impl HilbertCurve {
677    /// Create the L-system for a Hilbert curve.
678    pub fn lsystem() -> LSystem {
679        LSystem::new(
680            vec!['A', 'B', 'F', '+', '-'],
681            vec![
682                ProductionRule::new('A', "+BF-AFA-FB+"),
683                ProductionRule::new('B', "-AF+BFB+FA-"),
684            ],
685            "A",
686        )
687    }
688    /// Generate Hilbert curve segments.
689    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
690        let ls = Self::lsystem();
691        ls.turtle_interpret(generations, step_size, 90.0)
692    }
693}
694/// Methods for estimating the fractal dimension of a point set.
695pub struct FractalDimension;
696impl FractalDimension {
697    /// Estimate the box-counting (Minkowski) dimension of a 2D point set.
698    ///
699    /// Counts the number of boxes of side `epsilon` needed to cover the point set,
700    /// for geometrically decreasing `epsilon` values. Returns `(dimension, r_squared)`
701    /// where `r_squared` is the coefficient of determination of the linear fit.
702    pub fn box_counting(points: &[Point2], num_scales: usize) -> (f64, f64) {
703        if points.is_empty() || num_scales < 2 {
704            return (0.0, 0.0);
705        }
706        let (min_x, max_x, min_y, max_y) = Self::bounding_box(points);
707        let extent = (max_x - min_x).max(max_y - min_y).max(1e-15);
708        let mut log_eps = Vec::with_capacity(num_scales);
709        let mut log_n = Vec::with_capacity(num_scales);
710        for i in 0..num_scales {
711            let epsilon = extent / (2.0_f64).powi(i as i32 + 1);
712            if epsilon < 1e-15 {
713                break;
714            }
715            let mut occupied = std::collections::HashSet::new();
716            for p in points {
717                let ix = ((p.x - min_x) / epsilon).floor() as i64;
718                let iy = ((p.y - min_y) / epsilon).floor() as i64;
719                occupied.insert((ix, iy));
720            }
721            let count = occupied.len() as f64;
722            if count > 0.0 {
723                log_eps.push(epsilon.ln());
724                log_n.push(count.ln());
725            }
726        }
727        if log_eps.len() < 2 {
728            return (0.0, 0.0);
729        }
730        let (slope, r_sq) = Self::linear_regression(&log_eps, &log_n);
731        (-slope, r_sq)
732    }
733    /// Estimate the correlation dimension of a point set.
734    ///
735    /// Uses the Grassberger-Procaccia algorithm. For various radii `r`,
736    /// counts pairs of points within distance `r`, then fits `log C(r)` vs `log r`.
737    pub fn correlation_dimension(points: &[Point2], num_scales: usize) -> (f64, f64) {
738        let n = points.len();
739        if n < 10 || num_scales < 2 {
740            return (0.0, 0.0);
741        }
742        let mut max_dist = 0.0_f64;
743        let mut min_dist = f64::MAX;
744        let num_pairs = n * (n - 1) / 2;
745        let mut distances = Vec::with_capacity(num_pairs);
746        for i in 0..n {
747            for j in (i + 1)..n {
748                let d = points[i].distance(&points[j]);
749                distances.push(d);
750                if d > max_dist {
751                    max_dist = d;
752                }
753                if d > 0.0 && d < min_dist {
754                    min_dist = d;
755                }
756            }
757        }
758        if max_dist < 1e-15 {
759            return (0.0, 0.0);
760        }
761        distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
762        let mut log_r = Vec::with_capacity(num_scales);
763        let mut log_c = Vec::with_capacity(num_scales);
764        let r_min = min_dist.max(max_dist * 0.001);
765        let r_max = max_dist * 0.5;
766        let ratio = (r_max / r_min).powf(1.0 / (num_scales as f64 - 1.0));
767        for i in 0..num_scales {
768            let r = r_min * ratio.powi(i as i32);
769            let count = distances.partition_point(|&d| d <= r);
770            if count > 0 {
771                let c = count as f64 / num_pairs as f64;
772                log_r.push(r.ln());
773                log_c.push(c.ln());
774            }
775        }
776        if log_r.len() < 2 {
777            return (0.0, 0.0);
778        }
779        let (slope, r_sq) = Self::linear_regression(&log_r, &log_c);
780        (slope, r_sq)
781    }
782    /// Estimate Hausdorff dimension using box-counting with Richardson extrapolation.
783    ///
784    /// This provides a more accurate estimate by using multiple scale ranges.
785    pub fn hausdorff_estimate(points: &[Point2], num_scales: usize) -> f64 {
786        if points.is_empty() || num_scales < 4 {
787            return 0.0;
788        }
789        let (dim1, _) = Self::box_counting(points, num_scales);
790        let half = num_scales / 2;
791        let (dim2, _) = Self::box_counting(points, half);
792        0.6 * dim1 + 0.4 * dim2
793    }
794    /// Compute bounding box of a point set.
795    pub(super) fn bounding_box(points: &[Point2]) -> (f64, f64, f64, f64) {
796        let mut min_x = f64::MAX;
797        let mut max_x = f64::MIN;
798        let mut min_y = f64::MAX;
799        let mut max_y = f64::MIN;
800        for p in points {
801            if p.x < min_x {
802                min_x = p.x;
803            }
804            if p.x > max_x {
805                max_x = p.x;
806            }
807            if p.y < min_y {
808                min_y = p.y;
809            }
810            if p.y > max_y {
811                max_y = p.y;
812            }
813        }
814        (min_x, max_x, min_y, max_y)
815    }
816    /// Simple linear regression returning (slope, r_squared).
817    fn linear_regression(x: &[f64], y: &[f64]) -> (f64, f64) {
818        let n = x.len() as f64;
819        let sum_x: f64 = x.iter().sum();
820        let sum_y: f64 = y.iter().sum();
821        let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
822        let sum_x2: f64 = x.iter().map(|xi| xi * xi).sum();
823        let sum_y2: f64 = y.iter().map(|yi| yi * yi).sum();
824        let denom = n * sum_x2 - sum_x * sum_x;
825        if denom.abs() < 1e-30 {
826            return (0.0, 0.0);
827        }
828        let slope = (n * sum_xy - sum_x * sum_y) / denom;
829        let ss_tot = sum_y2 - sum_y * sum_y / n;
830        let ss_res = sum_y2 - slope * sum_xy - (sum_y - slope * sum_x) * sum_y / n;
831        let r_sq = if ss_tot.abs() > 1e-30 {
832            1.0 - ss_res / ss_tot
833        } else {
834            0.0
835        };
836        (slope, r_sq.clamp(0.0, 1.0))
837    }
838}
839/// Sierpinski triangle IFS preset.
840///
841/// The Sierpinski triangle is constructed from 3 affine transforms,
842/// each scaling by 1/2 and translating to a corner of the triangle.
843pub struct SierpinskiTriangle;
844impl SierpinskiTriangle {
845    /// Create the IFS for a Sierpinski triangle.
846    pub fn ifs() -> IteratedFunctionSystem {
847        let transforms = vec![
848            AffineTransform2D::new(0.5, 0.0, 0.0, 0.5, 0.0, 0.0),
849            AffineTransform2D::new(0.5, 0.0, 0.0, 0.5, 0.5, 0.0),
850            AffineTransform2D::new(0.5, 0.0, 0.0, 0.5, 0.25, 0.433),
851        ];
852        IteratedFunctionSystem::with_equal_probabilities(transforms)
853    }
854    /// Theoretical Hausdorff dimension: log(3)/log(2) ≈ 1.585.
855    pub fn theoretical_dimension() -> f64 {
856        (3.0_f64).ln() / (2.0_f64).ln()
857    }
858    /// Generate Sierpinski triangle points via chaos game.
859    pub fn generate(num_points: usize) -> Vec<Point2> {
860        let ifs = Self::ifs();
861        ifs.chaos_game(Point2::new(0.0, 0.0), num_points + 100, 100)
862    }
863}
864/// A 2D affine transformation: 2x2 matrix \[\[a, b\\], \[c, d\]] + translation (e, f).
865///
866/// Applies as: x' = a*x + b*y + e, y' = c*x + d*y + f
867#[derive(Debug, Clone, Copy, PartialEq)]
868pub struct AffineTransform2D {
869    /// Matrix element (0,0).
870    pub a: f64,
871    /// Matrix element (0,1).
872    pub b: f64,
873    /// Matrix element (1,0).
874    pub c: f64,
875    /// Matrix element (1,1).
876    pub d: f64,
877    /// Translation x.
878    pub e: f64,
879    /// Translation y.
880    pub f: f64,
881}
882impl AffineTransform2D {
883    /// Create a new affine transform.
884    pub fn new(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Self {
885        Self { a, b, c, d, e, f }
886    }
887    /// Identity transform.
888    pub fn identity() -> Self {
889        Self::new(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
890    }
891    /// Create a rotation transform.
892    pub fn rotation(angle_rad: f64) -> Self {
893        let cos = angle_rad.cos();
894        let sin = angle_rad.sin();
895        Self::new(cos, -sin, sin, cos, 0.0, 0.0)
896    }
897    /// Create a scaling transform.
898    pub fn scaling(sx: f64, sy: f64) -> Self {
899        Self::new(sx, 0.0, 0.0, sy, 0.0, 0.0)
900    }
901    /// Create a translation transform.
902    pub fn translation(tx: f64, ty: f64) -> Self {
903        Self::new(1.0, 0.0, 0.0, 1.0, tx, ty)
904    }
905    /// Apply this transform to a point.
906    pub fn apply(&self, p: &Point2) -> Point2 {
907        Point2 {
908            x: self.a * p.x + self.b * p.y + self.e,
909            y: self.c * p.x + self.d * p.y + self.f,
910        }
911    }
912    /// Compose two transforms: self followed by other.
913    ///
914    /// The result transform applies self first, then other.
915    pub fn compose(&self, other: &AffineTransform2D) -> AffineTransform2D {
916        AffineTransform2D {
917            a: other.a * self.a + other.b * self.c,
918            b: other.a * self.b + other.b * self.d,
919            c: other.c * self.a + other.d * self.c,
920            d: other.c * self.b + other.d * self.d,
921            e: other.a * self.e + other.b * self.f + other.e,
922            f: other.c * self.e + other.d * self.f + other.f,
923        }
924    }
925    /// Determinant of the linear part.
926    pub fn determinant(&self) -> f64 {
927        self.a * self.d - self.b * self.c
928    }
929    /// Inverse transform, if the determinant is nonzero.
930    pub fn inverse(&self) -> Option<AffineTransform2D> {
931        let det = self.determinant();
932        if det.abs() < 1e-15 {
933            return None;
934        }
935        let inv_det = 1.0 / det;
936        let a = self.d * inv_det;
937        let b = -self.b * inv_det;
938        let c = -self.c * inv_det;
939        let d = self.a * inv_det;
940        let e = -(a * self.e + b * self.f);
941        let f = -(c * self.e + d * self.f);
942        Some(AffineTransform2D { a, b, c, d, e, f })
943    }
944    /// Contractivity factor (spectral norm estimate of the linear part).
945    pub fn contractivity(&self) -> f64 {
946        (self.a * self.a + self.b * self.b + self.c * self.c + self.d * self.d).sqrt()
947    }
948}
949/// Classification of an orbit in the Julia/Mandelbrot iteration.
950#[derive(Debug, Clone, PartialEq, Eq)]
951pub enum OrbitType {
952    /// Orbit escapes to infinity.
953    Escaping,
954    /// Orbit converges to a fixed point.
955    FixedPoint,
956    /// Orbit is periodic with the given period.
957    Periodic(usize),
958    /// Orbit is bounded but no periodicity detected.
959    Bounded,
960}
961/// Koch snowflake: three Koch curves forming a closed triangle.
962///
963/// The Koch snowflake starts from an equilateral triangle and applies the Koch
964/// curve construction to each side, producing a fractal with dimension log(4)/log(3).
965pub struct KochSnowflake;
966impl KochSnowflake {
967    /// Theoretical Hausdorff dimension: same as Koch curve, log(4)/log(3).
968    pub fn theoretical_dimension() -> f64 {
969        (4.0_f64).ln() / (3.0_f64).ln()
970    }
971    /// Generate the Koch snowflake as line segments by recursively subdividing
972    /// each side of an equilateral triangle.
973    pub fn generate(generations: usize, side_length: f64) -> Vec<(Point2, Point2)> {
974        let h = side_length * (3.0_f64).sqrt() / 2.0;
975        let vertices = [
976            Point2::new(0.0, 0.0),
977            Point2::new(side_length, 0.0),
978            Point2::new(side_length / 2.0, h),
979        ];
980        let mut segments = Vec::new();
981        for i in 0..3 {
982            let a = vertices[i];
983            let b = vertices[(i + 1) % 3];
984            Self::subdivide(a, b, generations, &mut segments);
985        }
986        segments
987    }
988    /// Recursively subdivide one edge of the snowflake.
989    fn subdivide(a: Point2, b: Point2, depth: usize, out: &mut Vec<(Point2, Point2)>) {
990        if depth == 0 {
991            out.push((a, b));
992            return;
993        }
994        let dx = b.x - a.x;
995        let dy = b.y - a.y;
996        let p1 = Point2::new(a.x + dx / 3.0, a.y + dy / 3.0);
997        let p3 = Point2::new(a.x + 2.0 * dx / 3.0, a.y + 2.0 * dy / 3.0);
998        let cos60 = 0.5_f64;
999        let sin60 = (3.0_f64).sqrt() / 2.0;
1000        let mx = dx / 3.0;
1001        let my = dy / 3.0;
1002        let apex = Point2::new(
1003            p1.x + mx * cos60 - my * sin60,
1004            p1.y + mx * sin60 + my * cos60,
1005        );
1006        Self::subdivide(a, p1, depth - 1, out);
1007        Self::subdivide(p1, apex, depth - 1, out);
1008        Self::subdivide(apex, p3, depth - 1, out);
1009        Self::subdivide(p3, b, depth - 1, out);
1010    }
1011    /// Count the number of segments after `n` generations.
1012    pub fn segment_count(n: usize) -> usize {
1013        3 * 4_usize.pow(n as u32)
1014    }
1015    /// Perimeter of the snowflake after `n` generations with initial side `s`.
1016    pub fn perimeter(n: usize, side_length: f64) -> f64 {
1017        3.0 * side_length * (4.0 / 3.0_f64).powi(n as i32)
1018    }
1019    /// Area of the Koch snowflake after `n` generations.
1020    pub fn area(n: usize, side_length: f64) -> f64 {
1021        let a0 = (3.0_f64).sqrt() / 4.0 * side_length * side_length;
1022        let mut area = a0;
1023        for k in 1..=n {
1024            let num_new = 3 * 4_usize.pow((k - 1) as u32);
1025            let triangle_side = side_length / 3.0_f64.powi(k as i32);
1026            let triangle_area = (3.0_f64).sqrt() / 4.0 * triangle_side * triangle_side;
1027            area += num_new as f64 * triangle_area;
1028        }
1029        area
1030    }
1031}
1032/// Multifractal analysis tools.
1033///
1034/// Multifractal analysis examines the distribution of a measure over a fractal
1035/// set, computing the spectrum of generalized dimensions D(q) and the
1036/// singularity spectrum f(alpha).
1037pub struct Multifractal;
1038impl Multifractal {
1039    /// Compute the generalized fractal dimension D(q) for a 2D point set
1040    /// using the box-counting approach.
1041    ///
1042    /// `q` is the moment order (q=0 gives box-counting dim, q=1 info dim, q=2 correlation dim).
1043    /// Returns `(dimension, r_squared)`.
1044    pub fn generalized_dimension(points: &[Point2], q: f64, num_scales: usize) -> (f64, f64) {
1045        if points.is_empty() || num_scales < 2 {
1046            return (0.0, 0.0);
1047        }
1048        let (min_x, max_x, min_y, max_y) = FractalDimension::bounding_box(points);
1049        let extent = (max_x - min_x).max(max_y - min_y).max(1e-15);
1050        let n_total = points.len() as f64;
1051        let mut log_eps = Vec::with_capacity(num_scales);
1052        let mut log_partition = Vec::with_capacity(num_scales);
1053        for i in 0..num_scales {
1054            let epsilon = extent / (2.0_f64).powi(i as i32 + 1);
1055            if epsilon < 1e-15 {
1056                break;
1057            }
1058            let mut box_counts: HashMap<(i64, i64), usize> = HashMap::new();
1059            for p in points {
1060                let ix = ((p.x - min_x) / epsilon).floor() as i64;
1061                let iy = ((p.y - min_y) / epsilon).floor() as i64;
1062                *box_counts.entry((ix, iy)).or_insert(0) += 1;
1063            }
1064            if (q - 1.0).abs() < 1e-10 {
1065                let mut entropy = 0.0;
1066                for &count in box_counts.values() {
1067                    let pi = count as f64 / n_total;
1068                    if pi > 0.0 {
1069                        entropy -= pi * pi.ln();
1070                    }
1071                }
1072                if entropy > 0.0 {
1073                    log_eps.push(epsilon.ln());
1074                    log_partition.push(entropy);
1075                }
1076            } else {
1077                let partition: f64 = box_counts
1078                    .values()
1079                    .map(|&count| (count as f64 / n_total).powf(q))
1080                    .sum();
1081                if partition > 0.0 {
1082                    log_eps.push(epsilon.ln());
1083                    log_partition.push(partition.ln());
1084                }
1085            }
1086        }
1087        if log_eps.len() < 2 {
1088            return (0.0, 0.0);
1089        }
1090        let (slope, r_sq) = FractalDimension::linear_regression(&log_eps, &log_partition);
1091        if (q - 1.0).abs() < 1e-10 {
1092            (-slope, r_sq)
1093        } else {
1094            (slope / (q - 1.0), r_sq)
1095        }
1096    }
1097    /// Compute the multifractal spectrum D(q) for a range of q values.
1098    ///
1099    /// Returns a vector of `(q, D_q)` pairs.
1100    pub fn spectrum(points: &[Point2], q_values: &[f64], num_scales: usize) -> Vec<(f64, f64)> {
1101        q_values
1102            .iter()
1103            .map(|&q| {
1104                let (dq, _) = Self::generalized_dimension(points, q, num_scales);
1105                (q, dq)
1106            })
1107            .collect()
1108    }
1109    /// Compute the singularity spectrum f(alpha) from the D(q) spectrum.
1110    ///
1111    /// Uses the Legendre transform: alpha(q) = d(tau(q))/dq, f(alpha) = q*alpha - tau(q),
1112    /// where tau(q) = (q-1)*D(q).
1113    ///
1114    /// Returns a vector of `(alpha, f_alpha)` pairs.
1115    pub fn singularity_spectrum(dq_spectrum: &[(f64, f64)]) -> Vec<(f64, f64)> {
1116        if dq_spectrum.len() < 3 {
1117            return Vec::new();
1118        }
1119        let tau: Vec<(f64, f64)> = dq_spectrum
1120            .iter()
1121            .map(|&(q, dq)| (q, (q - 1.0) * dq))
1122            .collect();
1123        let mut result = Vec::new();
1124        for i in 1..(tau.len() - 1) {
1125            let dq = tau[i + 1].0 - tau[i - 1].0;
1126            if dq.abs() < 1e-15 {
1127                continue;
1128            }
1129            let alpha = (tau[i + 1].1 - tau[i - 1].1) / dq;
1130            let f_alpha = tau[i].0 * alpha - tau[i].1;
1131            result.push((alpha, f_alpha));
1132        }
1133        result
1134    }
1135}
1136/// Dragon curve L-system preset.
1137pub struct DragonCurve;
1138impl DragonCurve {
1139    /// Create the L-system for a Dragon curve.
1140    pub fn lsystem() -> LSystem {
1141        LSystem::new(
1142            vec!['F', 'G', '+', '-'],
1143            vec![
1144                ProductionRule::new('F', "F+G"),
1145                ProductionRule::new('G', "F-G"),
1146            ],
1147            "F",
1148        )
1149    }
1150    /// Generate Dragon curve segments via turtle graphics.
1151    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
1152        let ls = Self::lsystem();
1153        ls.turtle_interpret(generations, step_size, 90.0)
1154    }
1155}
1156/// Barnsley fern IFS preset.
1157///
1158/// The classic Barnsley fern uses 4 affine transforms with specific probabilities
1159/// to generate a realistic fern shape.
1160pub struct BarnsleyFern;
1161impl BarnsleyFern {
1162    /// Create the IFS for a Barnsley fern.
1163    pub fn ifs() -> IteratedFunctionSystem {
1164        let transforms = vec![
1165            AffineTransform2D::new(0.0, 0.0, 0.0, 0.16, 0.0, 0.0),
1166            AffineTransform2D::new(0.85, 0.04, -0.04, 0.85, 0.0, 1.6),
1167            AffineTransform2D::new(0.20, -0.26, 0.23, 0.22, 0.0, 1.6),
1168            AffineTransform2D::new(-0.15, 0.28, 0.26, 0.24, 0.0, 0.44),
1169        ];
1170        let probabilities = vec![0.01, 0.85, 0.07, 0.07];
1171        IteratedFunctionSystem::new(transforms, probabilities)
1172    }
1173    /// Generate Barnsley fern points.
1174    pub fn generate(num_points: usize) -> Vec<Point2> {
1175        let ifs = Self::ifs();
1176        ifs.chaos_game(Point2::new(0.0, 0.0), num_points + 200, 200)
1177    }
1178}
1179/// A 3D point for fractal geometry (no nalgebra).
1180#[derive(Debug, Clone, Copy, PartialEq)]
1181pub struct Point3 {
1182    /// X coordinate.
1183    pub x: f64,
1184    /// Y coordinate.
1185    pub y: f64,
1186    /// Z coordinate.
1187    pub z: f64,
1188}
1189impl Point3 {
1190    /// Create a new 3D point.
1191    pub fn new(x: f64, y: f64, z: f64) -> Self {
1192        Self { x, y, z }
1193    }
1194    /// Euclidean distance to another point.
1195    pub fn distance(&self, other: &Point3) -> f64 {
1196        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2))
1197            .sqrt()
1198    }
1199}
1200/// A deterministic context-free L-system (D0L-system).
1201///
1202/// An L-system generates strings by parallel rewriting: at each step,
1203/// every character in the current string is replaced according to the
1204/// production rules simultaneously.
1205#[derive(Debug, Clone)]
1206pub struct LSystem {
1207    /// The alphabet of valid symbols.
1208    pub alphabet: Vec<char>,
1209    /// Production rules mapping symbols to replacement strings.
1210    pub rules: HashMap<char, String>,
1211    /// The starting string (axiom).
1212    pub axiom: String,
1213    /// Default turning angle in degrees (for turtle interpretation).
1214    pub angle_deg: f64,
1215    /// Default step size (for turtle interpretation).
1216    pub step_size: f64,
1217}
1218/// State of an L-system after evolution.
1219#[derive(Debug, Clone)]
1220pub struct LSystemEvolution {
1221    /// The generated string.
1222    pub string: String,
1223    /// The generation number.
1224    pub generation: usize,
1225}
1226impl LSystem {
1227    /// Create a new L-system.
1228    pub fn new(alphabet: Vec<char>, rules: Vec<ProductionRule>, axiom: impl Into<String>) -> Self {
1229        let mut rule_map = HashMap::new();
1230        for rule in rules {
1231            rule_map.insert(rule.predecessor, rule.successor);
1232        }
1233        Self {
1234            alphabet,
1235            rules: rule_map,
1236            axiom: axiom.into(),
1237            angle_deg: 90.0,
1238            step_size: 1.0,
1239        }
1240    }
1241    /// Koch curve L-system (angle = 60°).
1242    pub fn koch_curve() -> Self {
1243        let mut ls = Self::new(vec!['F'], vec![ProductionRule::new('F', "F+F-F-F+F")], "F");
1244        ls.angle_deg = 90.0;
1245        ls
1246    }
1247    /// Sierpinski triangle L-system (angle = 120°).
1248    pub fn sierpinski() -> Self {
1249        let mut ls = Self::new(
1250            vec!['A', 'B'],
1251            vec![
1252                ProductionRule::new('A', "B-A-B"),
1253                ProductionRule::new('B', "A+B+A"),
1254            ],
1255            "A",
1256        );
1257        ls.angle_deg = 60.0;
1258        ls
1259    }
1260    /// Dragon curve L-system (angle = 90°).
1261    pub fn dragon_curve() -> Self {
1262        let mut ls = Self::new(
1263            vec!['X', 'Y'],
1264            vec![
1265                ProductionRule::new('X', "X+YF+"),
1266                ProductionRule::new('Y', "-FX-Y"),
1267            ],
1268            "FX",
1269        );
1270        ls.angle_deg = 90.0;
1271        ls
1272    }
1273    /// Plant branching L-system (angle = 25°).
1274    pub fn plant_branching() -> Self {
1275        let mut ls = Self::new(
1276            vec!['X', 'F'],
1277            vec![
1278                ProductionRule::new('X', "F+[[X]-X]-F[-FX]+X"),
1279                ProductionRule::new('F', "FF"),
1280            ],
1281            "X",
1282        );
1283        ls.angle_deg = 25.0;
1284        ls
1285    }
1286    /// Evolve the L-system for `n` generations, returning the state.
1287    pub fn evolve(&self, n: usize) -> LSystemEvolution {
1288        LSystemEvolution {
1289            string: self.iterate(n),
1290            generation: n,
1291        }
1292    }
1293    /// Interpret a previously evolved state as turtle graphics segments.
1294    pub fn interpret(&self, state: &LSystemEvolution) -> Vec<(Point2, Point2)> {
1295        let angle_rad = self.angle_deg * std::f64::consts::PI / 180.0;
1296        let step = self.step_size;
1297        let mut x = 0.0_f64;
1298        let mut y = 0.0_f64;
1299        let mut heading = 0.0_f64;
1300        let mut stack: Vec<(f64, f64, f64)> = Vec::new();
1301        let mut segments = Vec::new();
1302        for ch in state.string.chars() {
1303            match ch {
1304                'F' | 'G' | 'A' | 'B' => {
1305                    let nx = x + step * heading.cos();
1306                    let ny = y + step * heading.sin();
1307                    segments.push((Point2::new(x, y), Point2::new(nx, ny)));
1308                    x = nx;
1309                    y = ny;
1310                }
1311                '+' => heading += angle_rad,
1312                '-' => heading -= angle_rad,
1313                '[' => stack.push((x, y, heading)),
1314                ']' => {
1315                    if let Some((sx, sy, sh)) = stack.pop() {
1316                        x = sx;
1317                        y = sy;
1318                        heading = sh;
1319                    }
1320                }
1321                _ => {}
1322            }
1323        }
1324        segments
1325    }
1326    /// Generate the string after `n` iterations.
1327    pub fn iterate(&self, n: usize) -> String {
1328        let mut current = self.axiom.clone();
1329        for _ in 0..n {
1330            let mut next = String::with_capacity(current.len() * 2);
1331            for ch in current.chars() {
1332                if let Some(replacement) = self.rules.get(&ch) {
1333                    next.push_str(replacement);
1334                } else {
1335                    next.push(ch);
1336                }
1337            }
1338            current = next;
1339        }
1340        current
1341    }
1342    /// Count the length of the string after `n` iterations without building it.
1343    pub fn iterate_length(&self, n: usize) -> usize {
1344        let mut lengths: HashMap<char, usize> = HashMap::new();
1345        for &ch in &self.alphabet {
1346            lengths.insert(ch, 1);
1347        }
1348        for _ in 0..n {
1349            let mut new_lengths: HashMap<char, usize> = HashMap::new();
1350            for &ch in &self.alphabet {
1351                if let Some(replacement) = self.rules.get(&ch) {
1352                    let len: usize = replacement
1353                        .chars()
1354                        .map(|c| lengths.get(&c).copied().unwrap_or(1))
1355                        .sum();
1356                    new_lengths.insert(ch, len);
1357                } else {
1358                    new_lengths.insert(ch, *lengths.get(&ch).unwrap_or(&1));
1359                }
1360            }
1361            lengths = new_lengths;
1362        }
1363        self.axiom
1364            .chars()
1365            .map(|c| lengths.get(&c).copied().unwrap_or(1))
1366            .sum()
1367    }
1368    /// Interpret the generated string as turtle graphics commands.
1369    ///
1370    /// - `F`, `G`: move forward by `step_size`
1371    /// - `+`: turn left by `angle_deg` degrees
1372    /// - `-`: turn right by `angle_deg` degrees
1373    /// - `[`: push state
1374    /// - `]`: pop state
1375    ///
1376    /// Returns a list of line segments as (start, end) pairs.
1377    pub fn turtle_interpret(
1378        &self,
1379        n: usize,
1380        step_size: f64,
1381        angle_deg: f64,
1382    ) -> Vec<(Point2, Point2)> {
1383        let string = self.iterate(n);
1384        let angle_rad = angle_deg * std::f64::consts::PI / 180.0;
1385        let mut x = 0.0_f64;
1386        let mut y = 0.0_f64;
1387        let mut heading = 0.0_f64;
1388        let mut stack: Vec<(f64, f64, f64)> = Vec::new();
1389        let mut segments = Vec::new();
1390        for ch in string.chars() {
1391            match ch {
1392                'F' | 'G' => {
1393                    let nx = x + step_size * heading.cos();
1394                    let ny = y + step_size * heading.sin();
1395                    segments.push((Point2::new(x, y), Point2::new(nx, ny)));
1396                    x = nx;
1397                    y = ny;
1398                }
1399                '+' => {
1400                    heading += angle_rad;
1401                }
1402                '-' => {
1403                    heading -= angle_rad;
1404                }
1405                '[' => {
1406                    stack.push((x, y, heading));
1407                }
1408                ']' => {
1409                    if let Some((sx, sy, sh)) = stack.pop() {
1410                        x = sx;
1411                        y = sy;
1412                        heading = sh;
1413                    }
1414                }
1415                _ => {}
1416            }
1417        }
1418        segments
1419    }
1420    /// Add a production rule.
1421    pub fn add_rule(&mut self, predecessor: char, successor: impl Into<String>) {
1422        let succ: String = successor.into();
1423        self.rules.insert(predecessor, succ);
1424        if !self.alphabet.contains(&predecessor) {
1425            self.alphabet.push(predecessor);
1426        }
1427    }
1428}
1429/// A Julia set for the quadratic map z_{n+1} = z_n^2 + c with fixed c.
1430///
1431/// Points z_0 in the complex plane are classified by orbit behavior.
1432pub struct JuliaSet {
1433    /// Real part of the constant c.
1434    pub cr: f64,
1435    /// Imaginary part of the constant c.
1436    pub ci: f64,
1437    /// Maximum iterations.
1438    pub max_iter: u32,
1439    /// Escape radius squared.
1440    pub escape_radius_sq: f64,
1441}
1442impl JuliaSet {
1443    /// Create a new Julia set with given c parameter.
1444    pub fn new(cr: f64, ci: f64, max_iter: u32, escape_radius: f64) -> Self {
1445        Self {
1446            cr,
1447            ci,
1448            max_iter,
1449            escape_radius_sq: escape_radius * escape_radius,
1450        }
1451    }
1452    /// Classic Julia set c = -0.7 + 0.27015i.
1453    pub fn classic() -> Self {
1454        Self::new(-0.7, 0.27015, 256, 2.0)
1455    }
1456    /// Dendrite Julia set c = 0 + i.
1457    pub fn dendrite() -> Self {
1458        Self::new(0.0, 1.0, 256, 2.0)
1459    }
1460    /// Compute escape time for an initial point z0 = (zr, zi).
1461    pub fn escape_time(&self, zr: f64, zi: f64) -> Option<u32> {
1462        let mut zr = zr;
1463        let mut zi = zi;
1464        for i in 0..self.max_iter {
1465            let zr2 = zr * zr;
1466            let zi2 = zi * zi;
1467            if zr2 + zi2 > self.escape_radius_sq {
1468                return Some(i);
1469            }
1470            let new_zi = 2.0 * zr * zi + self.ci;
1471            zr = zr2 - zi2 + self.cr;
1472            zi = new_zi;
1473        }
1474        None
1475    }
1476    /// Check if a point is in the Julia set (orbit does not escape).
1477    pub fn is_in_set(&self, zr: f64, zi: f64) -> bool {
1478        self.escape_time(zr, zi).is_none()
1479    }
1480    /// Classify the orbit of z0: returns `OrbitType`.
1481    pub fn classify_orbit(&self, zr: f64, zi: f64) -> OrbitType {
1482        let mut zr = zr;
1483        let mut zi = zi;
1484        let mut history_r = Vec::with_capacity(self.max_iter as usize);
1485        let mut history_i = Vec::with_capacity(self.max_iter as usize);
1486        for _ in 0..self.max_iter {
1487            let zr2 = zr * zr;
1488            let zi2 = zi * zi;
1489            if zr2 + zi2 > self.escape_radius_sq {
1490                return OrbitType::Escaping;
1491            }
1492            history_r.push(zr);
1493            history_i.push(zi);
1494            let new_zi = 2.0 * zr * zi + self.ci;
1495            zr = zr2 - zi2 + self.cr;
1496            zi = new_zi;
1497        }
1498        let n = history_r.len();
1499        if n < 2 {
1500            return OrbitType::Bounded;
1501        }
1502        let last_r = history_r[n - 1];
1503        let last_i = history_i[n - 1];
1504        for period in 1..=16.min(n - 1) {
1505            let prev_r = history_r[n - 1 - period];
1506            let prev_i = history_i[n - 1 - period];
1507            let dr = (last_r - prev_r).abs();
1508            let di = (last_i - prev_i).abs();
1509            if dr < 1e-10 && di < 1e-10 {
1510                if period == 1 {
1511                    return OrbitType::FixedPoint;
1512                }
1513                return OrbitType::Periodic(period);
1514            }
1515        }
1516        OrbitType::Bounded
1517    }
1518    /// Smooth escape time for coloring.
1519    pub fn smooth_escape_time(&self, zr: f64, zi: f64) -> f64 {
1520        let mut zr = zr;
1521        let mut zi = zi;
1522        for i in 0..self.max_iter {
1523            let zr2 = zr * zr;
1524            let zi2 = zi * zi;
1525            if zr2 + zi2 > self.escape_radius_sq {
1526                let modulus = (zr2 + zi2).sqrt();
1527                let mu = i as f64 - modulus.ln().ln() / (2.0_f64).ln();
1528                return mu;
1529            }
1530            let new_zi = 2.0 * zr * zi + self.ci;
1531            zr = zr2 - zi2 + self.cr;
1532            zi = new_zi;
1533        }
1534        self.max_iter as f64
1535    }
1536    /// Render a grid of escape times.
1537    pub fn render_grid(
1538        &self,
1539        x_min: f64,
1540        x_max: f64,
1541        y_min: f64,
1542        y_max: f64,
1543        width: usize,
1544        height: usize,
1545    ) -> Vec<Vec<Option<u32>>> {
1546        let dx = (x_max - x_min) / width as f64;
1547        let dy = (y_max - y_min) / height as f64;
1548        (0..height)
1549            .map(|j| {
1550                let zi = y_min + (j as f64 + 0.5) * dy;
1551                (0..width)
1552                    .map(|i| {
1553                        let zr = x_min + (i as f64 + 0.5) * dx;
1554                        self.escape_time(zr, zi)
1555                    })
1556                    .collect()
1557            })
1558            .collect()
1559    }
1560}
1561/// An axis-aligned box in 3D, used for Menger sponge cubes.
1562#[derive(Debug, Clone, Copy, PartialEq)]
1563pub struct Aabb3 {
1564    /// Minimum corner.
1565    pub min: Point3,
1566    /// Maximum corner.
1567    pub max: Point3,
1568}
1569impl Aabb3 {
1570    /// Create a new AABB.
1571    pub fn new(min: Point3, max: Point3) -> Self {
1572        Self { min, max }
1573    }
1574    /// Side length (assumes cubic).
1575    pub fn side(&self) -> f64 {
1576        self.max.x - self.min.x
1577    }
1578    /// Volume.
1579    pub fn volume(&self) -> f64 {
1580        (self.max.x - self.min.x) * (self.max.y - self.min.y) * (self.max.z - self.min.z)
1581    }
1582    /// Center point.
1583    pub fn center(&self) -> Point3 {
1584        Point3::new(
1585            (self.min.x + self.max.x) / 2.0,
1586            (self.min.y + self.max.y) / 2.0,
1587            (self.min.z + self.max.z) / 2.0,
1588        )
1589    }
1590}
1591/// Morton (Z-order) curve utilities.
1592///
1593/// Maps 2D coordinates to a 1D index using bit-interleaving, producing
1594/// a space-filling Z-shaped traversal pattern.
1595pub struct MortonCurve;
1596impl MortonCurve {
1597    /// Encode 2D coordinates into a Morton code.
1598    ///
1599    /// Interleaves the bits of `x` and `y` (each up to 16 bits).
1600    pub fn encode(x: u32, y: u32) -> u64 {
1601        Self::spread_bits(x as u64) | (Self::spread_bits(y as u64) << 1)
1602    }
1603    /// Decode a Morton code back to 2D coordinates.
1604    pub fn decode(code: u64) -> (u32, u32) {
1605        let x = Self::compact_bits(code) as u32;
1606        let y = Self::compact_bits(code >> 1) as u32;
1607        (x, y)
1608    }
1609    /// Spread bits: insert a zero between each bit.
1610    fn spread_bits(mut v: u64) -> u64 {
1611        v &= 0x0000_0000_FFFF_FFFF;
1612        v = (v | (v << 16)) & 0x0000_FFFF_0000_FFFF;
1613        v = (v | (v << 8)) & 0x00FF_00FF_00FF_00FF;
1614        v = (v | (v << 4)) & 0x0F0F_0F0F_0F0F_0F0F;
1615        v = (v | (v << 2)) & 0x3333_3333_3333_3333;
1616        v = (v | (v << 1)) & 0x5555_5555_5555_5555;
1617        v
1618    }
1619    /// Compact bits: remove every other bit.
1620    fn compact_bits(mut v: u64) -> u64 {
1621        v &= 0x5555_5555_5555_5555;
1622        v = (v | (v >> 1)) & 0x3333_3333_3333_3333;
1623        v = (v | (v >> 2)) & 0x0F0F_0F0F_0F0F_0F0F;
1624        v = (v | (v >> 4)) & 0x00FF_00FF_00FF_00FF;
1625        v = (v | (v >> 8)) & 0x0000_FFFF_0000_FFFF;
1626        v = (v | (v >> 16)) & 0x0000_0000_FFFF_FFFF;
1627        v
1628    }
1629    /// Generate the Z-order traversal as a list of 2D points for an `n x n` grid.
1630    ///
1631    /// `n` should be a power of 2.
1632    pub fn generate_path(n: u32) -> Vec<(u32, u32)> {
1633        let total = n * n;
1634        let mut pairs: Vec<(u64, u32, u32)> = Vec::with_capacity(total as usize);
1635        for y in 0..n {
1636            for x in 0..n {
1637                pairs.push((Self::encode(x, y), x, y));
1638            }
1639        }
1640        pairs.sort_by_key(|&(code, _, _)| code);
1641        pairs.iter().map(|&(_, x, y)| (x, y)).collect()
1642    }
1643    /// 3D Morton code encoding.
1644    pub fn encode_3d(x: u32, y: u32, z: u32) -> u64 {
1645        Self::spread_bits_3d(x as u64)
1646            | (Self::spread_bits_3d(y as u64) << 1)
1647            | (Self::spread_bits_3d(z as u64) << 2)
1648    }
1649    /// Spread bits for 3D: insert two zeros between each bit.
1650    fn spread_bits_3d(mut v: u64) -> u64 {
1651        v &= 0x001F_FFFF;
1652        v = (v | (v << 32)) & 0x001F_0000_0000_FFFF;
1653        v = (v | (v << 16)) & 0x001F_0000_FF00_00FF;
1654        v = (v | (v << 8)) & 0x100F_00F0_0F00_F00F;
1655        v = (v | (v << 4)) & 0x10C3_0C30_C30C_30C3;
1656        v = (v | (v << 2)) & 0x1249_2492_4924_9249;
1657        v
1658    }
1659}
1660/// A 2D point used in fractal geometry computations.
1661#[derive(Debug, Clone, Copy, PartialEq)]
1662pub struct Point2 {
1663    /// X coordinate.
1664    pub x: f64,
1665    /// Y coordinate.
1666    pub y: f64,
1667}
1668impl Point2 {
1669    /// Create a new 2D point.
1670    pub fn new(x: f64, y: f64) -> Self {
1671        Self { x, y }
1672    }
1673    /// Euclidean distance to another point.
1674    pub fn distance(&self, other: &Point2) -> f64 {
1675        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
1676    }
1677    /// Squared distance to another point.
1678    pub fn distance_sq(&self, other: &Point2) -> f64 {
1679        (self.x - other.x).powi(2) + (self.y - other.y).powi(2)
1680    }
1681}
1682/// Menger sponge generator.
1683///
1684/// The Menger sponge is a 3D fractal formed by recursively removing the center
1685/// and face-center sub-cubes from a cube. Its Hausdorff dimension is
1686/// log(20)/log(3) ≈ 2.727.
1687pub struct MengerSponge;
1688impl MengerSponge {
1689    /// Theoretical Hausdorff dimension: log(20)/log(3).
1690    pub fn theoretical_dimension() -> f64 {
1691        (20.0_f64).ln() / (3.0_f64).ln()
1692    }
1693    /// Generate the Menger sponge as a list of cubes (AABBs) at the given recursion level.
1694    ///
1695    /// `level` 0 returns a single unit cube, level 1 returns 20 sub-cubes, etc.
1696    pub fn generate(level: usize, origin: Point3, size: f64) -> Vec<Aabb3> {
1697        if level == 0 {
1698            return vec![Aabb3::new(
1699                origin,
1700                Point3::new(origin.x + size, origin.y + size, origin.z + size),
1701            )];
1702        }
1703        let sub = size / 3.0;
1704        let mut cubes = Vec::new();
1705        for ix in 0..3_u8 {
1706            for iy in 0..3_u8 {
1707                for iz in 0..3_u8 {
1708                    let center_count = u8::from(ix == 1) + u8::from(iy == 1) + u8::from(iz == 1);
1709                    if center_count >= 2 {
1710                        continue;
1711                    }
1712                    let o = Point3::new(
1713                        origin.x + ix as f64 * sub,
1714                        origin.y + iy as f64 * sub,
1715                        origin.z + iz as f64 * sub,
1716                    );
1717                    cubes.extend(Self::generate(level - 1, o, sub));
1718                }
1719            }
1720        }
1721        cubes
1722    }
1723    /// Number of cubes at a given level.
1724    pub fn cube_count(level: usize) -> usize {
1725        20_usize.pow(level as u32)
1726    }
1727    /// Total volume at a given level (unit cube start).
1728    pub fn total_volume(level: usize) -> f64 {
1729        (20.0 / 27.0_f64).powi(level as i32)
1730    }
1731}
1732/// Koch curve L-system preset.
1733///
1734/// The Koch curve replaces each line segment with 4 segments arranged
1735/// in a triangular bump, creating a fractal curve of dimension log(4)/log(3).
1736pub struct KochCurve;
1737impl KochCurve {
1738    /// Create the L-system for a Koch curve.
1739    pub fn lsystem() -> LSystem {
1740        LSystem::new(
1741            vec!['F', '+', '-'],
1742            vec![ProductionRule::new('F', "F+F--F+F")],
1743            "F",
1744        )
1745    }
1746    /// Theoretical dimension: log(4)/log(3) ≈ 1.2619.
1747    pub fn theoretical_dimension() -> f64 {
1748        (4.0_f64).ln() / (3.0_f64).ln()
1749    }
1750    /// Generate Koch curve segments via turtle graphics.
1751    pub fn generate(generations: usize, step_size: f64) -> Vec<(Point2, Point2)> {
1752        let ls = Self::lsystem();
1753        ls.turtle_interpret(generations, step_size, 60.0)
1754    }
1755    /// Compute the number of segments after `n` generations.
1756    pub fn segment_count(n: usize) -> usize {
1757        4_usize.pow(n as u32)
1758    }
1759}