henad 0.3.0

A parallel agent-based modelling engine for millions of agents on one machine, with its app and its command line.
Documentation

henad

Henad is a parallel agent-based modelling engine, built to run millions of agents at interactive speeds on one machine.

This is the crate that a program depends on. It re-exports the engine, the sweeps and the model authoring API under one module tree, with the example models, the app and the command line behind features.

Feature Adds
example-models The ten example models, at henad::models
app The app as a library, at henad::app
cli The command line as a library, at henad::cli, on native targets
testing The checks a model's tests run against its entry, at henad::testing

No feature is on by default, and no feature changes a result.

A complete program

The program below builds an example model, runs it, reads its statistics, edits a parameter live, fires an action, runs a small sweep, reads the sweep back, rebuilds one run, and opens the app on it.

// Cargo.toml: henad = { version = "0.3", features = ["example-models", "app"] }
// Copy the template's profile block, `[profile.dev] opt-level = 1`, `[profile.dev.package."*"] opt-level = 2` and
// `[profile.release] opt-level = 2`. Without it a debug build runs the example kernels at opt-level 0, and a release
// build at 3 where Henad measures at 2. A crate that registers its own models also calls `henad_build::stamp_commit()`
// from build.rs, with henad-build under [build-dependencies]. This example registers no models.

use std::io::Write;
use std::ops::ControlFlow;
use std::path::Path;

use henad::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    henad::install_panic_hook();
    let models = henad::models::example_models();
    // A sweep rejects a folder that already holds results. Each run of the program starts from an empty folder.
    let folder = std::env::temp_dir().join("sir-rates");
    if folder.exists() {
        std::fs::remove_dir_all(&folder)?;
    }
    let replay = study(&models, &folder, &mut std::io::stdout())?;

    // Then open the app on the run the study rebuilt.
    let options = AppOptions::new(models, "SIR study", henad::build_info!()).opening(AppOpening::Run {
        replay,
        open_at: OpenAt::Start,
    });
    henad::app::run_native(options)?;
    Ok(())
}

/// Runs SIR from `models`, sweeps it into `folder` and rebuilds one run of the sweep, writes what it finds to `out`,
/// and returns the rebuilt run's replay.
fn study(models: &ModelSet, folder: &Path, out: &mut impl Write) -> Result<Replay, Box<dyn std::error::Error>> {
    let sir = models.get("sir").ok_or("the example set lacks SIR")?;

    // SIR on a 256 by 256 grid with seed 7. One cell in a thousand starts infected, an outbreak at tick 50 adds more,
    // and an infection rate of 0.05 spreads the epidemic slowly up to tick 100.
    let mut simulation = sir
        .setup()
        .set("grid_width", 256u32)?
        .set("grid_height", 256u32)?
        .set("initial_infected_pct", 0.001f32)?
        .set_text("infection_rate", "0.05")?
        .with_seed(7)
        .act_at("seed_outbreak", 50)?
        .build(None)?;
    simulation.run_to(100)?;
    let stats = simulation.stats()?;
    let susceptible = stats.scalar("Susceptible");
    writeln!(out, "tick {}: {susceptible:?} susceptible", stats.tick())?;

    // A more infectious variant arrives. A live edit raises the infection rate and a second outbreak seeds it. Up to
    // 400 more ticks follow, sampled every 20, stopping once nobody is infected.
    simulation.set_param("infection_rate", 0.3f32)?;
    simulation.act("seed_outbreak")?;
    let mut samples = Vec::new();
    let flow = simulation.run_sampled(500, 20, |sample| {
        samples.push((sample.tick(), sample.scalar("Susceptible"), sample.scalar("Infected")));
        match sample.scalar("Infected") {
            Some(infected) if infected < 1.0 => ControlFlow::Break(sample.tick()),
            _ => ControlFlow::Continue(()),
        }
    })?;
    for (tick, susceptible, infected) in samples {
        writeln!(out, "tick {tick}: {susceptible:?} susceptible, {infected:?} infected")?;
    }
    if let ControlFlow::Break(tick) = flow {
        writeln!(out, "the epidemic ended by tick {tick}")?;
    }

    // Three infection rates, four replicates each, written to `folder`.
    let loaded = LoadedSpec::parse(
        r#"
        model = "sir"
        [set]
        grid_width = 128
        grid_height = 128
        [run]
        steps = 300
        replicates = 4
        [[block]]
        design = "factorial"
        factors = [{ param = "infection_rate", values = [0.2, 0.3, 0.4] }]
        "#,
    )?;
    let mut options = SweepOptions::new(Provenance::new(henad::build_info!(), std::env::args().collect()));
    options.spec_source = loaded.spec_source.clone();
    options.apply_execution(&loaded.execution);
    let record = run_spec(
        sir,
        None,
        &loaded.spec,
        SweepOutput::Directory(folder.to_owned()),
        &options,
        &mut NoProgress,
    )?;
    let counts = &record.report.counts;
    writeln!(out, "{} of {} runs ok", counts.ok, counts.rows)?;

    // Read the folder back and rebuild run 5 headlessly.
    let results = ResultSet::open_dir(folder, 64 << 20)?;
    let replay = results.replay(sir.schema(), 5)?;
    let mut rebuilt = RunSetup::from_replay(sir, &replay)?.build(None)?;
    rebuilt.run_to(replay.ticks)?;
    let infected = rebuilt.stats()?.scalar("Infected");
    writeln!(out, "run 5 ends with {infected:?} infected")?;
    Ok(replay)
}

The user guide covers the app, sweeps and writing your own models.

License

Licensed under MIT or Apache-2.0, at your option.