wiki-horocycle-flow.rs
4.5 kB · rust · 152 lines
1use std::collections::BTreeSet;23use figures::{ink, save, Board, Color, Ramp};4use mrlyrs::core::error::Result;5use mrlyrs::num::factor::gcd;67const HEIGHT: f64 = 0.01;8const ROOF: f64 = 1.6;9const SAMPLES: usize = 20000;10const THICK: f64 = 1.5;11const EDGE: f64 = 2.6;12const STEP: f64 = 1.5;1314struct Canvas {15 left: f64,16 bottom: f64,17 scale: f64,18 floor: f64,19 top: f64,20}2122impl Canvas {23 fn at(&self, x: f64, y: f64) -> (f64, f64) {24 (25 self.left + x * self.scale,26 self.bottom - (y - self.floor) * self.scale,27 )28 }2930 fn inside(&self, x: f64, y: f64) -> bool {31 x.abs() <= 0.5 && x * x + y * y >= 1.0 && y <= self.top32 }33}3435fn main() -> Result<()> {36 let mut board = Board::square();37 let frame = board.frame(0.08);38 let floor = 3f64.sqrt() / 2.0;39 let scale = frame.h / ROOF;40 let canvas = Canvas {41 left: frame.x + frame.w / 2.0,42 bottom: frame.y + frame.h,43 scale,44 floor,45 top: floor + ROOF,46 };47 let cusps = cusps();48 let deepest = cusps.iter().map(|&(_, c)| c).max().unwrap_or(1);49 let ramp = Ramp::tone(ink::blue(), ink::pink());50 let mut drawn = 0usize;51 for &(a, c) in &cusps {52 let tone = ramp.at((c - 1) as f64 / (deepest - 1).max(1) as f64);53 drawn += trace(&mut board, &canvas, a, c, tone);54 }55 outline(&mut board, &canvas);56 let hits = fold(&cusps);57 assert_eq!((cusps.len(), drawn, hits), (243, 110, 239));58 save("wiki-horocycle-flow", &board)?;59 Ok(())60}6162fn cusps() -> Vec<(i64, i64)> {63 let floor = 3f64.sqrt() / 2.0;64 let mut out = Vec::new();65 let mut c = 1i64;66 while 1.0 / ((c * c) as f64 * HEIGHT) >= floor {67 let radius = 1.0 / (2.0 * (c * c) as f64 * HEIGHT);68 let reach = ((radius + 0.5) * c as f64).floor() as i64;69 for a in -reach..=reach {70 if gcd(a.unsigned_abs() as u128, c as u128) == 1 {71 out.push((a, c));72 }73 }74 c += 1;75 }76 out77}7879fn trace(board: &mut Board, canvas: &Canvas, a: i64, c: i64, tone: Color) -> usize {80 let centre = a as f64 / c as f64;81 let radius = 1.0 / (2.0 * (c * c) as f64 * HEIGHT);82 let dt = STEP / (radius * canvas.scale);83 let steps = (std::f64::consts::TAU / dt).ceil() as usize;84 let mut run: Vec<(f64, f64)> = Vec::new();85 let mut runs = 0usize;86 for k in 0..=steps {87 let t = k as f64 * std::f64::consts::TAU / steps as f64 - std::f64::consts::FRAC_PI_2;88 let x = centre + radius * t.cos();89 let y = radius + radius * t.sin();90 if canvas.inside(x, y) {91 run.push(canvas.at(x, y));92 } else if !run.is_empty() {93 board.polyline(&run, THICK, tone);94 run.clear();95 runs += 1;96 }97 }98 if !run.is_empty() {99 board.polyline(&run, THICK, tone);100 runs += 1;101 }102 runs103}104105fn outline(board: &mut Board, canvas: &Canvas) {106 let tone = ink::fg();107 let floor = canvas.floor;108 let top = canvas.top;109 board.segment(canvas.at(-0.5, floor), canvas.at(-0.5, top), EDGE, tone);110 board.segment(canvas.at(0.5, floor), canvas.at(0.5, top), EDGE, tone);111 let pts: Vec<(f64, f64)> = (0..=240)112 .map(|k| {113 let t = std::f64::consts::PI * (1.0 / 3.0 + k as f64 / 720.0);114 canvas.at(t.cos(), t.sin())115 })116 .collect();117 board.polyline(&pts, EDGE, tone);118}119120fn fold(cusps: &[(i64, i64)]) -> usize {121 let known: BTreeSet<(i64, i64)> = cusps.iter().copied().collect();122 let mut hit = BTreeSet::new();123 for k in 0..SAMPLES {124 let (mut x, mut y) = ((k as f64 + 0.5) / SAMPLES as f64, HEIGHT);125 let mut m = [[1i64, 0], [0, 1]];126 loop {127 let n = x.round();128 x -= n;129 m = [130 [m[0][0] - n as i64 * m[1][0], m[0][1] - n as i64 * m[1][1]],131 m[1],132 ];133 let norm = x * x + y * y;134 if norm >= 1.0 {135 break;136 }137 (x, y) = (-x / norm, y / norm);138 m = [[-m[1][0], -m[1][1]], m[0]];139 }140 let (mut a, mut c) = (m[0][0], m[1][0]);141 if c < 0 {142 (a, c) = (-a, -c);143 }144 let centre = a as f64 / c as f64;145 let radius = 1.0 / (2.0 * (c * c) as f64 * HEIGHT);146 let off = ((x - centre).powi(2) + (y - radius).powi(2)).sqrt() - radius;147 assert!(off.abs() < 1e-6 * radius);148 assert!(known.contains(&(a, c)));149 hit.insert((a, c));150 }151 hit.len()152}