1const SQRT_3: f64 = 1.7320508075688772;
39
40pub const COVERING_RADIUS: f64 = 1.0 / SQRT_3;
42
43pub const CELL_AREA: f64 = SQRT_3 / 2.0;
45
46const OMEGA_RE: f64 = -0.5;
48const OMEGA_IM: f64 = SQRT_3 / 2.0;
49
50pub const WEYL_ORDER: usize = 6;
52
53pub const SAFE_THRESHOLD: f64 = COVERING_RADIUS / 2.0;
55
56const WEYL_PERMS: [(usize, usize, usize); 6] = [
58 (0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0), ];
65
66const EVEN_CHAMBERS: [usize; 3] = [0, 2, 5];
68const ODD_CHAMBERS: [usize; 3] = [1, 3, 4];
70
71#[derive(Debug, Clone, Copy)]
73pub struct SnapResult {
74 pub dodecet: u16,
76 pub snap_a: i32,
78 pub snap_b: i32,
79 pub error: f64,
81 pub error_normalized: f64,
83 pub error_level: u8,
85 pub angle_level: u8,
87 pub chamber: u8,
89 pub parity: i8,
91 pub is_safe: bool,
93}
94
95impl SnapResult {
96 pub fn to_hex(&self) -> String {
98 format!("{:03X}", self.dodecet)
99 }
100
101 pub fn parity_str(&self) -> &'static str {
103 if self.parity > 0 { "even (+)" } else { "odd (-)" }
104 }
105
106 pub fn funnel_position(&self) -> f64 {
110 (self.error / COVERING_RADIUS).sqrt()
111 }
112
113 pub fn precision_feeling(&self) -> f64 {
116 if self.error > 0.0 {
117 1.0 / self.error
118 } else {
119 f64::INFINITY
120 }
121 }
122
123 pub fn decode_dodecet(dodecet: u16) -> (u8, u8, u8, bool) {
125 let err_level = ((dodecet >> 8) & 0xF) as u8;
126 let angle_level = ((dodecet >> 4) & 0xF) as u8;
127 let chamber_byte = (dodecet & 0xF) as u8;
128 let chamber = chamber_byte & 0x7;
129 let safe = (chamber_byte >> 3) & 1 == 0;
130 (err_level, angle_level, chamber, safe)
131 }
132
133 pub fn cdf_below(&self) -> f64 {
136 std::f64::consts::PI * self.error * self.error / CELL_AREA
137 }
138}
139
140pub struct EisensteinConstraint {
142 pub funnel_width: f64,
145}
146
147impl Default for EisensteinConstraint {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl EisensteinConstraint {
154 pub fn new() -> Self {
155 EisensteinConstraint { funnel_width: 1.0 }
156 }
157
158 pub fn with_funnel(mut self, width: f64) -> Self {
159 self.funnel_width = width.clamp(0.0, 1.0);
160 self
161 }
162
163 pub fn snap(&self, x: f64, y: f64) -> SnapResult {
167 let a_f = x - y * OMEGA_RE / OMEGA_IM;
169 let b_f = y / OMEGA_IM;
170
171 let a0 = a_f.round() as i32;
172 let b0 = b_f.round() as i32;
173
174 let mut best_a = a0;
176 let mut best_b = b0;
177 let mut best_err = f64::MAX;
178
179 for da in -1..=1i32 {
180 for db in -1..=1i32 {
181 let ca = a0 + da;
182 let cb = b0 + db;
183 let cx = ca as f64 + cb as f64 * OMEGA_RE;
184 let cy = cb as f64 * OMEGA_IM;
185 let err = ((x - cx).powi(2) + (y - cy).powi(2)).sqrt();
186 if err < best_err {
187 best_a = ca;
188 best_b = cb;
189 best_err = err;
190 }
191 }
192 }
193
194 let chamber = Self::classify_chamber(x, y);
196 let parity = if EVEN_CHAMBERS.contains(&(chamber as usize)) { 1 } else { -1 };
197
198 let err_norm = (best_err / COVERING_RADIUS).min(1.0);
200 let err_level = (err_norm * 15.0).round() as u8;
201
202 let dx = x - (best_a as f64 + best_b as f64 * OMEGA_RE);
204 let dy = y - (best_b as f64 * OMEGA_IM);
205 let angle_level = if dx != 0.0 || dy != 0.0 {
206 let angle = dy.atan2(dx);
207 let norm = ((angle + std::f64::consts::PI) / (2.0 * std::f64::consts::PI));
208 (norm * 16.0).floor() as u8 % 16
209 } else {
210 0
211 };
212
213 let is_safe = best_err < SAFE_THRESHOLD;
215 let safe_bit: u8 = if is_safe { 0 } else { 1 };
216 let chamber_byte = (safe_bit << 3) | (chamber as u8 & 0x7);
217
218 let dodecet = ((err_level as u16) << 8)
220 | ((angle_level as u16) << 4)
221 | (chamber_byte as u16);
222
223 SnapResult {
224 dodecet,
225 snap_a: best_a,
226 snap_b: best_b,
227 error: best_err,
228 error_normalized: err_norm,
229 error_level: err_level,
230 angle_level,
231 chamber: chamber as u8,
232 parity,
233 is_safe,
234 }
235 }
236
237 fn classify_chamber(x: f64, y: f64) -> usize {
239 let b1 = x - y * OMEGA_RE / OMEGA_IM;
240 let b2 = y / OMEGA_IM;
241 let b3 = -(b1 + b2);
242
243 let vals = [b1, b2, b3];
244 let indices = [0usize, 1, 2];
245 let mut sorted = indices;
246 sorted.sort_by(|&a, &b| vals[b].partial_cmp(&vals[a]).unwrap_or(std::cmp::Ordering::Equal));
247
248 let perm = (sorted[0], sorted[1], sorted[2]);
249 WEYL_PERMS.iter().position(|&p| p == perm).unwrap_or(0)
250 }
251
252 pub fn merge(&self, results: &[SnapResult]) -> SnapResult {
256 if results.is_empty() {
257 return self.snap(0.0, 0.0);
258 }
259 if results.len() == 1 {
260 return results[0];
261 }
262
263 let max_err = results.iter().map(|r| r.error).fold(f64::NEG_INFINITY, f64::max);
265 let err_level = (max_err / COVERING_RADIUS * 15.0).round() as u8;
266
267 let mut angle_votes = [0u16; 16];
269 for r in results {
270 angle_votes[r.angle_level as usize] += 1;
271 }
272 let angle_level = angle_votes.iter().enumerate()
273 .max_by_key(|(_, &v)| v)
274 .map(|(i, _)| i as u8)
275 .unwrap_or(0);
276
277 let mut chamber_votes = [0u16; 6];
279 for r in results {
280 chamber_votes[r.chamber as usize] += 1;
281 }
282 let chamber = chamber_votes.iter().enumerate()
283 .max_by_key(|(_, &v)| v)
284 .map(|(i, _)| i as u8)
285 .unwrap_or(0) as u8;
286
287 let all_safe = results.iter().all(|r| r.is_safe);
289 let safe_bit: u8 = if all_safe { 0 } else { 1 };
290 let chamber_byte = (safe_bit << 3) | (chamber & 0x7);
291
292 let dodecet = ((err_level as u16) << 8)
293 | ((angle_level as u16) << 4)
294 | (chamber_byte as u16);
295
296 let parity = if EVEN_CHAMBERS.contains(&(chamber as usize)) { 1 } else { -1 };
297
298 SnapResult {
299 dodecet,
300 snap_a: 0, snap_b: 0,
302 error: max_err,
303 error_normalized: max_err / COVERING_RADIUS,
304 error_level: err_level,
305 angle_level,
306 chamber,
307 parity,
308 is_safe: all_safe,
309 }
310 }
311
312 pub fn deadband(&self, t: f64) -> f64 {
318 COVERING_RADIUS * (1.0 - t).sqrt().max(0.0)
319 }
320
321 pub fn check(&self, x: f64, y: f64) -> ConstraintVerdict {
323 let result = self.snap(x, y);
324 let threshold = self.deadband(self.funnel_width);
325
326 if result.error <= threshold {
327 ConstraintVerdict::Satisfied(result)
328 } else {
329 ConstraintVerdict::Violated(result)
330 }
331 }
332}
333
334#[derive(Debug)]
336pub enum ConstraintVerdict {
337 Satisfied(SnapResult),
338 Violated(SnapResult),
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_covering_radius() {
347 assert!((COVERING_RADIUS - 1.0 / SQRT_3).abs() < 1e-10);
348 assert!((COVERING_RADIUS - 0.577350269).abs() < 1e-6);
349 }
350
351 #[test]
352 fn test_snap_origin() {
353 let ec = EisensteinConstraint::new();
354 let result = ec.snap(0.0, 0.0);
355 assert_eq!(result.snap_a, 0);
356 assert_eq!(result.snap_b, 0);
357 assert!(result.error < 1e-10);
358 assert!(result.is_safe);
359 assert_eq!(result.error_level, 0);
360 }
361
362 #[test]
363 fn test_snap_near_lattice_point() {
364 let ec = EisensteinConstraint::new();
365 let result = ec.snap(1.0, 0.0);
367 assert_eq!(result.snap_a, 1);
368 assert_eq!(result.snap_b, 0);
369 assert!(result.error < 0.01);
370 }
371
372 #[test]
373 fn test_covering_radius_never_exceeded() {
374 let ec = EisensteinConstraint::new();
375 for _ in 0..10000 {
376 let x = rand_float(-10.0, 10.0);
377 let y = rand_float(-10.0, 10.0);
378 let result = ec.snap(x, y);
379 assert!(result.error <= COVERING_RADIUS + 1e-6,
380 "Error {:.6} exceeds ρ {:.6} at ({:.2}, {:.2})",
381 result.error, COVERING_RADIUS, x, y);
382 }
383 }
384
385 #[test]
386 fn test_dodecet_roundtrip() {
387 let ec = EisensteinConstraint::new();
388 let result = ec.snap(1.5, 2.3);
389 let (err, angle, ch, safe) = SnapResult::decode_dodecet(result.dodecet);
390 assert_eq!(err, result.error_level);
391 assert_eq!(angle, result.angle_level);
392 assert_eq!(ch, result.chamber);
393 assert_eq!(safe, result.is_safe);
394 }
395
396 #[test]
397 fn test_weyl_invariance() {
398 let ec = EisensteinConstraint::new();
399 let mut errors_by_chamber: [Vec<f64>; 6] = Default::default();
400 for _ in 0..10000 {
401 let x = rand_float(-5.0, 5.0);
402 let y = rand_float(-5.0, 5.0);
403 let result = ec.snap(x, y);
404 errors_by_chamber[result.chamber as usize].push(result.error);
405 }
406 let means: Vec<f64> = errors_by_chamber.iter()
407 .filter(|v| !v.is_empty())
408 .map(|v| v.iter().sum::<f64>() / v.len() as f64)
409 .collect();
410 let max_spread = means.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
411 - means.iter().cloned().fold(f64::INFINITY, f64::min);
412 assert!(max_spread / means[0] < 0.05,
414 "Chamber means too spread: {:?}", means);
415 }
416
417 #[test]
418 fn test_merge_pessimistic() {
419 let ec = EisensteinConstraint::new();
420 let r1 = ec.snap(0.1, 0.1); let r2 = ec.snap(0.5, 0.3); let merged = ec.merge(&[r1, r2]);
423 assert!(merged.error >= r1.error - 1e-6);
425 assert!(merged.error >= r2.error - 1e-6);
426 }
427
428 #[test]
429 fn test_deadband_funnel() {
430 let ec = EisensteinConstraint::new();
431 assert!((ec.deadband(0.0) - COVERING_RADIUS).abs() < 1e-10);
432 assert!(ec.deadband(0.5) < COVERING_RADIUS);
433 assert!(ec.deadband(0.5) > 0.0);
434 assert!((ec.deadband(1.0)).abs() < 1e-10);
435 }
436
437 #[test]
438 fn test_right_skew() {
439 let ec = EisensteinConstraint::new();
440 let mut high_count = 0;
441 let total = 10000;
442 for _ in 0..total {
443 let x = rand_float(-5.0, 5.0);
444 let y = rand_float(-5.0, 5.0);
445 let result = ec.snap(x, y);
446 if result.error_level >= 8 {
447 high_count += 1;
448 }
449 }
450 assert!(high_count as f64 / total as f64 > 0.60,
452 "Expected >60% at levels 8-15, got {:.1}%",
453 high_count as f64 / total as f64 * 100.0);
454 }
455
456 fn rand_float(min: f64, max: f64) -> f64 {
457 use std::time::{SystemTime, UNIX_EPOCH};
458 static mut SEED: u64 = 0;
459 unsafe {
460 if SEED == 0 {
461 SEED = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() as u64;
462 }
463 SEED = SEED.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
464 let x = SEED;
465 min + (max - min) * ((x >> 33) as f64 / (1u64 << 31) as f64)
466 }
467 }
468}