Skip to main content

proof_engine/number_theory/
continued_fractions.rs

1//! Continued fractions: representation, convergents, and visualization.
2
3use glam::{Vec2, Vec3, Vec4};
4
5/// A continued fraction [a0; a1, a2, ...] representing a0 + 1/(a1 + 1/(a2 + ...)).
6#[derive(Debug, Clone, PartialEq)]
7pub struct ContinuedFraction {
8    pub integer_part: i64,
9    pub coefficients: Vec<u64>,
10}
11
12impl ContinuedFraction {
13    /// Create from explicit parts.
14    pub fn new(integer_part: i64, coefficients: Vec<u64>) -> Self {
15        Self { integer_part, coefficients }
16    }
17
18    /// Build a continued fraction from a rational number p/q.
19    pub fn from_rational(mut num: i64, mut den: i64) -> Self {
20        if den == 0 {
21            return Self { integer_part: 0, coefficients: vec![] };
22        }
23        if den < 0 {
24            num = -num;
25            den = -den;
26        }
27
28        // Handle negative: use floor division
29        let integer_part = num.div_euclid(den);
30        let mut remainder = num.rem_euclid(den);
31        let mut coefficients = Vec::new();
32        let mut d = den;
33
34        while remainder != 0 {
35            let new_d = remainder;
36            let q = d / remainder;
37            remainder = d % remainder;
38            d = new_d;
39            coefficients.push(q as u64);
40        }
41
42        Self { integer_part, coefficients }
43    }
44
45    /// Build a continued fraction from a floating point number, computing up to max_terms.
46    pub fn from_f64(x: f64, max_terms: usize) -> Self {
47        let integer_part = x.floor() as i64;
48        let mut frac = x - integer_part as f64;
49        let mut coefficients = Vec::new();
50
51        for _ in 0..max_terms {
52            if frac.abs() < 1e-12 {
53                break;
54            }
55            let recip = 1.0 / frac;
56            let a = recip.floor() as u64;
57            if a == 0 {
58                break;
59            }
60            coefficients.push(a);
61            frac = recip - a as f64;
62        }
63
64        Self { integer_part, coefficients }
65    }
66
67    /// Compute convergents (rational approximations) as (numerator, denominator) pairs.
68    /// Returns all convergents from [a0] to [a0; a1, ..., an].
69    pub fn convergents(&self) -> Vec<(i64, i64)> {
70        let mut result = Vec::new();
71        let mut h_prev: i64 = 1;
72        let mut k_prev: i64 = 0;
73        let mut h_curr: i64 = self.integer_part;
74        let mut k_curr: i64 = 1;
75        result.push((h_curr, k_curr));
76
77        for &a in &self.coefficients {
78            let a = a as i64;
79            let h_next = a * h_curr + h_prev;
80            let k_next = a * k_curr + k_prev;
81            h_prev = h_curr;
82            k_prev = k_curr;
83            h_curr = h_next;
84            k_curr = k_next;
85            result.push((h_curr, k_curr));
86        }
87
88        result
89    }
90
91    /// Evaluate the continued fraction as an f64.
92    pub fn to_f64(&self) -> f64 {
93        let mut value = 0.0f64;
94        for &a in self.coefficients.iter().rev() {
95            value = 1.0 / (a as f64 + value);
96        }
97        self.integer_part as f64 + value
98    }
99
100    /// Number of terms (not counting integer part).
101    pub fn len(&self) -> usize {
102        self.coefficients.len()
103    }
104
105    pub fn is_empty(&self) -> bool {
106        self.coefficients.is_empty()
107    }
108}
109
110/// Compute the periodic continued fraction for sqrt(n), where n is not a perfect square.
111/// Returns a ContinuedFraction where coefficients form the repeating period.
112pub fn sqrt_cf(n: u64) -> ContinuedFraction {
113    let sqrt_n = (n as f64).sqrt();
114    let a0 = sqrt_n.floor() as u64;
115    if a0 * a0 == n {
116        // Perfect square
117        return ContinuedFraction {
118            integer_part: a0 as i64,
119            coefficients: vec![],
120        };
121    }
122
123    let mut coefficients = Vec::new();
124    let mut m: i64 = 0;
125    let mut d: i64 = 1;
126    let mut a = a0 as i64;
127
128    loop {
129        m = d * a - m;
130        d = (n as i64 - m * m) / d;
131        if d == 0 {
132            break;
133        }
134        a = (a0 as i64 + m) / d;
135        coefficients.push(a as u64);
136        // Period ends when a == 2 * a0
137        if a == 2 * a0 as i64 {
138            break;
139        }
140    }
141
142    ContinuedFraction {
143        integer_part: a0 as i64,
144        coefficients,
145    }
146}
147
148/// Render convergents as points on a number line converging to a target value.
149pub struct ConvergentVisualizer {
150    pub origin: Vec3,
151    pub scale: f32,
152    pub target: f64,
153}
154
155pub struct ConvergentGlyph {
156    pub index: usize,
157    pub value: f64,
158    pub numerator: i64,
159    pub denominator: i64,
160    pub position: Vec3,
161    pub color: Vec4,
162    pub character: char,
163}
164
165impl ConvergentVisualizer {
166    pub fn new(origin: Vec3, scale: f32, target: f64) -> Self {
167        Self { origin, scale, target }
168    }
169
170    /// Render the convergents of a continued fraction on a number line.
171    pub fn render(&self, cf: &ContinuedFraction) -> Vec<ConvergentGlyph> {
172        let convergents = cf.convergents();
173        convergents
174            .iter()
175            .enumerate()
176            .map(|(i, &(h, k))| {
177                let value = h as f64 / k as f64;
178                let error = (value - self.target).abs();
179                let x_offset = (value - self.target) as f32 * self.scale;
180                let y_offset = i as f32 * 0.5; // stack vertically
181                let err_color = (1.0 - (error as f32 * 10.0).min(1.0)).max(0.0);
182                ConvergentGlyph {
183                    index: i,
184                    value,
185                    numerator: h,
186                    denominator: k,
187                    position: self.origin + Vec3::new(x_offset, y_offset, 0.0),
188                    color: Vec4::new(1.0 - err_color, err_color, 0.3, 1.0),
189                    character: if i % 2 == 0 { '+' } else { '-' },
190                }
191            })
192            .collect()
193    }
194}
195
196// ─── Tests ──────────────────────────────────────────────────────────────────
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn rational_cf() {
204        // 355/113 = [3; 7, 16, 11] -- wait, 355/113 = [3; 7, 16]
205        // Actually: 355 = 3*113 + 16, 113 = 7*16 + 1, 16 = 16*1
206        // So [3; 7, 16]
207        let cf = ContinuedFraction::from_rational(355, 113);
208        assert_eq!(cf.integer_part, 3);
209        assert_eq!(cf.coefficients, vec![7, 16]);
210    }
211
212    #[test]
213    fn rational_roundtrip() {
214        let cf = ContinuedFraction::from_rational(22, 7);
215        assert_eq!(cf.integer_part, 3);
216        assert_eq!(cf.coefficients, vec![7]);
217        let val = cf.to_f64();
218        assert!((val - 22.0 / 7.0).abs() < 1e-12);
219    }
220
221    #[test]
222    fn pi_cf() {
223        let cf = ContinuedFraction::from_f64(std::f64::consts::PI, 10);
224        assert_eq!(cf.integer_part, 3);
225        // pi = [3; 7, 15, 1, 292, ...]
226        assert_eq!(cf.coefficients[0], 7);
227        assert_eq!(cf.coefficients[1], 15);
228        assert_eq!(cf.coefficients[2], 1);
229        assert_eq!(cf.coefficients[3], 292);
230    }
231
232    #[test]
233    fn convergents_of_pi() {
234        let cf = ContinuedFraction::from_f64(std::f64::consts::PI, 5);
235        let conv = cf.convergents();
236        // First convergent: 3/1
237        assert_eq!(conv[0], (3, 1));
238        // Second: 22/7
239        assert_eq!(conv[1], (22, 7));
240        // Third: 333/106
241        assert_eq!(conv[2], (333, 106));
242        // Fourth: 355/113
243        assert_eq!(conv[3], (355, 113));
244    }
245
246    #[test]
247    fn to_f64_accuracy() {
248        let cf = ContinuedFraction::from_f64(std::f64::consts::E, 15);
249        let val = cf.to_f64();
250        assert!((val - std::f64::consts::E).abs() < 1e-10);
251    }
252
253    #[test]
254    fn sqrt_2_cf() {
255        let cf = sqrt_cf(2);
256        assert_eq!(cf.integer_part, 1);
257        // sqrt(2) = [1; 2, 2, 2, ...] period = [2]
258        assert_eq!(cf.coefficients[0], 2);
259        // Last element should be 2*a0 = 2
260        assert_eq!(*cf.coefficients.last().unwrap(), 2);
261    }
262
263    #[test]
264    fn sqrt_3_cf() {
265        let cf = sqrt_cf(3);
266        assert_eq!(cf.integer_part, 1);
267        // sqrt(3) = [1; 1, 2] period = [1, 2]
268        assert_eq!(cf.coefficients, vec![1, 2]);
269    }
270
271    #[test]
272    fn sqrt_7_cf() {
273        let cf = sqrt_cf(7);
274        assert_eq!(cf.integer_part, 2);
275        // sqrt(7) = [2; 1, 1, 1, 4]
276        assert_eq!(cf.coefficients, vec![1, 1, 1, 4]);
277    }
278
279    #[test]
280    fn perfect_square_cf() {
281        let cf = sqrt_cf(9);
282        assert_eq!(cf.integer_part, 3);
283        assert!(cf.coefficients.is_empty());
284    }
285
286    #[test]
287    fn convergent_visualizer() {
288        let cf = ContinuedFraction::from_f64(std::f64::consts::PI, 5);
289        let vis = ConvergentVisualizer::new(Vec3::ZERO, 100.0, std::f64::consts::PI);
290        let glyphs = vis.render(&cf);
291        assert!(!glyphs.is_empty());
292        // Later convergents should be closer to the target
293        let last = glyphs.last().unwrap();
294        assert!((last.value - std::f64::consts::PI).abs() < 1e-6);
295    }
296
297    #[test]
298    fn negative_rational() {
299        let cf = ContinuedFraction::from_rational(-7, 3);
300        // -7/3 = -3 + 2/3, so [floor(-7/3)=-3; ...] = [-3; 3] since 1/(3/2) needs...
301        // -7 = -3*3 + 2, so integer_part = -3, remainder = 2
302        // 3 = 1*2 + 1, 2 = 2*1 => coefficients = [1, 2]
303        assert_eq!(cf.integer_part, -3);
304        let val = cf.to_f64();
305        assert!((val - (-7.0 / 3.0)).abs() < 1e-12);
306    }
307
308    #[test]
309    fn from_rational_zero() {
310        let cf = ContinuedFraction::from_rational(0, 1);
311        assert_eq!(cf.integer_part, 0);
312        assert!(cf.coefficients.is_empty());
313    }
314}