dissection.rs
13.4 kB · rust · 376 lines
1use crate::num::design;2use crate::num::factor::{gcd, mobius_sieve, totient};3use crate::num::prime::flags;4use std::f64::consts::PI;56/// The bar region A sets on the certificate exponent, `alpha_1 < 1/5`: the whole `l^1` mass against the minor-arc `x^(4/5)`.7pub const BAR_A: f64 = 0.2;8/// The bar region B sets, `alpha_1 < 1/4`: the hybrid `l^1` mass against the `d^(-1/2)` decay.9pub const BAR_B: f64 = 0.25;10/// The constant `gamma'` of the digit-uniform chain, `(2/pi)(gamma + log(8/pi))` rounded up at seven decimals.11pub const GAMMA: f64 = 0.9625229;12/// The base the chain is scanned to; from here the closed-form cap `1 + sqrt(2 (2/pi) log base + 0.97)` keeps it below the bar.13pub const CAP_BASE: u64 = 1272;14/// The least base whose every one-missing-digit set carries a window certificate below `1/5`, `lab/py/prime-dissection`, verb `window`.15pub const WINDOW_WALL: u64 = 301;16/// The least base whose every one-missing-digit set carries a per-digit shifted-grid certificate below `1/5`, `lab/py/digit-transform-norms`, verbs `fifth` and `fifthbelow`; base 114 missing 56 is certified above.17pub const DIGIT_WALL: u64 = 115;18/// The least base with some missing digit certified below `1/5`, `lab/py/digit-transform-norms`, verb `fifth`.19pub const FIRST_BELOW: u64 = 65;20/// The sets below [`DIGIT_WALL`] certified below `1/5`, as `(base, missing digit)`, printed by `lab/py/digit-transform-norms`, verb `fifth`.21pub const CERTIFIED: [(u64, u64); 1] = [(65, 0)];2223/// The four regions the dissection cuts the frequencies `a/y` into by their Dirichlet fraction `l/d` at `Q = y^(3/5)` and height `h = |a d - l y|`.24#[derive(Clone, Copy, Debug, PartialEq, Eq)]25pub enum Region {26 /// The minor arcs, `d >= y^(2/5)`.27 A,28 /// The middle, `d < y^(2/5)` and `max(d, h) >= Z`.29 B,30 /// Near a fraction whose denominator has a prime outside the base, `d < Z` and `h < Z`.31 C1,32 /// Near a fraction whose denominator divides a power of the base, `d < Z` and `h < Z`.33 C2,34}3536/// Returns the last continued-fraction convergent `l/d` of `a/y` whose denominator is at most the cap, the Dirichlet fraction of the dissection.37///38/// ```39/// assert_eq!(mrlyrs::num::dissection::fraction(333, 1000, 63), (1, 3));40/// ```41pub fn fraction(a: u64, y: u64, cap: u64) -> (u64, u64) {42 let (mut p0, mut q0, mut p1, mut q1) = (0u64, 1u64, 1u64, 0u64);43 let (mut n, mut d) = (a, y);44 let mut best = (0, 1);45 while d > 0 {46 let c = n / d;47 let (p2, q2) = (c * p1 + p0, c * q1 + q0);48 if q2 > cap {49 break;50 }51 (p0, q0, p1, q1) = (p1, q1, p2, q2);52 best = (p2, q2);53 (n, d) = (d, n - c * d);54 }55 best56}5758/// Returns `Q = floor(y^(3/5))`, the largest denominator the dissection admits, exact in integers.59///60/// ```61/// assert_eq!(mrlyrs::num::dissection::cap(1000), 63);62/// ```63pub fn cap(y: u64) -> u64 {64 let cube = u128::from(y).pow(3);65 let mut c = (y as f64).powf(0.6) as u64;66 while u128::from(c + 1).pow(5) <= cube {67 c += 1;68 }69 while c > 0 && u128::from(c).pow(5) > cube {70 c -= 1;71 }72 c73}7475fn smooth(base: u64, mut d: u64) -> bool {76 loop {77 let g = gcd(u128::from(d), u128::from(base)) as u64;78 if g == 1 {79 return d == 1;80 }81 while d.is_multiple_of(g) {82 d /= g;83 }84 }85}8687/// Returns the region of every frequency `a/y`, `a < y = base^level`, at the cut `Z`, the fraction taken by [`fraction`] at [`cap`].88pub fn regions(base: u64, level: u32, z: u64) -> Vec<Region> {89 let y = base.pow(level);90 let top = cap(y);91 let square = u128::from(y).pow(2);92 (0..y)93 .map(|a| {94 let (l, d) = fraction(a, y, top);95 let h = (i128::from(a) * i128::from(d) - i128::from(l) * i128::from(y)).unsigned_abs();96 if u128::from(d).pow(5) >= square {97 Region::A98 } else if d < z && h < u128::from(z) {99 if smooth(base, d) {100 Region::C2101 } else {102 Region::C1103 }104 } else {105 Region::B106 }107 })108 .collect()109}110111fn modulus(base: u64, missing: &[u64], digits: &[u64], a: u64, y: u64) -> f64 {112 let turn =113 |k: u64| PI * ((u128::from(k) * u128::from(a) % (2 * u128::from(y))) as f64) / y as f64;114 let term = |k: u64| (2.0 * turn(k)).sin_cos();115 let (im, re) = if missing.len() < digits.len() {116 let kernel = if a == 0 {117 base as f64118 } else {119 turn(base).sin() / turn(1).sin()120 };121 let (sin, cos) = turn(base - 1).sin_cos();122 missing123 .iter()124 .fold((kernel * sin, kernel * cos), |(im, re), &e| {125 let (s, c) = term(e);126 (im - s, re - c)127 })128 } else {129 digits.iter().fold((0.0, 0.0), |(im, re), &d| {130 let (s, c) = term(d);131 (im + s, re + c)132 })133 };134 re.hypot(im)135}136137fn rise(base: u64, digits: &[u64], row: &[f64]) -> Vec<f64> {138 let missing: Vec<u64> = (0..base).filter(|d| !digits.contains(d)).collect();139 let below = row.len() as u64;140 let y = below * base;141 (0..y)142 .map(|a| modulus(base, &missing, digits, a, y) * row[(a % below) as usize])143 .collect()144}145146/// Returns `|hat F_level(a/base^level)|` at every `a < base^level`, built one digit at a time from `hat F_j(t) = hat F(t) hat F_(j-1)(base t)`.147///148/// ```149/// let row = mrlyrs::num::dissection::weights(3, &[0, 1], 1);150/// assert_eq!(row.iter().map(|w| format!("{w:.3}")).collect::<Vec<_>>(), ["2.000", "1.000", "1.000"]);151/// ```152pub fn weights(base: u64, digits: &[u64], level: u32) -> Vec<f64> {153 (0..level).fold(vec![1.0], |row, _| rise(base, digits, &row))154}155156/// Returns the unshifted masses `c_j = sum_(a < base^j) |hat F_j(a/base^j)|` for `j = 0..=level`, the `l^1` mass region A pays, `c_0 = 1`.157pub fn masses(base: u64, digits: &[u64], level: u32) -> Vec<f64> {158 let mut row = vec![1.0];159 let mut out = vec![1.0];160 for _ in 0..level {161 row = rise(base, digits, &row);162 out.push(row.iter().sum());163 }164 out165}166167/// Returns the `l^1` exponent the top two masses read, `log_base(c_j/(fill c_(j-1)))`: a reading of the growth region A pays, never a certificate.168pub fn reading(base: u64, fill: usize, masses: &[f64]) -> f64 {169 let n = masses.len();170 if n < 2 {171 return f64::NAN;172 }173 (masses[n - 1] / (fill as f64 * masses[n - 2])).ln() / (base as f64).ln()174}175176fn cubic(base: u64, z: f64) -> f64 {177 let slope = 2.0 / PI;178 (z - 1.0).powi(3)179 - (slope * (base as f64).ln() * z180 + GAMMA * (z - 1.0)181 + slope * (z - 1.0).powi(2) / (base as f64 * z - 1.0))182}183184/// Returns the root `z > 1` of the digit-uniform chain at one missing digit, `(z-1)^3 = (2/pi)(log base) z + gamma'(z-1) + (2/pi)(z-1)^2/(base z - 1)`, by bisection.185pub fn chain_root(base: u64) -> f64 {186 let (mut low, mut high) = (1.0, 11.0);187 while cubic(base, high) < 0.0 {188 high *= 2.0;189 }190 for _ in 0..200 {191 let mid = 0.5 * (low + high);192 if cubic(base, mid) < 0.0 {193 low = mid;194 } else {195 high = mid;196 }197 }198 high199}200201/// Returns the chain's certificate exponent at one missing digit, `alpha_1 = log_base(z base/(base - 1))`, the same at every missing digit.202pub fn chain_exponent(base: u64) -> f64 {203 let q = base as f64;204 (chain_root(base) * q / (q - 1.0)).ln() / q.ln()205}206207/// Returns the chain's margin at the bar, the cubic cleared of denominators at `w = base^(1/5)(1 - 1/base)`, positive exactly when the root sits below `w` and `alpha_1 < 1/5`.208pub fn chain_margin(base: u64) -> f64 {209 let q = base as f64;210 cubic(base, q.powf(BAR_A) * (1.0 - 1.0 / q))211}212213/// Returns the chain's wall, one past the last base up to [`CAP_BASE`] whose margin is not positive.214pub fn chain_wall() -> u64 {215 (3..=CAP_BASE)216 .rev()217 .find(|&q| chain_margin(q) <= 0.0)218 .map_or(3, |q| q + 1)219}220221/// Returns how the theorem reaches the set missing one digit: `proof` from the chain's wall, `certificate` at every base from [`DIGIT_WALL`] below it and at the [`CERTIFIED`] sets, `none` elsewhere.222///223/// ```224/// use mrlyrs::num::dissection::reach;225/// assert_eq!([reach(584, 3), reach(115, 57), reach(65, 0), reach(65, 32)], ["proof", "certificate", "certificate", "none"]);226/// ```227pub fn reach(base: u64, missing: u64) -> &'static str {228 if base >= chain_wall() {229 "proof"230 } else if base >= DIGIT_WALL || CERTIFIED.contains(&(base, missing)) {231 "certificate"232 } else {233 "none"234 }235}236237/// Returns `kappa_F = (base/phi(base)) #{f in F : gcd(f, base) = 1}/fill`, the main-term constant of the prime count, as a reduced fraction.238///239/// ```240/// assert_eq!(mrlyrs::num::dissection::kappa(10, &[1, 2, 3, 4, 6, 7, 8, 9]), (5, 4));241/// ```242pub fn kappa(base: u64, digits: &[u64]) -> (u64, u64) {243 let units = digits244 .iter()245 .filter(|&&f| gcd(u128::from(f), u128::from(base)) == 1)246 .count() as u64;247 let top = base * units;248 let bottom = totient(base as usize) as u64 * digits.len() as u64;249 let g = gcd(u128::from(top), u128::from(bottom)).max(1) as u64;250 (top / g, bottom / g)251}252253/// Returns whether the digit set keeps two consecutive digits, the hypothesis region C1 reads.254pub fn consecutive(digits: &[u64]) -> bool {255 digits.iter().any(|d| digits.contains(&(d + 1)))256}257258/// The set's own sums read on a log grid of `x`: the mass `A_F(x)`, the meter `M_F(x)` and the prime count `psi_F(x) = sum Lambda(n)` over the elements up to `x`.259pub struct Tally {260 /// The log of `x` at every sample, uniform from the first element to the last.261 pub log_x: Vec<f64>,262 /// The mass `A_F(x)`, the count of elements up to `x`.263 pub count: Vec<u64>,264 /// The meter `M_F(x)`, the sum of `mu(n)` over the elements up to `x`.265 pub meter: Vec<i64>,266 /// The prime count `psi_F(x)`, the sum of `Lambda(n)` over the elements up to `x`.267 pub primes: Vec<f64>,268}269270/// Tallies the set below `base^level` on a log grid of the given size, with the Mobius values and the primes sieved to the span.271pub fn tally(base: u64, digits: &[u64], level: usize, samples: usize) -> Tally {272 let values = design::elements(base, digits, level);273 let top = base.pow(level as u32) as usize;274 let mu = mobius_sieve(top);275 let prime = flags(top);276 let mut powers: Vec<(u64, f64)> = Vec::new();277 for p in (2..=top).take_while(|p| p * p <= top).filter(|&p| prime[p]) {278 let mut power = p * p;279 while power <= top {280 powers.push((power as u64, (p as f64).ln()));281 power = match power.checked_mul(p) {282 Some(next) => next,283 None => break,284 };285 }286 }287 powers.sort_by_key(|row| row.0);288 let mut meter = Vec::with_capacity(values.len());289 let mut psi = Vec::with_capacity(values.len());290 let (mut m, mut s) = (0i64, 0.0f64);291 for &n in &values {292 m += i64::from(mu[n as usize]);293 if prime[n as usize] {294 s += (n as f64).ln();295 } else if let Ok(slot) = powers.binary_search_by_key(&n, |row| row.0) {296 s += powers[slot].1;297 }298 meter.push(m);299 psi.push(s);300 }301 let log_x = design::log_grid(&values, samples);302 let slots: Vec<usize> = log_x303 .iter()304 .map(|&t| values.partition_point(|&v| (v as f64).ln() <= t))305 .collect();306 Tally {307 count: slots.iter().map(|&k| k as u64).collect(),308 meter: slots309 .iter()310 .map(|&k| if k == 0 { 0 } else { meter[k - 1] })311 .collect(),312 primes: slots313 .iter()314 .map(|&k| if k == 0 { 0.0 } else { psi[k - 1] })315 .collect(),316 log_x,317 }318}319320#[cfg(test)]321mod tests {322 use super::*;323324 fn missing(base: u64, digit: u64) -> Vec<u64> {325 (0..base).filter(|&d| d != digit).collect()326 }327328 #[test]329 fn the_paper_figure_cut_holds_732_202_26_40() {330 let cut = regions(10, 3, 8);331 let count = |r: Region| cut.iter().filter(|&&c| c == r).count();332 assert_eq!(333 [334 count(Region::A),335 count(Region::B),336 count(Region::C1),337 count(Region::C2)338 ],339 [732, 202, 26, 40]340 );341 }342343 #[test]344 fn the_mass_ratios_match_the_wall_readings() {345 let ratio = |base: u64, digit: u64, level: u32| {346 let c = masses(base, &missing(base, digit), level);347 format!("{:.4}", c[level as usize] / c[level as usize - 1])348 };349 assert_eq!(ratio(33, 16, 4), "74.1654");350 assert_eq!(ratio(33, 0, 4), "70.5661");351 assert_eq!(ratio(17, 8, 5), "35.5234");352 }353354 #[test]355 fn the_two_digit_column_reads_log_two_over_log_three() {356 let c = masses(3, &[0, 1], 1);357 assert_eq!((reading(3, 2, &c) * 1e6).floor(), 630929.0);358 }359360 #[test]361 fn the_chain_clears_the_bar_from_584() {362 assert_eq!(chain_wall(), 584);363 assert_eq!(format!("{:.4e}", chain_margin(584)), "6.0170e-3");364 assert_eq!(format!("{:.4e}", chain_margin(583)), "-8.3138e-3");365 assert_eq!((chain_exponent(584) * 1e6).ceil(), 199983.0);366 }367368 #[test]369 fn the_tally_meets_the_count_by_hand() {370 let t = tally(10, &missing(10, 7), 6, 64);371 assert_eq!(372 (t.count[63], t.meter[63], format!("{:.4}", t.primes[63])),373 (531440, -9, "441976.4885".to_string())374 );375 }376}