press.rs
21.9 kB · rust · 648 lines
1use mrlycore::errors::{value_error, Result};2use mrlymath::bang::factory::{code_to_corners, MagicLayer};3use mrlymath::bang::universe::Code;45/// The largest corner count the tally press accepts, keeping its table a million rows.6pub const CORNERS: usize = 20;78fn corner_count(dimension: usize, base: usize) -> usize {9 let count = base.pow(dimension as u32);10 assert!(count < 128, "too many corners for a u128 code");11 count12}1314/// Returns the corner-usage mask of a number, one bit per digit vector its expansion uses.15///16/// A number is read in base `base` to the power of the dimension, so each digit is one17/// residue corner of the design cube, and zero uses exactly the zero corner.18///19/// ```20/// assert_eq!(mrlylab::press::usage(0, 2, 2), 1);21/// assert_eq!(mrlylab::press::usage(6, 2, 2), 0b0110);22/// ```23pub fn usage(number: u128, dimension: usize, base: usize) -> Code {24 let radix = corner_count(dimension, base) as u128;25 if number == 0 {26 return 1;27 }28 let mut out: Code = 0;29 let mut rest = number;30 while rest > 0 {31 out |= 1 << (rest % radix);32 rest /= radix;33 }34 out35}3637/// Returns whether every digit vector of the number lies in the design.38///39/// This is the scalar membership rule of the sequence press: the number's base40/// `base` to the dimension digits, read as residue corners, must all be filled.41/// At dimension one it is the classic restricted-digit set.42///43/// ```44/// let members: Vec<u128> = (0..30).filter(|&n| mrlylab::press::member(0b0111, n, 2, 2)).collect();45/// assert_eq!(members, vec![0, 1, 2, 4, 5, 6, 8, 9, 10, 16, 17, 18, 20, 21, 22, 24, 25, 26]);46/// ```47pub fn member(code: Code, number: u128, dimension: usize, base: usize) -> bool {48 usage(number, dimension, base) & !code == 049}5051/// Returns the count of distinct digit vectors the number uses.52pub fn distinct(number: u128, dimension: usize, base: usize) -> u32 {53 usage(number, dimension, base).count_ones()54}5556/// Returns the number of designs of the dimension and base that contain the number.57///58/// A design contains the number exactly when it fills every used corner, so the count59/// is two to the free corners, and the average membership over all designs is one over60/// two to the distinct-vector count.61///62/// ```63/// assert_eq!(mrlylab::press::containing(6, 2, 2), 4);64/// ```65pub fn containing(number: u128, dimension: usize, base: usize) -> u128 {66 1 << (corner_count(dimension, base) as u32 - distinct(number, dimension, base))67}6869/// Splits a number into its dimension coordinates, one base digit peeled per axis in parallel.70///71/// ```72/// assert_eq!(mrlylab::press::coordinates(6, 2, 2), vec![1, 2]);73/// ```74pub fn coordinates(number: u128, dimension: usize, base: usize) -> Vec<u128> {75 let radix = corner_count(dimension, base) as u128;76 let mut out = vec![0u128; dimension];77 let mut rest = number;78 let mut place: u128 = 1;79 while rest > 0 {80 let mut corner = rest % radix;81 for axis in (0..dimension).rev() {82 out[axis] += (corner % base as u128) * place;83 corner /= base as u128;84 }85 rest /= radix;86 place *= base as u128;87 }88 out89}9091/// Weaves dimension coordinates back into their single interleaved number.92///93/// Panics when the woven number passes a hundred and twenty-eight bits.94pub fn interleave(coords: &[u128], base: usize) -> u128 {95 assert!(96 !coords.is_empty(),97 "interleave needs at least one coordinate"98 );99 let dimension = coords.len();100 let radix = corner_count(dimension, base) as u128;101 let mut digits = Vec::new();102 let mut rest: Vec<u128> = coords.to_vec();103 while rest.iter().any(|&c| c > 0) {104 let mut corner: u128 = 0;105 for value in rest.iter_mut() {106 corner = corner * base as u128 + *value % base as u128;107 *value /= base as u128;108 }109 digits.push(corner);110 }111 let mut out: u128 = 0;112 for &corner in digits.iter().rev() {113 out = out114 .checked_mul(radix)115 .and_then(|v| v.checked_add(corner))116 .expect("the woven number passes a hundred and twenty-eight bits");117 }118 out119}120121/// Returns the first members of a design in ascending order.122///123/// Stops early where the next member would pass a hundred and twenty-eight bits.124///125/// ```126/// assert_eq!(mrlylab::press::members(0b10, 1, 2, 5), vec![1, 3, 7, 15, 31]);127/// ```128pub fn members(code: Code, dimension: usize, base: usize, count: usize) -> Vec<u128> {129 let radix = corner_count(dimension, base) as u128;130 let allowed: Vec<u128> = (0..radix).filter(|&i| (code >> i) & 1 == 1).collect();131 let mut out = Vec::with_capacity(count);132 if count == 0 || allowed.is_empty() {133 return out;134 }135 if allowed[0] == 0 {136 out.push(0);137 }138 let mut length = 1usize;139 while out.len() < count {140 let mut slots = vec![0usize; length];141 if allowed[0] == 0 {142 slots[0] = 1;143 if allowed.len() == 1 {144 return out;145 }146 }147 'level: loop {148 let mut value: u128 = 0;149 let mut fits = true;150 for &slot in &slots {151 match value152 .checked_mul(radix)153 .and_then(|v| v.checked_add(allowed[slot]))154 {155 Some(next) => value = next,156 None => {157 fits = false;158 break;159 }160 }161 }162 if !fits {163 return out;164 }165 out.push(value);166 if out.len() == count {167 return out;168 }169 for place in (0..length).rev() {170 slots[place] += 1;171 if slots[place] < allowed.len() {172 continue 'level;173 }174 slots[place] = usize::from(place == 0 && allowed[0] == 0);175 }176 break;177 }178 length += 1;179 if radix.checked_pow(length as u32 - 1).is_none() {180 return out;181 }182 }183 out184}185186/// Counts the members of a design below the limit.187///188/// ```189/// assert_eq!(mrlylab::press::count_below(0b0111, 2, 2, 27), 18);190/// ```191pub fn count_below(code: Code, dimension: usize, base: usize, limit: u128) -> u128 {192 let radix = corner_count(dimension, base) as u128;193 let allowed: Vec<u128> = (0..radix).filter(|&i| (code >> i) & 1 == 1).collect();194 if limit == 0 || allowed.is_empty() {195 return 0;196 }197 let mut digits = Vec::new();198 let mut rest = limit;199 while rest > 0 {200 digits.push(rest % radix);201 rest /= radix;202 }203 digits.reverse();204 let k = allowed.len() as u128;205 let lead = allowed.iter().filter(|&&v| v != 0).count() as u128;206 let mut total: u128 = if allowed[0] == 0 { 1 } else { 0 };207 let mut power: u128 = 1;208 for _ in 1..digits.len() {209 total += lead * power;210 power *= k;211 }212 for (place, &digit) in digits.iter().enumerate() {213 let below = allowed214 .iter()215 .filter(|&&v| v < digit && (place > 0 || v != 0))216 .count() as u128;217 let tail = digits.len() - place - 1;218 total += below * k.pow(tail as u32);219 if !allowed.contains(&digit) {220 break;221 }222 }223 total224}225226/// The tally press: one pass over the integers weighs every design of a universe at once.227///228/// Each added number lands its weight in the bucket of its corner-usage mask, and a229/// design's total is the sum over the submasks of its code, so a single sweep prices230/// a Mertens sum, a member count or a prime count for all two to the corners designs.231pub struct Press {232 /// The design dimension of the universe.233 pub dimension: usize,234 /// The numeral base of the universe.235 pub base: usize,236 corners: usize,237 tallies: Vec<i128>,238}239240impl Press {241 /// Builds an empty press over every design of the dimension and base.242 ///243 /// Panics past twenty corners, where the bucket table leaves a million rows.244 pub fn new(dimension: usize, base: usize) -> Press {245 let corners = corner_count(dimension, base);246 assert!(247 corners <= CORNERS,248 "the tally press holds at most twenty corners"249 );250 Press {251 dimension,252 base,253 corners,254 tallies: vec![0; 1 << corners],255 }256 }257 /// Adds a weighted number to its usage bucket.258 pub fn add(&mut self, number: u128, weight: i128) {259 self.tallies[usage(number, self.dimension, self.base) as usize] += weight;260 }261 /// Returns the total weight the design at a code has collected.262 pub fn total(&self, code: Code) -> i128 {263 let mut sum = self.tallies[0];264 let mut sub = code;265 while sub != 0 {266 sum += self.tallies[sub as usize];267 sub = (sub - 1) & code;268 }269 sum270 }271 /// Returns every design's total in code order by one subset-sum transform.272 pub fn totals(&self) -> Vec<i128> {273 let mut out = self.tallies.clone();274 for bit in 0..self.corners {275 for mask in 0..out.len() {276 if mask >> bit & 1 == 1 {277 out[mask] += out[mask ^ (1 << bit)];278 }279 }280 }281 out282 }283}284285fn layer_radix(layer: &MagicLayer) -> u128 {286 (layer.number as u128).pow(layer.design.dim as u32)287}288289/// Returns the allowed digit table of one magic layer, one flag per cell of its tile.290///291/// A cell is allowed when its coordinate residues form a filled corner, which is the292/// tile the layer renders read as a digit alphabet.293pub fn layer_table(layer: &MagicLayer) -> Result<Vec<bool>> {294 let corners = code_to_corners(layer.design.code, layer.design.dim, layer.design.base)?;295 let dimension = layer.design.dim;296 let base = layer.design.base;297 let side = layer.number;298 let mut out = Vec::with_capacity(side.pow(dimension as u32));299 for cell in 0..side.pow(dimension as u32) {300 let mut rest = cell;301 let mut residue = vec![0u8; dimension];302 for axis in (0..dimension).rev() {303 residue[axis] = ((rest % side) % base) as u8;304 rest /= side;305 }306 out.push(corners.contains(&residue));307 }308 Ok(out)309}310311fn word_tables(layers: &[MagicLayer]) -> Result<Vec<Vec<bool>>> {312 if layers.is_empty() {313 return value_error("a word needs at least one layer.");314 }315 let dimension = layers[0].design.dim;316 if layers.iter().any(|l| l.design.dim != dimension) {317 return value_error("all word layers must have the same dimension.");318 }319 layers.iter().map(layer_table).collect()320}321322/// Counts the members of a magic word from its layer fills, without enumeration.323pub fn word_count(layers: &[MagicLayer]) -> Result<u128> {324 let tables = word_tables(layers)?;325 Ok(tables326 .iter()327 .map(|t| t.iter().filter(|&&b| b).count() as u128)328 .product())329}330331/// Returns whether the number lies in the magic word's composed design.332///333/// The number is read in the word's mixed radix, one digit per layer with the first334/// layer most significant, and every digit must land on an allowed cell of its tile.335/// A number past the word's domain is an error.336pub fn word_member(layers: &[MagicLayer], number: u128) -> Result<bool> {337 let tables = word_tables(layers)?;338 let mut rest = number;339 let mut ok = true;340 for (layer, table) in layers.iter().zip(&tables).rev() {341 let radix = layer_radix(layer);342 ok &= table[(rest % radix) as usize];343 rest /= radix;344 }345 if rest > 0 {346 return value_error(format!("number {number} lies past the word's domain."));347 }348 Ok(ok)349}350351/// Enumerates every member of the magic word in ascending order.352///353/// The member count is the product of the layer fills, so measure with `word_count`354/// before pressing a word too rich to hold.355pub fn word_members(layers: &[MagicLayer]) -> Result<Vec<u128>> {356 let tables = word_tables(layers)?;357 let alphabets: Vec<Vec<u128>> = tables358 .iter()359 .map(|t| {360 t.iter()361 .enumerate()362 .filter(|(_, &b)| b)363 .map(|(i, _)| i as u128)364 .collect()365 })366 .collect();367 if alphabets.iter().any(|a| a.is_empty()) {368 return Ok(Vec::new());369 }370 let radixes: Vec<u128> = layers.iter().map(layer_radix).collect();371 let mut out = Vec::new();372 let mut slots = vec![0usize; layers.len()];373 loop {374 let mut value: u128 = 0;375 for (place, &slot) in slots.iter().enumerate() {376 value = value * radixes[place] + alphabets[place][slot];377 }378 out.push(value);379 let mut place = layers.len();380 loop {381 if place == 0 {382 return Ok(out);383 }384 place -= 1;385 slots[place] += 1;386 if slots[place] < alphabets[place].len() {387 break;388 }389 slots[place] = 0;390 }391 }392}393394/// Returns the diagonal slice profile of a magic word by the substitution product.395///396/// The profile of a tile lists, per coordinate sum, its filled cells, and the profile397/// of a Kronecker word is the product of its layer profiles with strides, so no cell398/// of the composed design is ever enumerated.399pub fn word_profile(layers: &[MagicLayer]) -> Result<Vec<u128>> {400 let tables = word_tables(layers)?;401 let dimension = layers[0].design.dim;402 let mut out = vec![1u128];403 let mut stride: usize = 1;404 for (layer, table) in layers.iter().zip(&tables).rev() {405 let side = layer.number;406 let mut profile = vec![0u128; dimension * (side - 1) + 1];407 for (cell, &filled) in table.iter().enumerate() {408 if filled {409 let mut rest = cell;410 let mut total = 0usize;411 for _ in 0..dimension {412 total += rest % side;413 rest /= side;414 }415 profile[total] += 1;416 }417 }418 let mut next = vec![0u128; (profile.len() - 1) * stride + out.len()];419 for (i, &a) in profile.iter().enumerate() {420 if a == 0 {421 continue;422 }423 for (j, &b) in out.iter().enumerate() {424 next[i * stride + j] += a * b;425 }426 }427 out = next;428 stride *= side;429 }430 Ok(out)431}432433/// Returns the diagonal slice profile of one design pressed to a fractal level.434pub fn profile(code: Code, dimension: usize, base: usize, level: usize) -> Result<Vec<u128>> {435 if level < 1 {436 return value_error("level must be at least 1.");437 }438 let layer = MagicLayer::new(mrlymath::name::Bang::new(code, dimension, base), base);439 word_profile(&vec![layer; level])440}441442#[cfg(test)]443mod tests {444 use super::*;445 use mrlymath::bang::factory::create;446 use mrlymath::name::Bang;447448 #[test]449 fn usage_of_zero_is_the_zero_corner() {450 assert_eq!(usage(0, 2, 2), 1);451 assert_eq!(usage(0, 1, 10), 1);452 assert_eq!(distinct(0, 2, 2), 1);453 }454455 #[test]456 fn membership_matches_a_digit_check_at_base_ten() {457 let no_seven: Code = !(1 << 7) & ((1 << 10) - 1);458 for n in 0..10_000u128 {459 let digits_clean = !n.to_string().contains('7');460 assert_eq!(member(no_seven, n, 1, 10), digits_clean, "{n}");461 }462 }463464 #[test]465 fn members_of_the_repunit_design_are_the_mersenne_numbers() {466 assert_eq!(members(0b10, 1, 2, 6), vec![1, 3, 7, 15, 31, 63]);467 }468469 #[test]470 fn members_walk_ascending_and_agree_with_membership() {471 for code in [0b0111u128, 0b0110, 0b1001, 0b1111] {472 let list = members(code, 2, 2, 40);473 for pair in list.windows(2) {474 assert!(pair[0] < pair[1]);475 }476 let scanned: Vec<u128> = (0..200).filter(|&n| member(code, n, 2, 2)).collect();477 let shared = list.len().min(scanned.len());478 assert_eq!(list[..shared], scanned[..shared], "{code}");479 }480 }481482 #[test]483 fn count_below_agrees_with_the_member_walk() {484 for code in [0b0111u128, 0b0110, 0b1011, 0b0001, 0b0000] {485 let list = members(code, 2, 2, 60);486 for limit in 0..300u128 {487 let walked = list.iter().filter(|&&m| m < limit).count() as u128;488 if list.len() < 60 || walked < 60 {489 assert_eq!(count_below(code, 2, 2, limit), walked, "{code} {limit}");490 }491 }492 }493 }494495 #[test]496 fn the_member_count_at_a_level_boundary_is_the_geometric_sum() {497 let code: Code = 0b0111;498 let k: u128 = 3;499 for level in 1..6u32 {500 let boundary = 4u128.pow(level);501 let full: u128 = 1 + (k - 1) * (k.pow(level) - 1) / (k - 1);502 assert_eq!(count_below(code, 2, 2, boundary), full);503 }504 }505506 #[test]507 fn membership_matches_the_rendered_fractal() {508 for code in [7u128, 6, 9, 11] {509 let level = 3;510 let tile = create(code, 2, 2, 2, level).unwrap();511 let side = 1u128 << level;512 for n in 0..4u128.pow(level as u32) {513 let coords = coordinates(n, 2, 2);514 let flat = (coords[0] * side + coords[1]) as usize;515 let filled = tile.bytes()[flat] == 1;516 let padded =517 member(code, n, 2, 2) && (code & 1 == 1 || n >= 4u128.pow(level as u32 - 1));518 assert_eq!(padded, filled, "{code} {n}");519 }520 }521 }522523 #[test]524 fn coordinates_and_interleave_round_trip() {525 for n in 0..5_000u128 {526 assert_eq!(interleave(&coordinates(n, 2, 2), 2), n);527 assert_eq!(interleave(&coordinates(n, 3, 2), 2), n);528 assert_eq!(interleave(&coordinates(n, 2, 3), 3), n);529 }530 }531532 #[test]533 fn containing_counts_the_designs_that_hold_the_number() {534 for n in 0..500u128 {535 let direct = (0..16u128).filter(|&code| member(code, n, 2, 2)).count();536 assert_eq!(containing(n, 2, 2), direct as u128, "{n}");537 }538 }539540 #[test]541 fn the_membership_average_over_all_designs_is_two_to_minus_distinct() {542 for n in 0..2_000u128 {543 assert_eq!(containing(n, 2, 2), 1 << (4 - distinct(n, 2, 2)));544 assert_eq!(containing(n, 3, 2), 1 << (8 - distinct(n, 3, 2)));545 }546 }547548 #[test]549 fn the_press_totals_agree_with_direct_member_sums() {550 let mut press = Press::new(2, 2);551 let weights: Vec<i128> = (0..600).map(|n| (n as i128 % 7) - 3).collect();552 for (n, &w) in weights.iter().enumerate() {553 press.add(n as u128, w);554 }555 let totals = press.totals();556 for code in 0..16u128 {557 let direct: i128 = weights558 .iter()559 .enumerate()560 .filter(|(n, _)| member(code, *n as u128, 2, 2))561 .map(|(_, &w)| w)562 .sum();563 assert_eq!(press.total(code), direct, "{code}");564 assert_eq!(totals[code as usize], direct, "{code}");565 }566 }567568 #[test]569 fn a_native_word_is_the_stationary_press() {570 let layer = MagicLayer::new(Bang::new(7, 2, 2), 2);571 let word = vec![layer; 3];572 for n in 0..64u128 {573 let padded = member(7, n, 2, 2);574 assert_eq!(word_member(&word, n).unwrap(), padded, "{n}");575 }576 assert_eq!(word_count(&word).unwrap(), 27);577 }578579 #[test]580 fn word_members_match_the_magic_tensor() {581 let word = [582 MagicLayer::new(Bang::new(7, 2, 2), 3),583 MagicLayer::new(Bang::new(14, 2, 2), 5),584 ];585 let tensor = mrlymath::bang::factory::magic(&word).unwrap();586 let side = 15u128;587 let list = word_members(&word).unwrap();588 assert_eq!(list.len() as u128, word_count(&word).unwrap());589 for n in 0..word.iter().map(layer_radix).product::<u128>() {590 let mut rest = n;591 let mut x = 0u128;592 let mut y = 0u128;593 let mut place = 1u128;594 for layer in word.iter().rev() {595 let cell = rest % layer_radix(layer);596 rest /= layer_radix(layer);597 let s = layer.number as u128;598 x += (cell / s) * place;599 y += (cell % s) * place;600 place *= s;601 }602 let filled = tensor.bytes()[(x * side + y) as usize] == 1;603 assert_eq!(word_member(&word, n).unwrap(), filled, "{n}");604 assert_eq!(list.contains(&n), filled, "{n}");605 }606 }607608 #[test]609 fn word_profiles_match_the_rendered_diagonal_sums() {610 let word = [611 MagicLayer::new(Bang::new(7, 2, 2), 3),612 MagicLayer::new(Bang::new(14, 2, 2), 5),613 ];614 let tensor = mrlymath::bang::factory::magic(&word).unwrap();615 let side = 15usize;616 let mut direct = vec![0u128; 2 * side - 1];617 for (flat, &b) in tensor.bytes().iter().enumerate() {618 if b == 1 {619 direct[flat / side + flat % side] += 1;620 }621 }622 assert_eq!(word_profile(&word).unwrap(), direct);623 }624625 #[test]626 fn profile_totals_are_the_fill_powers() {627 let native = profile(7, 2, 2, 4).unwrap();628 assert_eq!(native.iter().sum::<u128>(), 3u128.pow(4));629 let sponge = vec![MagicLayer::new(Bang::new(23, 3, 2), 3); 3];630 let classic = word_profile(&sponge).unwrap();631 assert_eq!(classic.iter().sum::<u128>(), 20u128.pow(3));632 }633634 #[test]635 #[should_panic(expected = "the tally press holds at most twenty corners")]636 fn the_press_refuses_a_universe_past_twenty_corners() {637 let _ = Press::new(5, 2);638 }639640 #[test]641 fn a_word_refuses_mismatched_dimensions_and_numbers_past_its_domain() {642 let plane = MagicLayer::new(Bang::new(7, 2, 2), 3);643 let cube = MagicLayer::new(Bang::new(23, 3, 2), 3);644 assert!(word_member(&[plane.clone(), cube], 0).is_err());645 assert!(word_member(&[plane], 9).is_err());646 assert!(word_member(&[], 0).is_err());647 }648}