ladder.rs
22.4 kB · rust · 585 lines
1use crate::design::elements;2use crate::zeta::{raise, Complex};3use mrlycore::errors::{value_error, Result};45/// The relative rounding allowance the double-precision ladder charges against the scale it carries.6///7/// The truncation bound is the proved one, carried by the same recursion that carries the value; this constant is a measured allowance for the arithmetic itself and is not a proof.8pub const ROUNDING: f64 = 1e-13;910const PEEL_TARGET: f64 = 100.0;11const SHIFT_START: f64 = 6.0;12const SHIFT_STEP: f64 = 6.0;13const SHIFT_CAP: f64 = 400.0;14const CUT_START: usize = 8;15const CUT_STEP: usize = 6;16const CUT_CAP: usize = 120;17const RATIO_CAP: f64 = 0.9;1819/// A digit design: the base `q`, the digit set `F` its elements are written with, and the peel depth `P` its ladder starts at.20///21/// The elements are the whole numbers whose base-`q` digits all lie in `F` with a nonzero leading digit, and the object is their Dirichlet series `zeta_F(s) = sum n^(-s)`, of abscissa `alpha = log_q k` for `k = card F`.22/// With `E_j` the sum over the elements of exactly `j` digits and `G_P = sum_(j >= P) E_j`, splitting an element on its last digit gives `(1 - k q^(-w)) G_P(w) = E_P(w) + sum_(l >= 1) binom(-w, l) q^(-w-l) gamma_l G_P(w+l)` with `gamma_l = sum_(a in F) a^l`, and `zeta_F = D_(P-1) + G_P` for the finite Dirichlet polynomial `D_(P-1)` over the elements below `q^(P-1)`.23/// The `l`-series has ratio `max F / q^P`, so the peel depth buys the convergence and the small quantity `G_P` is carried directly, never as a difference of two large ones.24#[derive(Clone, Debug, PartialEq)]25pub struct Design {26 base: u64,27 digits: Vec<u64>,28 peel: usize,29 lead: usize,30 largest: u64,31 low: Vec<f64>,32 mid: Vec<f64>,33 gamma: Vec<f64>,34}3536impl Design {37 /// Builds a design on the base and the digit set, choosing the peel depth, or an error when the base or the digits are out of range.38 ///39 /// ```40 /// let design = mrlynum::ladder::Design::new(3, &[0, 1]).unwrap();41 /// assert_eq!(design.peel(), 7);42 /// assert!((design.abscissa() - 0.630_929_753_571_457).abs() < 1e-14);43 /// ```44 pub fn new(base: u64, digits: &[u64]) -> Result<Design> {45 let mut set: Vec<u64> = digits.to_vec();46 set.sort_unstable();47 set.dedup();48 if set.len() < 2 {49 return value_error("a design needs at least two digits.");50 }51 let lead = set.iter().filter(|&&d| d > 0).count();52 if lead == 0 {53 return value_error("a design needs a nonzero digit.");54 }55 let size = set.len() as f64;56 let mut peel = 2usize;57 while lead as f64 * size.powi(peel as i32) <= PEEL_TARGET {58 peel += 1;59 }60 Design::with_peel(base, &set, peel)61 }6263 /// Builds a design at an explicit peel depth, at least two, or an error when the base, the digits or the depth are out of range.64 ///65 /// ```66 /// let shallow = mrlynum::ladder::Design::with_peel(2, &[0, 1], 4).unwrap();67 /// assert_eq!(shallow.peel(), 4);68 /// ```69 pub fn with_peel(base: u64, digits: &[u64], peel: usize) -> Result<Design> {70 let mut set: Vec<u64> = digits.to_vec();71 set.sort_unstable();72 set.dedup();73 if base < 2 {74 return value_error(format!("base {base} is under two."));75 }76 if set.len() < 2 {77 return value_error("a design needs at least two digits.");78 }79 if set.iter().any(|&d| d >= base) {80 return value_error(format!("a digit sits outside the base {base}."));81 }82 let lead = set.iter().filter(|&&d| d > 0).count();83 if lead == 0 {84 return value_error("a design needs a nonzero digit.");85 }86 if peel < 2 {87 return value_error(format!("peel depth {peel} is under two."));88 }89 if (base as f64).powi(peel as i32) > 9.007_199_254_740_992e15 {90 return value_error(format!("peel depth {peel} overruns the exact integers."));91 }92 let below = elements(base, &set, peel - 1);93 let through = elements(base, &set, peel);94 let low: Vec<f64> = below.iter().map(|&n| (n as f64).ln()).collect();95 let mid: Vec<f64> = through[below.len()..]96 .iter()97 .map(|&n| (n as f64).ln())98 .collect();99 let largest = *set.last().unwrap();100 let gamma: Vec<f64> = (0..=CUT_CAP)101 .map(|l| set.iter().map(|&a| (a as f64).powi(l as i32)).sum())102 .collect();103 Ok(Design {104 base,105 digits: set,106 peel,107 lead,108 largest,109 low,110 mid,111 gamma,112 })113 }114115 /// Returns the base.116 pub fn base(&self) -> u64 {117 self.base118 }119120 /// Returns the digit set, ascending.121 pub fn digits(&self) -> &[u64] {122 &self.digits123 }124125 /// Returns the peel depth.126 pub fn peel(&self) -> usize {127 self.peel128 }129130 /// Returns the abscissa `alpha = log_q k`.131 pub fn abscissa(&self) -> f64 {132 (self.digits.len() as f64).ln() / (self.base as f64).ln()133 }134135 /// Returns the pole spacing `2 pi / log q`.136 pub fn period(&self) -> f64 {137 2.0 * std::f64::consts::PI / (self.base as f64).ln()138 }139140 /// Returns the pole `s_(m,j) = alpha - m + 2 pi i j / log q`.141 pub fn pole(&self, m: usize, j: i64) -> Complex {142 Complex::new(self.abscissa() - m as f64, self.period() * j as f64)143 }144}145146// LADDER147148fn poly(logs: &[f64], w: Complex) -> Complex {149 let mut acc = Complex::new(0.0, 0.0);150 for &l in logs {151 acc = acc + ((-w) * l).exp();152 }153 acc154}155156fn poly_scale(logs: &[f64], sigma: f64) -> f64 {157 logs.iter().map(|&l| (-sigma * l).exp()).sum()158}159160fn tail(design: &Design, sigma: f64) -> f64 {161 let ratio = design.digits.len() as f64 * (design.base as f64).powf(-sigma);162 if ratio >= 1.0 {163 return f64::INFINITY;164 }165 design.lead as f64 * ratio.powi(design.peel as i32 - 1) / (1.0 - ratio)166}167168fn log_tail(design: &Design, sigma: f64) -> f64 {169 let ratio = design.digits.len() as f64 * (design.base as f64).powf(-sigma);170 if ratio >= 1.0 {171 return f64::INFINITY;172 }173 (design.lead as f64).ln() + (design.peel as f64 - 1.0) * ratio.ln() - (-ratio).ln_1p()174}175176fn log_binomial(top: f64, pick: usize) -> f64 {177 (1..=pick)178 .map(|i| ((top - pick as f64 + i as f64) / i as f64).ln())179 .sum()180}181182fn cut(design: &Design, w: Complex, depth: usize) -> f64 {183 let (span, sigma) = (w.abs(), w.re);184 let base = design.base as f64;185 let largest = design.largest as f64;186 let ratio = ((span + depth as f64 + 1.0) / (depth as f64 + 2.0)).max(1.0) * largest187 / base.powi(design.peel as i32);188 if ratio >= RATIO_CAP {189 return f64::INFINITY;190 }191 let rest = log_tail(design, sigma + depth as f64 + 1.0);192 if rest.is_infinite() {193 return f64::INFINITY;194 }195 let log = log_binomial(span + depth as f64, depth + 1)196 + (-sigma - depth as f64 - 1.0) * base.ln()197 + (design.digits.len() as f64).ln()198 + (depth as f64 + 1.0) * largest.ln()199 + rest;200 log.exp() / (1.0 - ratio)201}202203struct Rung {204 value: Complex,205 bound: f64,206 scale: f64,207}208209fn rung(design: &Design, s: Complex, shift: f64, depth: usize, whole: bool) -> Rung {210 let base = design.base as f64;211 let size = design.digits.len() as f64;212 let levels = (shift - s.re).ceil().max(0.0) as usize;213 if !whole && levels == 0 {214 let front = Complex::new(1.0, 0.0) - raise(base, -s) * size;215 let rest = front.abs() * tail(design, s.re);216 return Rung {217 value: Complex::new(0.0, 0.0),218 bound: rest,219 scale: rest,220 };221 }222 let mut value = vec![Complex::new(0.0, 0.0); levels + depth + 2];223 let mut bound = vec![0.0f64; levels + depth + 2];224 let mut scale = vec![0.0f64; levels + depth + 2];225 for j in levels..levels + depth + 2 {226 bound[j] = tail(design, s.re + j as f64);227 scale[j] = bound[j];228 }229 for j in (0..levels).rev() {230 let w = s + j as f64;231 let mut acc = poly(&design.mid, w);232 let mut err = cut(design, w, depth);233 let mut wide = poly_scale(&design.mid, w.re) + err;234 let mut coefficient = Complex::new(1.0, 0.0);235 for l in 1..=depth {236 coefficient = coefficient * ((-w) - (l as f64 - 1.0)) * (1.0 / l as f64);237 let weight = raise(base, (-w) - l as f64) * coefficient * design.gamma[l];238 acc = acc + weight * value[j + l];239 err += weight.abs() * bound[j + l];240 wide += weight.abs() * scale[j + l];241 }242 if !whole && j == 0 {243 return Rung {244 value: acc,245 bound: err,246 scale: wide,247 };248 }249 let den = Complex::new(1.0, 0.0) - raise(base, -w) * size;250 value[j] = acc / den;251 bound[j] = err / den.abs();252 scale[j] = wide / den.abs();253 }254 Rung {255 value: value[0],256 bound: bound[0],257 scale: scale[0],258 }259}260261fn crossing(design: &Design, s: Complex, whole: bool) -> Option<(i64, i64)> {262 let turn = s.im / design.period();263 let j = turn.round();264 if (turn - j).abs() > 1e-9 {265 return None;266 }267 let drop = design.abscissa() - s.re;268 let m = drop.round();269 let first = if whole { 0.0 } else { 1.0 };270 if (drop - m).abs() > 1e-9 || m < first {271 return None;272 }273 Some((m as i64, j as i64))274}275276fn tune(277 design: &Design,278 s: Complex,279 tolerance: f64,280 whole: bool,281 head: f64,282) -> Result<(Complex, f64)> {283 if let Some((m, j)) = crossing(design, s, whole) {284 return value_error(format!(285 "the ladder walks through the pole s_({m},{j}) = alpha - {m} + 2 pi i {j} / log q and cannot read that point."286 ));287 }288 let mut shift = SHIFT_START;289 let mut depth = CUT_START;290 let mut best = f64::INFINITY;291 while shift <= SHIFT_CAP && depth <= CUT_CAP {292 let step = rung(design, s, shift, depth, whole);293 let carried = step.bound + ROUNDING * (step.scale + head);294 if carried < tolerance {295 return Ok((step.value, carried));296 }297 best = best.min(carried);298 shift += SHIFT_STEP;299 depth += CUT_STEP;300 }301 value_error(format!(302 "tolerance {tolerance:e} is out of reach at s = {} + {}i, best bound {best:e}.",303 s.re, s.im304 ))305}306307// READINGS308309/// Returns `zeta_F(s)` and the bound it is known to, or an error when the tolerance is out of reach or the point sits on the blind lattice.310///311/// The bound is the propagated truncation bound, which is proved, plus [`ROUNDING`] times the scale the ladder carries, which is a measured allowance; the acceptance test charges every term the returned bound carries, the Dirichlet polynomial's own scale included, so the returned bound is never above the tolerance asked.312/// The recursion walks `w = s + j` upward and divides by `1 - k q^(-w)`, so it cannot read `s = alpha - m + 2 pi i j / log q` for a whole `m >= 0`: at those points it raises and names the pole. On a full digit set that lattice is `s = 1, 0, -1, -2, ...`, where `zeta_F` is `zeta` and only `s = 1` is singular.313///314/// ```315/// let design = mrlynum::ladder::Design::new(2, &[0, 1]).unwrap();316/// let s = mrlynum::zeta::Complex::new(2.0, 0.0);317/// let (value, bound) = mrlynum::ladder::zeta(&design, s, 1e-10).unwrap();318/// assert!((value.re - std::f64::consts::PI * std::f64::consts::PI / 6.0).abs() < bound);319/// ```320pub fn zeta(design: &Design, s: Complex, tolerance: f64) -> Result<(Complex, f64)> {321 let (value, bound) = tune(design, s, tolerance, true, poly_scale(&design.low, s.re))?;322 Ok((poly(&design.low, s) + value, bound))323}324325/// Returns the Lyndon cofactor `Z(s) = zeta_F(s) (1 - k q^(-s))` and the bound it is known to, or an error when the tolerance is out of reach or the point sits on the blind lattice.326///327/// The cofactor is the ladder numerator over the finite polynomial, so it is analytic on `Re s > alpha - 1` and a zero census on it needs no pole-free strip.328/// The blind lattice here is `s = alpha - m + 2 pi i j / log q` for a whole `m >= 1`: the numerator itself is read at `m = 0`, which is what makes the residue available.329pub fn cofactor(design: &Design, s: Complex, tolerance: f64) -> Result<(Complex, f64)> {330 let base = design.base as f64;331 let size = design.digits.len() as f64;332 let front = Complex::new(1.0, 0.0) - raise(base, -s) * size;333 let head = front.abs() * poly_scale(&design.low, s.re);334 let (value, bound) = tune(design, s, tolerance, false, head)?;335 Ok((front * poly(&design.low, s) + value, bound))336}337338/// Returns the residue of `zeta_F` at `s_(m,j) = alpha - m + 2 pi i j / log q` and the bound it is known to, or an error when the tolerance is out of reach.339///340/// At `m = 0` the factor `1 - k q^(-s)` has derivative `log q`, so the residue is the ladder numerator over `log q`; the column below comes off the same recursion, `(1 - q^m) R_m = sum_(l = 1)^m binom(-s_(m,j), l) q^(-s_(m,j)-l) gamma_l R_(m-l)`.341/// The head term `(1 - k q^(-s)) D_(P-1)(s)` is dropped, since the factor vanishes at the pole; in double precision the factor is not exactly zero, and the residue of the dropped term, about `1e-15` at base 10, is not charged separately and sits inside the carried scale.342pub fn residue(design: &Design, m: usize, j: i64, tolerance: f64) -> Result<(Complex, f64)> {343 let base = design.base as f64;344 let (head, err) = tune(design, design.pole(0, j), tolerance, false, 0.0)?;345 let mut values = vec![head * (1.0 / base.ln())];346 let mut bounds = vec![err / base.ln()];347 for level in 1..=m {348 let w = design.pole(level, j);349 let mut acc = Complex::new(0.0, 0.0);350 let mut err = 0.0;351 let mut coefficient = Complex::new(1.0, 0.0);352 for l in 1..=level {353 coefficient = coefficient * ((-w) - (l as f64 - 1.0)) * (1.0 / l as f64);354 let weight = raise(base, (-w) - l as f64) * coefficient * design.gamma[l];355 acc = acc + weight * values[level - l];356 err += weight.abs() * (bounds[level - l] + ROUNDING * values[level - l].abs());357 }358 let den = 1.0 - base.powi(level as i32);359 values.push(acc * (1.0 / den));360 bounds.push(err / den.abs());361 }362 Ok((values[m], bounds[m]))363}364365#[cfg(test)]366mod tests {367 use super::*;368 use crate::zeta::Line;369370 const PINNED: f64 = 1e-10;371372 fn near(value: Complex, re: f64, im: f64, bound: f64) {373 assert!(374 (value.re - re).abs() < bound && (value.im - im).abs() < bound,375 "{value:?} is not {re} + {im}i inside {bound:e}"376 );377 }378379 #[test]380 fn the_full_base_two_design_is_the_zeta_function() {381 let design = Design::new(2, &[0, 1]).unwrap();382 assert_eq!(design.peel(), 7);383 assert_eq!(design.abscissa(), 1.0);384 let pinned = [385 (Complex::new(2.0, 0.0), 1.644_934_066_848_226_4, 0.0),386 (387 Complex::new(0.3, 40.0),388 0.748_775_209_504_225_8,389 -1.440_885_440_634_440_5,390 ),391 (392 Complex::new(-1.0, 2.0),393 0.168_915_669_770_834_4,394 -0.070_515_988_908_254_42,395 ),396 ];397 for (s, re, im) in pinned {398 let (value, bound) = zeta(&design, s, 1e-8).unwrap();399 near(value, re, im, bound);400 }401 }402403 #[test]404 fn the_ladder_meets_the_critical_line_engine() {405 let design = Design::new(2, &[0, 1]).unwrap();406 let line = Line::new();407 for t in [12.0, 18.5] {408 let (value, bound) = zeta(&design, Complex::new(0.5, t), PINNED).unwrap();409 let want = line.maclaurin(t);410 assert!(411 (value - want).abs() < bound + 1e-9,412 "t {t} {value:?} {want:?}"413 );414 }415 }416417 #[test]418 fn the_residue_is_one_at_the_abscissa_and_zero_above_it() {419 let design = Design::new(2, &[0, 1]).unwrap();420 let (one, bound) = residue(&design, 0, 0, PINNED).unwrap();421 near(one, 1.0, 0.0, bound);422 let (off, bound) = residue(&design, 0, 1, PINNED).unwrap();423 near(off, 0.0, 0.0, bound);424 }425426 #[test]427 fn the_base_three_residues_meet_the_harvest_midpoints() {428 let pinned = [429 (430 vec![0u64, 1],431 [432 (0.799_023_642_655_835, 0.0),433 (0.231_891_517_689_918_2, -0.501_067_414_481_068_3),434 (0.003_963_750_587_374_229, -0.033_400_689_771_018_48),435 ],436 ),437 (438 vec![0, 2],439 [440 (0.515_977_601_099_115_1, 0.0),441 (0.135_292_875_345_483_3, 0.329_874_091_244_466_17),442 (-0.021_699_535_708_644_655, -0.000_946_804_031_498_436_2),443 ],444 ),445 ];446 for (digits, wanted) in pinned {447 let design = Design::new(3, &digits).unwrap();448 for (j, (re, im)) in wanted.iter().enumerate() {449 let (value, bound) = residue(&design, 0, j as i64, PINNED).unwrap();450 near(value, *re, *im, bound);451 }452 }453 }454455 #[test]456 fn the_second_digit_set_scales_the_first() {457 let one = Design::new(3, &[0, 1]).unwrap();458 let two = Design::new(3, &[0, 2]).unwrap();459 for s in [Complex::new(2.0, 3.0), Complex::new(0.8, 23.0)] {460 let (a, ba) = zeta(&two, s, PINNED).unwrap();461 let (b, bb) = zeta(&one, s, PINNED).unwrap();462 assert!((a - raise(2.0, -s) * b).abs() < ba + bb);463 }464 }465466 #[test]467 fn the_base_ten_design_misses_its_last_digit() {468 let design = Design::new(10, &[0, 1, 2, 3, 4, 5, 6, 7, 8]).unwrap();469 assert_eq!(design.peel(), 2);470 let (value, bound) = zeta(&design, Complex::new(2.0, 0.0), PINNED).unwrap();471 near(value, 1.623_924_927_084_640_5, 0.0, bound);472 let (value, bound) = zeta(&design, Complex::new(1.1, 3.0), PINNED).unwrap();473 near(474 value,475 0.618_774_761_630_085,476 -0.025_931_036_103_864_726,477 bound,478 );479 let (value, bound) = residue(&design, 0, 0, PINNED).unwrap();480 near(value, 1.022_344_896_589_484_1, 0.0, bound);481 }482483 #[test]484 fn the_cofactor_is_the_series_times_the_lyndon_factor() {485 let design = Design::new(2, &[0, 1]).unwrap();486 for s in [Complex::new(2.0, 0.0), Complex::new(0.3, 40.0)] {487 let (value, bound) = cofactor(&design, s, 1e-8).unwrap();488 let (series, other) = zeta(&design, s, 1e-8).unwrap();489 let front = Complex::new(1.0, 0.0) - raise(2.0, -s) * 2.0;490 assert!((value - front * series).abs() < bound + front.abs() * other);491 }492 let (value, bound) = cofactor(&design, Complex::new(2.0, 0.0), PINNED).unwrap();493 near(value, 0.822_467_033_424_113_2, 0.0, bound);494 }495496 #[test]497 fn the_bound_survives_a_change_of_peel_depth() {498 for s in [Complex::new(2.0, 0.0), Complex::new(0.3, 40.0)] {499 let shallow = Design::with_peel(2, &[0, 1], 4).unwrap();500 let deep = Design::with_peel(2, &[0, 1], 8).unwrap();501 let (a, ba) = zeta(&shallow, s, 1e-8).unwrap();502 let (b, bb) = zeta(&deep, s, 1e-8).unwrap();503 assert!((a - b).abs() < ba + bb, "{s:?} {a:?} {b:?} {ba:e} {bb:e}");504 }505 }506507 #[test]508 fn the_column_below_a_pole_is_the_contour_average_around_it() {509 let design = Design::new(3, &[0, 1]).unwrap();510 let (want, bound) = residue(&design, 1, 1, PINNED).unwrap();511 let centre = design.pole(1, 1);512 let (radius, nodes) = (0.05, 8);513 let mut got = Complex::new(0.0, 0.0);514 let mut carried = 0.0;515 for k in 0..nodes {516 let turn = Complex::turn(std::f64::consts::TAU * k as f64 / nodes as f64);517 let (value, other) = zeta(&design, centre + turn * radius, 1e-9).unwrap();518 got = got + turn * radius * value * (1.0 / nodes as f64);519 carried += radius * other / nodes as f64;520 }521 assert!(522 (got - want).abs() < bound + carried + 1e-9,523 "{got:?} {want:?}"524 );525 }526527 #[test]528 fn the_ladder_raises_when_the_tolerance_is_out_of_reach() {529 let design = Design::new(2, &[0, 1]).unwrap();530 assert!(zeta(&design, Complex::new(-1.0, 2.0), 1e-11).is_err());531 assert!(zeta(&design, Complex::new(-1.0, 2.0), 1e-9).is_ok());532 }533534 #[test]535 fn the_constructor_rejects_a_design_it_cannot_peel() {536 assert!(Design::new(5, &[3]).is_err());537 assert!(Design::new(5, &[0]).is_err());538 assert!(Design::new(1, &[0, 1]).is_err());539 assert!(Design::with_peel(5, &[0, 7], 3).is_err());540 assert!(Design::with_peel(2, &[0, 1], 1).is_err());541 }542543 #[test]544 fn the_returned_bound_never_exceeds_the_tolerance_asked() {545 let design = Design::new(2, &[0, 1]).unwrap();546 for s in [547 Complex::new(-2.0, 2.0),548 Complex::new(-6.0, 2.0),549 Complex::new(0.3, 40.0),550 Complex::new(2.0, 0.0),551 ] {552 for tolerance in [1e-1, 1e-5, 1e-8] {553 if let Ok((_, bound)) = zeta(&design, s, tolerance) {554 assert!(bound < tolerance, "zeta {s:?} {tolerance:e} {bound:e}");555 }556 if let Ok((_, bound)) = cofactor(&design, s, tolerance) {557 assert!(bound < tolerance, "cofactor {s:?} {tolerance:e} {bound:e}");558 }559 }560 }561 }562563 #[test]564 fn the_cofactor_bound_carries_the_lyndon_factor_on_an_empty_rung() {565 let design = Design::new(2, &[0, 1]).unwrap();566 let s = Complex::new(6.0, std::f64::consts::PI / 2f64.ln());567 let front = Complex::new(1.0, 0.0) - raise(2.0, -s) * 2.0;568 assert!((front.abs() - 1.031_25).abs() < 1e-12);569 let (_, bound) = cofactor(&design, s, 1e-7).unwrap();570 let (_, other) = zeta(&design, s, 1e-7).unwrap();571 assert!(bound > front.abs() * 9.6e-10, "{bound:e}");572 assert!(bound > other * 1.03, "{bound:e} {other:e}");573 }574575 #[test]576 fn the_ladder_names_the_pole_it_walks_through() {577 let design = Design::new(2, &[0, 1]).unwrap();578 for m in 0..3 {579 let s = design.pole(m, 0);580 let message = zeta(&design, s, 1e-7).unwrap_err().to_string();581 assert!(message.contains("walks through the pole"), "{message}");582 }583 assert!(zeta(&design, Complex::new(-1.0, 1e-6), 1e-7).is_ok());584 }585}