Skip to main content

quant_eval/
hyperquant_eval.rs

1//! HyperQuant evaluation harness.
2//!
3//! This module is intentionally local and fixture-driven. It measures the
4//! current `hyperquant` crate as an experimental primitive; it does not claim
5//! paper parity, model-quality preservation, or production admissibility.
6
7use crate::QuantEvalError;
8use hyperquant::{quantize_a2, quantize_z1, HyperQuantError, LatticeKind};
9use serde::{Deserialize, Serialize};
10
11/// Configuration for deterministic HyperQuant fixture evaluation.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct HyperQuantEvalConfig {
14    /// Vector dimension.
15    pub dim: usize,
16    /// Number of vectors in the synthetic fixture.
17    pub vectors: usize,
18    /// Deterministic fixture seed.
19    pub seed: u64,
20    /// Quantization scale passed to HyperQuant.
21    pub scale: f32,
22}
23
24impl HyperQuantEvalConfig {
25    /// A small fixture where points lie on the A2 triangular basis.
26    pub fn triangular_fixture() -> Self {
27        Self {
28            dim: 2,
29            vectors: 12,
30            seed: 0xA2,
31            scale: 1.0,
32        }
33    }
34}
35
36impl Default for HyperQuantEvalConfig {
37    fn default() -> Self {
38        Self {
39            dim: 16,
40            vectors: 64,
41            seed: 42,
42            scale: 8.0,
43        }
44    }
45}
46
47/// Per-lattice profile result.
48#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub struct HyperQuantProfileEval {
50    pub kind: LatticeKind,
51    pub mean_mse: f32,
52    pub max_mse: f32,
53    pub mean_bytes_per_vector: f32,
54    pub estimated_raw_bytes_per_vector: usize,
55    pub estimated_compressed_bytes_per_vector: usize,
56    pub rejected_vectors: usize,
57    pub receipt_count: usize,
58}
59
60/// Full HyperQuant evaluation result.
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct HyperQuantEvalResult {
63    pub config: HyperQuantEvalConfig,
64    pub profiles: Vec<HyperQuantProfileEval>,
65    pub claim_boundary: String,
66}
67
68impl HyperQuantEvalResult {
69    /// Return a profile by lattice kind.
70    pub fn profile(&self, kind: LatticeKind) -> Option<&HyperQuantProfileEval> {
71        self.profiles.iter().find(|profile| profile.kind == kind)
72    }
73}
74
75/// Run deterministic fixture evaluation for HyperQuant Z1 and A2.
76pub fn run_hyperquant_eval(
77    config: &HyperQuantEvalConfig,
78) -> Result<HyperQuantEvalResult, QuantEvalError> {
79    validate_config(config)?;
80    let vectors = generate_fixture_vectors(config);
81    let profiles = vec![
82        evaluate_profile(LatticeKind::Z1, config.scale, &vectors),
83        evaluate_profile(LatticeKind::A2, config.scale, &vectors),
84    ];
85
86    Ok(HyperQuantEvalResult {
87        config: *config,
88        profiles,
89        claim_boundary: "experimental primitive only; not paper parity or model-quality evidence"
90            .to_string(),
91    })
92}
93
94fn validate_config(config: &HyperQuantEvalConfig) -> Result<(), QuantEvalError> {
95    if config.dim == 0 {
96        return Err(QuantEvalError::InvalidCorpus(
97            "hyperquant eval dim must be > 0".to_string(),
98        ));
99    }
100    if config.vectors == 0 {
101        return Err(QuantEvalError::InvalidCorpus(
102            "hyperquant eval vectors must be > 0".to_string(),
103        ));
104    }
105    Ok(())
106}
107
108fn evaluate_profile(kind: LatticeKind, scale: f32, vectors: &[Vec<f32>]) -> HyperQuantProfileEval {
109    let mut mse_values = Vec::with_capacity(vectors.len());
110    let mut rejected_vectors = 0usize;
111    let mut receipt_count = 0usize;
112
113    for vector in vectors {
114        let result = match kind {
115            LatticeKind::Z1 => quantize_z1(vector, scale),
116            LatticeKind::A2 => quantize_a2(vector, scale),
117            LatticeKind::D4 | LatticeKind::E8 => Err(HyperQuantError::UnsupportedLattice(kind)),
118        };
119        match result {
120            Ok(result) => {
121                let receipt = result.receipt();
122                if receipt.mse.is_finite() {
123                    mse_values.push(receipt.mse);
124                    receipt_count += 1;
125                } else {
126                    rejected_vectors += 1;
127                }
128            }
129            Err(_) => rejected_vectors += 1,
130        }
131    }
132
133    let mean_mse = if mse_values.is_empty() {
134        0.0
135    } else {
136        mse_values.iter().sum::<f32>() / mse_values.len() as f32
137    };
138    let max_mse = mse_values.iter().copied().fold(0.0f32, f32::max);
139    let dim = vectors.first().map_or(0usize, Vec::len);
140    let raw_bytes = dim * core::mem::size_of::<f32>();
141    let compressed_bytes = dim * core::mem::size_of::<i16>();
142
143    HyperQuantProfileEval {
144        kind,
145        mean_mse,
146        max_mse,
147        mean_bytes_per_vector: compressed_bytes as f32,
148        estimated_raw_bytes_per_vector: raw_bytes,
149        estimated_compressed_bytes_per_vector: compressed_bytes,
150        rejected_vectors,
151        receipt_count,
152    }
153}
154
155fn generate_fixture_vectors(config: &HyperQuantEvalConfig) -> Vec<Vec<f32>> {
156    if config.dim == 2 && config.scale == 1.0 {
157        return triangular_vectors(config.vectors);
158    }
159
160    (0..config.vectors)
161        .map(|row| {
162            (0..config.dim)
163                .map(|col| deterministic_value(config.seed, row, col))
164                .collect()
165        })
166        .collect()
167}
168
169fn triangular_vectors(count: usize) -> Vec<Vec<f32>> {
170    const SQRT_3_OVER_2: f32 = 0.866_025_4;
171    (0..count)
172        .map(|i| {
173            let u = (i % 4) as f32 - 1.0;
174            let v = ((i / 4) % 4) as f32 - 1.0;
175            vec![u + 0.5 * v, SQRT_3_OVER_2 * v]
176        })
177        .collect()
178}
179
180fn deterministic_value(seed: u64, row: usize, col: usize) -> f32 {
181    let mut x = seed
182        ^ (row as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
183        ^ (col as u64).wrapping_mul(0xBF58_476D_1CE4_E5B9);
184    x ^= x >> 30;
185    x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
186    x ^= x >> 27;
187    x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
188    x ^= x >> 31;
189    let unit = (x as f64 / u64::MAX as f64) as f32;
190    unit * 2.0 - 1.0
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn deterministic_value_is_stable() {
199        assert_eq!(deterministic_value(1, 2, 3), deterministic_value(1, 2, 3));
200        assert_ne!(deterministic_value(1, 2, 3), deterministic_value(1, 2, 4));
201    }
202
203    #[test]
204    fn triangular_vectors_are_a2_points() {
205        let vectors = triangular_vectors(4);
206        let profile = evaluate_profile(LatticeKind::A2, 1.0, &vectors);
207        assert_eq!(profile.rejected_vectors, 0);
208        assert!(profile.max_mse < 1.0e-6);
209    }
210}