diagonal.rs

3.6 kB · rust · 101 lines

1use mrlycore::errors::{value_error, Result};2use mrlycore::tensor::Tensor;3use std::collections::BTreeMap;45/// The widest side a profile spans.6pub const WIDEST: usize = 1 << 18;78fn histogram(tile: &Tensor) -> BTreeMap<usize, u128> {9    let shape = &tile.shape;10    let mut out = BTreeMap::new();11    for (flat, &cell) in tile.bytes().iter().enumerate() {12        if cell == 0 {13            continue;14        }15        let mut rest = flat;16        let mut weight = 0;17        for &side in shape.iter().rev() {18            weight += rest % side;19            rest /= side;20        }21        *out.entry(weight).or_insert(0) += 1;22    }23    out24}2526/// Counts the filled cells of the tile's level-fold power on every diagonal plane `x_1 + ... + x_D = s`.27///28/// The count is the coefficient of the digit polynomial, the level-fold product of the tile's29/// weight sums, so no cell of the power is ever built; the tile must be a hypercube.30///31/// ```32/// let gasket = mrlymath::bang::factory::create(126, 2, 3, 2, 1).unwrap();33/// let counts = mrlymath::formulas::profile_of_tile(&gasket, 4).unwrap();34/// assert_eq!(counts[15..=30].iter().copied().collect::<Vec<u128>>(), vec![81u128; 16]);35/// ```36pub fn profile_of_tile(tile: &Tensor, level: u32) -> Result<Vec<u128>> {37    let dimension = tile.shape.len();38    let number = tile.shape.first().copied().unwrap_or(0);39    if tile.shape.iter().any(|&side| side != number) {40        return value_error("the profile wants a hypercube tile.");41    }42    if level < 1 {43        return value_error("level must be at least 1.");44    }45    let side = match number.checked_pow(level) {46        Some(side) if side <= WIDEST => side,47        _ => return value_error(format!("the side must stay at or below {WIDEST}.")),48    };49    let weights = histogram(tile);50    let span = dimension * (side - 1) + 1;51    let mut poly = vec![0u128; span];52    poly[0] = 1;53    let mut step = 1usize;54    for _ in 0..level {55        let mut next = vec![0u128; span];56        for (exponent, &count) in poly.iter().enumerate() {57            if count == 0 {58                continue;59            }60            for (&weight, &multiplicity) in &weights {61                let slot = exponent + step * weight;62                match count63                    .checked_mul(multiplicity)64                    .and_then(|added| next[slot].checked_add(added))65                {66                    Some(total) => next[slot] = total,67                    None => return value_error("the slice counts overflow a u128."),68                }69            }70        }71        poly = next;72        step *= number;73    }74    Ok(poly)75}7677#[cfg(test)]78mod tests {79    use super::*;80    use crate::bang::factory;81    #[test]82    fn the_profile_sums_to_the_fill_and_matches_a_rendered_count() {83        for code in [7u128, 9, 11] {84            let tile = factory::create(code, 3, 2, 2, 1).unwrap();85            let counts = profile_of_tile(&tile, 3).unwrap();86            let rendered = factory::create(code, 3, 2, 2, 3).unwrap();87            assert_eq!(counts.iter().sum::<u128>(), u128::from(rendered.sum()));88            let mut direct = vec![0u128; counts.len()];89            for (flat, &cell) in rendered.bytes().iter().enumerate() {90                if cell != 0 {91                    direct[flat / 27 + flat % 27] += 1;92                }93            }94            assert_eq!(counts, direct, "code={code}");95        }96        let tile = factory::create(23, 3, 3, 2, 1).unwrap();97        assert_eq!(profile_of_tile(&tile, 1).unwrap(), [1, 3, 3, 6, 3, 3, 1]);98        assert!(profile_of_tile(&tile, 0).is_err());99        assert!(profile_of_tile(&mrlycore::atoms::ones_3d(2), 20).is_err());100    }101}