Expand description
§Object-based Discrete-Event Modeling for Rust
Odem-rs is a discrete-event simulation library for Rust that uses async/await to model concurrent agents. It provides a deterministic, extensible framework for building Monte Carlo-style simulation models where agents interact through scheduled jobs.
§Key Features
- Process-Based Simulation: Models simulation entities as asynchronous agents with isolated state and internally concurrent jobs.
- Event-Driven Execution: Skips directly between significant events rather than executing at fixed time intervals.
- Deterministic & Portable: Uses deterministic PRNGs and cooperative concurrency for reproducible results.
- Flexible & Extensible: Provides library traits to tailor simulators and data structures for custom needs.
- Safe & Sound: Validated with Miri to detect undefined behavior and enforce stacked borrow rules.
- Integrated: Integrates seamlessly with
tracing,uom,rand, andrayon. - Embedded-friendly: Can be used in bare-metal environments with a reduced feature-set.
Try the interactive simulation dashboard to experiment with example models in your browser.
§Getting Started
§Installation
Add odem-rs to your Cargo.toml:
[dependencies]
odem-rs = "0.4"§Example: Barbershop Simulation
To get you started quickly, here is an example scenario featuring a simple barbershop model. Customers arrive at random intervals and request service from a single barber. Once a customer arrives, they occupy the barber for a random duration simulating the haircut, then release the barber for the next customer. The simulation will run for 12 hours.
use odem_rs::prelude::*;
#[derive(Config, Default)]
#[time(Time<f64>)]
struct Barbershop {
joe: Facility,
rng_stream: RngStream,
}
struct Customer(Time<f64>);
impl Behavior<Barbershop> for Customer {
type Output = ();
async fn actions(&self, sim: &Sim<Barbershop>) {
let chair = sim.global().joe.seize().await;
sim.advance(self.0).await;
chair.release();
}
}
#[odem_rs::main]
async fn main(sim: &Sim<Barbershop>) {
sim.fork(async {
let mut rng_a = sim.global().rng_stream.next_rng();
let mut rng_s = sim.global().rng_stream.next_rng();
loop {
let arrival = minute::new(rng_a.random_range(12.0..24.0));
let service = minute::new(rng_s.random_range(12.0..18.0));
sim.advance(arrival).await;
sim.spawn(Customer(service));
}
}).or(sim.advance(hour::new(12.0))).await;
}More examples can be found in the examples directory.
§Execution Model
Odem-rs follows a discrete-event execution model, where simulation advances incrementally based on scheduled events. At any given model time, all continuations (representing agents and jobs) that are scheduled for execution are processed instantaneously in terms of model time before the clock is advanced. Thus, the passing of model time is explicit, allowing for deterministic execution. The execution order is determined by:
- Model time: Monotonically increasing time preserves cause-and-effect relationships between processes.
- Rank (between agents): Defines execution priority among different agents.
- Precedence (between jobs): Ensures the correct sequencing of dependent jobs within one agent.
- Insertion Order: Resolves ties between jobs with equal precedence by executing them in the order they were scheduled.
Once all tasks at a given model time have executed, the simulation clock jumps to the next scheduled event. This execution model is managed by an event calendar integrated into the async/await runtime executor, ensuring deterministic ordering while leveraging Rust’s native concurrency.
§Feature Flags
Odem-rs uses Cargo feature flags to control optional functionality. All flags listed as “default” are enabled when you add odem-rs as a dependency without further configuration. To start from a minimal base, set default-features = false and enable only what you need.
| Feature | Default | Description |
|---|---|---|
std | yes | Standard library support (implies alloc). |
alloc | yes | Heap allocation for dynamic data structures. |
tracing | yes | Structured instrumentation via the tracing crate. |
debug-tracing | no | Debug-level trace output for internal state machines. |
uom | yes | Type-safe SI quantities for model time. |
rand_pcg | yes | PCG-family pseudo-random number generators. |
rand_xoshiro | no | XoShiRo-family pseudo-random number generators. |
rand_distr | yes | Probability distributions from the rand_distr crate. |
rayon | yes | Parallel iteration for independent simulation runs. |
libm | no | Software math functions for no_std environments. |
See the API documentation for detailed descriptions of each feature flag.
§Re-exported Crates
Every external crate whose types appear in the API of odem-rs is re-exported from the crate root, in the version odem-rs was built against. Reach for them through odem-rs rather than depending on them separately: a second copy of any of these crates in a dependency graph is a distinct set of types, and mixing the two produces errors that name the same type twice, or - for tracing - a subscriber that silently receives nothing.
| Crate | Version | Feature | Where it surfaces |
|---|---|---|---|
allocator_api2 | 0.4 | always | the allocator a configuration provides, Config::Alloc |
intrusive_collections | 0.10 | always | the link and adapters that fit continuations into a custom calendar |
rand | 0.10 | always | the generators an RngStream hands out |
uom | 0.38 | uom | the quantities that model time is made of |
tracing | 0.1 | tracing | the events the simulator emits |
rayon | 1 | rayon | the parallel iterator over random number streams |
typed_arena | 2 | alloc | the backing store of a dynamic Pool |
rand_pcg | 0.10 | rand_pcg | the PCG generator behind DefaultRng |
rand_xoshiro | 0.8 | rand_xoshiro | the generator behind DefaultRng when rand_pcg is off |
rand_distr | 0.6 | rand_distr | none of its types, but its distributions must agree with rand on rand_core |
use odem_rs::rand; // always available
use odem_rs::uom; // requires feature "uom"
use odem_rs::rayon; // requires feature "rayon"
use odem_rs::rand_distr; // requires feature "rand_distr"The re-exports do not create that coupling, they make it usable: the signatures already pin the version, and re-exporting is what lets a model name what it is pinned to. It follows that a major-version bump of any of these crates is a breaking change for odem-rs, and reaches models through a new version of it.
§Contributors & Acknowledgments
Odem-rs is the result of doctoral research and has benefited from contributions by:
- Lukas Markeffsky, with whom I enjoyed in-depth discussions on Rust’s type system and whose incredible insight and sharp mind helped a lot to find and resolve soundness issues in addition to improving the ergonomics of the library.
- Paula Wiesner, with whom I performed early prototyping of agent-based approaches and who is still interested in the progress of this project, despite having successfully moved past academia. I appreciate the enthusiasm!
I welcome contributions, feedback, and feature requests! Open an issue or submit a PR.
§License
Odem-rs is licensed under MIT. See LICENSE for details.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in odem-rs by you, shall be licensed as MIT, without any additional terms or conditions.
§Crate Organization
Odem-rs is structured into multiple crates for modularity, which can be accessed from the root crate as follows:
| Crate Name | Description |
|---|---|
odem_rs | Entry crate providing access to the core simulation framework. |
odem_rs::core | Defines the foundational structures and traits for event-driven modeling. |
odem_rs::sync | Provides synchronization and communication primitives for agent-job interaction. |
odem_rs::util | Utility crate with pools, PRNG streams, statistics, and physical quantities. |
The batteries included are the executor with its event calendar, the
concurrency primitives for the inside of an agent, the synchronization
structures (channels and control variables), the PRNG generator
streams and random variables, and the garbage-collected object pools.
The library traits tailor the simulator and its data structures to a
model, and the prelude gathers what most models need. The event
calendar is an extension point of its own: the #[calendar(...)]
attribute of derive(Config) selects an implementation of the
Calendar trait, whose module documentation walks through writing one.
§Running a Model
A model is a configuration type carrying the state the processes share,
and an async function over a simulation context. Start it with the
#[odem_rs::main] attribute, as the barbershop above does: it
writes the main function around the model and reports a deadlock.
Two more explicit entry points sit below it, and a model moves down the list when it outgrows a rung:
| Entry point | Configuration | Returns |
|---|---|---|
#[odem_rs::main] | default or an expression | what the model returns |
simulation | default | the configuration |
Simulator::run | any | what the model returns |
simulation is the one to reach for when a run is a replication among
many, since it hands back the configuration the run wrote its results
into; the RngStream documentation shows an experiment built that way.
§Feature Details
The feature table above lists what there is; this is what each flag does.
§Environment
std: Enables standard library support and impliesalloc. Disable this for bare-metal or WebAssembly targets that do not provide a standard library.alloc: Enables heap-allocated data structures such as dynamic channels, dynamic object pools, and per-type agent identifiers, and selects the global allocator as the default backing for processes created throughSim::spawn. Automatically activated bystd, but can be enabled independently on targets that provide an allocator without the full standard library. Without it, spawned processes require a user-supplied allocator - such as aStaticHeap- configured through the#[alloc]attribute ofderive(Config).libm: Provides software implementations of math functions (e.g.sqrt) via thelibmcrate. Only needed inno_stdenvironments where hardware floating-point or standard-library math is unavailable. Whenstdis active, math functions from the standard library are used instead.
§Integrations
tracing: Integrates with thetracingecosystem to provide structured, span-based instrumentation of agents and continuations. Useful for debugging simulation models and understanding execution flow.debug-tracing: Emits additional debug-level trace events for finite-state machine transitions and channel operations. Atracingsubscriber must be configured for these events to be visible.uom: Enables theuomcrate for modeling time with type-safe SI quantities. With this feature, model time can be expressed using units likehour::new(12.0)orsecond::new(0.5)rather than raw numeric values.
§Random Number Generators
The rand crate is always enabled. The following features control which
PRNG backends are available:
rand_pcg: Provides PCG-family generators (Pcg32,Pcg64Dxsm, etc.) and sets theDefaultRngtype alias toPcg64Dxsmon every target, so that a model draws the same numbers everywhere.rand_xoshiro: Provides XoShiRo-family generators (Xoshiro256PlusPlus, etc.) as an alternative backend, which setsDefaultRngtoXoshiro256PlusPluswhenrand_pcgis off.rand_distr: Re-exports therand_distrcrate with the probability distributions to sample from, in the version that matches the re-exportedrand.
§Parallelism
rayon: Enables parallel iteration support via therayoncrate, allowing independent simulation replications to run across multiple threads.
Re-exports§
pub use odem_rs_core as core;pub use odem_rs_sync as sync;pub use odem_rs_util as util;pub use uom;uompub use tracing;tracingpub use rayon;rayonpub use typed_arena;allocpub use rand_pcg;rand_pcgpub use rand_xoshiro;rand_xoshiropub use rand_distr;rand_distrpub use allocator_api2;pub use intrusive_collections;pub use rand;
Modules§
- error
- This module contains the definition of the combined error-type of the simulation library.
- prelude
- A module for re-exports of simulation-library concepts that are essential for most simulation models.
Attribute Macros§
- main
- Generates a synchronous main function that sets up a simulator, runs the provided asynchronous simulation logic, and handles the results.