Skip to main content

henad_core/authoring/model/
agent_model.rs

1//! Authoring API for models whose state is a population of agents.
2
3use crate::action::ActionDescriptor;
4use crate::authoring::model::field::{Extent, FieldLayer};
5use crate::metadata::LaneSpec;
6use crate::params::{ParamDescriptor, ParamValue};
7use crate::spatial_hash::SpatialHash;
8use crate::view::{StatDescriptor, StatValue};
9
10/// Struct-of-arrays agent storage.
11///
12/// The `agent_lanes!` macro writes the impl, and adds the chunked step driver `run_pass` to
13/// the generated type as an inherent method.
14pub trait AgentLanes: Send + Sync + 'static {
15    /// Lanes as declared, for the Model panel.
16    const LANES: &'static [LaneSpec];
17
18    /// Allocates `n` agents, each lane at its initial value.
19    fn alloc(n: usize) -> Self;
20    /// Number of agent slots.
21    fn len(&self) -> usize;
22    /// Returns whether there are no agent slots.
23    fn is_empty(&self) -> bool {
24        self.len() == 0
25    }
26    /// Swaps the double buffered lanes. It does nothing when no lane is double buffered.
27    fn swap(&mut self);
28    /// Heap memory held by the lanes, in bytes.
29    fn heap_bytes(&self) -> usize;
30
31    /// Returns the position lanes, `pos_x` and `pos_y`.
32    ///
33    /// The engine builds the neighbour index and the point view from them.
34    fn positions(&self) -> (&[f32], &[f32]);
35    /// Returns the position lanes, mutably.
36    fn positions_mut(&mut self) -> (&mut [f32], &mut [f32]);
37    /// Extends every lane to length `n`, filling new slots as [`Self::alloc`] does.
38    ///
39    /// Note that this never shrinks the lanes.
40    fn grow(&mut self, n: usize);
41
42    /// Returns one palette index per agent, or `None` to colour the whole population `PALETTE[0]`.
43    fn colors(&self) -> Option<&[u8]> {
44        None
45    }
46}
47
48/// Neighbour lookup rebuilt from agent positions each tick.
49pub trait NeighborIndex: Send + Sync + 'static {
50    /// Name of this index in the Model panel.
51    const KIND: &'static str;
52
53    /// Creates an empty index over `extent`, with cells `cell_size` wide.
54    fn new(extent: Extent, cell_size: f32) -> Self;
55    /// Rebuilds the index from agent positions, with cells `cell_size` wide.
56    fn rebuild(&mut self, pos_x: &[f32], pos_y: &[f32], cell_size: f32);
57    /// Heap memory held by the index, in bytes.
58    fn heap_bytes(&self) -> usize;
59}
60
61/// An index that holds nothing, for a model whose agents never look at one another.
62#[derive(Debug)]
63pub struct NoIndex;
64
65impl NeighborIndex for NoIndex {
66    const KIND: &'static str = "None";
67
68    fn new(_extent: Extent, _cell_size: f32) -> Self {
69        Self
70    }
71
72    fn rebuild(&mut self, _pos_x: &[f32], _pos_y: &[f32], _cell_size: f32) {}
73
74    fn heap_bytes(&self) -> usize {
75        0
76    }
77}
78
79impl NeighborIndex for SpatialHash {
80    const KIND: &'static str = "Spatial hash";
81
82    fn new(extent: Extent, cell_size: f32) -> Self {
83        Self::new(cell_size, extent.w, extent.h)
84    }
85
86    fn rebuild(&mut self, pos_x: &[f32], pos_y: &[f32], cell_size: f32) {
87        // Picks up a live edit to whatever parameter sets the cell size, then reindexes.
88        self.rebuild_with_cell_size(cell_size, pos_x, pos_y);
89        self.build(pos_x, pos_y);
90    }
91
92    fn heap_bytes(&self) -> usize {
93        Self::heap_bytes(self)
94    }
95}
96
97/// A per chunk reduction, merged in chunk order.
98pub trait ChunkTally: Default + Send + Sized + 'static {
99    /// Combines this tally with `other`, the tally that follows it in chunk order.
100    fn merge(self, other: Self) -> Self;
101}
102
103impl ChunkTally for () {
104    fn merge(self, (): Self) {}
105}
106
107/// Saturates at `u32::MAX`. A count over a whole run can pass it, and a wrapped total would look like a small count.
108impl ChunkTally for u32 {
109    fn merge(self, other: Self) -> Self {
110        self.saturating_add(other)
111    }
112}
113
114impl ChunkTally for u64 {
115    fn merge(self, other: Self) -> Self {
116        self + other
117    }
118}
119
120/// Context an agent kernel reads besides its own lanes.
121pub struct StepCtx<'a, A: AgentModel + ?Sized> {
122    /// Field layer, as an agent kernel reads it.
123    pub field: <A::Field as FieldLayer>::Read<'a>,
124    /// Neighbour index, rebuilt from the positions before the step.
125    pub index: &'a A::Index,
126    /// Hot parameters of this tick.
127    pub params: &'a A::Params,
128    /// World size.
129    pub extent: Extent,
130}
131
132impl<A: AgentModel + ?Sized> std::fmt::Debug for StepCtx<'_, A> {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        f.debug_struct("StepCtx")
135            .field("extent", &self.extent)
136            .finish_non_exhaustive()
137    }
138}
139
140/// A population of agents, optionally over a field.
141///
142/// The engine owns lane allocation, double buffering, chunking, seeding, parameter storage, the
143/// views, and the whole `SimState` impl.
144pub trait AgentModel: Send + Sync + 'static {
145    /// Name shown in the UI.
146    const NAME: &'static str;
147    /// Stable id that identifies the model in a model set, on the command line and in a spec file.
148    const ID: &'static str;
149    /// One-line description shown in the UI.
150    const DESCRIPTION: &'static str;
151    /// Agent colours. The field layer carries its own colours.
152    const PALETTE: &'static [[u8; 4]];
153    /// Stat series for the history chart. Declared once, so `stats` returns bare values.
154    const STATS: &'static [StatDescriptor];
155    /// One-off steps the user can trigger. Each gets a button in the Parameters panel.
156    const ACTIONS: &'static [ActionDescriptor] = &[];
157
158    /// Agents per chunk in a step pass.
159    ///
160    /// The chunk index seeds the RNG, so the value is a fixed constant and never derived from the thread count.
161    /// Keep it small enough that a typical population still splits across every core.
162    const CHUNK: usize = 512;
163
164    // Defaults for the three parameters the engine prepends.
165    /// Default of `num_agents`.
166    const DEFAULT_AGENTS: u32;
167    /// Upper bound of `num_agents`.
168    const MAX_AGENTS: u32 = 10_000_000;
169    /// Defaults of `world_width` and `world_height`.
170    const DEFAULT_EXTENT: Extent;
171
172    /// Agent storage, declared with `agent_lanes!`.
173    type Lanes: AgentLanes;
174    /// Grid layer under the population, or [`NoField`](crate::authoring::model::field::NoField) for a model
175    /// without a field.
176    type Field: FieldLayer;
177    /// Neighbour index, [`SpatialHash`] when agents read each other and [`NoIndex`] otherwise.
178    type Index: NeighborIndex;
179    /// Pre-extracted hot parameters, rebuilt once per tick.
180    type Params: Send + Sync;
181    /// Per chunk reduction, accumulated across ticks. `()` when there is nothing to count.
182    ///
183    /// The engine merges each tick's tally into one total for the whole run, and never resets it. A count of events
184    /// per tick therefore grows with the run. A `u32` total stops at `u32::MAX`, and a `u64` holds any count a run
185    /// can reach.
186    type Tally: ChunkTally;
187
188    /// Model parameters. `num_agents`, `world_width` and `world_height` are prepended by the
189    /// engine at indices 0, 1 and 2.
190    fn param_descriptors() -> Vec<ParamDescriptor>;
191    /// Extracts the hot parameters for one tick. `params` is this model's own slice, so its
192    /// indices are 0 based and cannot shift when the engine or a field layer changes.
193    fn from_params(params: &[ParamValue], extent: Extent) -> Self::Params;
194
195    /// Returns the neighbour index's cell size for these params.
196    ///
197    /// The engine reads it every tick, so a live edit takes effect.
198    fn index_cell_size(_params: &Self::Params) -> f32 {
199        1.0
200    }
201
202    /// Fills the lanes for a new run.
203    ///
204    /// `lanes` holds `num_agents` agents at their initial values, and `params` is this model's own slice.
205    fn init(lanes: &mut Self::Lanes, extent: Extent, params: &[ParamValue], rng: &mut u64);
206
207    /// Fills the field's deposit lanes before the step pass, without moving any agent.
208    ///
209    /// The default does nothing.
210    fn run_deposit_pass(
211        _lanes: &Self::Lanes,
212        _deposits: &mut <Self::Field as FieldLayer>::DepositLanes,
213        _ctx: &StepCtx<'_, Self>,
214    ) {
215    }
216
217    /// Runs the step pass and returns its tally.
218    ///
219    /// The body is normally one call to the `run_pass` method that `agent_lanes!` generates, with a per agent kernel.
220    fn run_step_pass(lanes: &mut Self::Lanes, ctx: &StepCtx<'_, Self>, seed: u64, tick: u64) -> Self::Tally;
221
222    /// Runs [`Self::ACTIONS`] entry `action` over the current lanes and field.
223    ///
224    /// Receives the arguments of `init` plus the field. An action is a setup step that the user requests
225    /// mid run. `rng` is a separate stream, so a press leaves the tick's draws where they were.
226    fn act(
227        _action: usize,
228        _lanes: &mut Self::Lanes,
229        _field: &mut Self::Field,
230        _extent: Extent,
231        _params: &[ParamValue],
232        _rng: &mut u64,
233    ) {
234    }
235
236    /// Current statistics, in [`Self::STATS`] order.
237    fn stats(lanes: &Self::Lanes, field: &Self::Field, tally: &Self::Tally) -> Vec<StatValue>;
238}
239
240#[cfg(test)]
241mod tests {
242    use super::ChunkTally;
243
244    /// A run-long `u32` count must saturate. Wrapped, it looks like a small number in a release build.
245    #[test]
246    fn a_u32_tally_saturates() {
247        assert_eq!(ChunkTally::merge(u32::MAX - 1, 5), u32::MAX);
248        assert_eq!(ChunkTally::merge(2_u32, 3), 5);
249    }
250}