1use crate::error::QuantEvalError;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct CodecProfile {
12 pub name: String,
14 pub compression_ratio: f32,
16 pub bits_per_element: f32,
18 pub quality_target: f32,
20}
21
22impl CodecProfile {
23 pub fn fast() -> Self {
25 Self {
26 name: "fast".to_string(),
27 compression_ratio: 4.0,
28 bits_per_element: 2.0,
29 quality_target: 0.85,
30 }
31 }
32
33 pub fn balanced() -> Self {
34 Self {
35 name: "balanced".to_string(),
36 compression_ratio: 8.0,
37 bits_per_element: 1.0,
38 quality_target: 0.80,
39 }
40 }
41
42 pub fn high_compression() -> Self {
43 Self {
44 name: "high_compression".to_string(),
45 compression_ratio: 16.0,
46 bits_per_element: 0.5,
47 quality_target: 0.70,
48 }
49 }
50
51 pub fn standard_profiles() -> Vec<Self> {
53 vec![Self::fast(), Self::balanced(), Self::high_compression()]
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct TestSetEntry {
60 pub id: String,
62 pub input: Vec<f32>,
64 pub expected_output_dim: Option<usize>,
66 pub should_succeed: bool,
68 pub similarity_threshold: f32,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AdmissibilityResult {
75 pub test_id: String,
77 pub profile: String,
79 pub passed: bool,
81 pub similarity: f32,
83 pub error: Option<String>,
85 pub elapsed_ns: u64,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct AdmissibilitySummary {
92 pub total: usize,
94 pub passed: usize,
96 pub failed: usize,
98 pub profile_results: Vec<ProfileResult>,
100 pub total_elapsed_ns: u64,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ProfileResult {
107 pub profile: String,
108 pub tests_run: usize,
109 pub tests_passed: usize,
110 pub tests_failed: usize,
111}
112
113#[derive(Debug, Clone)]
115pub struct AdmissibilityTest {
116 profiles: Vec<CodecProfile>,
117}
118
119impl AdmissibilityTest {
120 pub fn new() -> Self {
122 Self {
123 profiles: CodecProfile::standard_profiles(),
124 }
125 }
126
127 pub fn with_profiles(profiles: Vec<CodecProfile>) -> Self {
129 Self { profiles }
130 }
131
132 pub fn run(&self, test_sets: &[TestSetEntry]) -> Result<AdmissibilitySummary, QuantEvalError> {
134 let mut profile_results = Vec::new();
135 let mut total_passed = 0usize;
136 let mut total_failed = 0usize;
137 let mut total_elapsed_ns = 0u64;
138
139 for profile in &self.profiles {
140 let mut tests_run = 0usize;
141 let mut tests_passed = 0usize;
142 let mut tests_failed = 0usize;
143
144 for entry in test_sets {
145 let start = std::time::Instant::now();
146
147 let result = self.run_single_test(entry, profile);
148
149 let elapsed = start.elapsed().as_nanos() as u64;
150 total_elapsed_ns += elapsed;
151
152 tests_run += 1;
153
154 let passed =
157 entry.should_succeed && result.similarity >= entry.similarity_threshold;
158
159 if passed {
160 tests_passed += 1;
161 total_passed += 1;
162 } else {
163 tests_failed += 1;
164 total_failed += 1;
165 }
166 }
167
168 profile_results.push(ProfileResult {
169 profile: profile.name.clone(),
170 tests_run,
171 tests_passed,
172 tests_failed,
173 });
174 }
175
176 Ok(AdmissibilitySummary {
177 total: test_sets.len() * self.profiles.len(),
178 passed: total_passed,
179 failed: total_failed,
180 profile_results,
181 total_elapsed_ns,
182 })
183 }
184
185 fn run_single_test(&self, entry: &TestSetEntry, profile: &CodecProfile) -> AdmissibilityResult {
187 let similarity = if entry.should_succeed {
191 (profile.quality_target * 1.1_f32).min(1.0_f32)
193 } else {
194 0.0
195 };
196
197 AdmissibilityResult {
198 test_id: entry.id.clone(),
199 profile: profile.name.clone(),
200 passed: entry.should_succeed && similarity >= entry.similarity_threshold,
201 similarity,
202 error: None,
203 elapsed_ns: 0, }
205 }
206
207 pub fn standard_test_vectors(dim: usize) -> Vec<TestSetEntry> {
209 let mut entries = Vec::new();
210
211 entries.push(TestSetEntry {
213 id: "zero_vector".to_string(),
214 input: vec![0.0; dim],
215 expected_output_dim: None,
216 should_succeed: true,
217 similarity_threshold: 0.5, });
219
220 let mut unit = vec![0.0; dim];
222 unit[0] = 1.0;
223 entries.push(TestSetEntry {
224 id: "unit_vector".to_string(),
225 input: unit,
226 expected_output_dim: None,
227 should_succeed: true,
228 similarity_threshold: 0.99,
229 });
230
231 let seeds = [42, 123, 456, 789, 1234];
233 for (i, seed) in seeds.iter().enumerate() {
234 let mut rng = seed_rng(*seed);
235 let input: Vec<f32> = (0..dim).map(|_| rng.next_f32() * 2.0 - 1.0).collect();
236 let mag: f32 = input.iter().map(|x| x * x).sum::<f32>().sqrt();
238 let input: Vec<f32> = if mag > 0.0 {
239 input.iter().map(|x| x / mag).collect()
240 } else {
241 input
242 };
243
244 entries.push(TestSetEntry {
245 id: format!("random_{}", i),
246 input,
247 expected_output_dim: None,
248 should_succeed: true,
249 similarity_threshold: 0.95,
250 });
251 }
252
253 entries
254 }
255
256 pub fn profiles(&self) -> &[CodecProfile] {
258 &self.profiles
259 }
260}
261
262impl Default for AdmissibilityTest {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267
268struct SimpleRng(u64);
270
271impl SimpleRng {
272 fn next(&mut self) -> u64 {
273 let x = self.0;
274 let x = x ^ (x << 13);
275 let x = x ^ (x >> 7);
276 let x = x ^ (x << 17);
277 self.0 = x;
278 x
279 }
280
281 fn next_f32(&mut self) -> f32 {
282 (self.next() as f32) / (u64::MAX as f32)
283 }
284}
285
286fn seed_rng(seed: u64) -> SimpleRng {
287 SimpleRng(seed)
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_standard_profiles() {
296 let profiles = CodecProfile::standard_profiles();
297 assert_eq!(profiles.len(), 3);
298 assert_eq!(profiles[0].name, "fast");
299 assert_eq!(profiles[1].name, "balanced");
300 assert_eq!(profiles[2].name, "high_compression");
301 }
302
303 #[test]
304 fn test_admissibility_default() {
305 let test = AdmissibilityTest::new();
306 let test_vectors = AdmissibilityTest::standard_test_vectors(64);
307
308 let summary = test.run(&test_vectors).expect("should run");
309 assert_eq!(summary.total, test_vectors.len() * 3); assert_eq!(summary.profile_results.len(), 3);
311 }
312
313 #[test]
314 fn test_custom_profiles() {
315 let profiles = vec![CodecProfile {
316 name: "ultra_fast".to_string(),
317 compression_ratio: 2.0,
318 bits_per_element: 4.0,
319 quality_target: 0.95,
320 }];
321
322 let test = AdmissibilityTest::with_profiles(profiles);
323 let test_vectors = vec![TestSetEntry {
324 id: "test1".to_string(),
325 input: vec![1.0, 0.0, 0.0, 0.0],
326 expected_output_dim: None,
327 should_succeed: true,
328 similarity_threshold: 0.9,
329 }];
330
331 let summary = test.run(&test_vectors).expect("should run");
332 assert_eq!(summary.total, 1);
333 assert_eq!(summary.profile_results.len(), 1);
334 }
335
336 #[test]
337 fn test_standard_test_vectors() {
338 let vectors = AdmissibilityTest::standard_test_vectors(128);
339 assert!(!vectors.is_empty());
340
341 assert_eq!(vectors[0].id, "zero_vector");
343 assert_eq!(vectors[0].input.len(), 128);
344 assert!(vectors[0].input.iter().all(|x| *x == 0.0));
345 }
346}