simul is a discrete-event simulation library for running high-level
simulations of real-world problems and for running simulated experiments.
simul is a discrete-event simulator using incremental time progression,
with M/M/c queues for interactions
between agents. It also supports deterministic typed experiments and seeded
searches over caller-defined candidate spaces.
Use-cases:
- Discrete-event simulation
- Complex adaptive systems
- Random search
- Job-shop scheduling
- Birth-death processes
- Computer experiments
- Other: simulating logistics, operations research problems, running experiments to approximate a global optimum, simulating queueing systems, distributed systems, performance engineering/analysis, and so on.
Warning
Experimental and unstable. Almost all APIs are expected to change.
- For some examples, see the
examplessubdirectory. - For use cases where your agents need their own custom state, define a struct,
implement
Agent, and pass your agents intoSimulationvia constructingAgentInitializers.
experiment is independent of the agent engine. The consolidated API below is
available in this checkout; it has not been published as a new crates.io release.
- Schedule explicit
TrialSpecvalues withCandidateIdandReplicationId.run_serial_boundedrejects duplicate identities and oversized batches before evaluation, and returns records ordered byTrialId. - Replay uses the unchanged V1 keyed BLAKE3/ChaCha12 protocol. Retain
ReplayKey, candidate inputs, and evaluator configuration. Named common-random streams share noise across candidates;ValidationHoldoutseparates validation noise. - With
parallel,run_paralleluses a dedicated bounded Rayon pool and one mutable workspace per contiguous shard. Its factory isFnMut() -> Result<W, WE>, called on the caller thread. Explicit batch/output storage and the pool are prepared before factories, and every factory succeeds before evaluation starts. Requested workers are capped by work and available parallelism. Evaluators must use only their inputs, context, immutable configuration, and unobservable scratch for worker-count-independent results. Shards write directly into disjoint ranges of one final result allocation; initialization guards drop partial results on failure without a second buffer. - Setup failures are run-level errors; evaluator
Errvalues remain ordered trial records. With unwinding enabled, callback panics discard partial output and return a run-level error without retries. This does not roll back callback side effects, and cannot catch panics inpanic = "abort"builds. aggregate_scalarandaggregate_bernoulliapply explicit failure policies. Unrepresentable scalar arithmetic returnsArithmeticOverflow, never a successful non-finite summary.search::SeededRandomSearchprovides tokenized ask/tell search.CrossEntropyOptimizerandsimulated_annealing_search_with_rngremain engine-independent optimizer alternatives; aggregate noisy trials inside their objective rather than ranking a lucky single run.
The duplicate experiment::replicated API and its SplitMix64/Box–Muller replay
protocol are removed, not aliased. Old replay keys are not interchangeable with
V1 BLAKE3/ChaCha12 keys. The TrialKey alias is removed in favor of TrialId;
the simulation-coupled monte_carlo_search and simulated_annealing_search
wrappers are removed. See examples/simple_experiment.rs and
benches/deterministic_search_benchmark.rs for the typed replacement.
[dependencies]
simul = "0.5.1"use simul::agent::*;
use simul::Simulation;
use simul::SimulationParameters;
/// Example of a minimal, simple Simulation that can be executed.
fn main() {
// Runs a simulation with a producer that produces work at every tick of
// discrete time (period=1), and a consumer that cannot keep up (can only
// process that work every third tick).
let mut simulation = Simulation::new(SimulationParameters {
// We pass in two agents:
// `producer`: produces a message to the consumer every tick
// `consumer`: consumes w/ no side effects every second tick
// Agents are powerful, and you can pass-in custom implementations here.
agent_initializers: vec![
periodic_producer("producer", 1, "consumer"),
periodic_consumer("consumer", 2),
],
// We pass in a halt condition so the simulation knows when it is finished.
// In this case, it is "when the simulation is 10 ticks old, we're done."
halt_check: |s: &Simulation| s.time() == 10,
..Default::default()
});
// For massive simulations, you might block on this line for a long time.
simulation.run();
// Post-simulation, you can do analytics on the stored metrics, data, etc.
simulation
.agents()
.iter()
.for_each(|agent| println!("{agent:#?}"));
}- A simulation is a collection of
Agentsthat interact with each other viaMessages. - The simulation keeps a discrete time (u64) which is incremented on each tick of the Simulation.
- What an
Agentdoes at each tick of the simulation is provided by you in itson_tick()andon_message()methods. Agentsmust have a unique name.- If an
Agentwants to interact with anotherAgent, it can send aMessagevia the&mut ctx: AgentContextpassed intoon_tickandon_message.
The simulation runs all the logic of calling process(), distributing messages,
tracking metrics, incrementing time, and when to halt. A Simulation is
finished when the provided halt_check function returns true, or if an
Agent responds with a special Interrupt to halt the Simulation.
Create initial messages with Message::new or Message::new_with_payload, not
struct literals. Message clones share strings and payloads until modified;
timestamps remain independent. Field reads and copy-on-write field updates keep
their ordinary value semantics.
Here's an example of an outputted graph from a simulation run. In this
simulation, we show the average waiting time of customers in a line at a
cafe. The customers arrive at a Poisson-distributed arrival rate
(lambda<-60.0) and a Poisson-distributed coffee-serving rate with the
same distribution.
This simulation maps to the real world by assuming one tick of discrete-simulation time is equal to one second.
Basically, the barista serves coffees at around 60 seconds per drink and the customers arrive at about the same rate, both modeled by a stochastic Poisson generator.
This simulation has a halt_check condition of the simulation's time
being equal to 60*60*12, representing a full 12-hour day of the cafe
being open.
Issues, bugs, features are tracked in TODO.org
