Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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(&registry))
        .write::<Position>()?
        .build()?;

    let walk_query = step_query.clone();
    let mut model = ModelBuilder::new()
        .with_seed(SEED)
        .with_component_registry(Arc::clone(&registry))
        .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_queries derives the system's read/write set from the queries it runs, so the declared access matches what the system touches.
  • DetRng::from_context is 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_population materialises 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.