henad-core 0.3.0

Model authoring traits, primitives and shared types of Henad, a parallel agent-based modelling engine.
Documentation
//! Authoring API for network models, where nodes are joined by edges.
//!
//! Each tick runs a sequential global pass that can change the graph,
//! followed by a parallel node pass that can only read it.

use crate::action::ActionDescriptor;
use crate::authoring::model::agent_model::AgentLanes;
use crate::authoring::model::field::Extent;
use crate::network::Network;
use crate::params::{ParamDescriptor, ParamValue};
use crate::view::{StatDescriptor, StatValue};

// --8<-- [start:spring_params]
/// Constants for the spring layout, in units of the mean spacing between nodes.
#[derive(Clone, Copy, Debug)]
pub struct SpringParams {
    /// Strength of the pull along an edge, per unit of stretch.
    pub spring: f32,
    /// Rest length of an edge.
    pub length: f32,
    /// Strength of the push between nearby nodes.
    pub repulsion: f32,
    /// Distance beyond which nodes stop repelling each other.
    pub cutoff: f32,
    /// Stretch at which an edge's pull levels off.
    ///
    /// A longer edge pulls no harder than this, so a few long edges cannot fold the whole layout.
    pub saturation: f32,
}

impl SpringParams {
    /// Default constants, balanced so that a joined pair settles a little above the mean spacing.
    ///
    /// Note that much more `repulsion` pushes every node to the walls.
    pub const DEFAULT: Self = Self {
        spring: 0.3,
        length: 1.0,
        repulsion: 0.05,
        cutoff: 2.5,
        saturation: 1.0,
    };
}
// --8<-- [end:spring_params]

// --8<-- [start:nodes]
/// Mutable access to the lanes, the graph and the model's own state.
///
/// Nodes should be added and removed through [`Self::spawn`] and [`Self::retire`],
/// which keep the lanes and the graph the same length.
pub struct Nodes<'a, N: NetworkModel + ?Sized> {
    /// Node lanes, one slot per node, retired ones included.
    pub lanes: &'a mut N::Lanes,
    /// Graph joining the nodes.
    pub graph: &'a mut Network,
    /// Model's own state.
    pub aux: &'a mut N::Aux,
}
// --8<-- [end:nodes]

impl<N: NetworkModel + ?Sized> std::fmt::Debug for Nodes<'_, N> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Nodes")
            .field("graph", &self.graph)
            .finish_non_exhaustive()
    }
}

impl<N: NetworkModel + ?Sized> Nodes<'_, N> {
    /// Spawns a new node and returns its index, growing every lane to fit.
    ///
    /// Note that a reused slot keeps the lane values of the node previously retired from it.
    pub fn spawn(&mut self) -> u32 {
        let i = self.graph.spawn();
        self.lanes.grow(self.graph.slot_count());
        i
    }

    /// Retires node `i` along with its edges.
    ///
    /// Its position is set to `NaN`, which marks it as absent for drawing and export.
    pub fn retire(&mut self, i: u32) {
        self.graph.retire(i);
        let (pos_x, pos_y) = self.lanes.positions_mut();
        if let (Some(x), Some(y)) = (pos_x.get_mut(i as usize), pos_y.get_mut(i as usize)) {
            *x = f32::NAN;
            *y = f32::NAN;
        }
    }
}

// --8<-- [start:node_ctx]
/// The graph, hot parameters and extent, shared by every node kernel.
pub struct NodeCtx<'a, N: NetworkModel + ?Sized> {
    /// Graph joining the nodes.
    pub graph: &'a Network,
    /// Hot parameters of this tick.
    pub params: &'a N::Params,
    /// World size.
    pub extent: Extent,
}
// --8<-- [end:node_ctx]

impl<N: NetworkModel + ?Sized> std::fmt::Debug for NodeCtx<'_, N> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NodeCtx")
            .field("graph", &self.graph)
            .field("extent", &self.extent)
            .finish_non_exhaustive()
    }
}

/// A population of nodes joined by edges.
pub trait NetworkModel: Send + Sync + 'static {
    /// Name shown in the UI.
    const NAME: &'static str;
    /// Stable id that identifies the model in a model set, on the command line and in a spec file.
    const ID: &'static str;
    /// One-line description shown in the UI.
    const DESCRIPTION: &'static str;
    /// Node colours, indexed by the colour lane.
    const PALETTE: &'static [[u8; 4]];
    /// Edge colours, indexed by each edge's colour byte.
    const EDGE_PALETTE: &'static [[u8; 4]];
    /// Stat series shown in the history chart.
    const STATS: &'static [StatDescriptor];
    /// One-off actions, each shown as a button in the Parameters panel.
    const ACTIONS: &'static [ActionDescriptor] = &[];

    /// Number of nodes per chunk in the node pass. Each chunk gets its own RNG stream.
    const CHUNK: usize = 512;

    /// Default of `num_agents`, the node count.
    const DEFAULT_NODES: u32;
    /// Upper bound of `num_agents`.
    const MAX_NODES: u32 = 10_000_000;
    /// World the layout spreads nodes over. It is used for drawing only.
    const DEFAULT_EXTENT: Extent;
    /// Constants of the spring layout.
    const LAYOUT: SpringParams = SpringParams::DEFAULT;

    /// Node lanes, declared with `agent_lanes!`.
    type Lanes: AgentLanes;
    /// Hot parameters, extracted once per tick.
    type Params: Send + Sync;
    /// Model state kept outside the lanes and the graph, or `()` for a model without such state.
    type Aux: Default + Send + 'static;

    /// Returns the model's own parameters, which follow the engine's `num_agents`, `world_width` and `world_height`.
    fn param_descriptors() -> Vec<ParamDescriptor>;
    /// Extracts the hot parameters for one tick from this model's own slice of the params.
    fn from_params(params: &[ParamValue], extent: Extent) -> Self::Params;

    /// Returns whether edges are directed. The engine reads it every tick.
    fn directed(_params: &Self::Params) -> bool {
        false
    }

    /// Seeds the lanes and the edges.
    ///
    /// The graph starts with every node and no edges.
    fn init(nodes: &mut Nodes<'_, Self>, extent: Extent, params: &[ParamValue], rng: &mut u64);

    /// Runs a sequential pass before the node pass.
    ///
    /// Within a tick, this is the pass that can change the graph.
    fn run_global_pass(
        _nodes: &mut Nodes<'_, Self>,
        _params: &Self::Params,
        _extent: Extent,
        _rng: &mut u64,
        _tick: u64,
    ) {
    }

    /// Runs a parallel pass over every slot, including retired ones.
    ///
    /// This is usually one call to `lanes.run_pass`.
    fn run_node_pass(_lanes: &mut Self::Lanes, _ctx: &NodeCtx<'_, Self>, _seed: u64, _tick: u64) {}

    /// Runs entry `action` of [`Self::ACTIONS`] between ticks, drawing from its own RNG stream.
    fn act(_action: usize, _nodes: &mut Nodes<'_, Self>, _extent: Extent, _params: &[ParamValue], _rng: &mut u64) {}

    /// Prepares state for drawing.
    ///
    /// The engine calls it before each snapshot.
    fn prepare_view(_nodes: &mut Nodes<'_, Self>, _tick: u64) {}

    /// Returns the current statistics, in [`Self::STATS`] order.
    ///
    /// A stat that needs to traverse the graph should be computed in [`Self::prepare_view`] and kept in `aux`.
    fn stats(lanes: &Self::Lanes, graph: &Network, aux: &Self::Aux) -> Vec<StatValue>;

    /// Returns the heap memory held by `aux`, in bytes.
    ///
    /// The count is approximate, like [`crate::model::SimState::heap_bytes`].
    fn aux_heap_bytes(_aux: &Self::Aux) -> usize {
        0
    }
}