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}