zeta.rs
16.0 kB · rust · 485 lines
1use crate::classics::primes;2use crate::series::bernoulli;3use std::f64::consts::PI;4use std::ops::{Add, Div, Mul, Neg, Sub};56/// The t where the walk hands over from Euler-Maclaurin to Riemann-Siegel.7pub const JOIN: f64 = 20.0;8const SHIFT: usize = 10;9const TAIL: usize = 7;10const STEPS: usize = 10;11const STENCIL: f64 = 0.02;12const TOLERANCE: f64 = 1e-9;1314/// A complex number: a real and an imaginary part.15#[derive(Clone, Copy, Debug, Default, PartialEq)]16pub struct Complex {17 /// The real part.18 pub re: f64,19 /// The imaginary part.20 pub im: f64,21}2223impl Complex {24 /// Builds a complex number from its parts.25 pub const fn new(re: f64, im: f64) -> Complex {26 Complex { re, im }27 }28 /// Returns the modulus.29 pub fn abs(self) -> f64 {30 self.re.hypot(self.im)31 }32 /// Returns the principal argument.33 pub fn arg(self) -> f64 {34 self.im.atan2(self.re)35 }36 /// Returns the exponential.37 pub fn exp(self) -> Complex {38 let r = self.re.exp();39 Complex::new(r * self.im.cos(), r * self.im.sin())40 }41 /// Returns the principal logarithm.42 pub fn ln(self) -> Complex {43 Complex::new(self.abs().ln(), self.arg())44 }45 /// Returns a unit complex number at the given angle.46 pub fn turn(angle: f64) -> Complex {47 Complex::new(angle.cos(), angle.sin())48 }49}5051impl Add for Complex {52 type Output = Complex;53 fn add(self, other: Complex) -> Complex {54 Complex::new(self.re + other.re, self.im + other.im)55 }56}5758impl Add<f64> for Complex {59 type Output = Complex;60 fn add(self, other: f64) -> Complex {61 Complex::new(self.re + other, self.im)62 }63}6465impl Sub for Complex {66 type Output = Complex;67 fn sub(self, other: Complex) -> Complex {68 Complex::new(self.re - other.re, self.im - other.im)69 }70}7172impl Sub<f64> for Complex {73 type Output = Complex;74 fn sub(self, other: f64) -> Complex {75 Complex::new(self.re - other, self.im)76 }77}7879impl Mul for Complex {80 type Output = Complex;81 fn mul(self, other: Complex) -> Complex {82 Complex::new(83 self.re * other.re - self.im * other.im,84 self.re * other.im + self.im * other.re,85 )86 }87}8889impl Mul<f64> for Complex {90 type Output = Complex;91 fn mul(self, other: f64) -> Complex {92 Complex::new(self.re * other, self.im * other)93 }94}9596impl Div for Complex {97 type Output = Complex;98 fn div(self, other: Complex) -> Complex {99 let d = other.re * other.re + other.im * other.im;100 Complex::new(101 (self.re * other.re + self.im * other.im) / d,102 (self.im * other.re - self.re * other.im) / d,103 )104 }105}106107impl Neg for Complex {108 type Output = Complex;109 fn neg(self) -> Complex {110 Complex::new(-self.re, -self.im)111 }112}113114/// Returns a positive real base raised to a complex exponent.115pub fn raise(base: f64, exponent: Complex) -> Complex {116 (exponent * base.ln()).exp()117}118119/// Returns the Riemann-Siegel kernel, the cosine ratio that leads the remainder, in the form that stays finite at its removable points.120pub fn kernel(p: f64) -> f64 {121 let d = if p < 0.5 { 0.25 - p } else { p - 0.75 };122 let below = (2.0 * PI * d).sin();123 if below == 0.0 {124 return 0.5;125 }126 (PI * d * (1.0 + 2.0 * d)).sin() / below127}128129fn difference(order: usize, p: f64, step: f64) -> f64 {130 let mut sum = 0.0;131 let mut binomial = 1.0;132 for k in 0..=order {133 let offset = (order as f64 / 2.0 - k as f64) * step;134 let sign = if k.is_multiple_of(2) { 1.0 } else { -1.0 };135 sum += sign * binomial * kernel(p + offset);136 binomial = binomial * (order - k) as f64 / (k + 1) as f64;137 }138 sum / step.powi(order as i32)139}140141fn derivative(order: usize, p: f64) -> f64 {142 (4.0 * difference(order, p, STENCIL) - difference(order, p, 2.0 * STENCIL)) / 3.0143}144145/// Returns the first four Riemann-Siegel corrections at the fractional part p: the kernel and its derivatives by central differences with one Richardson step.146pub fn corrections(p: f64) -> [f64; 4] {147 let (pi2, pi4, pi6) = (PI * PI, PI.powi(4), PI.powi(6));148 [149 kernel(p),150 -derivative(3, p) / (96.0 * pi2),151 derivative(6, p) / (18_432.0 * pi4) + derivative(2, p) / (64.0 * pi2),152 -derivative(9, p) / (5_308_416.0 * pi6)153 - derivative(5, p) / (3_840.0 * pi4)154 - derivative(1, p) / (64.0 * pi2),155 ]156}157158/// The critical line: the Bernoulli numbers and the Euler-Maclaurin weights the two engines share, built once.159pub struct Line {160 bern: Vec<f64>,161 tail: Vec<f64>,162}163164impl Default for Line {165 fn default() -> Line {166 Line::new()167 }168}169170impl Line {171 /// Builds the line: the even Bernoulli numbers through the fourteenth and their Euler-Maclaurin weights.172 pub fn new() -> Line {173 let fractions = bernoulli(2 * TAIL + 1);174 let bern: Vec<f64> = (0..=TAIL)175 .map(|k| {176 let (num, den) = fractions[2 * k];177 num as f64 / den as f64178 })179 .collect();180 let mut factorial = 1.0;181 let tail = (0..=TAIL)182 .map(|k| {183 if k > 0 {184 factorial *= ((2 * k - 1) * 2 * k) as f64;185 }186 bern[k] / factorial187 })188 .collect();189 Line { bern, tail }190 }191 /// Returns the Riemann-Siegel theta: the argument of gamma at one quarter plus i t over two, less t ln pi over two, by Stirling's series after a shift of ten.192 pub fn theta(&self, t: f64) -> f64 {193 let z = Complex::new(0.25, 0.5 * t);194 let w = z + SHIFT as f64;195 let mut arg = ((w - 0.5) * w.ln() - w).im;196 for k in 0..SHIFT {197 arg -= (z + k as f64).arg();198 }199 let inverse = Complex::new(1.0, 0.0) / w;200 let square = inverse * inverse;201 let mut power = inverse;202 for k in 1..=TAIL {203 arg += self.bern[k] / ((2 * k * (2 * k - 1)) as f64) * power.im;204 power = power * square;205 }206 arg - 0.5 * t * PI.ln()207 }208 /// Returns zeta at one half plus i t by the complex Euler-Maclaurin sum: t plus ten terms and seven Bernoulli corrections.209 pub fn maclaurin(&self, t: f64) -> Complex {210 let s = Complex::new(0.5, t);211 let count = t.abs() as usize + SHIFT;212 let mut sum = Complex::default();213 for k in 1..=count {214 sum = sum + raise(k as f64, -s);215 }216 let base = count as f64;217 let mut out = sum + raise(base, -s + 1.0) / (s - 1.0) - raise(base, -s) * 0.5;218 let mut rising = s;219 let mut power = raise(base, -s - 1.0);220 let square = 1.0 / (base * base);221 for k in 1..=TAIL {222 out = out + rising * power * self.tail[k];223 rising = rising * (s + (2 * k - 1) as f64) * (s + (2 * k) as f64);224 power = power * square;225 }226 out227 }228 /// Returns Z(t) from the Euler-Maclaurin value turned onto the real axis.229 pub fn exact(&self, t: f64) -> f64 {230 (Complex::turn(self.theta(t)) * self.maclaurin(t)).re231 }232 /// Returns Z(t) by the Riemann-Siegel formula: the main sum and the first four corrections.233 pub fn siegel(&self, t: f64) -> f64 {234 let a = (t / (2.0 * PI)).sqrt();235 let whole = a.floor();236 let theta = self.theta(t);237 let mut sum = 0.0;238 for k in 1..=whole as usize {239 let kf = k as f64;240 sum += (theta - t * kf.ln()).cos() / kf.sqrt();241 }242 let sign = if (whole as u64).is_multiple_of(2) {243 -1.0244 } else {245 1.0246 };247 let mut weight = 1.0 / a.sqrt();248 let mut rest = 0.0;249 for c in corrections(a - whole) {250 rest += c * weight;251 weight /= a;252 }253 2.0 * sum + sign * rest254 }255 /// Returns Z(t): Euler-Maclaurin below the join, Riemann-Siegel above.256 pub fn z(&self, t: f64) -> f64 {257 if t < JOIN {258 self.exact(t)259 } else {260 self.siegel(t)261 }262 }263 /// Returns zeta on the line and Z(t) together, from the engine that serves the t.264 pub fn point(&self, t: f64) -> (Complex, f64) {265 if t < JOIN {266 let value = self.maclaurin(t);267 (value, (Complex::turn(self.theta(t)) * value).re)268 } else {269 let z = self.siegel(t);270 (Complex::turn(-self.theta(t)) * z, z)271 }272 }273 /// Returns the largest gap between the two engines over the t range on a grid.274 pub fn seam(&self, t0: f64, t1: f64, steps: usize) -> f64 {275 (0..=steps)276 .map(|k| {277 let t = t0 + (t1 - t0) * k as f64 / steps as f64;278 (self.siegel(t) - self.exact(t)).abs()279 })280 .fold(0.0, f64::max)281 }282 /// Returns the n-th Gram point, where theta is n pi, by Newton from the right.283 pub fn gram(&self, n: i64) -> f64 {284 let target = n as f64 * PI;285 let mut t = 2.0 * PI * (n as f64 + 2.0).max(3.0);286 for _ in 0..100 {287 let step = (self.theta(t) - target) / (0.5 * (t / (2.0 * PI)).ln());288 t -= step;289 if step.abs() < 1e-12 {290 break;291 }292 }293 t294 }295 fn brackets(&self, limit: f64, count: usize, exact: bool) -> Vec<(f64, f64)> {296 let z = |t: f64| if exact { self.exact(t) } else { self.z(t) };297 let mut out = Vec::new();298 let mut n = -1;299 let mut left = self.gram(n);300 let mut previous = left;301 let mut before = z(left);302 'walk: while out.len() < count && left < limit {303 let right = self.gram(n + 1);304 for k in 1..=STEPS {305 let t = (left + (right - left) * k as f64 / STEPS as f64).min(limit);306 let now = z(t);307 if before * now < 0.0 {308 out.push((previous, t));309 }310 previous = t;311 before = now;312 if t >= limit {313 break 'walk;314 }315 }316 left = right;317 n += 1;318 }319 out.truncate(count);320 out321 }322 fn bisect(&self, (mut a, mut b): (f64, f64)) -> f64 {323 let mut fa = self.exact(a);324 while b - a > TOLERANCE {325 let mid = 0.5 * (a + b);326 let fm = self.exact(mid);327 if fa * fm <= 0.0 {328 b = mid;329 } else {330 a = mid;331 fa = fm;332 }333 }334 0.5 * (a + b)335 }336 /// Returns the first zeros on the line: sign changes of Z between Gram points, refined by bisection on Euler-Maclaurin to a billionth.337 pub fn zeros(&self, count: usize) -> Vec<f64> {338 self.brackets(f64::INFINITY, count, true)339 .into_iter()340 .map(|pair| self.bisect(pair))341 .collect()342 }343 /// Counts the zeros on the line below t.344 pub fn count(&self, t: f64) -> usize {345 self.brackets(t, usize::MAX, false).len()346 }347}348349/// Returns the Chebyshev staircase at every whole number from one to x: the sum of ln p over the prime powers up to each.350pub fn psi_stair(x: usize) -> Vec<f64> {351 let mut jumps = vec![0.0; x + 1];352 for p in primes(x) {353 let mut q = p;354 while q <= x {355 jumps[q] += (p as f64).ln();356 q *= p;357 }358 }359 let mut sum = 0.0;360 jumps[1..]361 .iter()362 .map(|jump| {363 sum += jump;364 sum365 })366 .collect()367}368369/// Returns the von Mangoldt explicit formula at x over the zeros at the given ordinates and their mirrors: x less the sum of x to the rho over rho, less ln two pi, less half the ln of one minus x to the minus two.370pub fn psi_formula(x: f64, gammas: &[f64]) -> f64 {371 let log = x.ln();372 let waves: f64 = gammas373 .iter()374 .map(|&g| (0.5 * (g * log).cos() + g * (g * log).sin()) / (0.25 + g * g))375 .sum();376 x - 2.0 * x.sqrt() * waves - (2.0 * PI).ln() - 0.5 * (1.0 - 1.0 / (x * x)).ln()377}378379#[cfg(test)]380mod tests {381 use super::*;382383 fn classic(t: f64) -> f64 {384 0.5 * t * (t / (2.0 * PI)).ln() - 0.5 * t - PI / 8.0385 + 1.0 / (48.0 * t)386 + 7.0 / (5760.0 * t.powi(3))387 + 31.0 / (80640.0 * t.powi(5))388 }389390 #[test]391 fn the_complex_arithmetic_round_trips() {392 let z = Complex::new(-1.5, 2.25);393 let back = z.ln().exp();394 assert!((back - z).abs() < 1e-14);395 assert!((z / z - Complex::new(1.0, 0.0)).abs() < 1e-15);396 assert!((raise(2.0, Complex::new(3.0, 0.0)).re - 8.0).abs() < 1e-13);397 assert!((raise(4.0, Complex::new(0.5, 0.0)) - Complex::new(2.0, 0.0)).abs() < 1e-14);398 }399400 #[test]401 fn theta_meets_the_asymptotic_series_and_the_first_gram_points() {402 let line = Line::new();403 for t in [20.0, 50.0, 100.0, 200.0] {404 assert!((line.theta(t) - classic(t)).abs() < 1e-9, "t {t}");405 }406 assert!(line.theta(0.0).abs() < 1e-12);407 assert!((line.gram(-1) - 9.666_908).abs() < 1e-6);408 assert!((line.gram(0) - 17.845_600).abs() < 1e-6);409 assert!((line.gram(1) - 23.170_283).abs() < 1e-6);410 assert!(line.theta(line.gram(2)).abs() - 2.0 * PI < 1e-10);411 }412413 #[test]414 fn maclaurin_meets_the_known_values_on_the_line() {415 let line = Line::new();416 assert!((line.maclaurin(0.0).re + 1.460_354_508_809_586_8).abs() < 1e-10);417 assert!(line.maclaurin(0.0).im.abs() < 1e-12);418 let one = line.maclaurin(1.0);419 assert!((one.re - 0.143_936_427_077_189).abs() < 1e-9);420 assert!((one.im + 0.722_099_743_531_673).abs() < 1e-9);421 assert!(line.maclaurin(14.134_725).abs() < 1e-5);422 for t in [3.0, 25.0, 140.0] {423 let value = line.maclaurin(t);424 assert!(425 (Complex::turn(line.theta(t)) * value).im.abs() < 1e-9,426 "t {t}"427 );428 }429 }430431 #[test]432 fn the_kernel_pins_its_centre_and_its_removable_points() {433 assert!((kernel(0.5) - (3.0 * PI / 8.0).cos()).abs() < 1e-15);434 assert!((kernel(0.0) - (PI / 8.0).cos()).abs() < 1e-15);435 assert_eq!(kernel(0.25), 0.5);436 assert_eq!(kernel(0.75), 0.5);437 assert!((kernel(0.25 + 1e-9) - 0.5).abs() < 1e-7);438 assert!((kernel(0.75 - 1e-9) - 0.5).abs() < 1e-7);439 let direct = |p: f64| (2.0 * PI * (p * p - p - 1.0 / 16.0)).cos() / (2.0 * PI * p).cos();440 for p in [0.05, 0.1, 0.4, 0.5, 0.6, 0.9, 0.95] {441 assert!((kernel(p) - direct(p)).abs() < 1e-13, "p {p}");442 }443 assert!(corrections(0.5)[1].abs() < 1e-9);444 }445446 #[test]447 fn siegel_meets_maclaurin_beyond_the_join() {448 let line = Line::new();449 assert!(line.seam(JOIN, 60.0, 800) < 5e-5);450 assert!(line.seam(60.0, 250.0, 1900) < 5e-6);451 assert!((line.z(JOIN) - line.exact(JOIN)).abs() < 5e-5);452 }453454 #[test]455 fn the_zeros_and_their_count_are_the_classic_ones() {456 let line = Line::new();457 let first = line.zeros(5);458 let known = [14.134_725, 21.022_040, 25.010_858, 30.424_876, 32.935_062];459 for (got, want) in first.iter().zip(known) {460 assert!((got - want).abs() < 1e-6, "{got} {want}");461 }462 assert_eq!(line.count(100.0), 29);463 assert_eq!(line.count(200.0), 79);464 assert_eq!(line.count(10.0), 0);465 let hundred = line.zeros(100);466 assert_eq!(hundred.len(), 100);467 assert!((hundred[99] - 236.524_230).abs() < 1e-5);468 assert!(hundred.windows(2).all(|w| w[1] > w[0]));469 }470471 #[test]472 fn psi_pins_the_staircase_and_the_smooth_guess() {473 let stair = psi_stair(100);474 assert!((stair[9] - 7.832_0).abs() < 1e-4);475 assert!((stair[99] - 94.045_311).abs() < 1e-5);476 assert_eq!(stair[0], 0.0);477 assert!((stair[7] - 3.0 * 2f64.ln() - 3f64.ln() - 5f64.ln() - 7f64.ln()).abs() < 1e-12);478 assert!((psi_formula(10.0, &[]) - 8.167_1).abs() < 1e-4);479 let line = Line::new();480 let zeros = line.zeros(100);481 let close = psi_formula(100.0, &zeros);482 assert!((close - stair[99]).abs() < 1.0, "{close}");483 assert!((close - stair[99]).abs() < (psi_formula(100.0, &[]) - stair[99]).abs());484 }485}