henad-models 0.3.0

Example models for Henad, a parallel agent-based modelling engine.
Documentation
//! Conway's Game of Life as a [`GridModel`] on a torus.

use henad_compute::cpu::primitives::chunked::{STATS_CHUNK, reduce_chunks};
use henad_core::action::ActionDescriptor;
use henad_core::authoring::model::grid_model::GridModel;
use henad_core::authoring::primitives::rng::{below, next_bits};
use henad_core::grid::Grid2D;
use henad_core::helpers::{extract_f32, f32_param};
use henad_core::params::{ParamDescriptor, ParamValue};
use henad_core::topology::NeighborhoodKind;
use henad_core::view::{StatDescriptor, StatValue};

const DEAD: u8 = 0;
const ALIVE: u8 = 1;

henad_core::params! {
    const DENSITY = f32_param("density", "Initial Density", 0.3, 0.0, 1.0, Some(0.01)).on_reload();
}

// --8<-- [start:actions]
henad_core::actions! {
    const RANDOMISE = ActionDescriptor::new("randomise", "Randomise");
    const CLEAR = ActionDescriptor::new("clear", "Clear");
}
// --8<-- [end:actions]

/// Cell colours, dead then alive, shared with [`GpuGameOfLife`](crate::gpu_game_of_life::GpuGameOfLife).
///
/// Note that the GPU display shader holds its own copy of these colours as WGSL constants.
pub const PALETTE: [[u8; 4]; 2] = [
    [0x15, 0x15, 0x15, 0xFF], // Dead - dark gray
    [0x00, 0xE6, 0x76, 0xFF], // Alive - green
];

/// Conway's Game of Life as a [`GridModel`].
#[derive(Debug)]
pub struct GameOfLifeModel;

impl GridModel for GameOfLifeModel {
    const NAME: &'static str = "Game of Life";
    const ID: &'static str = "game_of_life";
    const DESCRIPTION: &'static str = "Conway's Game of Life on a toroidal grid";
    const PALETTE: &'static [[u8; 4]] = &PALETTE;
    const NEIGHBORHOOD: NeighborhoodKind = NeighborhoodKind::Moore;
    const STATS: &'static [StatDescriptor] = &[StatDescriptor::new("Alive", PALETTE[1])];
    // --8<-- [start:action_specs]
    const ACTIONS: &'static [ActionDescriptor] = ACTION_SPECS;
    // --8<-- [end:action_specs]
    type Params = ();

    fn param_descriptors() -> Vec<ParamDescriptor> {
        descriptors()
    }

    fn from_params(_params: &[ParamValue]) {}

    fn init(grid: &mut Grid2D<u8>, params: &[ParamValue], rng: &mut u64) {
        let density = extract_f32(params, DENSITY, 0.3);
        let threshold = (density * u32::MAX as f32) as u32;
        for cell in grid.current_mut().iter_mut() {
            *cell = if below(next_bits(rng), threshold) { ALIVE } else { DEAD };
        }
    }

    // --8<-- [start:step_cell]
    fn step_cell(cell: u8, neighbors: &[u8], _params: &(), _rng: &mut u64) -> u8 {
        let alive_count: u8 = neighbors.iter().sum();
        match (cell, alive_count) {
            (ALIVE, 2..=3) | (DEAD, 3) => ALIVE,
            _ => DEAD,
        }
    }
    // --8<-- [end:step_cell]

    // --8<-- [start:act]
    /// Randomise runs `init` again, at the density the model was built with.
    fn act(action: usize, grid: &mut Grid2D<u8>, params: &[ParamValue], rng: &mut u64) {
        match action {
            RANDOMISE => Self::init(grid, params, rng),
            CLEAR => grid.current_mut().fill(DEAD),
            _ => {}
        }
    }
    // --8<-- [end:act]

    fn stats(grid: &Grid2D<u8>) -> Vec<StatValue> {
        vec![StatValue::Scalar(count_alive(grid.current()) as f64)]
    }
}

// --8<-- [start:count_alive]
fn count_alive(cells: &[u8]) -> u64 {
    reduce_chunks(
        cells.len(),
        STATS_CHUNK,
        |r| cells[r].iter().filter(|&&c| c == ALIVE).count() as u64,
        |a, b| a + b,
        0,
    )
}
// --8<-- [end:count_alive]

#[cfg(test)]
mod tests {

    use super::*;
    use henad_compute::cpu::grid_engine::GridModelState;
    use henad_core::model::SimState as _;

    #[test]
    fn gol_step_cell_rules() {
        let p = ();
        let mut rng = 1u64;

        // A dead cell with three live neighbours comes alive.
        assert_eq!(
            GameOfLifeModel::step_cell(DEAD, &[1, 1, 1, 0, 0, 0, 0, 0], &p, &mut rng),
            ALIVE
        );
        // A dead cell with two live neighbours stays dead.
        assert_eq!(
            GameOfLifeModel::step_cell(DEAD, &[1, 1, 0, 0, 0, 0, 0, 0], &p, &mut rng),
            DEAD
        );
        // A live cell with two live neighbours survives.
        assert_eq!(
            GameOfLifeModel::step_cell(ALIVE, &[1, 1, 0, 0, 0, 0, 0, 0], &p, &mut rng),
            ALIVE
        );
        // A live cell with three live neighbours survives.
        assert_eq!(
            GameOfLifeModel::step_cell(ALIVE, &[1, 1, 1, 0, 0, 0, 0, 0], &p, &mut rng),
            ALIVE
        );
        // A live cell with one live neighbour dies of underpopulation.
        assert_eq!(
            GameOfLifeModel::step_cell(ALIVE, &[1, 0, 0, 0, 0, 0, 0, 0], &p, &mut rng),
            DEAD
        );
        // A live cell with four live neighbours dies of overpopulation.
        assert_eq!(
            GameOfLifeModel::step_cell(ALIVE, &[1, 1, 1, 1, 0, 0, 0, 0], &p, &mut rng),
            DEAD
        );
    }

    #[test]
    fn gol_blinker_period_2() {
        // Horizontal blinker across the middle row of a 5x5 grid.
        let params = vec![ParamValue::U32(5), ParamValue::U32(5), ParamValue::F32(0.0)];
        let horizontal = {
            let mut cells = vec![DEAD; 25];
            cells[11] = ALIVE;
            cells[12] = ALIVE;
            cells[13] = ALIVE;
            cells
        };
        let vertical = {
            let mut cells = vec![DEAD; 25];
            cells[7] = ALIVE;
            cells[12] = ALIVE;
            cells[17] = ALIVE;
            cells
        };

        let mut state = GridModelState::<GameOfLifeModel>::from_cells(&params, &horizontal)
            .expect("cell buffer matches the declared grid size");

        state.step();
        assert_eq!(
            state.grid_view().expect("grid view").cells,
            &vertical[..],
            "blinker did not rotate on the first tick"
        );

        state.step();
        assert_eq!(
            state.grid_view().expect("grid view").cells,
            &horizontal[..],
            "blinker did not return on the second tick"
        );
        assert_eq!(state.tick(), 2);
    }
}