lmm 0.2.4

A language agnostic framework for emulating reality.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
// Copyright 2026 Mahmoud Harmouch.
//
// Licensed under the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! # Procedural Image Generation ("Imagen")
//!
//! This module implements a deterministic, prompt-driven PPM image generator built entirely
//! from first principles - no external graphics crates required. Given a text prompt, it:
//!
//! 1. **Hashes** the prompt to a reproducible 64-bit seed via a FNV-1a variant.
//! 2. **Generates spectral colour fields** using sums of cosine waves with LCG-seeded parameters.
//! 3. **Applies a [`StyleMode`] modifier** (Wave, Radial, Orbital, Fractal, Flow, or Plasma).
//! 4. **Maps field values to RGB** through a [`Palette`] (bias + amplitude cosine map).
//! 5. **Writes** the result as a binary PPM file.
//!
//! [`StyleMode`] derives the full strum suite (`EnumString`, `EnumIter`, `AsRefStr`,
//! `Display`) so it can be round-tripped from strings without a hand-written `match`.
//!
//! # Examples
//!
//! ```no_run
//! use lmm::imagen::{ImagenParams, StyleMode, render};
//!
//! let params = ImagenParams {
//!     prompt: "the fabric of spacetime".into(),
//!     width: 256, height: 256, components: 12,
//!     style: StyleMode::Fractal,
//!     palette_name: "neon".into(),
//!     output: "/tmp/".into(),
//! };
//! let path = render(&params).unwrap();
//! println!("Saved to {path}");
//! ```

#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
use crate::error::{LmmError, Result};
use phf::{Map, phf_map};
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};

use std::f64::consts::TAU;
#[cfg(not(target_arch = "wasm32"))]
use std::fs::File;
#[cfg(not(target_arch = "wasm32"))]
use std::io::{BufWriter, Write};
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;

/// LCG multiplier (Knuth).
const LCG_MULTIPLIER: u64 = 6364136223846793005;
/// LCG additive constant.
const LCG_INCREMENT: u64 = 1442695040888963407;
/// FNV-1a 64-bit offset basis.
const FNV_OFFSET_BASIS: u64 = 14695981039346656037;
/// FNV-1a prime.
const FNV_PRIME: u64 = 1099511628211;
/// Per-character position multiplier used when hashing the prompt.
const PROMPT_INDEX_MULTIPLIER: u64 = 31;

/// Stride between spectral wave bands.
const WAVE_BAND_STRIDE: u64 = 997;
/// Stride between wave components within a band.
const WAVE_COMPONENT_STRIDE: u64 = 31337;
/// Minimum number of wave components.
const MIN_WAVE_COMPONENTS: usize = 3;
/// Maximum number of wave components.
const MAX_WAVE_COMPONENTS: usize = 32;
/// Maximum Mandelbrot iteration count for fractal style.
const FRACTAL_MAX_ITERATIONS: u32 = 32;
/// Escape radius squared for fractal iteration.
const FRACTAL_ESCAPE_RADIUS_SQ: f64 = 4.0;
/// Seed offset for the Gaussian sigma in the Orbital style.
const STYLE_SIGMA_SEED_OFFSET: u64 = 99;
/// Seed offset for the fractal c_real constant.
const FRACTAL_C_REAL_SEED_OFFSET: u64 = 77;
/// Seed offset for the fractal c_imag constant.
const FRACTAL_C_IMAG_SEED_OFFSET: u64 = 78;

/// Named palette constructors indexed by case-normalised name.
///
/// Lookup is O(1) via a compile-time perfect hash - no runtime `match`.
static NAMED_PALETTES: Map<&'static str, fn() -> Palette> = phf_map! {
    "warm"       => Palette::warm       as fn() -> Palette,
    "cool"       => Palette::cool       as fn() -> Palette,
    "neon"       => Palette::neon       as fn() -> Palette,
    "monochrome" => Palette::monochrome as fn() -> Palette,
    "mono"       => Palette::monochrome as fn() -> Palette,
};

/// Rendering style applied on top of the base spectral field.
///
/// Derives the full strum suite so the enum can be parsed from strings without a
/// hand-written `match`. `StyleMode::from_str("fractal")` is case-insensitive.
///
/// | Variant | Description |
/// |---|---|
/// | `Wave` | Raw spectral field; no modifier |
/// | `Radial` | Concentric ring interference |
/// | `Orbital` | Gaussian envelope × angular harmonics |
/// | `Fractal` | Mandelbrot-set iteration count |
/// | `Flow` | Stream-line vector projection |
/// | `Plasma` | Four-mode plasma sum |
///
/// # Examples
///
/// ```
/// use lmm::traits::Simulatable;
/// use lmm::imagen::StyleMode;
/// use std::str::FromStr;
///
/// let s: StyleMode = "fractal".parse().unwrap();
/// assert_eq!(s, StyleMode::Fractal);
/// assert_eq!(s.to_string(), "Fractal");
/// assert_eq!(s.as_ref(), "Fractal");
/// ```
#[derive(
    EnumString, EnumIter, AsRefStr, Display, Debug, Clone, Copy, PartialEq, Eq, Hash, Default,
)]
#[strum(ascii_case_insensitive)]
pub enum StyleMode {
    /// Raw spectral cosine field, no geometric modifier.
    #[default]
    Wave,
    /// Radially symmetric ring interference pattern.
    Radial,
    /// Gaussian-envelope orbital harmonics.
    Orbital,
    /// Mandelbrot-set escape-time colouring.
    Fractal,
    /// Stream-line flow-field projection.
    Flow,
    /// Four-component plasma superposition.
    Plasma,
}

/// RGB colour palette defined by per-channel bias and amplitude.
///
/// The mapping is: `channel_u8 = clamp(bias + amp × sin(field × 2π), 0, 1) × 255`.
///
/// # Examples
///
/// ```
/// use lmm::traits::Simulatable;
/// use lmm::imagen::Palette;
///
/// let p = Palette::warm();
/// assert!(p.r_bias > p.b_bias); // warm → more red
/// ```
#[derive(Debug, Clone)]
pub struct Palette {
    /// Red channel bias ∈ [0, 1].
    pub r_bias: f64,
    /// Green channel bias ∈ [0, 1].
    pub g_bias: f64,
    /// Blue channel bias ∈ [0, 1].
    pub b_bias: f64,
    /// Red channel amplitude ∈ [0, 1].
    pub r_amp: f64,
    /// Green channel amplitude ∈ [0, 1].
    pub g_amp: f64,
    /// Blue channel amplitude ∈ [0, 1].
    pub b_amp: f64,
}

impl Palette {
    /// Derives a pseudo-random palette from a 64-bit seed.
    pub(crate) fn from_seed(seed: u64) -> Self {
        let r_bias = lcg_unit(seed) * 0.5 + 0.25;
        let g_bias = lcg_unit(seed.wrapping_add(1)) * 0.5 + 0.25;
        let b_bias = lcg_unit(seed.wrapping_add(2)) * 0.5 + 0.25;
        let r_amp = lcg_unit(seed.wrapping_add(3)) * 0.4 + 0.3;
        let g_amp = lcg_unit(seed.wrapping_add(4)) * 0.4 + 0.3;
        let b_amp = lcg_unit(seed.wrapping_add(5)) * 0.4 + 0.3;
        Self {
            r_bias,
            g_bias,
            b_bias,
            r_amp,
            g_amp,
            b_amp,
        }
    }

    /// Warm palette: dominant reds, muted blues.
    pub fn warm() -> Self {
        Self {
            r_bias: 0.7,
            g_bias: 0.3,
            b_bias: 0.1,
            r_amp: 0.3,
            g_amp: 0.2,
            b_amp: 0.15,
        }
    }

    /// Cool palette: dominant blues, muted reds.
    pub fn cool() -> Self {
        Self {
            r_bias: 0.1,
            g_bias: 0.3,
            b_bias: 0.7,
            r_amp: 0.15,
            g_amp: 0.2,
            b_amp: 0.3,
        }
    }

    /// High-saturation neon palette.
    pub fn neon() -> Self {
        Self {
            r_bias: 0.1,
            g_bias: 0.9,
            b_bias: 0.5,
            r_amp: 0.5,
            g_amp: 0.5,
            b_amp: 0.5,
        }
    }

    /// Equal-bias greyscale palette.
    pub fn monochrome() -> Self {
        Self {
            r_bias: 0.5,
            g_bias: 0.5,
            b_bias: 0.5,
            r_amp: 0.45,
            g_amp: 0.45,
            b_amp: 0.45,
        }
    }

    /// Looks up a named palette by name, falling back to `Palette::from_seed(seed)`.
    ///
    /// The lookup uses the compile-time `NAMED_PALETTES` PHF map (O(1)).
    ///
    /// # Arguments
    ///
    /// * `name` - Palette name (case-insensitive).
    /// * `seed` - Seed used when `name` is not recognised.
    ///
    /// # Examples
    ///
    /// ```
    /// use lmm::imagen::Palette;
    ///
    /// let p = Palette::by_name("warm", 0);
    /// assert!(p.r_bias > 0.5);
    /// let p2 = Palette::by_name("unknown_xyz", 42);
    /// assert!(p2.r_bias > 0.0); // seed-derived
    /// ```
    pub fn by_name(name: &str, seed: u64) -> Self {
        let lower = name.to_lowercase();
        if let Some(ctor) = NAMED_PALETTES.get(lower.as_str()) {
            ctor()
        } else {
            Self::from_seed(seed)
        }
    }
}

fn lcg_unit(seed: u64) -> f64 {
    let x = seed
        .wrapping_mul(LCG_MULTIPLIER)
        .wrapping_add(LCG_INCREMENT);
    (x >> 33) as f64 / (u32::MAX as f64)
}

fn prompt_seed(text: &str) -> u64 {
    text.bytes()
        .enumerate()
        .fold(FNV_OFFSET_BASIS, |acc, (i, b)| {
            acc.wrapping_mul(FNV_PRIME)
                .wrapping_add(b as u64)
                .wrapping_add(i as u64 * PROMPT_INDEX_MULTIPLIER)
        })
}

#[derive(Debug, Clone)]
struct WaveComponent {
    amplitude: f64,
    freq_x: f64,
    freq_y: f64,
    phase: f64,
}

impl WaveComponent {
    fn from_seed(seed: u64, band: u64, component: u64) -> Self {
        let s = seed
            .wrapping_add(band * WAVE_BAND_STRIDE)
            .wrapping_add(component * WAVE_COMPONENT_STRIDE);
        Self {
            amplitude: lcg_unit(s) * 0.6 + 0.1,
            freq_x: lcg_unit(s.wrapping_add(1)) * 5.0 + 0.5,
            freq_y: lcg_unit(s.wrapping_add(2)) * 5.0 + 0.5,
            phase: lcg_unit(s.wrapping_add(3)) * TAU,
        }
    }

    fn evaluate(&self, nx: f64, ny: f64) -> f64 {
        self.amplitude * (TAU * self.freq_x * nx + TAU * self.freq_y * ny + self.phase).cos()
    }
}

fn spectral_field(components: &[WaveComponent], nx: f64, ny: f64) -> f64 {
    let raw: f64 = components.iter().map(|c| c.evaluate(nx, ny)).sum();
    (raw / components.len() as f64 + 1.0) * 0.5
}

fn apply_style(nx: f64, ny: f64, base: f64, style: StyleMode, seed: u64) -> f64 {
    let cx = nx - 0.5;
    let cy = ny - 0.5;
    let r = (cx * cx + cy * cy).sqrt();
    let theta = cy.atan2(cx);
    let sigma = lcg_unit(seed.wrapping_add(STYLE_SIGMA_SEED_OFFSET)) * 0.3 + 0.2;
    match style {
        StyleMode::Wave => base,
        StyleMode::Radial => {
            let radial_mod = (TAU * r * 4.0 + base * TAU).cos() * 0.5 + 0.5;
            (base + radial_mod) * 0.5
        }
        StyleMode::Orbital => {
            let envelope = (-r * r / (2.0 * sigma * sigma)).exp();
            let angular = (theta * 3.0 + base * TAU).cos() * 0.5 + 0.5;
            envelope * angular + (1.0 - envelope) * base
        }
        StyleMode::Fractal => {
            let mut z_r = cx * 3.0 + base;
            let mut z_i = cy * 3.0;
            let c_r = lcg_unit(seed.wrapping_add(FRACTAL_C_REAL_SEED_OFFSET)) * 2.0 - 1.0;
            let c_i = lcg_unit(seed.wrapping_add(FRACTAL_C_IMAG_SEED_OFFSET)) * 2.0 - 1.0;
            let mut iter = 0u32;
            while iter < FRACTAL_MAX_ITERATIONS && z_r * z_r + z_i * z_i < FRACTAL_ESCAPE_RADIUS_SQ
            {
                let tmp = z_r * z_r - z_i * z_i + c_r;
                z_i = 2.0 * z_r * z_i + c_i;
                z_r = tmp;
                iter += 1;
            }
            iter as f64 / FRACTAL_MAX_ITERATIONS as f64
        }
        StyleMode::Flow => {
            let stream_x = (TAU * ny * 2.0 + base).sin();
            let stream_y = (TAU * nx * 2.0 + base).cos();
            (cx * stream_x + cy * stream_y) * 0.5 + 0.5
        }
        StyleMode::Plasma => {
            let v1 = (TAU * (nx + base)).sin();
            let v2 = (TAU * (ny + base * 0.7)).sin();
            let v3 = (TAU * (nx + ny + base * 0.3) * 0.5).sin();
            let v4 = {
                let dx = nx - 0.5 + (base * TAU).cos() * 0.25;
                let dy = ny - 0.5 + (base * TAU).sin() * 0.25;
                (TAU * (dx * dx + dy * dy).sqrt() * 4.0).sin()
            };
            ((v1 + v2 + v3 + v4) * 0.25 + 1.0) * 0.5
        }
    }
}

fn field_to_rgb(r_field: f64, g_field: f64, b_field: f64, palette: &Palette) -> [u8; 3] {
    let map = |field: f64, bias: f64, amp: f64| -> u8 {
        let v = (bias + amp * (field * TAU).sin()).clamp(0.0, 1.0);
        (v * 255.0).round() as u8
    };
    [
        map(r_field, palette.r_bias, palette.r_amp),
        map(g_field, palette.g_bias, palette.g_amp),
        map(b_field, palette.b_bias, palette.b_amp),
    ]
}

/// Parameters controlling image generation.
///
/// # Examples
///
/// ```
/// use lmm::traits::Simulatable;
/// use lmm::imagen::{ImagenParams, StyleMode};
///
/// let params = ImagenParams {
///     prompt: "entropy".into(),
///     width: 64, height: 64,
///     components: 8,
///     style: StyleMode::Wave,
///     palette_name: "cool".into(),
///     output: "/tmp/".into(),
/// };
/// assert_eq!(params.width, 64);
/// ```
#[derive(Debug, Clone)]
pub struct ImagenParams {
    /// Text prompt - drives the deterministic seed.
    pub prompt: String,
    /// Output image width in pixels.
    pub width: u32,
    /// Output image height in pixels.
    pub height: u32,
    /// Number of spectral wave components per channel (clamped to [3, 32]).
    pub components: usize,
    /// Visual style modifier.
    pub style: StyleMode,
    /// Named palette (`"warm"`, `"cool"`, `"neon"`, `"monochrome"`) or seed-derived.
    pub palette_name: String,
    /// Output path (file or directory; `.ppm` extension is appended if directory).
    pub output: String,
}

/// Renders an image from `params` and returns the output file path.
///
/// Lookup of the named palette uses the compile-time `NAMED_PALETTES` PHF map (O(1));
/// unknown names fall back to the prompt-derived seed palette.
///
/// # Errors
///
/// Returns [`LmmError::Perception`] when the output file or directory cannot be created or written.
///
/// # Examples
///
/// ```no_run
/// use lmm::imagen::{ImagenParams, StyleMode, render};
///
/// let params = ImagenParams {
///     prompt: "symmetry".into(),
///     width: 64, height: 64, components: 6,
///     style: StyleMode::Radial,
///     palette_name: "cool".into(),
///     output: "/tmp/test.ppm".into(),
/// };
/// let path = render(&params).unwrap();
/// assert!(path.ends_with(".ppm"));
/// ```
#[cfg(not(target_arch = "wasm32"))]
pub fn render(params: &ImagenParams) -> Result<String> {
    let seed = prompt_seed(&params.prompt);
    let palette = Palette::by_name(&params.palette_name, seed);

    let n = params
        .components
        .clamp(MIN_WAVE_COMPONENTS, MAX_WAVE_COMPONENTS);
    let red_waves: Vec<WaveComponent> = (0..n)
        .map(|k| WaveComponent::from_seed(seed, 0, k as u64))
        .collect();
    let green_waves: Vec<WaveComponent> = (0..n)
        .map(|k| WaveComponent::from_seed(seed, 1, k as u64))
        .collect();
    let blue_waves: Vec<WaveComponent> = (0..n)
        .map(|k| WaveComponent::from_seed(seed, 2, k as u64))
        .collect();

    let w = params.width as usize;
    let h = params.height as usize;
    let mut pixels: Vec<u8> = Vec::with_capacity(w * h * 3);

    for py in 0..h {
        let ny = py as f64 / (h - 1).max(1) as f64;
        for px in 0..w {
            let nx = px as f64 / (w - 1).max(1) as f64;
            let r_raw = spectral_field(&red_waves, nx, ny);
            let g_raw = spectral_field(&green_waves, nx, ny);
            let b_raw = spectral_field(&blue_waves, nx, ny);
            let r_val = apply_style(nx, ny, r_raw, params.style, seed.wrapping_add(0));
            let g_val = apply_style(nx, ny, g_raw, params.style, seed.wrapping_add(1));
            let b_val = apply_style(nx, ny, b_raw, params.style, seed.wrapping_add(2));
            pixels.extend_from_slice(&field_to_rgb(r_val, g_val, b_val, &palette));
        }
    }

    let mut out_path = std::path::PathBuf::from(&params.output);
    if params.output.ends_with('/') || params.output.ends_with('\\') || out_path.is_dir() {
        if !out_path.exists() {
            std::fs::create_dir_all(&out_path)
                .map_err(|e| LmmError::Perception(format!("cannot create directory: {e}")))?;
        }
        let seed_hex = format!("{:08x}", seed as u32);
        let style_str = params.style.as_ref().to_lowercase();
        out_path.push(format!(
            "{}_{}_{}.ppm",
            style_str, params.palette_name, seed_hex
        ));
    } else if let Some(parent) = out_path.parent()
        && !parent.as_os_str().is_empty()
    {
        std::fs::create_dir_all(parent)
            .map_err(|e| LmmError::Perception(format!("cannot create directory: {e}")))?;
    }

    write_ppm(&out_path, w, h, &pixels)?;
    Ok(out_path.to_string_lossy().into_owned())
}

#[cfg(not(target_arch = "wasm32"))]
fn write_ppm(path: &Path, width: usize, height: usize, pixels: &[u8]) -> Result<()> {
    let file = File::create(path)
        .map_err(|e| LmmError::Perception(format!("cannot create output file: {e}")))?;
    let mut writer = BufWriter::new(file);
    write!(writer, "P6\n{} {}\n255\n", width, height)
        .map_err(|e| LmmError::Perception(format!("write error: {e}")))?;
    writer
        .write_all(pixels)
        .map_err(|e| LmmError::Perception(format!("write error: {e}")))?;
    Ok(())
}

// Copyright 2026 Mahmoud Harmouch.
//
// Licensed under the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.