1use crate::QuantEvalError;
8use hyperquant::{quantize_a2, quantize_z1, HyperQuantError, LatticeKind};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct HyperQuantEvalConfig {
14 pub dim: usize,
16 pub vectors: usize,
18 pub seed: u64,
20 pub scale: f32,
22}
23
24impl HyperQuantEvalConfig {
25 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#[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#[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 pub fn profile(&self, kind: LatticeKind) -> Option<&HyperQuantProfileEval> {
71 self.profiles.iter().find(|profile| profile.kind == kind)
72 }
73}
74
75pub 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}