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}