roulette.rs
14.8 kB · rust · 375 lines
1use crate::core::error::{value_error, Result};2use crate::math::spirograph::{trace, Pencil, Track};3use crate::num::factor::gcd;4use serde::{Deserialize, Serialize};5use std::collections::HashMap;67const GRID: (usize, usize) = (8, 1024);8const JOIN: f64 = 1e-5;910// THE NODES1112/// Every crossing of a traced roulette: the curves against themselves, the curves against one another, and how crowded the worst node is.13#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]14pub struct Nodes {15 /// The curves counted, in the order the pencils came in.16 pub curves: usize,17 /// How often each curve crosses itself, curve by curve.18 pub selves: Vec<usize>,19 /// How often each pair of curves crosses, the lower curve first, in lexicographic order.20 pub pairs: Vec<usize>,21 /// The most crossings one node carries: one at a plain double point, and `n(n - 1)/2` where `n` branches meet.22 pub most: usize,23 /// The nodes more than one crossing clusters at.24 pub crowded: usize,25 /// The distinct points the crossings sit at, one for every cluster.26 pub points: usize,27 /// The branches through every node added up, which is the edge count of the picture as a plane graph, `n` at a node where `n` branches meet and `2` times `points` when no node is crowded.28 pub branches: usize,29 /// The segment pairs that meet without crossing: collinear or end to end.30 pub touches: usize,31}3233impl Nodes {34 /// How often the curves `i` and `j` cross, either order, and zero when they are one curve.35 pub fn pair(&self, i: usize, j: usize) -> usize {36 if i == j || i >= self.curves || j >= self.curves {37 return 0;38 }39 let (i, j) = (i.min(j), i.max(j));40 self.pairs[i * self.curves - i * (i + 1) / 2 + j - i - 1]41 }4243 /// Every self crossing.44 pub fn selved(&self) -> usize {45 self.selves.iter().sum()46 }4748 /// Every crossing of two curves.49 pub fn paired(&self) -> usize {50 self.pairs.iter().sum()51 }5253 /// Every crossing, self and pair together, which counts a node where `n` branches meet `n(n - 1)/2` times; `points` is the count of distinct nodes and the two agree exactly when `crowded` is zero.54 pub fn total(&self) -> usize {55 self.selved() + self.paired()56 }57}5859/// Counts the nodes of the roulette the pencils draw on the track: `mrlyrs::math::spirograph::trace` at `samples` points a pencil, every pair of polyline segments tested for a proper crossing by orientation signs on a grid of buckets, and crossings within `tol` of the picture's longer side read as one node. A pair of segments is counted in one bucket alone, the first they share, so no crossing is counted twice; the sign of an orientation is `side`, exact for any endpoints whose two differences are exact, which two `f32` endpoints are while the picture's coordinates keep their exponents within 29 of one another, as these pictures do. A seat at the wheel's centre draws one circle `b` times over and the count is meaningless there, the passes crossing one another as the sampling wanders.60///61/// ```62/// use mrlyrs::math::spirograph::{pencils, track};63/// let seats = pencils(&[1, 0, 0, 1], 2, 2, "fill", 0.5, 0.0, 1).unwrap();64/// let path = track("in", 3, 1, 4, 1).unwrap();65/// assert_eq!(mrlyrs::math::roulette::nodes(&path, &seats, 600, 4e-4).unwrap().total(), 6);66/// ```67///68/// # Errors69///70/// Errors on an empty pencil list, under three samples, a tolerance outside zero to one, or a trace with no extent.71pub fn nodes(track: &Track, pencils: &[Pencil], samples: usize, tol: f64) -> Result<Nodes> {72 if pencils.is_empty() {73 return value_error("a roulette needs a pencil.");74 }75 if samples < 3 {76 return value_error("a node count needs at least three samples.");77 }78 if !(0.0..1.0).contains(&tol) {79 return value_error("the tolerance is a fraction of the picture's longer side.");80 }81 let trail = trace(track, pencils, samples)?;82 let (curves, steps) = (pencils.len(), samples - 1);83 let at = |k: usize, i: usize| {84 let base = (k * samples + i) * 2;85 [f64::from(trail[base]), f64::from(trail[base + 1])]86 };87 let (low, high) = box_of(&trail);88 let span = (high[0] - low[0]).max(high[1] - low[1]);89 if span <= 0.0 {90 return value_error("the trace has no extent.");91 }92 let size = [93 (high[0] - low[0]).max(span * 1e-9),94 (high[1] - low[1]).max(span * 1e-9),95 ];96 let g = ((curves * steps) as f64).sqrt() as usize;97 let g = g.clamp(GRID.0, GRID.1);98 let cell = |p: [f64; 2]| {99 let place = |v: f64, k: usize| (((v - low[k]) / size[k] * g as f64) as usize).min(g - 1);100 (place(p[0], 0), place(p[1], 1))101 };102 let joined: Vec<bool> = (0..curves)103 .map(|k| {104 let (first, last) = (at(k, 0), at(k, steps));105 (first[0] - last[0]).hypot(first[1] - last[1]) < span * JOIN106 })107 .collect();108 let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); g * g];109 for k in 0..curves {110 for i in 0..steps {111 let (lo, hi) = ends(at(k, i), at(k, i + 1));112 let ((x0, y0), (x1, y1)) = (cell(lo), cell(hi));113 for x in x0..=x1 {114 for y in y0..=y1 {115 buckets[x * g + y].push((k * steps + i) as u32);116 }117 }118 }119 }120 let mut out = Nodes {121 curves,122 selves: vec![0; curves],123 pairs: vec![0; curves * (curves - 1) / 2],124 ..Nodes::default()125 };126 let mut found: Vec<[f64; 2]> = Vec::new();127 for x in 0..g {128 for y in 0..g {129 let list = &buckets[x * g + y];130 for (u, &left) in list.iter().enumerate() {131 for &right in &list[u + 1..] {132 let (p, q) = (left as usize, right as usize);133 let ((kp, ip), (kq, iq)) = ((p / steps, p % steps), (q / steps, q % steps));134 if kp == kq && neighbours(ip, iq, steps, joined[kp]) {135 continue;136 }137 let (a, b) = (at(kp, ip), at(kp, ip + 1));138 let (c, d) = (at(kq, iq), at(kq, iq + 1));139 let (first, second) = (ends(a, b), ends(c, d));140 let (one, two) = (cell(first.0), cell(second.0));141 if (one.0.max(two.0), one.1.max(two.1)) != (x, y) {142 continue;143 }144 let (s1, s2) = (side(a, b, c), side(a, b, d));145 let (s3, s4) = (side(c, d, a), side(c, d, b));146 if s1 * s2 < 0 && s3 * s4 < 0 {147 if kp == kq {148 out.selves[kp] += 1;149 } else {150 let (i, j) = (kp.min(kq), kp.max(kq));151 out.pairs[i * curves - i * (i + 1) / 2 + j - i - 1] += 1;152 }153 found.push(meet(a, b, c, d));154 } else if s1 * s2 <= 0 && s3 * s4 <= 0 {155 out.touches += 1;156 }157 }158 }159 }160 }161 let (most, crowded, points, branches) = crowd(&found, tol * span);162 out.most = most;163 out.crowded = crowded;164 out.points = points;165 out.branches = branches;166 Ok(out)167}168169/// One pencil for every distinct curve, the coincidence law read on the exact seats when `exact` says the seats carry no jitter: the first pencil of each family, in the order they came in. On a circle the seats fall into classes under the rotation group of order `gcd(b, 4)`, which is the clause `mrlyrs::math::spirograph::distinct` and `mrlyrs::math::spirograph::representatives` read; on a line and on a polygon every distinct seat draws its own curve, two seats of one radius on a line drawing translates of one shape and never one curve.170pub fn spread(track: &Track, pencils: &[Pencil], exact: bool) -> Vec<Pencil> {171 if !exact {172 return pencils.to_vec();173 }174 let mut seen: HashMap<(i64, i64), usize> = HashMap::new();175 let mut out = Vec::new();176 for pencil in pencils {177 let key = match track.kind.as_str() {178 "in" | "out" => least(pencil.seat, gcd(track.ratio.1 as u128, 4) as usize),179 _ => (out.len() as i64, 1),180 };181 if seen.insert(key, out.len()).is_none() {182 out.push(*pencil);183 }184 }185 out186}187188fn least(seat: (i64, i64), order: usize) -> (i64, i64) {189 let quarter = |(u, v): (i64, i64)| (-v, u);190 let (mut best, mut turned) = (seat, seat);191 for _ in 1..order {192 for _ in 0..4 / order {193 turned = quarter(turned);194 }195 best = best.min(turned);196 }197 best198}199200// THE GEOMETRY201202fn box_of(trail: &[f32]) -> ([f64; 2], [f64; 2]) {203 let mut low = [f64::MAX; 2];204 let mut high = [f64::MIN; 2];205 for pair in trail.chunks_exact(2) {206 for k in 0..2 {207 let value = f64::from(pair[k]);208 low[k] = low[k].min(value);209 high[k] = high[k].max(value);210 }211 }212 (low, high)213}214215fn ends(a: [f64; 2], b: [f64; 2]) -> ([f64; 2], [f64; 2]) {216 (217 [a[0].min(b[0]), a[1].min(b[1])],218 [a[0].max(b[0]), a[1].max(b[1])],219 )220}221222fn neighbours(i: usize, j: usize, steps: usize, joined: bool) -> bool {223 let (i, j) = (i.min(j), i.max(j));224 j == i + 1 || (joined && i == 0 && j == steps - 1)225}226227/// Which side of the line from `a` to `b` the point `c` lies: plus one to the left, minus one to the right, zero on it. The sign is exact whenever the two differences `b - a` and `c - a` are exact, whatever the size of the products: the determinant is taken by the fused multiply-add identity of Kahan, whose error is at most twice the rounding unit times the determinant itself, so it can neither flip a sign nor invent one.228pub fn side(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> i32 {229 let (ux, uy) = (b[0] - a[0], b[1] - a[1]);230 let (vx, vy) = (c[0] - a[0], c[1] - a[1]);231 let cross = uy * vx;232 let slip = f64::mul_add(uy, vx, -cross);233 let turn = f64::mul_add(ux, vy, -cross) - slip;234 match turn.partial_cmp(&0.0) {235 Some(std::cmp::Ordering::Greater) => 1,236 Some(std::cmp::Ordering::Less) => -1,237 _ => 0,238 }239}240241fn meet(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> [f64; 2] {242 let run = [b[0] - a[0], b[1] - a[1]];243 let other = [d[0] - c[0], d[1] - c[1]];244 let denominator = run[0] * other[1] - run[1] * other[0];245 let step = ((c[0] - a[0]) * other[1] - (c[1] - a[1]) * other[0]) / denominator;246 [a[0] + step * run[0], a[1] + step * run[1]]247}248249// THE CROWD250251fn crowd(found: &[[f64; 2]], tol: f64) -> (usize, usize, usize, usize) {252 if found.is_empty() {253 return (0, 0, 0, 0);254 }255 if tol <= 0.0 {256 return (1, 0, found.len(), 2 * found.len());257 }258 let key = |p: [f64; 2]| ((p[0] / tol).floor() as i64, (p[1] / tol).floor() as i64);259 let mut cells: HashMap<(i64, i64), Vec<usize>> = HashMap::new();260 for (index, &point) in found.iter().enumerate() {261 cells.entry(key(point)).or_default().push(index);262 }263 let mut parent: Vec<usize> = (0..found.len()).collect();264 for (index, &point) in found.iter().enumerate() {265 let (x, y) = key(point);266 for dx in -1..=1 {267 for dy in -1..=1 {268 for &other in cells.get(&(x + dx, y + dy)).into_iter().flatten() {269 let far = (found[other][0] - point[0]).hypot(found[other][1] - point[1]);270 if other != index && far <= tol {271 join(&mut parent, index, other);272 }273 }274 }275 }276 }277 let mut sizes: HashMap<usize, usize> = HashMap::new();278 for index in 0..found.len() {279 *sizes.entry(root(&mut parent, index)).or_default() += 1;280 }281 let meeting = |count: usize| {282 let mut n = 2;283 while n * (n - 1) / 2 < count {284 n += 1;285 }286 n287 };288 (289 sizes.values().copied().max().unwrap_or(0),290 sizes.values().filter(|&&count| count > 1).count(),291 sizes.len(),292 sizes.values().map(|&count| meeting(count)).sum(),293 )294}295296fn root(parent: &mut [usize], mut index: usize) -> usize {297 while parent[index] != index {298 parent[index] = parent[parent[index]];299 index = parent[index];300 }301 index302}303304fn join(parent: &mut [usize], a: usize, b: usize) {305 let (a, b) = (root(parent, a), root(parent, b));306 if a != b {307 parent[a.max(b)] = a.min(b);308 }309}310311#[cfg(test)]312mod tests {313 use super::*;314 use crate::math::spirograph::{distinct, pencils, track};315316 fn carpet() -> Vec<u8> {317 vec![1, 1, 1, 1, 0, 1, 1, 1, 1]318 }319320 #[test]321 fn two_curves_on_one_orbit_cross_twice_the_ratio_whatever_their_seats() {322 let seats = pencils(&carpet(), 3, 3, "fill", 0.3, 0.0, 1).unwrap();323 for a in [3, 4, 5, 6, 7] {324 let path = track("in", a, 1, 4, 1).unwrap();325 let count = nodes(&path, &seats, 3000, 4e-4).unwrap();326 assert_eq!(count.paired(), 56 * a);327 assert_eq!(count.selved(), 0);328 assert_eq!((count.most, count.crowded), (1, 0));329 }330 }331332 #[test]333 fn a_curve_below_the_threshold_crosses_itself_a_times_b_less_one() {334 let seats = pencils(&[1, 0, 0, 0, 0, 0, 0, 1, 0], 3, 3, "fill", 0.9, 0.0, 1).unwrap();335 let path = track("in", 7, 4, 4, 1).unwrap();336 let count = nodes(&path, &seats, 6000, 4e-4).unwrap();337 assert_eq!(count.selves, vec![21, 21]);338 assert_eq!(count.pair(0, 1), 56);339 assert_eq!(count.total(), 98);340 }341342 #[test]343 fn the_orientation_sign_holds_where_the_plain_determinant_rounds_to_nothing() {344 let far = 134_217_728.0;345 let (b, c) = ([far + 1.0, far], [far, far - 1.0]);346 assert_eq!(side([0.0, 0.0], b, c), -1);347 assert_eq!(348 ((b[0] * c[1]) - (b[1] * c[0])).partial_cmp(&0.0),349 Some(std::cmp::Ordering::Equal)350 );351 assert_eq!(side([0.0, 0.0], [1.0, 1.0], [3.0, 3.0]), 0);352 assert_eq!(side([0.0, 0.0], c, b), 1);353 }354355 #[test]356 fn an_alignment_reach_crowds_the_nodes_and_the_points_fall_short() {357 let seats = pencils(&carpet(), 3, 3, "fill", 0.79, 0.0, 1).unwrap();358 let path = track("in", 7, 3, 4, 1).unwrap();359 let count = nodes(&path, &seats, 6000, 4e-4).unwrap();360 assert_eq!((count.total(), count.crowded, count.most), (1288, 28, 6));361 assert_eq!((count.points, count.branches), (1148, 2352));362 }363364 #[test]365 fn the_coincident_pencils_collapse_to_one_pencil_a_curve() {366 let seats = pencils(&carpet(), 3, 3, "fill", 0.9, 0.0, 1).unwrap();367 for (kind, ring, wheel) in [("in", 7, 4), ("in", 7, 2), ("in", 7, 3), ("out", 5, 8)] {368 let path = track(kind, ring, wheel, 4, 1).unwrap();369 assert_eq!(370 spread(&path, &seats, true).len(),371 distinct(&path, &seats, true)372 );373 }374 }375}