1#[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
26pub 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
34fn gcd(a: i64, b: i64) -> i64 { if b == 0 { a } else { gcd(b, a % b) } }
36
37fn 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
44pub 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 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 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 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
94pub 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 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
135pub 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 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 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}