Your first model
This chapter builds the first_model example: a population of walkers on an
integer number line. Each tick, every walker takes one deterministic step left
or right; a reduction then reports the population's spread.
Run it with:
cargo run --example first_model --features model
The code below is included from the compiled example. The full source is
examples/first_model.rs.
The component
A component is a plain Copy struct. Each walker holds one integer position:
#![allow(unused)] fn main() { use std::sync::{Arc, RwLock}; use syren::agents::AgentTemplate; use syren::model::ModelBuilder; use syren::{advanced::EntityShards, ComponentRegistry, DetRng, FnSystem, QueryBuilder, Welford}; /// Each walker holds a single integer position on a number line. #[derive(Clone, Copy, Default)] struct Position { x: i64, } }
Building the model
Register the component, freeze the registry, then describe the population and
the system with ModelBuilder. The system's access set is derived from the
query it runs, and the model seed is set with
with_seed:
#![allow(unused)] fn main() { // Register the component up front, then freeze the registry so archetype // layouts are fixed for the life of the model. let registry = Arc::new(RwLock::new(ComponentRegistry::new())); let position_id = registry.write().unwrap().register::<Position>()?; registry.write().unwrap().freeze(); // Every walker starts at the origin. let population: Vec<Position> = vec![Position { x: 0 }; WALKERS]; // A query that writes `Position`. The system derives its access set from // this query (`from_queries`), so the declared access cannot drift from // what the system actually touches. let step_query = QueryBuilder::with_registry(Arc::clone(®istry)) .write::<Position>()? .build()?; let walk_query = step_query.clone(); let mut model = ModelBuilder::new() .with_seed(SEED) .with_component_registry(Arc::clone(®istry)) .with_shards(EntityShards::new(1)?) .with_agent_template( AgentTemplate::builder("walker") .with_component::<Position>(position_id)? .with_capacity(WALKERS) .build(), )? .with_agent_population("walker", position_id, population)? .with_system(FnSystem::from_queries( 0, "random_walk", &[&step_query], move |ecs| { // `run_context` carries the seed, tick, and system id. Salting a // per-entity `DetRng` with the entity's identity makes each // walker's step independent of the order workers visit rows in, // so the walk is identical whatever the thread count. let context = ecs.run_context(); ecs.for_each_entity_w1::<Position>(walk_query.clone(), move |entity, pos| { let mut rng = DetRng::from_context(context, u64::from(entity.index())); pos.x += if rng.next_below(2) == 0 { -1 } else { 1 }; }) }, )) .build()?; }
This build shows three points:
FnSystem::from_queriesderives the system's read/write set from the queries it runs, so the declared access matches what the system touches.DetRng::from_contextis salted with the entity's identity, so a walker's step does not depend on the order in which worker threads visit rows. The same seed produces the same walk at any thread count.with_agent_populationmaterialises the whole component column at once rather than spawning agents individually.
Running and summarising
Run a fixed number of ticks, then reduce over the population. Welford
accumulates count, mean, and variance in a single pass and combines partials
from parallel chunks:
#![allow(unused)] fn main() { model.run(TICKS)?; }
#![allow(unused)] fn main() { // A single-pass reduction over the population: `Welford` accumulates count, // mean, and variance and combines partials from parallel chunks stably. let summary_query = QueryBuilder::with_registry(registry) .read::<Position>()? .build()?; let stats = model.ecs().world_ref().reduce_read::<Position, Welford>( summary_query, Welford::default, |acc, pos| acc.push(pos.x as f64), |acc, other| acc.combine(other), )?; println!( "after {TICKS} ticks: count={} mean={:.4} variance={:.4}", stats.n, stats.mean, stats.variance() ); }
For a symmetric ±1 walk over 50 ticks, the mean is near zero and the variance is near the number of steps.
Next: running ticks and reading and recording results.