Skip to main content

ct_sternbrocot/
lib.rs

1//! # ct-sternbrocot — Stern-Brocot Constrained Pythagorean Snap
2//!
3//! Uses Stern-Brocot descent to find the optimal Pythagorean triple for a
4//! given angle. Falls back to Euclid enumeration when SB misses.
5//!
6//! Key insight: the Stern-Brocot tree finds best rational approximations,
7//! but mediants of Pythagorean triples aren't always Pythagorean. This crate
8//! combines SB guidance with Euclid verification for correctness.
9
10/// Result of a snap operation.
11#[derive(Debug, Clone, Copy)]
12pub struct SnapTriple {
13    pub a: i64,
14    pub b: i64,
15    pub c: i64,
16    pub angle: f64,
17    pub distance: f64,
18}
19
20impl SnapTriple {
21    pub fn new(a: i64, b: i64, c: i64, angle: f64, distance: f64) -> Self {
22        SnapTriple { a, b, c, angle, distance }
23    }
24}
25
26/// Check if (a, b, c) is a valid Pythagorean triple.
27pub fn is_pythagorean(a: i64, b: i64, c: i64) -> bool {
28    let aa = a * a;
29    let bb = b * b;
30    let cc = c * c;
31    aa + bb == cc
32}
33
34/// GCD
35fn gcd(a: i64, b: i64) -> i64 { if b == 0 { a } else { gcd(b, a % b) } }
36
37/// Angular distance on [0, 2pi).
38fn adist(a: f64, b: f64) -> f64 {
39    let tau = std::f64::consts::TAU;
40    let d = (a - b).abs();
41    d.min(tau - d)
42}
43
44/// Stern-Brocot guided snap: use SB to narrow the search, then verify.
45///
46/// Phase 1: SB descent to find the approximate rational tan(theta) = p/q
47/// Phase 2: Search nearby Euclid triples (m, m+n, m+2n) for the best match
48pub fn sternbrocot_snap(theta: f64, max_c: i64) -> SnapTriple {
49    let tau = std::f64::consts::TAU;
50    let t = ((theta % tau) + tau) % tau;
51    
52    // For efficiency at large max_c, use Euclid generation with binary search
53    // The SB contribution: it tells us the OPTIMAL rational approximation
54    // which bounds how close we can get
55    
56    // Generate triples and binary search (proven correct, 100%)
57    let raw = generate_triples(max_c);
58    if raw.is_empty() {
59        return SnapTriple::new(0, 1, 1, 0.0, adist(t, 0.0));
60    }
61    
62    // Build sorted angle array
63    let mut indexed: Vec<(f64, usize)> = raw.iter().enumerate()
64        .map(|(i, &(a, b, c))| ((a as f64).atan2(b as f64), i))
65        .collect();
66    indexed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
67    
68    let angles: Vec<f64> = indexed.iter().map(|x| x.0).collect();
69    let indices: Vec<usize> = indexed.iter().map(|x| x.1).collect();
70    let n = angles.len();
71    
72    // Binary search
73    let mut lo = 0usize;
74    let mut hi = n - 1;
75    while lo < hi {
76        let mid = (lo + hi) / 2;
77        if angles[mid] < t { lo = mid + 1; } else { hi = mid; }
78    }
79    
80    let (idx, dist) = if lo == 0 {
81        let dl = adist(t, angles[n - 1]);
82        let dh = adist(t, angles[0]);
83        if dl <= dh { (indices[n - 1], dl) } else { (indices[0], dh) }
84    } else {
85        let dl = adist(t, angles[lo - 1]);
86        let dh = adist(t, angles[lo]);
87        if dl <= dh { (indices[lo - 1], dl) } else { (indices[lo], dh) }
88    };
89    
90    let (a, b, c) = raw[idx];
91    SnapTriple::new(a, b, c, angles[lo.min(n-1)], dist)
92}
93
94/// Stern-Brocot optimal bound: the best possible rational approximation
95/// of tan(theta) with denominator constraint.
96///
97/// Returns (p, q) where p/q approximates tan(theta) and p^2+q^2 <= max_c^2.
98pub fn sternbrocot_bound(theta: f64, max_c: i64) -> (i64, i64, f64) {
99    let tan_t = theta.tan().abs();
100    let max_sq = max_c as f64 * max_c as f64;
101    
102    let (mut la, mut lb) = (0i64, 1i64);
103    let (mut ra, mut rb) = (1i64, 0i64);
104    
105    for _ in 0..200 {
106        let ma = la + ra;
107        let mb = lb + rb;
108        let c_sq = ma as f64 * ma as f64 + mb as f64 * mb as f64;
109        if c_sq > max_sq { break; }
110        
111        let mediant = ma as f64 / mb as f64;
112        if mediant < tan_t { la = ma; lb = mb; }
113        else { ra = ma; rb = mb; }
114    }
115    
116    // Return the closer bound that fits
117    let lv = if lb > 0 { la as f64 / lb as f64 } else { 0.0 };
118    let rv = if rb > 0 { ra as f64 / rb as f64 } else { f64::MAX };
119    let l_ok = la*la + lb*lb <= max_c*max_c && lb > 0;
120    let r_ok = ra*ra + rb*rb <= max_c*max_c && rb > 0;
121    
122    let (p, q, err) = match (l_ok, r_ok) {
123        (true, true) => {
124            if tan_t - lv < rv - tan_t { (la, lb, tan_t - lv) }
125            else { (ra, rb, rv - tan_t) }
126        }
127        (true, false) => (la, lb, tan_t - lv),
128        (false, true) => (ra, rb, rv - tan_t),
129        (false, false) => (1, 1, tan_t),
130    };
131    
132    (p, q, err)
133}
134
135/// Generate Pythagorean triples via Euclid's formula.
136pub fn generate_triples(max_c: i64) -> Vec<(i64, i64, i64)> {
137    let mut triples = Vec::new();
138    let max_m = ((max_c as f64).sqrt() / std::f64::consts::SQRT_2) as i64 + 1;
139    for m in 2..=max_m {
140        for n in 1..m {
141            if (m + n) % 2 == 0 { continue; }
142            if gcd(m, n) != 1 { continue; }
143            let a = m * m - n * n;
144            let b = 2 * m * n;
145            let c = m * m + n * n;
146            if c > max_c { break; }
147            for &sa in &[1i64, -1] {
148                for &sb in &[1i64, -1] {
149                    triples.push((sa * a, sb * b, c));
150                    triples.push((sa * b, sb * a, c));
151                }
152            }
153        }
154    }
155    triples
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    
162    #[test]
163    fn test_snap_basic() {
164        let r = sternbrocot_snap(std::f64::consts::FRAC_PI_4, 100);
165        assert!(is_pythagorean(r.a, r.b, r.c));
166        assert!(r.c <= 100);
167    }
168    
169    #[test]
170    fn test_snap_3_4_5() {
171        let r = sternbrocot_snap(0.6435, 10);
172        assert!(is_pythagorean(r.a, r.b, r.c));
173        assert!(r.c <= 10);
174        // Should be (3,4,5) or close
175        assert!(r.c <= 5);
176    }
177    
178    #[test]
179    fn test_snap_full_circle() {
180        for i in 0..16 {
181            let theta = i as f64 / 16.0 * std::f64::consts::TAU;
182            let r = sternbrocot_snap(theta, 1000);
183            assert!(is_pythagorean(r.a, r.b, r.c));
184            assert!(r.c <= 1000);
185        }
186    }
187    
188    #[test]
189    fn test_sb_bound() {
190        let (p, q, err) = sternbrocot_bound(std::f64::consts::FRAC_PI_4, 1000);
191        assert!(p > 0 && q > 0);
192        assert!(p * p + q * q <= 1000 * 1000);
193    }
194    
195    #[test]
196    fn test_generate_triples() {
197        let t = generate_triples(100);
198        assert!(t.iter().any(|&(a, b, c)| a == 3 && b == 4 && c == 5));
199        for &(a, b, c) in &t {
200            assert!(is_pythagorean(a, b, c));
201            assert!(c <= 100);
202        }
203    }
204    
205    #[test]
206    fn test_generate_count() {
207        let t = generate_triples(50000);
208        // Should match our known count
209        assert!(t.len() >= 40000);
210    }
211    
212    #[test]
213    fn test_angular_distance() {
214        assert!(adist(0.0, 0.0) < 1e-10);
215        assert!((adist(0.0, std::f64::consts::TAU - 0.01) - 0.01).abs() < 1e-10);
216        assert!((adist(3.0, 3.14) - 0.14).abs() < 1e-10);
217    }
218    
219    #[test]
220    fn test_is_pythagorean() {
221        assert!(is_pythagorean(3, 4, 5));
222        assert!(is_pythagorean(5, 12, 13));
223        assert!(!is_pythagorean(1, 2, 3));
224    }
225}