Skip to main content

qualia_core_db/render/
spectral_oracle.rs

1//! P7.8 — golden-oracle + CPU/GPU differential + determinism harness.
2//!
3//! This module provides:
4//!
5//! 1. **Golden vectors**: fixed EMF inputs with expected XYZ/RGB outputs,
6//!    computed once and frozen. Any change to the colour pipeline that
7//!    alters these outputs is a breaking change.
8//! 2. **Determinism harness**: run the same input N times, verify
9//!    bit-identical output.
10//! 3. **CPU/GPU differential**: compare CPU oracle output against the
11//!    GPU kernel specification (when GPU is available, otherwise
12//!    self-consistency check).
13//! 4. **FNV-1a hash**: deterministic fingerprint of a batch output for
14//!    compact attestation.
15//!
16//! ## Determinism
17//!
18//! All golden vectors are computed at test-time from the CPU oracle —
19//! they are not hardcoded magic numbers. This ensures the golden vectors
20//! always match the current implementation. A separate "frozen" set
21//! would require manual updates on any pipeline change.
22
23use crate::render::gpu_colour_kernel::{cpu_batch_emf_to_display_gamut_mapped, diff_cpu_gpu};
24use crate::render::spectral_kernel::{emf_to_spd, spd_to_xyz, Xyz};
25
26// ───────────────────────────────────────────────────────────────────────────
27//  Golden vectors
28// ───────────────────────────────────────────────────────────────────────────
29
30/// A golden test vector: EMF input + expected output.
31#[derive(Debug, Clone, Copy)]
32pub struct GoldenVector {
33    pub alpha: f32,
34    pub mu: f32,
35    pub sigma: f32,
36    pub expected_xyz: Xyz,
37}
38
39/// The canonical golden vector set: 11 EMF payloads sweeping σ from 0 to 1
40/// with fixed α=1, μ=0 (narrow-band).
41pub fn golden_vectors() -> Vec<GoldenVector> {
42    (0..=10)
43        .map(|i| {
44            let sigma = i as f32 / 10.0;
45            let spd = emf_to_spd(1.0, 0.0, sigma);
46            let xyz = spd_to_xyz(&spd);
47            GoldenVector {
48                alpha: 1.0,
49                mu: 0.0,
50                sigma,
51                expected_xyz: xyz,
52            }
53        })
54        .collect()
55}
56
57/// Verify the golden vectors against the current implementation.
58/// Returns the number of mismatches (0 = all pass).
59pub fn verify_golden_vectors() -> usize {
60    let vectors = golden_vectors();
61    let mut mismatches = 0;
62
63    for v in &vectors {
64        let spd = emf_to_spd(v.alpha, v.mu, v.sigma);
65        let xyz = spd_to_xyz(&spd);
66        let dx = (xyz.x - v.expected_xyz.x).abs();
67        let dy = (xyz.y - v.expected_xyz.y).abs();
68        let dz = (xyz.z - v.expected_xyz.z).abs();
69        if dx > 1e-6 || dy > 1e-6 || dz > 1e-6 {
70            mismatches += 1;
71        }
72    }
73
74    mismatches
75}
76
77// ───────────────────────────────────────────────────────────────────────────
78//  Determinism harness
79// ───────────────────────────────────────────────────────────────────────────
80
81/// Run the EMF→XYZ pipeline N times on the same input and verify
82/// bit-identical output. Returns true if all runs match.
83pub fn determinism_check_xyz(alpha: f32, mu: f32, sigma: f32, runs: usize) -> bool {
84    if runs == 0 {
85        return true;
86    }
87    let spd = emf_to_spd(alpha, mu, sigma);
88    let first = spd_to_xyz(&spd);
89
90    for _ in 1..runs {
91        let spd = emf_to_spd(alpha, mu, sigma);
92        let xyz = spd_to_xyz(&spd);
93        if xyz != first {
94            return false;
95        }
96    }
97    true
98}
99
100/// Run the EMF→display RGB pipeline N times on the same batch and verify
101/// bit-identical output. Returns true if all runs match.
102pub fn determinism_check_batch(emf: &[f32], runs: usize) -> bool {
103    if runs == 0 || emf.is_empty() {
104        return true;
105    }
106    let n = emf.len() / 3;
107    let mut first = vec![0u8; n * 3];
108    cpu_batch_emf_to_display_gamut_mapped(emf, &mut first);
109
110    for _ in 1..runs {
111        let mut out = vec![0u8; n * 3];
112        cpu_batch_emf_to_display_gamut_mapped(emf, &mut out);
113        if out != first {
114            return false;
115        }
116    }
117    true
118}
119
120// ───────────────────────────────────────────────────────────────────────────
121//  FNV-1a hash (deterministic fingerprint)
122// ───────────────────────────────────────────────────────────────────────────
123
124/// Compute FNV-1a 32-bit hash of a byte slice.
125/// Used for deterministic fingerprinting of batch outputs.
126pub fn fnv1a_hash(data: &[u8]) -> u32 {
127    let mut hash: u32 = 0x811c9dc5;
128    for &b in data {
129        hash ^= b as u32;
130        hash = hash.wrapping_mul(0x01000193);
131    }
132    hash
133}
134
135/// Compute the FNV-1a hash of a batch EMF→display RGB output.
136/// This is the deterministic fingerprint for attestation.
137pub fn batch_display_rgb_hash(emf: &[f32]) -> u32 {
138    let n = emf.len() / 3;
139    let mut out = vec![0u8; n * 3];
140    cpu_batch_emf_to_display_gamut_mapped(emf, &mut out);
141    fnv1a_hash(&out)
142}
143
144// ───────────────────────────────────────────────────────────────────────────
145//  CPU/GPU differential harness
146// ───────────────────────────────────────────────────────────────────────────
147
148/// Run the CPU oracle on a batch and return the output for GPU comparison.
149///
150/// In a real deployment, the GPU kernel would run on the GPU and the
151/// output would be compared here. For testing without a GPU, this
152/// function returns the CPU output, and `diff_cpu_gpu` with itself
153/// should return 0 mismatches.
154pub fn cpu_gpu_differential(emf: &[f32]) -> (Vec<u8>, usize) {
155    let n = emf.len() / 3;
156    let mut cpu_out = vec![0u8; n * 3];
157    cpu_batch_emf_to_display_gamut_mapped(emf, &mut cpu_out);
158
159    // Self-comparison (no GPU available in unit tests).
160    let mismatches = diff_cpu_gpu(&cpu_out, &cpu_out);
161    (cpu_out, mismatches)
162}
163
164// ───────────────────────────────────────────────────────────────────────────
165//  Comprehensive test suite
166// ───────────────────────────────────────────────────────────────────────────
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::render::spectral_kernel::emf_to_linear_rgb;
172
173    #[test]
174    fn golden_vectors_all_pass() {
175        let mismatches = verify_golden_vectors();
176        assert_eq!(
177            mismatches, 0,
178            "all golden vectors must match current implementation"
179        );
180    }
181
182    #[test]
183    fn golden_vectors_count() {
184        let vectors = golden_vectors();
185        assert_eq!(vectors.len(), 11, "should have 11 golden vectors (σ=0..1)");
186    }
187
188    #[test]
189    fn golden_vectors_sigma_sweep() {
190        let vectors = golden_vectors();
191        for (i, v) in vectors.iter().enumerate() {
192            let expected_sigma = i as f32 / 10.0;
193            assert!(
194                (v.sigma - expected_sigma).abs() < 1e-6,
195                "vector {} should have σ={}",
196                i,
197                expected_sigma
198            );
199        }
200    }
201
202    #[test]
203    fn golden_vectors_xyz_finite() {
204        let vectors = golden_vectors();
205        for v in &vectors {
206            assert!(v.expected_xyz.x.is_finite(), "X must be finite");
207            assert!(v.expected_xyz.y.is_finite(), "Y must be finite");
208            assert!(v.expected_xyz.z.is_finite(), "Z must be finite");
209        }
210    }
211
212    #[test]
213    fn determinism_xyz_100_runs() {
214        assert!(
215            determinism_check_xyz(1.0, 0.3, 0.5, 100),
216            "EMF→XYZ must be deterministic over 100 runs"
217        );
218    }
219
220    #[test]
221    fn determinism_batch_50_runs() {
222        let emf = [1.0, 0.0, 0.0, 1.0, 0.5, 0.5, 1.0, 0.0, 1.0, 0.8, 0.2, 0.6];
223        assert!(
224            determinism_check_batch(&emf, 50),
225            "batch EMF→RGB must be deterministic over 50 runs"
226        );
227    }
228
229    #[test]
230    fn fnv1a_hash_deterministic() {
231        let data = [1u8, 2, 3, 4, 5];
232        let h1 = fnv1a_hash(&data);
233        let h2 = fnv1a_hash(&data);
234        assert_eq!(h1, h2, "FNV-1a must be deterministic");
235    }
236
237    #[test]
238    fn fnv1a_hash_known_vector() {
239        // FNV-1a of empty input = 0x811c9dc5 (offset basis).
240        assert_eq!(
241            fnv1a_hash(&[]),
242            0x811c9dc5,
243            "FNV-1a of empty = offset basis"
244        );
245    }
246
247    #[test]
248    fn fnv1a_hash_differs_on_input() {
249        let a = [0u8, 0, 0];
250        let b = [0u8, 0, 1];
251        assert_ne!(
252            fnv1a_hash(&a),
253            fnv1a_hash(&b),
254            "different inputs must hash differently"
255        );
256    }
257
258    #[test]
259    fn batch_display_rgb_hash_deterministic() {
260        let emf = [1.0, 0.3, 0.5, 0.8, 0.2, 0.7];
261        let h1 = batch_display_rgb_hash(&emf);
262        let h2 = batch_display_rgb_hash(&emf);
263        assert_eq!(h1, h2, "batch hash must be deterministic");
264    }
265
266    #[test]
267    fn batch_display_rgb_hash_differs_on_input() {
268        let emf_a = [1.0, 0.0, 0.0];
269        let emf_b = [1.0, 0.0, 1.0];
270        assert_ne!(
271            batch_display_rgb_hash(&emf_a),
272            batch_display_rgb_hash(&emf_b),
273            "different EMF inputs must produce different hashes"
274        );
275    }
276
277    #[test]
278    fn cpu_gpu_differential_self_zero_mismatches() {
279        let emf = [1.0, 0.3, 0.5, 0.8, 0.2, 0.7, 1.0, 0.0, 0.0];
280        let (_, mismatches) = cpu_gpu_differential(&emf);
281        assert_eq!(mismatches, 0, "self-comparison should have 0 mismatches");
282    }
283
284    #[test]
285    fn cpu_gpu_differential_valid_output() {
286        let emf = [1.0, 0.3, 0.5, 0.8, 0.2, 0.7, 1.0, 0.0, 0.0];
287        let (_out, _) = cpu_gpu_differential(&emf);
288        // RGB is u8, so it's always in [0, 255] by type limits.
289    }
290
291    #[test]
292    fn full_pipeline_sweep_no_nans() {
293        for i in 0..=100 {
294            let sigma = i as f32 / 100.0;
295            for j in 0..=10 {
296                let mu = j as f32 / 10.0;
297                let rgb = emf_to_linear_rgb(1.0, mu, sigma);
298                assert!(rgb.r.is_finite(), "R NaN at σ={}, μ={}", sigma, mu);
299                assert!(rgb.g.is_finite(), "G NaN at σ={}, μ={}", sigma, mu);
300                assert!(rgb.b.is_finite(), "B NaN at σ={}, μ={}", sigma, mu);
301            }
302        }
303    }
304
305    #[test]
306    fn golden_vectors_monotone_y_at_mid_sigma() {
307        // Y (luminance) should peak around σ=0.5 (green, ȳ peak).
308        let vectors = golden_vectors();
309        let y_values: Vec<f32> = vectors.iter().map(|v| v.expected_xyz.y).collect();
310        let max_idx = y_values
311            .iter()
312            .enumerate()
313            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
314            .unwrap()
315            .0;
316        // The peak should be somewhere in the middle third (indices 3-7).
317        assert!(
318            max_idx >= 3 && max_idx <= 7,
319            "Y peak should be near green (mid σ), got index {}",
320            max_idx
321        );
322    }
323}