#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "rand-mock")]
use rand::Rng;
pub const VORONOI_CIRCUMRADIUS: f64 = 0.5773502691896257645091487805019574556476;
pub const VORONOI_INRADIUS: f64 = 0.5;
pub const LATTICE_STEPS: [(i32, i32); 6] = [
(1, 0),
(-1, 1),
(0, -1),
(-1, 0),
(1, -1),
(0, 1),
];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Eisenstein {
pub a: i64,
pub b: i64,
}
impl Eisenstein {
#[inline]
pub const fn new(a: i64, b: i64) -> Self {
Self { a, b }
}
#[inline]
pub fn to_cartesian(self) -> (f64, f64) {
let x = self.a as f64 - 0.5 * self.b as f64;
let y = 0.86602540378443864676372317075294_f64 * self.b as f64;
(x, y)
}
#[inline]
pub fn norm(self) -> f64 {
let (x, y) = self.to_cartesian();
(x * x + y * y).sqrt()
}
#[inline]
pub fn dist(self, other: Self) -> f64 {
let (x1, y1) = self.to_cartesian();
let (x2, y2) = other.to_cartesian();
let dx = x1 - x2;
let dy = y1 - y2;
(dx * dx + dy * dy).sqrt()
}
}
#[inline]
pub fn step_vector(index: usize) -> (i32, i32) {
debug_assert!(index < 6, "step index must be 0..5, got {}", index);
LATTICE_STEPS[index]
}
pub fn snap_to_lattice(x: f64, y: f64) -> (i64, i64, f64) {
let b_float = 2.0 * y / 1.7320508075688772_f64; let a_float = x + 0.5 * b_float;
let a_round = a_float.round() as i64;
let b_round = b_float.round() as i64;
let mut best_a = a_round;
let mut best_b = b_round;
let mut best_d2 = f64::INFINITY;
for da in -1..=1 {
for db in -1..=1 {
let ca = a_round + da;
let cb = b_round + db;
let (lx, ly) = Eisenstein::new(ca, cb).to_cartesian();
let dx = x - lx;
let dy = y - ly;
let d2 = dx * dx + dy * dy;
if d2 < best_d2 {
best_d2 = d2;
best_a = ca;
best_b = cb;
}
}
}
(best_a, best_b, best_d2.sqrt())
}
#[derive(Debug, Clone)]
pub struct BoundedDrift<F> {
pub a: i64,
pub b: i64,
epsilon: F,
step_count: usize,
total_error: F,
max_step_error: F,
}
impl<F: Float> BoundedDrift<F> {
#[inline]
pub fn new(epsilon: F) -> Self {
Self {
a: 0,
b: 0,
epsilon,
step_count: 0,
total_error: F::zero(),
max_step_error: F::zero(),
}
}
#[inline]
pub fn step(&mut self, index: usize) {
let (da, db) = LATTICE_STEPS[index];
self.a += da as i64;
self.b += db as i64;
self.step_count += 1;
self.total_error = self.total_error + self.epsilon;
self.max_step_error = self.max_step_error.max(self.epsilon);
}
#[inline]
pub fn step_with_error(&mut self, index: usize, actual_error: F) {
let (da, db) = LATTICE_STEPS[index];
self.a += da as i64;
self.b += db as i64;
self.step_count += 1;
debug_assert!(
actual_error < self.epsilon,
"actual_error ({:?}) must be < epsilon ({:?})",
actual_error,
self.epsilon
);
self.total_error = self.total_error + self.epsilon;
self.max_step_error = self.max_step_error.max(actual_error);
}
#[inline]
pub fn holonomy(&self) -> F {
let (x, y) = Eisenstein::new(self.a, self.b).to_cartesian();
F::sqrt(F::from_f64(x * x + y * y))
}
#[inline]
pub fn bound(&self) -> F {
let n = F::from_usize(self.step_count);
n * self.epsilon
}
#[inline]
pub fn check_bound(&self) -> bool {
self.holonomy() <= self.bound()
}
#[inline]
pub fn steps(&self) -> usize {
self.step_count
}
#[inline]
pub fn total_error(&self) -> F {
self.total_error
}
#[inline]
pub fn max_step_error(&self) -> F {
self.max_step_error
}
#[inline]
pub fn reset(&mut self) {
self.a = 0;
self.b = 0;
self.step_count = 0;
self.total_error = F::zero();
self.max_step_error = F::zero();
}
}
#[cfg(feature = "rand-mock")]
pub fn walk_cycle<F: Float>(
steps: &[usize],
epsilon: F,
rng: &mut impl Rng,
) -> (F, F, F) {
let mut bd = BoundedDrift::new(epsilon);
for &idx in steps {
let error = F::from_f64(rng.gen::<f64>() * epsilon.to_f64());
bd.step_with_error(idx, error);
}
(bd.holonomy(), bd.bound(), bd.max_step_error())
}
pub fn walk_cycle_worst_case<F: Float>(steps: &[usize], epsilon: F) -> (F, F) {
let mut bd = BoundedDrift::new(epsilon);
for &idx in steps {
bd.step(idx);
}
(bd.holonomy(), bd.bound())
}
pub trait Float:
Clone + Copy + PartialOrd + core::fmt::Debug +
core::ops::Add<Output = Self> +
core::ops::Mul<Output = Self> +
{
fn zero() -> Self;
fn sqrt(val: Self) -> Self;
fn from_usize(n: usize) -> Self;
fn to_f64(self) -> f64;
fn from_f64(val: f64) -> Self;
fn max(self, other: Self) -> Self;
}
impl Float for f32 {
#[inline]
fn zero() -> Self { 0.0 }
#[inline]
fn sqrt(val: Self) -> Self { val.sqrt() }
#[inline]
fn from_usize(n: usize) -> Self { n as Self }
#[inline]
fn to_f64(self) -> f64 { self as f64 }
#[inline]
fn from_f64(val: f64) -> Self { val as Self }
#[inline]
fn max(self, other: Self) -> Self { if self >= other { self } else { other } }
}
impl Float for f64 {
#[inline]
fn zero() -> Self { 0.0 }
#[inline]
fn sqrt(val: Self) -> Self { val.sqrt() }
#[inline]
fn from_usize(n: usize) -> Self { n as Self }
#[inline]
fn to_f64(self) -> f64 { self }
#[inline]
fn from_f64(val: f64) -> Self { val }
#[inline]
fn max(self, other: Self) -> Self { if self >= other { self } else { other } }
}
#[derive(Debug, Clone)]
pub struct Cycle<const N: usize> {
pub steps: [usize; N],
}
impl<const N: usize> Cycle<N> {
#[inline]
pub const fn new(steps: [usize; N]) -> Self {
Self { steps }
}
#[inline]
pub fn walk<F: Float>(&self, epsilon: F) -> (F, F) {
let mut bd = BoundedDrift::new(epsilon);
for &idx in &self.steps {
bd.step(idx);
}
(bd.holonomy(), bd.bound())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eisenstein_norm() {
let p = Eisenstein::new(0, 0);
assert_eq!(p.norm(), 0.0);
let p = Eisenstein::new(1, 0);
assert!((p.norm() - 1.0).abs() < 1e-10);
let p = Eisenstein::new(0, 1);
assert!((p.norm() - 1.0).abs() < 1e-10);
let p = Eisenstein::new(1, 1);
assert!((p.norm() - 1.0).abs() < 1e-10);
}
#[test]
fn test_snap_to_lattice_exact() {
for &(a, b) in &LATTICE_STEPS {
let (x, y) = Eisenstein::new(a as i64, b as i64).to_cartesian();
let (sa, sb, d) = snap_to_lattice(x, y);
assert_eq!(sa, a as i64);
assert_eq!(sb, b as i64);
assert!(d < 1e-10);
}
}
#[test]
fn test_snap_to_lattice_origin() {
let (sa, sb, d) = snap_to_lattice(0.0, 0.0);
assert_eq!(sa, 0);
assert_eq!(sb, 0);
assert!(d < 1e-10);
}
#[test]
fn test_snap_to_lattice_perturbed() {
let (x, y) = (0.3, 0.2);
let (_, _, d) = snap_to_lattice(x, y);
assert!(d < 0.57736); let dist_00 = ((x-0.0).powi(2) + (y-0.0).powi(2)).sqrt();
let dist_01 = ((x-0.5).powi(2) + (y-0.866).powi(2)).sqrt(); let dist_other = d;
assert!(dist_other <= dist_00.min(dist_01) + 1e-6);
}
#[test]
fn test_bounded_drift_zero_epsilon() {
let mut bd = BoundedDrift::<f64>::new(0.0);
bd.step(3); bd.step(0); assert_eq!(bd.holonomy(), 0.0);
assert!(bd.check_bound());
}
#[test]
fn test_bounded_drift_identity_cycle() {
let mut bd = BoundedDrift::<f64>::new(0.0);
let cycle = [0, 1, 2, 3, 4, 5];
for &idx in &cycle {
bd.step(idx);
}
assert_eq!(bd.a, 0);
assert_eq!(bd.b, 0);
assert_eq!(bd.holonomy(), 0.0);
}
#[test]
fn test_bounded_drift_with_error_below_bound() {
let mut bd = BoundedDrift::<f64>::new(0.5);
for _ in 0..10 {
bd.step(0); }
bd.reset();
for _ in 0..100 {
bd.step(0); bd.step(3); }
let h = bd.holonomy();
let b = bd.bound();
assert!(h <= b, "holonomy {} > bound {}", h, b);
}
#[test]
fn test_walk_cycle_worst_case() {
let steps = vec![0, 1, 2, 3, 4, 5];
let eps = 1.0_f64;
let (hol, bound) = walk_cycle_worst_case(&steps, eps);
assert!(hol <= bound, "holonomy {} > bound {}", hol, bound);
}
#[test]
fn test_cycle_const_generic() {
let cycle = Cycle::<3>::new([0, 2, 3]); let (hol, bound) = cycle.walk(0.5_f64);
assert!(hol <= bound);
}
#[test]
fn test_snap_distance_within_voronoi() {
for _ in 0..1000 {
let x = (rand::random::<f64>() - 0.5) * 10.0;
let y = (rand::random::<f64>() - 0.5) * 10.0;
let (_, _, d) = snap_to_lattice(x, y);
assert!(d <= VORONOI_CIRCUMRADIUS + 1e-10,
"snap distance {} > covering radius {}", d, VORONOI_CIRCUMRADIUS);
}
}
#[test]
fn test_random_walk_bound() {
use rand::Rng;
let mut rng = rand::thread_rng();
for n in [10, 50, 100] {
for eps in [0.5, 1.0, 2.0] {
for _ in 0..100 {
let mut a = 0i64; let mut b = 0i64;
let mut steps = Vec::with_capacity(n);
for _ in 0..n.saturating_sub(2) {
let idx = rng.gen_range(0..6);
steps.push(idx);
let (da, db) = LATTICE_STEPS[idx];
a += da as i64; b += db as i64;
}
for i in 0..6 {
for j in 0..6 {
let (d1, e1) = LATTICE_STEPS[i];
let (d2, e2) = LATTICE_STEPS[j];
if a + d1 as i64 + d2 as i64 == 0 && b + e1 as i64 + e2 as i64 == 0 {
steps.push(i);
steps.push(j);
break;
}
}
if steps.len() == n { break; }
}
if steps.len() == n {
let mut bd = BoundedDrift::new(eps);
for &idx in &steps {
bd.step(idx);
}
assert!(bd.check_bound(),
"n={} eps={}: hol={} > bound={}", n, eps, bd.holonomy(), bd.bound());
}
}
}
}
}
}