lattice.rs
8.9 kB · rust · 281 lines
1use crate::factor::coprime;2use crate::series;3use std::f64::consts::PI;45/// Sieves the Euler totients of zero through n.6///7/// ```8/// assert_eq!(mrlynum::lattice::totients(6), vec![0, 1, 1, 2, 2, 4, 2]);9/// ```10pub fn totients(n: usize) -> Vec<u64> {11 let mut phi: Vec<u64> = (0..=n as u64).collect();12 for p in 2..=n {13 if phi[p] == p as u64 {14 for m in (p..=n).step_by(p) {15 phi[m] -= phi[m] / p as u64;16 }17 }18 }19 phi20}2122/// Counts the ordered pairs of coprime coordinates between one and n: twice the totient sum less one.23pub fn coprime_pairs(n: usize) -> u64 {24 if n == 0 {25 return 0;26 }27 2 * totients(n)[1..].iter().sum::<u64>() - 128}2930/// Estimates pi from visibility: the density of coprime pairs in the n-by-n window tends to six over pi squared.31pub fn pi_estimate(n: usize) -> f64 {32 let density = coprime_pairs(n) as f64 / (n as f64 * n as f64);33 (6.0 / density).sqrt()34}3536/// The rational factor r with zeta of the dimension equal to r times pi to the dimension, read off the Bernoulli fraction; none at an odd dimension or past twelve.37///38/// ```39/// assert!((mrlynum::lattice::zeta_factor(4).unwrap() - 1.0 / 90.0).abs() < 1e-15);40/// ```41pub fn zeta_factor(dimension: u32) -> Option<f64> {42 if dimension == 0 || !dimension.is_multiple_of(2) || dimension > 12 {43 return None;44 }45 let d = dimension as usize;46 let (num, den) = series::bernoulli(d + 1)[d];47 let sign = if (d / 2).is_multiple_of(2) { -1.0 } else { 1.0 };48 let factorial = (1..=d).fold(1.0f64, |out, k| out * k as f64);49 Some(sign * (num as f64 / den as f64) * 2f64.powi(d as i32) / (2.0 * factorial))50}5152/// The value zeta takes at a whole argument above one: the exact Bernoulli form at an even one, the Euler-Maclaurin sum at an odd one.53///54/// Panics at a whole argument of one or below, where the sum does not converge.55pub fn zeta_whole(s: u32) -> f64 {56 assert!(s > 1, "zeta needs a whole argument above one");57 match zeta_factor(s) {58 Some(factor) => factor * PI.powi(s as i32),59 None => series::zeta(f64::from(s), 20_000),60 }61}6263/// The density the visible count of a window in the dimension walks to: one over zeta of the dimension.64pub fn visible_density(dimension: u32) -> f64 {65 1.0 / zeta_whole(dimension)66}6768/// Recovers the constant the dimension hides from the visible count of the window: pi at an even dimension, zeta of the dimension at an odd one.69pub fn recovered(n: usize, dimension: u32) -> f64 {70 let density = series::visible(n, dimension) as f64 / (n as f64).powi(dimension as i32);71 let zeta = 1.0 / density;72 match zeta_factor(dimension) {73 Some(factor) => (zeta / factor).powf(1.0 / f64::from(dimension)),74 None => zeta,75 }76}7778/// A visible node: a reduced fraction and the brightness a stack of scales one through the window gives it.79#[derive(Clone, Copy, Debug, PartialEq, Eq)]80pub struct Node {81 /// The numerator, coprime to the denominator.82 pub num: u64,83 /// The denominator.84 pub den: u64,85 /// The count of scales putting a line here: the floor of the window over the denominator.86 pub brightness: u64,87}8889/// A grid crossing of two visible nodes, its brightness the separable product.90#[derive(Clone, Copy, Debug, PartialEq, Eq)]91pub struct Node2d {92 /// The horizontal node.93 pub x: Node,94 /// The vertical node.95 pub y: Node,96 /// The product of the two axis brightnesses.97 pub brightness: u64,98}99100/// Lists the visible nodes of a window in ascending value: every reduced fraction with denominator at most n.101pub fn nodes(n: usize) -> Vec<Node> {102 let mut out = Vec::new();103 for den in 1..=n {104 for num in 0..=den {105 if coprime(num, den) {106 out.push(Node {107 num: num as u64,108 den: den as u64,109 brightness: (n / den) as u64,110 });111 }112 }113 }114 out.sort_by(|a, b| (a.num as u128 * b.den as u128).cmp(&(b.num as u128 * a.den as u128)));115 out116}117118/// Walks the Farey sequence of the order by the Stern-Brocot mediant recurrence from zero over one to one over one, an independent route to the same nodes.119pub fn farey(order: usize) -> Vec<Node> {120 let mut out = Vec::new();121 if order == 0 {122 return out;123 }124 let n = order as u64;125 let (mut a, mut b, mut c, mut d) = (0u64, 1u64, 1u64, n);126 out.push(Node {127 num: a,128 den: b,129 brightness: n / b,130 });131 while c <= n {132 let k = (n + b) / d;133 (a, b, c, d) = (c, d, k * c - a, k * d - b);134 out.push(Node {135 num: a,136 den: b,137 brightness: n / b,138 });139 }140 out141}142143/// Lists the grid crossings of a window's nodes, row-major over the ascending axis nodes.144pub fn grid(n: usize) -> Vec<Node2d> {145 let axis = nodes(n);146 let mut out = Vec::with_capacity(axis.len() * axis.len());147 for &y in &axis {148 for &x in &axis {149 out.push(Node2d {150 x,151 y,152 brightness: x.brightness * y.brightness,153 });154 }155 }156 out157}158159/// Counts the nodes window n lights that window n minus one lacked: two at window one, phi of n after.160pub fn new_nodes(n: usize) -> u64 {161 if n == 0 {162 return 0;163 }164 (nodes(n).len() - nodes(n - 1).len()) as u64165}166167#[cfg(test)]168mod tests {169 use super::*;170 use std::f64::consts::PI;171172 #[test]173 fn totients_match_hand_checked_values() {174 let phi = totients(12);175 assert_eq!(phi, vec![0, 1, 1, 2, 2, 4, 2, 6, 4, 6, 4, 10, 4]);176 }177178 #[test]179 fn coprime_pairs_match_a_brute_count() {180 for n in [1usize, 2, 3, 10, 20] {181 let mut brute = 0;182 for a in 1..=n {183 for b in 1..=n {184 if coprime(a, b) {185 brute += 1;186 }187 }188 }189 assert_eq!(coprime_pairs(n), brute, "window {n}");190 }191 }192193 #[test]194 fn pi_estimate_converges_by_a_hundred_thousand() {195 assert!((pi_estimate(100_000) - PI).abs() < 1e-4);196 }197198 #[test]199 fn the_zeta_factor_is_the_known_even_fraction() {200 assert!((zeta_factor(2).unwrap() - 1.0 / 6.0).abs() < 1e-15);201 assert!((zeta_factor(4).unwrap() - 1.0 / 90.0).abs() < 1e-15);202 assert!((zeta_factor(6).unwrap() - 1.0 / 945.0).abs() < 1e-15);203 assert_eq!(zeta_factor(3), None);204 assert_eq!(zeta_factor(14), None);205 }206207 #[test]208 fn the_visible_density_is_one_over_the_whole_zeta() {209 assert!((visible_density(2) - 6.0 / (PI * PI)).abs() < 1e-15);210 assert!((zeta_whole(2) - PI * PI / 6.0).abs() < 1e-15);211 assert!((zeta_whole(3) - 1.202_056_903_159_594).abs() < 1e-9);212 }213214 #[test]215 fn the_window_recovers_pi_in_the_even_dimensions() {216 assert!((recovered(1_000, 2) - pi_estimate(1_000)).abs() < 1e-12);217 assert!((recovered(1_000, 2) - PI).abs() < 2e-3);218 assert!((recovered(1_000, 4) - PI).abs() < 2e-3);219 assert!((recovered(1_000, 3) - 1.202_056_903).abs() < 2e-3);220 }221222 #[test]223 fn node_counts_follow_the_farey_sequence() {224 for (n, count) in [(1, 2), (2, 3), (3, 5), (4, 7), (5, 11), (6, 13)] {225 assert_eq!(nodes(n).len(), count, "window {n}");226 }227 }228229 #[test]230 fn nodes_are_reduced_bright_and_ascending() {231 let all = nodes(12);232 for pair in all.windows(2) {233 assert!(pair[0].num * pair[1].den < pair[1].num * pair[0].den);234 }235 for node in all {236 assert!(coprime(node.num as usize, node.den as usize));237 assert_eq!(node.brightness, 12 / node.den);238 }239 }240241 #[test]242 fn the_farey_walk_lands_on_the_window_nodes_exactly() {243 assert!(farey(0).is_empty());244 for n in 1..=50 {245 assert_eq!(farey(n), nodes(n), "order {n}");246 }247 let ends = farey(7);248 assert_eq!((ends[0].num, ends[0].den), (0, 1));249 assert_eq!((ends.last().unwrap().num, ends.last().unwrap().den), (1, 1));250 }251252 #[test]253 fn the_farey_length_is_one_past_the_totient_sum() {254 let phi = totients(50);255 for n in 1..=50usize {256 let want = 1 + phi[1..=n].iter().sum::<u64>() as usize;257 assert_eq!(farey(n).len(), want, "order {n}");258 }259 }260261 #[test]262 fn grid_brightness_is_separable() {263 let flat = grid(3);264 assert_eq!(flat.len(), 25);265 for cross in flat {266 assert_eq!(cross.brightness, cross.x.brightness * cross.y.brightness);267 }268 let corner = &grid(3)[0];269 assert_eq!((corner.x.num, corner.y.num), (0, 0));270 assert_eq!(corner.brightness, 9);271 }272273 #[test]274 fn new_nodes_is_the_totient() {275 let phi = totients(50);276 assert_eq!(new_nodes(1), 2);277 for (n, &expected) in phi.iter().enumerate().skip(2) {278 assert_eq!(new_nodes(n), expected, "window {n}");279 }280 }281}