Skip to main content

proof_engine/number_theory/
zeta.rs

1//! Riemann zeta function, critical strip rendering, and zero detection.
2
3use glam::{Vec2, Vec3, Vec4};
4use std::f64::consts::PI;
5
6// ─── Complex number type ────────────────────────────────────────────────────
7
8/// Minimal complex number for zeta computations.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Complex {
11    pub re: f64,
12    pub im: f64,
13}
14
15impl Complex {
16    pub const ZERO: Complex = Complex { re: 0.0, im: 0.0 };
17    pub const ONE: Complex = Complex { re: 1.0, im: 0.0 };
18
19    pub fn new(re: f64, im: f64) -> Self {
20        Self { re, im }
21    }
22
23    pub fn norm_sq(self) -> f64 {
24        self.re * self.re + self.im * self.im
25    }
26
27    pub fn norm(self) -> f64 {
28        self.norm_sq().sqrt()
29    }
30
31    pub fn arg(self) -> f64 {
32        self.im.atan2(self.re)
33    }
34
35    pub fn conj(self) -> Self {
36        Self { re: self.re, im: -self.im }
37    }
38
39    pub fn exp(self) -> Self {
40        let r = self.re.exp();
41        Self {
42            re: r * self.im.cos(),
43            im: r * self.im.sin(),
44        }
45    }
46
47    pub fn ln(self) -> Self {
48        Self {
49            re: self.norm().ln(),
50            im: self.arg(),
51        }
52    }
53
54    /// self^p via exp(p * ln(self))
55    pub fn powc(self, p: Complex) -> Self {
56        if self.norm_sq() == 0.0 {
57            return Complex::ZERO;
58        }
59        (p * self.ln()).exp()
60    }
61
62    /// self^r for real exponent
63    pub fn powr(self, r: f64) -> Self {
64        self.powc(Complex::new(r, 0.0))
65    }
66
67    pub fn sin(self) -> Self {
68        Self {
69            re: self.re.sin() * self.im.cosh(),
70            im: self.re.cos() * self.im.sinh(),
71        }
72    }
73
74    pub fn cos(self) -> Self {
75        Self {
76            re: self.re.cos() * self.im.cosh(),
77            im: -self.re.sin() * self.im.sinh(),
78        }
79    }
80}
81
82impl std::ops::Add for Complex {
83    type Output = Self;
84    fn add(self, rhs: Self) -> Self {
85        Self { re: self.re + rhs.re, im: self.im + rhs.im }
86    }
87}
88
89impl std::ops::Sub for Complex {
90    type Output = Self;
91    fn sub(self, rhs: Self) -> Self {
92        Self { re: self.re - rhs.re, im: self.im - rhs.im }
93    }
94}
95
96impl std::ops::Mul for Complex {
97    type Output = Self;
98    fn mul(self, rhs: Self) -> Self {
99        Self {
100            re: self.re * rhs.re - self.im * rhs.im,
101            im: self.re * rhs.im + self.im * rhs.re,
102        }
103    }
104}
105
106impl std::ops::Div for Complex {
107    type Output = Self;
108    fn div(self, rhs: Self) -> Self {
109        let d = rhs.norm_sq();
110        Self {
111            re: (self.re * rhs.re + self.im * rhs.im) / d,
112            im: (self.im * rhs.re - self.re * rhs.im) / d,
113        }
114    }
115}
116
117impl std::ops::Mul<f64> for Complex {
118    type Output = Self;
119    fn mul(self, rhs: f64) -> Self {
120        Self { re: self.re * rhs, im: self.im * rhs }
121    }
122}
123
124impl std::ops::Mul<Complex> for f64 {
125    type Output = Complex;
126    fn mul(self, rhs: Complex) -> Complex {
127        Complex { re: self * rhs.re, im: self * rhs.im }
128    }
129}
130
131impl std::ops::AddAssign for Complex {
132    fn add_assign(&mut self, rhs: Self) {
133        self.re += rhs.re;
134        self.im += rhs.im;
135    }
136}
137
138impl std::ops::Neg for Complex {
139    type Output = Self;
140    fn neg(self) -> Self {
141        Self { re: -self.re, im: -self.im }
142    }
143}
144
145// ─── Gamma function (Lanczos approximation) ─────────────────────────────────
146
147fn gamma(z: Complex) -> Complex {
148    // Reflection for Re(z) < 0.5
149    if z.re < 0.5 {
150        let pi_z = Complex::new(PI, 0.0) * z;
151        return Complex::new(PI, 0.0) / (pi_z.sin() * gamma(Complex::new(1.0, 0.0) - z));
152    }
153
154    let z = z - Complex::ONE;
155    let g = 7.0;
156    let coefs: [f64; 9] = [
157        0.99999999999980993,
158        676.5203681218851,
159        -1259.1392167224028,
160        771.32342877765313,
161        -176.61502916214059,
162        12.507343278686905,
163        -0.13857109526572012,
164        9.9843695780195716e-6,
165        1.5056327351493116e-7,
166    ];
167
168    let mut x = Complex::new(coefs[0], 0.0);
169    for (i, &c) in coefs.iter().enumerate().skip(1) {
170        x += Complex::new(c, 0.0) / (z + Complex::new(i as f64, 0.0));
171    }
172
173    let t = z + Complex::new(g + 0.5, 0.0);
174    let sqrt_2pi = Complex::new((2.0 * PI).sqrt(), 0.0);
175    sqrt_2pi * t.powc(z + Complex::new(0.5, 0.0)) * (-t).exp() * x
176}
177
178// ─── Zeta function ──────────────────────────────────────────────────────────
179
180/// Riemann zeta function via the Borwein method (Dirichlet eta + analytic continuation).
181///
182/// Uses the globally convergent series based on Chebyshev-like coefficients
183/// (Borwein 1995). Accurate to ~15 digits for Re(s) > -10.
184pub fn zeta(s: Complex) -> Complex {
185    // Pole at s = 1
186    if (s.re - 1.0).abs() < 1e-14 && s.im.abs() < 1e-14 {
187        return Complex::new(f64::INFINITY, 0.0);
188    }
189
190    // Functional equation for Re(s) < 0: zeta(s) = 2^s * pi^(s-1) * sin(pi*s/2) * Gamma(1-s) * zeta(1-s)
191    if s.re < 0.0 {
192        let one_minus_s = Complex::new(1.0, 0.0) - s;
193        let two_s = Complex::new(2.0, 0.0).powc(s);
194        let pi_s_minus_1 = Complex::new(PI, 0.0).powc(s - Complex::ONE);
195        let sin_term = (Complex::new(PI, 0.0) * s * 0.5).sin();
196        let gam = gamma(one_minus_s);
197        let zeta_1ms = zeta(one_minus_s);
198        return two_s * pi_s_minus_1 * sin_term * gam * zeta_1ms;
199    }
200
201    // Borwein method: use eta function with n terms
202    let n = 30usize;
203    // Precompute d_k coefficients
204    let mut d = vec![0.0f64; n + 1];
205    d[0] = 1.0;
206    // d_k = sum_{j=0}^{k} n! / (j! * (n-j)!) -- actually d_k = n * sum ...
207    // Simpler: use the partial sums of binomial coefficients
208    {
209        let mut binom = 1.0f64;
210        for k in 0..=n {
211            d[k] = binom;
212            if k < n {
213                binom *= (n - k) as f64 / (k + 1) as f64;
214            }
215        }
216        // d_k = partial sums: d[k] = sum_{j=0}^{k} C(n, j)
217        // Actually we want d_k = n * sum_{j=0..k} (-1)^j C(n,j) ... let's use the
218        // standard globally convergent approach instead.
219    }
220
221    // Standard approach: zeta(s) = (1 / (1 - 2^{1-s})) * eta(s)
222    // eta(s) = sum_{k=1}^{inf} (-1)^{k-1} / k^s
223    // Accelerated via Euler-Maclaurin / Cohen-Villegas-Zagier acceleration
224
225    // Cohen-Villegas-Zagier: pick N terms
226    let nn = 40usize;
227    let mut sum = Complex::ZERO;
228    let mut c = Complex::ZERO; // Kahan compensation
229    // d_k weights
230    let mut dk = 0.0f64;
231    let dn = {
232        let mut v = 0.0f64;
233        let mut binom = 1.0f64;
234        for j in 0..=nn {
235            v += binom;
236            if j < nn {
237                binom *= (nn - j) as f64 / (j + 1) as f64;
238            }
239        }
240        v
241    };
242
243    let mut binom = 1.0f64;
244    dk = 0.0;
245    for k in 0..=nn {
246        dk += binom;
247        let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
248        let term_weight = sign * (dk - dn);
249        let k1 = Complex::new((k + 1) as f64, 0.0);
250        let term = Complex::new(term_weight, 0.0) / k1.powc(s);
251        // Kahan summation
252        let y = term - c;
253        let t = sum + y;
254        c = (t - sum) - y;
255        sum = t;
256        if k < nn {
257            binom *= (nn - k) as f64 / (k + 1) as f64;
258        }
259    }
260
261    let eta = sum * Complex::new(-1.0 / dn, 0.0);
262
263    // zeta(s) = eta(s) / (1 - 2^{1-s})
264    let two_1ms = Complex::new(2.0, 0.0).powc(Complex::new(1.0, 0.0) - s);
265    let denom = Complex::ONE - two_1ms;
266
267    if denom.norm_sq() < 1e-30 {
268        // s is near a zero of the denominator; fall back to direct Dirichlet series
269        let mut direct = Complex::ZERO;
270        for k in 1..=200 {
271            direct += Complex::new(1.0, 0.0) / Complex::new(k as f64, 0.0).powc(s);
272        }
273        return direct;
274    }
275
276    eta / denom
277}
278
279/// Zeta on the critical line: s = 0.5 + it.
280pub fn zeta_on_critical_line(t: f64) -> Complex {
281    zeta(Complex::new(0.5, t))
282}
283
284/// Hardy's Z-function: real-valued function whose zeros on the real line
285/// correspond to zeros of zeta on the critical line.
286/// Z(t) = e^{i*theta(t)} * zeta(1/2 + it)
287/// where theta(t) is the Riemann-Siegel theta function.
288pub fn z_function(t: f64) -> f64 {
289    let theta = riemann_siegel_theta(t);
290    let z = zeta_on_critical_line(t);
291    // Z(t) = exp(i*theta) * zeta(1/2+it)  — the result should be real
292    let rot = Complex::new(theta.cos(), theta.sin());
293    let result = rot * z;
294    result.re
295}
296
297/// Riemann-Siegel theta function.
298fn riemann_siegel_theta(t: f64) -> f64 {
299    // theta(t) = arg(Gamma(1/4 + it/2)) - t/2 * ln(pi)
300    // Stirling approximation:
301    // theta(t) ≈ t/2 * ln(t/(2*pi*e)) - pi/8 + 1/(48*t) + ...
302    let term1 = (t / 2.0) * ((t / (2.0 * PI)).ln()) - t / 2.0 - PI / 8.0;
303    let term2 = 1.0 / (48.0 * t);
304    let term3 = 7.0 / (5760.0 * t * t * t);
305    term1 + term2 + term3
306}
307
308/// Find approximate locations of non-trivial zeros of zeta on the critical line
309/// in the interval [t_min, t_max] by detecting sign changes of Z(t).
310pub fn find_zeros(t_min: f64, t_max: f64, resolution: usize) -> Vec<f64> {
311    let mut zeros = Vec::new();
312    let step = (t_max - t_min) / resolution as f64;
313    let mut prev = z_function(t_min);
314    let mut t = t_min + step;
315    for _ in 1..=resolution {
316        let current = z_function(t);
317        if prev * current < 0.0 {
318            // Bisect to refine
319            let zero = bisect_zero(t - step, t, 50);
320            zeros.push(zero);
321        }
322        prev = current;
323        t += step;
324    }
325    zeros
326}
327
328fn bisect_zero(mut a: f64, mut b: f64, iterations: usize) -> f64 {
329    for _ in 0..iterations {
330        let mid = (a + b) / 2.0;
331        let fa = z_function(a);
332        let fm = z_function(mid);
333        if fa * fm <= 0.0 {
334            b = mid;
335        } else {
336            a = mid;
337        }
338    }
339    (a + b) / 2.0
340}
341
342// ─── Renderers ──────────────────────────────────────────────────────────────
343
344/// Renders the critical strip 0 < Re(s) < 1 as a colored grid.
345pub struct CriticalStripRenderer {
346    pub re_range: (f64, f64),
347    pub im_range: (f64, f64),
348    pub resolution: (usize, usize),
349}
350
351/// A single colored cell in the critical strip visualization.
352pub struct StripCell {
353    pub position: Vec2,
354    pub color: Vec4,
355    pub zeta_value: Complex,
356}
357
358impl CriticalStripRenderer {
359    pub fn new(
360        re_range: (f64, f64),
361        im_range: (f64, f64),
362        resolution: (usize, usize),
363    ) -> Self {
364        Self { re_range, im_range, resolution }
365    }
366
367    /// Default critical strip: Re in [0, 1], Im in [-30, 30].
368    pub fn default_strip() -> Self {
369        Self::new((0.0, 1.0), (-30.0, 30.0), (50, 300))
370    }
371
372    /// Generate the grid of colored cells.
373    pub fn render(&self) -> Vec<StripCell> {
374        let (re_min, re_max) = self.re_range;
375        let (im_min, im_max) = self.im_range;
376        let (nx, ny) = self.resolution;
377        let re_step = (re_max - re_min) / nx as f64;
378        let im_step = (im_max - im_min) / ny as f64;
379
380        let mut cells = Vec::with_capacity(nx * ny);
381        for i in 0..nx {
382            for j in 0..ny {
383                let re = re_min + (i as f64 + 0.5) * re_step;
384                let im = im_min + (j as f64 + 0.5) * im_step;
385                let s = Complex::new(re, im);
386                let z = zeta(s);
387                let mag = z.norm().ln().max(-5.0).min(5.0);
388                let phase = z.arg();
389
390                // Map magnitude to brightness, phase to hue
391                let brightness = ((mag + 5.0) / 10.0) as f32;
392                let hue = ((phase + PI) / (2.0 * PI)) as f32;
393                let color = hsv_to_rgba(hue, 0.8, brightness);
394
395                cells.push(StripCell {
396                    position: Vec2::new(i as f32, j as f32),
397                    color,
398                    zeta_value: z,
399                });
400            }
401        }
402        cells
403    }
404}
405
406/// Marks non-trivial zeros with special glyphs.
407pub struct ZeroMarker {
408    pub origin: Vec3,
409    pub scale: f32,
410}
411
412/// A glyph at a zero location.
413pub struct ZeroGlyph {
414    pub t: f64,
415    pub position: Vec3,
416    pub character: char,
417    pub color: Vec4,
418}
419
420impl ZeroMarker {
421    pub fn new(origin: Vec3, scale: f32) -> Self {
422        Self { origin, scale }
423    }
424
425    /// Find zeros and produce glyphs.
426    pub fn mark_zeros(&self, t_min: f64, t_max: f64, resolution: usize) -> Vec<ZeroGlyph> {
427        let zeros = find_zeros(t_min, t_max, resolution);
428        zeros
429            .into_iter()
430            .enumerate()
431            .map(|(i, t)| {
432                let y = (t - t_min) / (t_max - t_min);
433                ZeroGlyph {
434                    t,
435                    position: self.origin
436                        + Vec3::new(0.5 * self.scale, y as f32 * self.scale, 0.0),
437                    character: '\u{2742}', // ❂
438                    color: Vec4::new(1.0, 0.2, 0.2, 1.0),
439                }
440            })
441            .collect()
442    }
443}
444
445fn hsv_to_rgba(h: f32, s: f32, v: f32) -> Vec4 {
446    let h = h * 6.0;
447    let c = v * s;
448    let x = c * (1.0 - (h % 2.0 - 1.0).abs());
449    let m = v - c;
450    let (r, g, b) = if h < 1.0 {
451        (c, x, 0.0)
452    } else if h < 2.0 {
453        (x, c, 0.0)
454    } else if h < 3.0 {
455        (0.0, c, x)
456    } else if h < 4.0 {
457        (0.0, x, c)
458    } else if h < 5.0 {
459        (x, 0.0, c)
460    } else {
461        (c, 0.0, x)
462    };
463    Vec4::new(r + m, g + m, b + m, 1.0)
464}
465
466// ─── Tests ──────────────────────────────────────────────────────────────────
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    fn approx(a: f64, b: f64, eps: f64) -> bool {
473        (a - b).abs() < eps
474    }
475
476    #[test]
477    fn complex_arithmetic() {
478        let a = Complex::new(1.0, 2.0);
479        let b = Complex::new(3.0, -1.0);
480        let sum = a + b;
481        assert!(approx(sum.re, 4.0, 1e-12));
482        assert!(approx(sum.im, 1.0, 1e-12));
483        let prod = a * b;
484        // (1+2i)(3-i) = 3 -i +6i -2i^2 = 5 + 5i
485        assert!(approx(prod.re, 5.0, 1e-12));
486        assert!(approx(prod.im, 5.0, 1e-12));
487    }
488
489    #[test]
490    fn complex_exp_ln() {
491        let z = Complex::new(1.0, PI);
492        let e = z.exp();
493        // e^(1+i*pi) = e * (cos(pi) + i*sin(pi)) = -e + 0i
494        assert!(approx(e.re, -std::f64::consts::E, 1e-10));
495        assert!(approx(e.im, 0.0, 1e-10));
496    }
497
498    #[test]
499    fn zeta_at_2() {
500        // zeta(2) = pi^2/6 ≈ 1.6449340668
501        let z = zeta(Complex::new(2.0, 0.0));
502        assert!(approx(z.re, PI * PI / 6.0, 1e-6));
503        assert!(approx(z.im, 0.0, 1e-6));
504    }
505
506    #[test]
507    fn zeta_at_minus_1() {
508        // zeta(-1) = -1/12
509        let z = zeta(Complex::new(-1.0, 0.0));
510        assert!(approx(z.re, -1.0 / 12.0, 1e-4));
511        assert!(approx(z.im, 0.0, 1e-4));
512    }
513
514    #[test]
515    fn zeta_at_4() {
516        // zeta(4) = pi^4/90
517        let z = zeta(Complex::new(4.0, 0.0));
518        assert!(approx(z.re, PI.powi(4) / 90.0, 1e-6));
519        assert!(z.im.abs() < 1e-6);
520    }
521
522    #[test]
523    fn first_zero_approx() {
524        // First non-trivial zero at t ≈ 14.134725
525        let zeros = find_zeros(13.0, 16.0, 2000);
526        assert!(!zeros.is_empty(), "should find at least one zero");
527        assert!(
528            approx(zeros[0], 14.134725, 0.01),
529            "first zero should be near 14.1347, got {}",
530            zeros[0]
531        );
532    }
533
534    #[test]
535    fn z_function_sign_change() {
536        // Z(t) should change sign near t ≈ 14.134
537        let a = z_function(14.0);
538        let b = z_function(14.5);
539        assert!(
540            a * b < 0.0,
541            "Z should change sign between 14 and 14.5: Z(14)={}, Z(14.5)={}",
542            a,
543            b
544        );
545    }
546
547    #[test]
548    fn critical_strip_renderer() {
549        let r = CriticalStripRenderer::new((0.0, 1.0), (-5.0, 5.0), (5, 10));
550        let cells = r.render();
551        assert_eq!(cells.len(), 50);
552    }
553
554    #[test]
555    fn zero_marker_produces_glyphs() {
556        let marker = ZeroMarker::new(Vec3::ZERO, 10.0);
557        let glyphs = marker.mark_zeros(13.0, 16.0, 2000);
558        assert!(!glyphs.is_empty());
559    }
560}