Skip to main content

holonomy_bounded/
lib.rs

1//! # Holonomy-Bounded
2//!
3//! A production-quality Rust implementation of the **Bounded Drift Theorem**
4//! for Eisenstein lattice snap operations.
5//!
6//! ## Theorem
7//!
8//! For a closed cycle of `n` Eisenstein snap operations, each with error
9//! bounded by `ε`, the total holonomy (drift) satisfies:
10//!
11//! ```text
12//! holonomy ≤ n · ε
13//! ```
14//!
15//! This bound is **tight** for worst-case adversarial errors (tightness ~ 1.0
16//! for small n), but for random errors the typical drift scales as
17//! `O(√n · ε)`.
18//!
19//! ## Eisenstein Lattice
20//!
21//! The Eisenstein lattice ℤ[ω] consists of points `a + bω` where
22//! `ω = e^(2πi/3) = (-1 + i√3)/2`. The lattice has 6-fold rotational symmetry
23//! and forms a regular hexagonal tiling of the complex plane.
24//!
25//! The Voronoi cell of each lattice point is a regular hexagon with:
26//! - **Inradius:** 0.5 (distance from center to edge midpoint)
27//! - **Circumradius:** `1/√3 ≈ 0.57735` (distance from center to vertex)
28//!
29//! ## Usage
30//!
31//! ```rust
32//! use holonomy_bounded::BoundedDrift;
33//!
34//! // Create a float-64 bounded drift tracker with epsilon=0.5
35//! let mut bd = BoundedDrift::<f64>::new(0.5);
36//!
37//! // Walk a closed hexagon (6 steps that sum to zero on the lattice)
38//! let cycle = [0, 1, 2, 3, 4, 5];
39//! for &idx in &cycle { bd.step(idx); }
40//!
41//! // After a closed cycle, holonomy is bounded by n*epsilon = 6*0.5 = 3.0
42//! let holonomy = bd.holonomy();
43//! assert!(holonomy <= bd.bound(), "hol={} > bound={}", holonomy, bd.bound());
44//! ```
45
46#![cfg_attr(not(feature = "std"), no_std)]
47#![warn(missing_docs)]
48#![warn(rustdoc::missing_crate_level_docs)]
49
50#[cfg(feature = "std")]
51extern crate std;
52
53#[cfg(feature = "rand-mock")]
54use rand::Rng;
55
56// ──────────────────────────────────────────────────────────────────────────────
57//  Constants
58// ──────────────────────────────────────────────────────────────────────────────
59
60/// The circumradius of the Eisenstein lattice Voronoi cell: `1/√(3)`.
61///
62/// This is the maximum distance from any point in the complex plane to
63/// the nearest Eisenstein lattice point. Equivalent to the covering radius.
64pub const VORONOI_CIRCUMRADIUS: f64 = 0.5773502691896257645091487805019574556476;
65
66/// The inradius of the Eisenstein lattice Voronoi cell: `0.5`.
67///
68/// The maximum distance from a lattice point to the boundary of its
69/// Voronoi cell. Points within this distance of a lattice point always
70/// snap to that point.
71pub const VORONOI_INRADIUS: f64 = 0.5;
72
73/// The six primitive Eisenstein lattice vectors in `(a, b)` coordinates.
74///
75/// These are the unit steps in the Eisenstein integer lattice:
76///
77/// | Index | Vector     | Description          |
78/// |-------|------------|----------------------|
79/// | 0     | `+1`       | `a += 1`             |
80/// | 1     | `-1 + ω`   | `a -= 1, b += 1`     |
81/// | 2     | `-ω`       | `b -= 1`             |
82/// | 3     | `-1`       | `a -= 1`             |
83/// | 4     | `1 - ω`    | `a += 1, b -= 1`     |
84/// | 5     | `+ω`       | `b += 1`             |
85pub const LATTICE_STEPS: [(i32, i32); 6] = [
86    (1, 0),
87    (-1, 1),
88    (0, -1),
89    (-1, 0),
90    (1, -1),
91    (0, 1),
92];
93
94// ──────────────────────────────────────────────────────────────────────────────
95//  Eisenstein Integer
96// ──────────────────────────────────────────────────────────────────────────────
97
98/// An Eisenstein integer `a + bω`.
99///
100/// `ω = e^(2πi/3) = (-1 + i√3)/2` is a primitive cube root of unity.
101/// Eisenstein integers form a hexagonal lattice in the complex plane.
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct Eisenstein {
104    /// Coefficient of 1
105    pub a: i64,
106    /// Coefficient of ω
107    pub b: i64,
108}
109
110impl Eisenstein {
111    /// Create a new Eisenstein integer.
112    #[inline]
113    pub const fn new(a: i64, b: i64) -> Self {
114        Self { a, b }
115    }
116
117    /// Convert to Cartesian `(x, y)` coordinates.
118    ///
119    /// The conversion is:
120    /// ```text
121    /// x = a - b/2
122    /// y = (√3 / 2) · b
123    /// ```
124    #[inline]
125    pub fn to_cartesian(self) -> (f64, f64) {
126        let x = self.a as f64 - 0.5 * self.b as f64;
127        let y = 0.86602540378443864676372317075294_f64 * self.b as f64;
128        (x, y)
129    }
130
131    /// Euclidean norm of this Eisenstein integer (in the complex plane).
132    #[inline]
133    pub fn norm(self) -> f64 {
134        let (x, y) = self.to_cartesian();
135        (x * x + y * y).sqrt()
136    }
137
138    /// Distance between two Eisenstein integers.
139    #[inline]
140    pub fn dist(self, other: Self) -> f64 {
141        let (x1, y1) = self.to_cartesian();
142        let (x2, y2) = other.to_cartesian();
143        let dx = x1 - x2;
144        let dy = y1 - y2;
145        (dx * dx + dy * dy).sqrt()
146    }
147}
148
149// ──────────────────────────────────────────────────────────────────────────────
150//  Lattice Operations
151// ──────────────────────────────────────────────────────────────────────────────
152
153/// Convert a lattice step index to a directional vector in `(a, b)` Eisenstein
154/// coordinates.
155#[inline]
156pub fn step_vector(index: usize) -> (i32, i32) {
157    debug_assert!(index < 6, "step index must be 0..5, got {}", index);
158    LATTICE_STEPS[index]
159}
160
161/// Find the nearest lattice point to a given Cartesian position `(x, y)`.
162///
163/// Returns `(a, b, distance)` where `(a, b)` are the Eisenstein coordinates
164/// of the nearest lattice point and `distance` is the Euclidean distance
165/// from the input to that point.
166///
167/// The maximum snap distance (covering radius) is `VORONOI_CIRCUMRADIUS ≈ 0.577`.
168pub fn snap_to_lattice(x: f64, y: f64) -> (i64, i64, f64) {
169    // Convert (x, y) to approximate Eisenstein coordinates.
170    // x = a - b/2, y = (√3/2) · b
171    // => b = 2y/√3, a = x + b/2
172    let b_float = 2.0 * y / 1.7320508075688772_f64; // 1/0.577...
173    let a_float = x + 0.5 * b_float;
174
175    let a_round = a_float.round() as i64;
176    let b_round = b_float.round() as i64;
177
178    // Search a 3×3 neighborhood around the rounded point.
179    let mut best_a = a_round;
180    let mut best_b = b_round;
181    let mut best_d2 = f64::INFINITY;
182
183    for da in -1..=1 {
184        for db in -1..=1 {
185            let ca = a_round + da;
186            let cb = b_round + db;
187            let (lx, ly) = Eisenstein::new(ca, cb).to_cartesian();
188            let dx = x - lx;
189            let dy = y - ly;
190            let d2 = dx * dx + dy * dy;
191
192            if d2 < best_d2 {
193                best_d2 = d2;
194                best_a = ca;
195                best_b = cb;
196            }
197        }
198    }
199
200    (best_a, best_b, best_d2.sqrt())
201}
202
203// ──────────────────────────────────────────────────────────────────────────────
204//  BoundedDrift
205// ──────────────────────────────────────────────────────────────────────────────
206
207/// Tracks the drift of a sequence of snap operations on the Eisenstein lattice.
208///
209/// Each step moves along a lattice direction, then applies an imperfect snap
210/// that introduces error bounded by `ε` (epsilon). The cumulative drift
211/// (holonomy) after `n` steps is bounded by `n · ε`.
212///
213/// # Type Parameters
214///
215/// - `F`: The floating-point type used for tracking. Either `f32` or `f64`.
216///
217/// # Example
218///
219/// ```rust
220/// use holonomy_bounded::BoundedDrift;
221///
222/// let mut tracker = BoundedDrift::<f64>::new(0.1);
223/// tracker.step(3); // a -= 1
224/// tracker.step(0); // a += 1 (back to origin ideally)
225/// println!("Holonomy: {}", tracker.holonomy());
226/// ```
227///
228/// # Safety Proof
229///
230/// For each step, the error `e_i` satisfies `0 ≤ e_i < ε`.
231/// After `n` steps, by the triangle inequality:
232/// ```text
233/// holonomy = ||P_n - P_0|| = ||Σ_i e_i · v_i|| ≤ Σ_i ||e_i · v_i|| = Σ_i e_i < n · ε
234/// ```
235/// where `v_i` are unit vectors. The Eisenstein lattice ensures that
236/// adjacent lattice points are unit distance apart, so the maximum
237/// drift from a single mis-snap is at most `ε`.
238#[derive(Debug, Clone)]
239pub struct BoundedDrift<F> {
240    /// Current position (Eisenstein coordinates)
241    pub a: i64,
242    /// Current position (Eisenstein coordinates)
243    pub b: i64,
244    /// Error bound per step
245    epsilon: F,
246    /// Number of steps taken
247    step_count: usize,
248    /// Total accumulated error (Σ e_i)
249    total_error: F,
250    /// Maximum single-step error observed
251    max_step_error: F,
252}
253
254impl<F: Float> BoundedDrift<F> {
255    /// Create a new bounded drift tracker with a given per-step error bound.
256    ///
257    /// The tracker starts at the origin `(0, 0)`.
258    #[inline]
259    pub fn new(epsilon: F) -> Self {
260        Self {
261            a: 0,
262            b: 0,
263            epsilon,
264            step_count: 0,
265            total_error: F::zero(),
266            max_step_error: F::zero(),
267        }
268    }
269
270    /// Take a step in direction `index`, applying the cumulative drift model.
271    ///
272    /// The direction index corresponds to `LATTICE_STEPS`:
273    /// - 0: `+1` (`a += 1`)
274    /// - 1: `-1 + ω` (`a -= 1, b += 1`)
275    /// - 2: `-ω` (`b -= 1`)
276    /// - 3: `-1` (`a -= 1`)
277    /// - 4: `1 - ω` (`a += 1, b -= 1`)
278    /// - 5: `+ω` (`b += 1`)
279    ///
280    /// After each step, the drift from origin is bounded by
281    /// `step_count * epsilon`.
282    ///
283    /// # Panics
284    ///
285    /// Panics if `index >= 6`.
286    #[inline]
287    pub fn step(&mut self, index: usize) {
288        let (da, db) = LATTICE_STEPS[index];
289        self.a += da as i64;
290        self.b += db as i64;
291        self.step_count += 1;
292
293        // In the worst-case drift model, the error at each step is ε.
294        // We track it as the maximum possible error accumulated.
295        self.total_error = self.total_error + self.epsilon;
296        self.max_step_error = self.max_step_error.max(self.epsilon);
297    }
298
299    /// Record a step with a *known* actual error.
300    ///
301    /// This is useful for simulations that compute the actual snap error
302    /// and want to track both the empirical drift and the theoretical bound.
303    ///
304    /// The `actual_error` must be `< epsilon` for the bound to apply.
305    #[inline]
306    pub fn step_with_error(&mut self, index: usize, actual_error: F) {
307        let (da, db) = LATTICE_STEPS[index];
308        self.a += da as i64;
309        self.b += db as i64;
310        self.step_count += 1;
311
312        debug_assert!(
313            actual_error < self.epsilon,
314            "actual_error ({:?}) must be < epsilon ({:?})",
315            actual_error,
316            self.epsilon
317        );
318
319        // The effective error is the max of actual and epsilon
320        // (we use the worst-case bound for the theoretical guarantee)
321        self.total_error = self.total_error + self.epsilon;
322        self.max_step_error = self.max_step_error.max(actual_error);
323    }
324
325    /// Current holonomy (drift from origin) as Euclidean distance.
326    ///
327    /// This is the actual distance from the current position to the origin,
328    /// in the complex plane (Cartesian metric).
329    #[inline]
330    pub fn holonomy(&self) -> F {
331        let (x, y) = Eisenstein::new(self.a, self.b).to_cartesian();
332        F::sqrt(F::from_f64(x * x + y * y))
333    }
334
335    /// The theoretical bound on holonomy after `n` steps: `n · ε`.
336    #[inline]
337    pub fn bound(&self) -> F {
338        let n = F::from_usize(self.step_count);
339        n * self.epsilon
340    }
341
342    /// Check whether the current holonomy satisfies the bound.
343    #[inline]
344    pub fn check_bound(&self) -> bool {
345        self.holonomy() <= self.bound()
346    }
347
348    /// Number of steps taken so far.
349    #[inline]
350    pub fn steps(&self) -> usize {
351        self.step_count
352    }
353
354    /// Total accumulated error (Σ e_i, bounded by n·ε).
355    #[inline]
356    pub fn total_error(&self) -> F {
357        self.total_error
358    }
359
360    /// Maximum single-step error observed.
361    #[inline]
362    pub fn max_step_error(&self) -> F {
363        self.max_step_error
364    }
365
366    /// Reset the tracker to the origin.
367    #[inline]
368    pub fn reset(&mut self) {
369        self.a = 0;
370        self.b = 0;
371        self.step_count = 0;
372        self.total_error = F::zero();
373        self.max_step_error = F::zero();
374    }
375}
376
377// ──────────────────────────────────────────────────────────────────────────────
378//  Walk a cycle
379// ──────────────────────────────────────────────────────────────────────────────
380
381/// Walk a closed cycle on the Eisenstein lattice and measure drift.
382///
383/// Given a sequence of step indices forming a closed cycle (net displacement
384/// zero on the ideal lattice), this function applies the bounded drift model
385/// and returns the holonomy after completing the cycle.
386///
387/// The optional random-error model applies random perturbation bounded by `ε`
388/// at each snap if a random number generator is provided. Without it, the
389/// worst-case (adversarial) bound `nε` is used.
390///
391/// # Returns
392///
393/// `(holonomy, bound, max_step_error)`
394#[cfg(feature = "rand-mock")]
395pub fn walk_cycle<F: Float>(
396    steps: &[usize],
397    epsilon: F,
398    rng: &mut impl Rng,
399) -> (F, F, F) {
400    let mut bd = BoundedDrift::new(epsilon);
401
402    for &idx in steps {
403        // Generate a random error in [0, epsilon)
404        let error = F::from_f64(rng.gen::<f64>() * epsilon.to_f64());
405        bd.step_with_error(idx, error);
406    }
407
408    (bd.holonomy(), bd.bound(), bd.max_step_error())
409}
410
411/// Walk a cycle with worst-case (maximal) error at each step.
412///
413/// This tests the theoretical upper bound: each step incurs the maximum
414/// allowed error ε.
415pub fn walk_cycle_worst_case<F: Float>(steps: &[usize], epsilon: F) -> (F, F) {
416    let mut bd = BoundedDrift::new(epsilon);
417
418    for &idx in steps {
419        bd.step(idx);
420    }
421
422    (bd.holonomy(), bd.bound())
423}
424
425// ──────────────────────────────────────────────────────────────────────────────
426//  Float Trait (no_std compatible, minimal)
427// ──────────────────────────────────────────────────────────────────────────────
428
429/// Minimal floating-point trait for generic drift tracking.
430///
431/// This trait provides the operations needed by `BoundedDrift` on the
432/// floating-point type `F`. Implementations are provided for `f32` and `f64`.
433pub trait Float:
434    Clone + Copy + PartialOrd + core::fmt::Debug +
435    core::ops::Add<Output = Self> +
436    core::ops::Mul<Output = Self> +
437{
438    /// Zero value.
439    fn zero() -> Self;
440    /// Square root.
441    fn sqrt(val: Self) -> Self;
442    /// Convert from `usize`.
443    fn from_usize(n: usize) -> Self;
444    /// Convert to `f64`.
445    fn to_f64(self) -> f64;
446    /// Convert from `f64`.
447    fn from_f64(val: f64) -> Self;
448    /// Maximum of two values.
449    fn max(self, other: Self) -> Self;
450}
451
452impl Float for f32 {
453    #[inline]
454    fn zero() -> Self { 0.0 }
455    #[inline]
456    fn sqrt(val: Self) -> Self { val.sqrt() }
457    #[inline]
458    fn from_usize(n: usize) -> Self { n as Self }
459    #[inline]
460    fn to_f64(self) -> f64 { self as f64 }
461    #[inline]
462    fn from_f64(val: f64) -> Self { val as Self }
463    #[inline]
464    fn max(self, other: Self) -> Self { if self >= other { self } else { other } }
465}
466
467impl Float for f64 {
468    #[inline]
469    fn zero() -> Self { 0.0 }
470    #[inline]
471    fn sqrt(val: Self) -> Self { val.sqrt() }
472    #[inline]
473    fn from_usize(n: usize) -> Self { n as Self }
474    #[inline]
475    fn to_f64(self) -> f64 { self }
476    #[inline]
477    fn from_f64(val: f64) -> Self { val }
478    #[inline]
479    fn max(self, other: Self) -> Self { if self >= other { self } else { other } }
480}
481
482// ──────────────────────────────────────────────────────────────────────────────
483//  Const Generics Helpers
484// ──────────────────────────────────────────────────────────────────────────────
485
486/// A fixed-size cycle on the Eisenstein lattice.
487///
488/// This is useful for compile-time-known cycle sizes.
489#[derive(Debug, Clone)]
490pub struct Cycle<const N: usize> {
491    /// Step indices (0..5 each)
492    pub steps: [usize; N],
493}
494
495impl<const N: usize> Cycle<N> {
496    /// Create a new cycle from step indices.
497    #[inline]
498    pub const fn new(steps: [usize; N]) -> Self {
499        Self { steps }
500    }
501
502    /// Walk this cycle with bounded drift and check the theorem.
503    #[inline]
504    pub fn walk<F: Float>(&self, epsilon: F) -> (F, F) {
505        let mut bd = BoundedDrift::new(epsilon);
506        for &idx in &self.steps {
507            bd.step(idx);
508        }
509        (bd.holonomy(), bd.bound())
510    }
511}
512
513// ──────────────────────────────────────────────────────────────────────────────
514//  Tests
515// ──────────────────────────────────────────────────────────────────────────────
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn test_eisenstein_norm() {
523        let p = Eisenstein::new(0, 0);
524        assert_eq!(p.norm(), 0.0);
525
526        let p = Eisenstein::new(1, 0);
527        assert!((p.norm() - 1.0).abs() < 1e-10);
528
529        let p = Eisenstein::new(0, 1);
530        assert!((p.norm() - 1.0).abs() < 1e-10);
531
532        let p = Eisenstein::new(1, 1);
533        // 1 + ω = (1 - 1/2, √3/2) = (0.5, √3/2), norm = 1
534        assert!((p.norm() - 1.0).abs() < 1e-10);
535    }
536
537    #[test]
538    fn test_snap_to_lattice_exact() {
539        // Points exactly on lattice should snap to themselves
540        for &(a, b) in &LATTICE_STEPS {
541            let (x, y) = Eisenstein::new(a as i64, b as i64).to_cartesian();
542            let (sa, sb, d) = snap_to_lattice(x, y);
543            assert_eq!(sa, a as i64);
544            assert_eq!(sb, b as i64);
545            assert!(d < 1e-10);
546        }
547    }
548
549    #[test]
550    fn test_snap_to_lattice_origin() {
551        let (sa, sb, d) = snap_to_lattice(0.0, 0.0);
552        assert_eq!(sa, 0);
553        assert_eq!(sb, 0);
554        assert!(d < 1e-10);
555    }
556
557    #[test]
558    fn test_snap_to_lattice_perturbed() {
559        // Pick a random point near the center of a Voronoi cell
560        // It should snap to the correct lattice point
561        let (x, y) = (0.3, 0.2);
562        let (_, _, d) = snap_to_lattice(x, y);
563        assert!(d < 0.57736); // within covering radius
564        // The nearest point to (0.3, 0.2) should be (0, 1) or (0, 0)
565        let dist_00 = ((x-0.0).powi(2) + (y-0.0).powi(2)).sqrt();
566        let dist_01 = ((x-0.5).powi(2) + (y-0.866).powi(2)).sqrt(); // approx (0,1)
567        let dist_other = d;
568        assert!(dist_other <= dist_00.min(dist_01) + 1e-6);
569    }
570
571    #[test]
572    fn test_bounded_drift_zero_epsilon() {
573        let mut bd = BoundedDrift::<f64>::new(0.0);
574        bd.step(3); // a -= 1
575        bd.step(0); // a += 1 => back to origin
576        assert_eq!(bd.holonomy(), 0.0);
577        assert!(bd.check_bound());
578    }
579
580    #[test]
581    fn test_bounded_drift_identity_cycle() {
582        // A cycle of 6 steps around a hexagon should close exactly
583        // with zero error (hexagon around origin)
584        let mut bd = BoundedDrift::<f64>::new(0.0);
585        let cycle = [0, 1, 2, 3, 4, 5];
586        for &idx in &cycle {
587            bd.step(idx);
588        }
589        // This hexagon won't close because the steps are all positive.
590        // The actual closed hexagon is: 0, 1, 2, 3, 4, 5 which forms
591        // a closed cycle on the lattice (sum of all 6 steps = (0,0)).
592        assert_eq!(bd.a, 0);
593        assert_eq!(bd.b, 0);
594        assert_eq!(bd.holonomy(), 0.0);
595    }
596
597    #[test]
598    fn test_bounded_drift_with_error_below_bound() {
599        let mut bd = BoundedDrift::<f64>::new(0.5);
600        for _ in 0..10 {
601            bd.step(0); // a += 1
602        }
603        bd.reset();
604
605        // Walk a cycle with errors
606        for _ in 0..100 {
607            // Cycle: forward + backward
608            bd.step(0); // a += 1
609            bd.step(3); // a -= 1
610        }
611
612        // Holonomy should be small (close to zero with errors cancelling)
613        let h = bd.holonomy();
614        let b = bd.bound();
615        assert!(h <= b, "holonomy {} > bound {}", h, b);
616    }
617
618    #[test]
619    fn test_walk_cycle_worst_case() {
620        let steps = vec![0, 1, 2, 3, 4, 5];
621        let eps = 1.0_f64;
622        let (hol, bound) = walk_cycle_worst_case(&steps, eps);
623        // With worst-case error ε at each step, the actual position after
624        // a closed cycle wanders due to accumulated errors.
625        // The bound is 6 * 1.0 = 6.0
626        assert!(hol <= bound, "holonomy {} > bound {}", hol, bound);
627    }
628
629    #[test]
630    fn test_cycle_const_generic() {
631        let cycle = Cycle::<3>::new([0, 2, 3]); // not necessarily closed
632        let (hol, bound) = cycle.walk(0.5_f64);
633        assert!(hol <= bound);
634    }
635
636    #[test]
637    fn test_snap_distance_within_voronoi() {
638        // Any point should snap to a lattice point within the covering radius
639        for _ in 0..1000 {
640            let x = (rand::random::<f64>() - 0.5) * 10.0;
641            let y = (rand::random::<f64>() - 0.5) * 10.0;
642            let (_, _, d) = snap_to_lattice(x, y);
643            assert!(d <= VORONOI_CIRCUMRADIUS + 1e-10,
644                "snap distance {} > covering radius {}", d, VORONOI_CIRCUMRADIUS);
645        }
646    }
647
648    #[test]
649    fn test_random_walk_bound() {
650        // Quick empirical check: random walks should satisfy bound
651        use rand::Rng;
652        let mut rng = rand::thread_rng();
653
654        for n in [10, 50, 100] {
655            for eps in [0.5, 1.0, 2.0] {
656                for _ in 0..100 {
657                    // Generate random closed cycle
658                    let mut a = 0i64; let mut b = 0i64;
659                    let mut steps = Vec::with_capacity(n);
660                    for _ in 0..n.saturating_sub(2) {
661                        let idx = rng.gen_range(0..6);
662                        steps.push(idx);
663                        let (da, db) = LATTICE_STEPS[idx];
664                        a += da as i64; b += db as i64;
665                    }
666                    // Close with 2 steps
667                    for i in 0..6 {
668                        for j in 0..6 {
669                            let (d1, e1) = LATTICE_STEPS[i];
670                            let (d2, e2) = LATTICE_STEPS[j];
671                            if a + d1 as i64 + d2 as i64 == 0 && b + e1 as i64 + e2 as i64 == 0 {
672                                steps.push(i);
673                                steps.push(j);
674                                break;
675                            }
676                        }
677                        if steps.len() == n { break; }
678                    }
679
680                    if steps.len() == n {
681                        let mut bd = BoundedDrift::new(eps);
682                        for &idx in &steps {
683                            bd.step(idx);
684                        }
685                        assert!(bd.check_bound(),
686                            "n={} eps={}: hol={} > bound={}", n, eps, bd.holonomy(), bd.bound());
687                    }
688                }
689            }
690        }
691    }
692}