tx2-iff 0.1.0

PPF-IFF (Involuted Fractal Format) - Image codec using Physics-Prime Factorization, 360-prime quantization, and symplectic warping
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
//! Deterministic noise generation using PPF-based hash
//!
//! This module implements deterministic noise functions based on Physics-Prime
//! Factorization (PPF) for texture synthesis in Layer 2. All operations are
//! deterministic and produce identical results across platforms.
//!
//! ## Mathematical Foundation
//!
//! The noise function uses the 360-prime pattern to generate spatially coherent
//! noise with spectral properties suitable for natural texture synthesis.
//!
//! ```text
//! ppf_noise(x, y, seed) = hash(prime_factorize(x) ⊕ prime_factorize(y) ⊕ seed)
//! ```
//!
//! ## Multi-Octave Noise
//!
//! Fractal Brownian Motion (fBm) combines multiple octaves:
//! ```text
//! fbm(x, y) = ∑ᵢ (1/2^i) × ppf_noise(2^i × x, 2^i × y, seed + i)
//! ```

use crate::fixed::Fixed;
use crate::prime::PpfHash;
use serde::{Deserialize, Serialize};

/// Noise parameters for texture synthesis
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct NoiseParams {
    /// Seed for deterministic generation
    pub seed: u32,
    /// Number of octaves (1-8)
    pub octaves: u8,
    /// Base frequency scale (log₂)
    pub scale: u8,
    /// Amplitude decay between octaves (0.0-1.0)
    pub persistence: f32,
    /// Frequency increase between octaves (typically 2.0)
    pub lacunarity: f32,
}

impl Default for NoiseParams {
    fn default() -> Self {
        NoiseParams {
            seed: 0,
            octaves: 4,
            scale: 1,
            persistence: 0.5,
            lacunarity: 2.0,
        }
    }
}

/// PPF-based deterministic noise generator
pub struct PpfNoise {
    /// Hash function using 360-prime pattern
    hash: PpfHash,
    /// Noise parameters
    params: NoiseParams,
}

impl PpfNoise {
    /// Create a new noise generator with given parameters
    pub fn new(params: NoiseParams) -> Self {
        PpfNoise {
            hash: PpfHash::new(params.seed),
            params,
        }
    }

    /// Generate noise value at integer coordinates
    /// Returns value in range [0, 1)
    pub fn noise_2d(&self, x: i32, y: i32) -> f32 {
        // Use PPF hash to generate deterministic noise
        self.hash.hash(x as u32, y as u32)
    }

    /// Generate noise value at fixed-point coordinates
    pub fn noise_2d_fixed(&self, x: Fixed, y: Fixed) -> Fixed {
        // Get integer and fractional parts
        let xi = x.to_int();
        let yi = y.to_int();
        let xf = x.fract();
        let yf = y.fract();

        // Sample at four corners
        let n00 = self.noise_2d(xi, yi);
        let n10 = self.noise_2d(xi + 1, yi);
        let n01 = self.noise_2d(xi, yi + 1);
        let n11 = self.noise_2d(xi + 1, yi + 1);

        // Bilinear interpolation using fixed-point math
        let n00_fixed = Fixed::from_f32(n00);
        let n10_fixed = Fixed::from_f32(n10);
        let n01_fixed = Fixed::from_f32(n01);
        let n11_fixed = Fixed::from_f32(n11);

        // Interpolate in x
        let nx0 = n00_fixed + (n10_fixed - n00_fixed) * xf;
        let nx1 = n01_fixed + (n11_fixed - n01_fixed) * xf;

        // Interpolate in y
        nx0 + (nx1 - nx0) * yf
    }

    /// Fractal Brownian Motion (fBm) - multi-octave noise
    pub fn fbm(&self, x: Fixed, y: Fixed) -> Fixed {
        let mut total = Fixed::ZERO;
        let mut amplitude = Fixed::ONE;
        let mut frequency = Fixed::from_f32(1.0 / (1 << self.params.scale) as f32);
        let persistence = Fixed::from_f32(self.params.persistence);
        let lacunarity = Fixed::from_f32(self.params.lacunarity);

        for octave in 0..self.params.octaves {
            // Create octave-specific hash
            let octave_hash = PpfHash::new(self.params.seed.wrapping_add(octave as u32));
            let octave_noise = PpfNoise {
                hash: octave_hash,
                params: self.params,
            };

            // Sample at current frequency
            let sample_x = x * frequency;
            let sample_y = y * frequency;
            let noise_val = octave_noise.noise_2d_fixed(sample_x, sample_y);

            // Accumulate
            total = total + noise_val * amplitude;

            // Update for next octave
            amplitude = amplitude * persistence;
            frequency = frequency * lacunarity;
        }

        total
    }

    /// Turbulence function - absolute value of fBm
    pub fn turbulence(&self, x: Fixed, y: Fixed) -> Fixed {
        let mut total = Fixed::ZERO;
        let mut amplitude = Fixed::ONE;
        let mut frequency = Fixed::from_f32(1.0 / (1 << self.params.scale) as f32);
        let persistence = Fixed::from_f32(self.params.persistence);
        let lacunarity = Fixed::from_f32(self.params.lacunarity);

        for octave in 0..self.params.octaves {
            let octave_hash = PpfHash::new(self.params.seed.wrapping_add(octave as u32));
            let octave_noise = PpfNoise {
                hash: octave_hash,
                params: self.params,
            };

            let sample_x = x * frequency;
            let sample_y = y * frequency;
            let noise_val = octave_noise.noise_2d_fixed(sample_x, sample_y);

            // Take absolute value for turbulence
            let turbulence_val = noise_val.abs();

            total = total + turbulence_val * amplitude;
            amplitude = amplitude * persistence;
            frequency = frequency * lacunarity;
        }

        total
    }

    /// Ridged multi-fractal noise
    pub fn ridged(&self, x: Fixed, y: Fixed) -> Fixed {
        let mut total = Fixed::ZERO;
        let mut amplitude = Fixed::ONE;
        let mut frequency = Fixed::from_f32(1.0 / (1 << self.params.scale) as f32);
        let persistence = Fixed::from_f32(self.params.persistence);
        let lacunarity = Fixed::from_f32(self.params.lacunarity);

        for octave in 0..self.params.octaves {
            let octave_hash = PpfHash::new(self.params.seed.wrapping_add(octave as u32));
            let octave_noise = PpfNoise {
                hash: octave_hash,
                params: self.params,
            };

            let sample_x = x * frequency;
            let sample_y = y * frequency;
            let noise_val = octave_noise.noise_2d_fixed(sample_x, sample_y);

            // Create ridges: 1 - abs(noise - 0.5) * 2
            let centered = noise_val - Fixed::HALF;
            let ridge = Fixed::ONE - centered.abs() * Fixed::from_int(2);

            total = total + ridge * amplitude;
            amplitude = amplitude * persistence;
            frequency = frequency * lacunarity;
        }

        total
    }

    /// Warped noise - use noise to warp the domain of another noise
    pub fn warped(&self, x: Fixed, y: Fixed, warp_strength: Fixed) -> Fixed {
        // Use noise to compute domain warp
        let warp_x = self.fbm(x, y) * warp_strength;
        let warp_y = self.fbm(x + Fixed::from_int(100), y + Fixed::from_int(100)) * warp_strength;

        // Sample at warped coordinates
        self.fbm(x + warp_x, y + warp_y)
    }

    /// Cellular/Worley noise - distance to closest point
    pub fn cellular(&self, x: Fixed, y: Fixed, cell_size: Fixed) -> Fixed {
        // Get cell coordinates
        let cell_x = (x / cell_size).floor();
        let cell_y = (y / cell_size).floor();

        let mut min_dist = Fixed::from_int(1000);

        // Check neighboring cells
        for dy in -1..=1 {
            for dx in -1..=1 {
                let neighbor_x = cell_x + Fixed::from_int(dx);
                let neighbor_y = cell_y + Fixed::from_int(dy);

                // Get random point in cell using hash
                let hash_x = self.hash.hash(neighbor_x.to_int() as u32, neighbor_y.to_int() as u32);
                let hash_y = self.hash.hash(
                    (neighbor_x.to_int() as u32).wrapping_add(1),
                    (neighbor_y.to_int() as u32).wrapping_add(1),
                );

                // Point position in cell
                let point_x = (neighbor_x + Fixed::from_f32(hash_x)) * cell_size;
                let point_y = (neighbor_y + Fixed::from_f32(hash_y)) * cell_size;

                // Distance to point
                let dx = x - point_x;
                let dy = y - point_y;
                let dist_sq = dx * dx + dy * dy;
                let dist = dist_sq.sqrt().unwrap_or(Fixed::ZERO);

                if dist < min_dist {
                    min_dist = dist;
                }
            }
        }

        // Normalize to [0, 1]
        (min_dist / cell_size).min(Fixed::ONE)
    }
}

/// Perlin-style noise using gradient vectors
pub struct PerlinNoise {
    /// Hash for gradient selection
    hash: PpfHash,
}

impl PerlinNoise {
    /// Create new Perlin noise generator
    pub fn new(seed: u32) -> Self {
        PerlinNoise {
            hash: PpfHash::new(seed),
        }
    }

    /// Get gradient vector at integer coordinates
    fn gradient(&self, x: i32, y: i32) -> (Fixed, Fixed) {
        // Use hash to select gradient direction
        let hash_val = self.hash.hash(x as u32, y as u32);
        let angle = Fixed::from_f32(hash_val * 2.0 * std::f32::consts::PI);

        (angle.cos(), angle.sin())
    }

    /// Perlin noise at fixed-point coordinates
    pub fn noise(&self, x: Fixed, y: Fixed) -> Fixed {
        // Get integer and fractional parts
        let xi = x.floor().to_int();
        let yi = y.floor().to_int();
        let xf = x - Fixed::from_int(xi);
        let yf = y - Fixed::from_int(yi);

        // Get gradients at four corners
        let g00 = self.gradient(xi, yi);
        let g10 = self.gradient(xi + 1, yi);
        let g01 = self.gradient(xi, yi + 1);
        let g11 = self.gradient(xi + 1, yi + 1);

        // Compute dot products
        let d00 = g00.0 * xf + g00.1 * yf;
        let d10 = g10.0 * (xf - Fixed::ONE) + g10.1 * yf;
        let d01 = g01.0 * xf + g01.1 * (yf - Fixed::ONE);
        let d11 = g11.0 * (xf - Fixed::ONE) + g11.1 * (yf - Fixed::ONE);

        // Smooth interpolation (fade function)
        let u = self.fade(xf);
        let v = self.fade(yf);

        // Bilinear interpolation
        let x1 = self.lerp(d00, d10, u);
        let x2 = self.lerp(d01, d11, u);
        self.lerp(x1, x2, v)
    }

    /// Fade function: 6t^5 - 15t^4 + 10t^3
    fn fade(&self, t: Fixed) -> Fixed {
        let t2 = t * t;
        let t3 = t2 * t;
        let t4 = t3 * t;
        let t5 = t4 * t;

        t3 * Fixed::from_int(10) - t4 * Fixed::from_int(15) + t5 * Fixed::from_int(6)
    }

    /// Linear interpolation
    fn lerp(&self, a: Fixed, b: Fixed, t: Fixed) -> Fixed {
        a + (b - a) * t
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_noise_determinism() {
        let params = NoiseParams {
            seed: 42,
            octaves: 4,
            scale: 1,
            persistence: 0.5,
            lacunarity: 2.0,
        };
        let noise = PpfNoise::new(params);

        // Same coordinates should give same result
        let n1 = noise.noise_2d(10, 20);
        let n2 = noise.noise_2d(10, 20);
        assert_eq!(n1, n2);

        // Test multiple times to ensure determinism
        for _ in 0..100 {
            let n3 = noise.noise_2d(10, 20);
            assert_eq!(n1, n3);
        }
    }

    #[test]
    fn test_noise_range() {
        let params = NoiseParams::default();
        let noise = PpfNoise::new(params);

        // Test many points to ensure range [0, 1)
        for x in -100..100 {
            for y in -100..100 {
                let n = noise.noise_2d(x, y);
                assert!(n >= 0.0 && n < 1.0, "Noise value {} out of range", n);
            }
        }
    }

    #[test]
    fn test_fbm_properties() {
        let params = NoiseParams {
            seed: 123,
            octaves: 4,
            scale: 1,
            persistence: 0.5,
            lacunarity: 2.0,
        };
        let noise = PpfNoise::new(params);

        let x = Fixed::from_f32(10.5);
        let y = Fixed::from_f32(20.3);

        let fbm_val = noise.fbm(x, y);

        // FBM should be in reasonable range
        assert!(fbm_val >= Fixed::ZERO);
        assert!(fbm_val <= Fixed::from_int(2));

        // Same coordinates should give same result
        let fbm_val2 = noise.fbm(x, y);
        assert_eq!(fbm_val, fbm_val2);
    }

    #[test]
    fn test_turbulence() {
        let params = NoiseParams::default();
        let noise = PpfNoise::new(params);

        let x = Fixed::from_int(5);
        let y = Fixed::from_int(7);

        let turb = noise.turbulence(x, y);

        // Turbulence should always be positive
        assert!(turb >= Fixed::ZERO);
    }

    #[test]
    fn test_perlin_determinism() {
        let perlin = PerlinNoise::new(42);

        let x = Fixed::from_f32(10.5);
        let y = Fixed::from_f32(20.3);

        let n1 = perlin.noise(x, y);
        let n2 = perlin.noise(x, y);

        assert_eq!(n1, n2);
    }

    #[test]
    fn test_different_seeds() {
        let noise1 = PpfNoise::new(NoiseParams {
            seed: 42,
            ..Default::default()
        });
        let noise2 = PpfNoise::new(NoiseParams {
            seed: 43,
            ..Default::default()
        });

        let n1 = noise1.noise_2d(10, 20);
        let n2 = noise2.noise_2d(10, 20);

        // Different seeds should give different results
        assert_ne!(n1, n2);
    }

    #[test]
    fn test_cellular_noise() {
        let params = NoiseParams::default();
        let noise = PpfNoise::new(params);

        let x = Fixed::from_int(5);
        let y = Fixed::from_int(7);
        let cell_size = Fixed::from_int(2);

        let cell = noise.cellular(x, y, cell_size);

        // Cellular noise should be in [0, 1]
        assert!(cell >= Fixed::ZERO);
        assert!(cell <= Fixed::ONE);
    }

    #[test]
    fn test_warped_noise() {
        let params = NoiseParams::default();
        let noise = PpfNoise::new(params);

        let x = Fixed::from_int(5);
        let y = Fixed::from_int(7);
        let warp_strength = Fixed::from_int(1);

        let warped = noise.warped(x, y, warp_strength);

        // Should produce valid noise value
        assert!(warped >= Fixed::ZERO);
    }

    #[test]
    fn test_octave_variation() {
        let x = Fixed::from_int(10);
        let y = Fixed::from_int(20);

        // More octaves should produce more detail
        let noise_2 = PpfNoise::new(NoiseParams {
            octaves: 2,
            ..Default::default()
        });
        let noise_8 = PpfNoise::new(NoiseParams {
            octaves: 8,
            ..Default::default()
        });

        let fbm_2 = noise_2.fbm(x, y);
        let fbm_8 = noise_8.fbm(x, y);

        // Values should be different
        assert_ne!(fbm_2, fbm_8);
    }
}