paper-first-base-below-a-quarter.rs
6.8 kB · rust · 235 lines
1use mrlycore::errors::Result;2use mrlycore::MrlyError;3use mrlyfig::out::root;4use mrlyfig::{ink, save, Board};5use std::path::PathBuf;67const NAME: &str = "paper-first-base-below-a-quarter";8const LOW: usize = 10;9const HIGH: usize = 40;10const SUB: usize = 4;11const CAP: f64 = 6.0e6;12const QUARTER: f64 = 0.25;13const RUNGS: usize = 395;1415// LADDER1617fn weights(q: usize, a0: usize, nd: u32, m: usize) -> Vec<f64> {18 let w = q.pow(nd);19 let inv = 1.0 / (q as f64 - 1.0);20 let c = 2.0 * a0 as f64 - q as f64 + 1.0;21 let mass: usize = (0..q).filter(|a| *a != a0).sum();22 let lip = 2.0 * std::f64::consts::PI * mass as f64 * inv;23 let slack = lip / (2.0 * w as f64 * m as f64) + 1e-12;24 let step = 1.0 / (w as f64 * m as f64);25 let mut g = vec![0.0; w];26 for cell in 0..w.div_ceil(2) {27 let mut top = 0.0f64;28 for r in 0..m {29 let t = ((cell * m + r) as f64 + 0.5) * step;30 let d = (std::f64::consts::PI * q as f64 * t).sin() / (std::f64::consts::PI * t).sin();31 let e = d * d - 2.0 * d * (std::f64::consts::PI * c * t).cos() + 1.0;32 let v = if e > 0.0 { e.sqrt() * inv } else { 0.0 };33 top = top.max(v);34 }35 let u = (top + slack).min(1.0);36 g[cell] = u;37 g[w - 1 - cell] = u;38 }39 g40}4142fn sweep(g: &[f64], q: usize, nd: u32, y: &[f64]) -> Vec<f64> {43 let s = q.pow(nd - 1);44 let p = q.pow(nd - 2);45 (0..s)46 .map(|v| {47 let b = (v % p) * q;48 (0..q).map(|c| g[v * q + c] * y[b + c]).sum()49 })50 .collect()51}5253fn power(g: &[f64], q: usize, nd: u32, seed: Option<Vec<f64>>, steps: usize) -> Vec<f64> {54 let s = q.pow(nd - 1);55 let mut y = seed.unwrap_or_else(|| vec![1.0; s]);56 for _ in 0..steps {57 let z = sweep(g, q, nd, &y);58 let top = z.iter().copied().fold(0.0f64, f64::max);59 if top <= 0.0 {60 return y;61 }62 y = z.into_iter().map(|x| x / top).collect();63 }64 y65}6667fn bound(g: &[f64], q: usize, nd: u32, y: &[f64]) -> f64 {68 let z = sweep(g, q, nd, y);69 let mu = z70 .iter()71 .zip(y.iter())72 .map(|(a, b)| a / b)73 .fold(0.0f64, f64::max);74 mu.ln() / (q as f64).ln()75}7677fn lift(y: &[f64], q: usize) -> Vec<f64> {78 (0..y.len() * q).map(|v| y[v / q]).collect()79}8081fn exponent(q: usize, a0: usize) -> f64 {82 let mut nd = 3u32;83 let start = weights(q, a0, nd, SUB);84 let mut y = power(&start, q, nd, None, 300);85 let mut e = bound(&start, q, nd, &y);86 while e >= QUARTER && nd < 5 && (q as f64).powi(nd as i32 + 1) <= CAP {87 let g = weights(q, a0, nd + 1, SUB);88 let z = power(&g, q, nd + 1, Some(lift(&y, q)), 40);89 let step = bound(&g, q, nd + 1, &z);90 let gain = e - step;91 nd += 1;92 y = z;93 e = step;94 if e >= QUARTER && gain <= e - QUARTER {95 break;96 }97 }98 e99}100101fn ladder() -> Vec<(usize, f64)> {102 let mut rungs: Vec<(usize, f64)> = Vec::new();103 for q in LOW..=HIGH {104 for a0 in 0..q.div_ceil(2) {105 rungs.push((q, exponent(q, a0)));106 }107 }108 rungs109}110111// DATA112113fn path() -> PathBuf {114 root()115 .join("files")116 .join("figures")117 .join("data")118 .join(format!("{NAME}.json"))119}120121fn write_data(rungs: &[(usize, f64)]) -> Result<PathBuf> {122 let file = path();123 let folder = file.parent().unwrap().to_path_buf();124 std::fs::create_dir_all(&folder)125 .map_err(|e| MrlyError::Value(format!("cannot make {folder:?}: {e}")))?;126 let bases: Vec<String> = rungs.iter().map(|rung| rung.0.to_string()).collect();127 let exponents: Vec<String> = rungs.iter().map(|rung| format!("{}", rung.1)).collect();128 let text = format!(129 "{{\"base\":[{}],\"exponent\":[{}]}}",130 bases.join(","),131 exponents.join(",")132 );133 std::fs::write(&file, text)134 .map_err(|e| MrlyError::Value(format!("cannot write {file:?}: {e}")))?;135 Ok(file)136}137138fn field(text: &str, name: &str) -> Vec<f64> {139 let head = format!("\"{name}\":[");140 let Some(start) = text.find(&head) else {141 return Vec::new();142 };143 let body = &text[start + head.len()..];144 let end = body.find(']').unwrap_or(0);145 body[..end]146 .split(',')147 .filter_map(|token| token.parse().ok())148 .collect()149}150151fn read_data() -> Result<Vec<(usize, f64)>> {152 let file = path();153 let raw = std::fs::read_to_string(&file)154 .map_err(|e| MrlyError::Value(format!("cannot read {file:?}: {e}; run -- compute")))?;155 let text: String = raw.chars().filter(|c| !c.is_whitespace()).collect();156 let bases = field(&text, "base");157 let exponents = field(&text, "exponent");158 Ok(bases159 .iter()160 .zip(exponents.iter())161 .map(|(base, exponent)| (*base as usize, *exponent))162 .collect())163}164165// PRESS166167fn compute() -> Result<()> {168 let rungs = ladder();169 let below = |q: usize| rungs.iter().filter(|r| r.0 == q && r.1 < QUARTER).count();170 let sets = |q: usize| q.div_ceil(2);171 assert_eq!(rungs.len(), RUNGS, "the ladder has 395 rungs");172 assert!((LOW..21).all(|q| below(q) == 0), "no base under 21 clears");173 assert_eq!(below(21), 1, "base 21 clears at exactly one digit");174 assert!(175 (34..=HIGH).all(|q| below(q) == sets(q)),176 "every base from 34 clears at every digit"177 );178 let gold = rungs.iter().filter(|r| r.1 < QUARTER).count();179 assert_eq!(gold, 163, "163 rungs sit under the quarter");180 let file = write_data(&rungs)?;181 println!(182 "{NAME} {} rungs {gold} under the quarter -> {file:?}",183 RUNGS184 );185 Ok(())186}187188fn draw() -> Result<()> {189 let rungs = read_data()?;190 assert_eq!(rungs.len(), RUNGS);191 let lo = rungs.iter().map(|r| r.1).fold(f64::MAX, f64::min);192 let hi = rungs.iter().map(|r| r.1).fold(f64::MIN, f64::max);193 let pad = (hi - lo) * 0.06;194 let (foot, head) = (lo - pad, hi + pad);195 assert!(196 foot < QUARTER && QUARTER < head,197 "the quarter line is inside"198 );199200 let mut board = Board::square();201 let frame = board.frame(0.08);202 let span = (HIGH - LOW + 1) as f64;203 let slot = frame.w / span;204 let dash = slot * 0.72;205 let place = |e: f64| frame.y + frame.h * (head - e) / (head - foot);206207 let line = place(QUARTER);208 board.rect(209 frame.x,210 line - 1.0,211 frame.w,212 2.0,213 ink::fade(ink::blue(), 0.9),214 );215216 for (q, e) in &rungs {217 let x = frame.x + ((q - LOW) as f64 + 0.5) * slot - dash / 2.0;218 let y = place(*e);219 let (color, thick) = if *e < QUARTER {220 (ink::yellow(), 3.8)221 } else {222 (ink::fade(ink::dim(), 0.7), 2.6)223 };224 board.rect(x, y - thick / 2.0, dash, thick, color);225 }226 save(NAME, &board)?;227 Ok(())228}229230fn main() -> Result<()> {231 if std::env::args().nth(1).as_deref() == Some("compute") {232 return compute();233 }234 draw()235}