factory.rs
9.8 kB · rust · 295 lines
1use super::universe::Code;2use crate::name::{Bang, Named};3use crate::rules;4use mrlycore::errors::{value_error, Result};5use mrlycore::Tensor;6use std::collections::HashSet;78/// One ordered layer of a magic composition: a coded design at its own side number.9#[derive(Clone, Debug, PartialEq, Eq)]10pub struct MagicLayer {11 /// The layer's coded design.12 pub design: Bang,13 /// The layer's side number.14 pub number: usize,15}1617impl MagicLayer {18 /// Pins a design to the side number it renders at.19 pub fn new(design: Bang, number: usize) -> MagicLayer {20 MagicLayer { design, number }21 }22}2324/// Returns every base-q residue corner of a dimension in row-major order.25pub fn residue_corners(dimension: usize, base: usize) -> Vec<Vec<u8>> {26 let count = base.pow(dimension as u32);27 (0..count)28 .map(|i| {29 (0..dimension)30 .map(|j| ((i / base.pow((dimension - 1 - j) as u32)) % base) as u8)31 .collect()32 })33 .collect()34}3536/// Returns the code count of a dimension and base, two to the number of corners.37pub fn total_codes(dimension: usize, base: usize) -> Code {38 let cells = base.pow(dimension as u32);39 assert!(cells < 128, "too many cells for a u128 code");40 1 << cells41}4243/// Unpacks a code into its filled residue corners, or an error when the code is out of range.44pub fn code_to_corners(code: Code, dimension: usize, base: usize) -> Result<Vec<Vec<u8>>> {45 let cells = residue_corners(dimension, base);46 if code >= (1 << cells.len()) {47 return value_error(format!(48 "code {code} out of range for dimension {dimension} base {base} (0..{}).",49 (1u128 << cells.len()) - 150 ));51 }52 Ok(cells53 .into_iter()54 .enumerate()55 .filter(|(i, _)| (code >> i) & 1 == 1)56 .map(|(_, c)| c)57 .collect())58}5960/// Returns the code of the design filled wherever a corner's residue sum lands in the levels.61///62/// ```63/// assert_eq!(mrlymath::bang::factory::levels_code(3, 2, &[0, 1]), 23);64/// assert_eq!(mrlymath::bang::factory::levels_code(2, 2, &[0, 1]), 7);65/// ```66pub fn levels_code(dimension: usize, base: usize, levels: &[usize]) -> Code {67 let filled: Vec<Vec<u8>> = residue_corners(dimension, base)68 .into_iter()69 .filter(|corner| levels.contains(&corner.iter().map(|&b| b as usize).sum()))70 .collect();71 corners_to_code(&filled, dimension, base)72}7374/// Packs filled residue corners back into their code.75pub fn corners_to_code(filled: &[Vec<u8>], dimension: usize, base: usize) -> Code {76 let cells = residue_corners(dimension, base);77 let wanted: HashSet<&Vec<u8>> = filled.iter().collect();78 cells79 .iter()80 .enumerate()81 .filter(|(_, c)| wanted.contains(c))82 .map(|(i, _)| 1 << i)83 .sum()84}8586fn render(87 filled: &[Vec<u8>],88 number: usize,89 dimension: usize,90 base: usize,91 level: usize,92) -> Result<Tensor> {93 if level < 1 {94 return value_error("level must be at least 1.");95 }96 let wanted: HashSet<Vec<u8>> = filled.iter().cloned().collect();97 let tile = rules::render(|p| wanted.contains(p), number, dimension, base)?;98 Ok(tile.fractal(level))99}100101/// Renders a coded design to a tensor at its side number, dimension, base and fractal level.102///103/// ```104/// let menger = mrlymath::bang::factory::create(23, 3, 3, 2, 1).unwrap();105/// assert_eq!(menger.shape, vec![3, 3, 3]);106/// assert_eq!(menger.sum(), 20);107/// ```108pub fn create(109 code: Code,110 number: usize,111 dimension: usize,112 base: usize,113 level: usize,114) -> Result<Tensor> {115 let filled = code_to_corners(code, dimension, base)?;116 render(&filled, number, dimension, base, level)117}118119/// Renders a design from its canonical JSON name, or an error for any other object.120pub fn create_named(spec: &str, number: usize, level: usize) -> Result<Tensor> {121 let bang = Bang::from_json(spec)?;122 create(bang.code, number, bang.dim, bang.base, level)123}124125/// Composes the layers into one mixed-design cell by the ordered Kronecker product, first layer outermost, or an error below two layers or across dimensions.126///127/// A run of one repeated layer is the ordinary self-similar fractal level.128///129/// ```130/// use mrlymath::bang::{magic, MagicLayer};131/// use mrlymath::name::Bang;132/// let carpet = MagicLayer::new(Bang::new(7, 2, 2), 3);133/// let net = MagicLayer::new(Bang::new(14, 2, 2), 7);134/// let void = MagicLayer::new(Bang::new(9, 2, 2), 5);135/// assert_eq!(magic(&[carpet, net, void]).unwrap().shape, vec![105, 105]);136/// ```137pub fn magic(layers: &[MagicLayer]) -> Result<Tensor> {138 if layers.len() < 2 {139 return value_error("magic needs at least two layers.");140 }141 let dimension = layers[0].design.dim;142 if layers.iter().any(|l| l.design.dim != dimension) {143 return value_error("magic layers must share one dimension.");144 }145 let mut out: Option<Tensor> = None;146 for layer in layers {147 let next = create(148 layer.design.code,149 layer.number,150 dimension,151 layer.design.base,152 1,153 )?;154 out = Some(match out {155 Some(tile) => tile.kron(&next),156 None => next,157 });158 }159 Ok(out.expect("two or more layers leave a tile"))160}161162/// Composes JSON-named layers in order, or an error for any object that is not a bang.163pub fn magic_named(layers: &[(&str, usize)]) -> Result<Tensor> {164 let parsed: Result<Vec<MagicLayer>> = layers165 .iter()166 .map(|(spec, number)| Ok(MagicLayer::new(Bang::from_json(spec)?, *number)))167 .collect();168 magic(&parsed?)169}170171/// Renders a design straight from its filled residue corners.172pub fn create_from_corners(173 filled: &[Vec<u8>],174 number: usize,175 dimension: usize,176 base: usize,177 level: usize,178) -> Result<Tensor> {179 render(filled, number, dimension, base, level)180}181182#[cfg(test)]183mod tests {184 use super::*;185 #[test]186 fn menger_carpet_code() {187 assert_eq!(levels_code(3, 2, &[0, 1]), 23);188 let truth = create(23, 3, 3, 2, 1).unwrap();189 assert_eq!(190 create_named(r#"{"kind":"bang","dim":3,"code":23}"#, 3, 1).unwrap(),191 truth192 );193 assert_eq!(truth.sum(), 20);194 assert_eq!(truth.shape, vec![3, 3, 3]);195 }196 #[test]197 fn menger_holds_its_pinned_bytes() {198 let truth = create(23, 3, 3, 2, 1).unwrap();199 let pinned = vec![200 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1,201 ];202 assert_eq!(truth.bytes(), pinned);203 }204 #[test]205 fn create_named_takes_the_canonical_name_only() {206 assert!(create_named(r#"{"kind":"bang","dim":2,"base":3,"code":100}"#, 3, 1).is_ok());207 for bad in ["bang dim 3, code 23", "bang_dim=3_code=23", "{}", "23"] {208 assert!(create_named(bad, 3, 1).is_err(), "{bad}");209 }210 }211 #[test]212 fn code_corner_round_trip() {213 for d in 2..=3 {214 for code in [0, 1, 7, total_codes(d, 2) - 1] {215 let filled = code_to_corners(code, d, 2).unwrap();216 assert_eq!(corners_to_code(&filled, d, 2), code);217 }218 }219 }220 #[test]221 fn out_of_range_rejected() {222 assert!(code_to_corners(16, 2, 2).is_err());223 assert!(code_to_corners(100, 2, 2).is_err());224 }225 #[test]226 fn all_3d_codes_render() {227 for code in 0..256 {228 let arr = create(code, 3, 3, 2, 1).unwrap();229 assert_eq!(arr.shape, vec![3, 3, 3]);230 let filled = code_to_corners(code, 3, 2).unwrap();231 assert_eq!(232 arr.sum(),233 create_from_corners(&filled, 3, 3, 2, 1).unwrap().sum()234 );235 }236 }237 #[test]238 fn fractal_level() {239 let code = levels_code(3, 2, &[0, 1]);240 let base = create(code, 3, 3, 2, 1).unwrap();241 let lvl3 = create(code, 3, 3, 2, 3).unwrap();242 assert_eq!(lvl3.sum(), base.sum().pow(3));243 assert_eq!(lvl3.shape, vec![27, 27, 27]);244 }245 #[test]246 fn base3_has_more_corners() {247 assert_eq!(residue_corners(3, 2).len(), 8);248 assert_eq!(residue_corners(3, 3).len(), 27);249 }250 #[test]251 fn magic_recovers_the_self_similar_level() {252 let carpet = MagicLayer::new(Bang::new(7, 2, 2), 3);253 assert_eq!(254 magic(&[carpet.clone(), carpet.clone(), carpet]).unwrap(),255 create(7, 3, 2, 2, 3).unwrap()256 );257 }258 #[test]259 fn magic_composes_mixed_designs_in_order() {260 let carpet = MagicLayer::new(Bang::new(7, 2, 2), 3);261 let net = MagicLayer::new(Bang::new(14, 2, 2), 7);262 let void = MagicLayer::new(Bang::new(9, 2, 2), 5);263 let got = magic(&[carpet, net, void]).unwrap();264 let expected = create(7, 3, 2, 2, 1)265 .unwrap()266 .kron(&create(14, 7, 2, 2, 1).unwrap())267 .kron(&create(9, 5, 2, 2, 1).unwrap());268 assert_eq!(got, expected);269 assert_eq!(got.shape, vec![105, 105]);270 assert_eq!(got.sum(), 8 * 33 * 13);271 }272 #[test]273 fn magic_accepts_mixed_bases_and_canonical_names() {274 let got = magic_named(&[275 (r#"{"kind":"bang","dim":2,"code":7}"#, 3),276 (r#"{"kind":"bang","dim":2,"base":3,"code":98}"#, 3),277 ])278 .unwrap();279 let expected = magic(&[280 MagicLayer::new(Bang::new(7, 2, 2), 3),281 MagicLayer::new(Bang::new(98, 2, 3), 3),282 ])283 .unwrap();284 assert_eq!(got, expected);285 assert_eq!(got.shape, vec![9, 9]);286 }287 #[test]288 fn magic_rejects_too_few_or_mismatched_layers() {289 let plane = MagicLayer::new(Bang::new(7, 2, 2), 3);290 let cube = MagicLayer::new(Bang::new(23, 3, 2), 3);291 assert!(magic(&[]).is_err());292 assert!(magic(std::slice::from_ref(&plane)).is_err());293 assert!(magic(&[plane, cube]).is_err());294 }295}