A Discrete-Event Simulation (DES) framework for generalised simulation modelling.

An event-driven simulation architecture provides a flexible framework for implementing various simulation modelling paradigms:
- System Dynamics: Events represent the flow of resources or information between interconnected nodes, evolving over time through differential or difference equations. A periodic time-stepping event approximates continuous changes at fixed intervals.
- Agent-Based Modelling (ABM): Events represent agent decisions, message exchanges, and state transitions, allowing agents to interact asynchronously with each other and their environment.
- Discrete-Event Simulation (DES): Events represent state changes occurring at specific points in time, dynamically scheduling the next event without requiring fixed time steps.
- Discrete-Time Simulation (DTS): Events represent updates to the system state at uniform, fixed time steps, ensuring that all changes occur at regular intervals, regardless of necessity.
The framework is inspired by the DEVS (Discrete EVent System Specification) formalism and the SimRS DEVS implementation.
As a first application of the framework, Simcraft provides a domain-specific language (DSL) for easily defining resource flow models as defined in the "Engineering Emergence: Applied Theory for Game Design" paper by Joris Dormans.
The DSL allows you to define processes (e.g., Source, Pool, Drain nodes) and connections (i.e. flows) between them in a declarative way.
use simcraft::dsl::*;
use simcraft::simulator::Simulate;
use simcraft::utils::errors::SimulationError;
fn main() -> Result<(), SimulationError> {
// Create a simulation using the DSL
let mut sim = simulation! {
processes {
source "source1" {}
pool "pool1" {}
}
connections {
"source1.out" -> "pool1.in" {
id: "conn1",
flow_rate: 1.0
}
}
}?;
// Run the simulation for 5 steps
let results = sim.step_n(5)?;
// Process the results
println!("Final time: {}", results.last().unwrap().time);
Ok(())
}
Or equivalent using the framework directly:
use simcraft::prelude::*;
use simcraft::model::nodes::{Source, Pool};
use simcraft::simulator::simulation_trait::StatefulSimulation;
fn main() -> Result<(), SimulationError> {
// Create processes
let source = Source::builder()
.id("source1")
.build()
.unwrap();
let pool = Pool::builder()
.id("pool1")
.build()
.unwrap();
// Create connection
let connection = Connection::new(
"conn1".to_string(),
"source1".to_string(),
Some("out".to_string()),
"pool1".to_string(),
Some("in".to_string()),
Some(1.0),
);
// Create simulation and add processes
let mut sim = Simulation::new(vec![], vec![])?;
sim.add_process(source)?;
sim.add_process(pool)?;
sim.add_connection(connection)?;
// Run the simulation for 5 steps
let _ = sim.step_n(5)?;
// Get final state
let final_state = sim.get_simulation_state();
println!("Final time: {}", final_state.time);
Ok(())
}
Or equivalent using YAML format:
name: "Basic Source to Pool"
description: "Simple example showing a source flowing to a pool"
processes:
- id: "source1"
type: "Source"
triggerMode: "Automatic"
action: "PushAny"
- id: "pool1"
type: "Pool"
triggerMode: "Automatic"
action: "PullAny"
connections:
- id: "conn1"
sourceID: "source1"
targetID: "pool1"
flowRate: 1.0
The DSL supports the following process types:
A source process generates resources.
source "source1" {
// Attributes can be added here
}
A pool process stores resources.
pool "pool1" {
capacity: 10.0 // Optional capacity limit
}
A drain process consumes resources, effectively removing them from the simulation.
drain "drain1" {
// Attributes can be added here
}
A delay process holds resources for a period before releasing them. It can be used to model processing time or transport delays. The delay process supports two modes:
- Delay Mode (default): Each resource is delayed independently for the specified time period before being released.
- Queue Mode: Resources are accumulated and released in fixed amounts after the delay period, like a batch processor.
// Delay mode (default) - each resource is delayed independently
delay "delay1" {
action: DelayAction::Delay // Optional: this is the default
}
// Queue mode - resources are released in batches
delay "delay2" {
action: DelayAction::Queue,
release_amount: 2.0 // Optional: amount to release per cycle (default: 1.0)
}
The delay time is determined by the flow_rate
of the outgoing connection, where a flow rate of 1.0 equals one time unit of delay.
Connections define how resources flow between processes.
"source1.out" -> "pool1.in" {
id: "conn1",
flow_rate: 1.0 // Optional flow rate
}
The connection syntax uses the format "process_id.port"
for both source and target endpoints. If the port is omitted, the default port for the process type is used.
You can use the run_simulation!
macro to create and run a simulation in one step:
let results = run_simulation! {
steps: 5, // Run for 5 steps
processes {
source "source1" {}
pool "pool1" {}
}
connections {
"source1.out" -> "pool1.in" {
id: "conn1",
flow_rate: 1.0
}
}
}?;
Or run until a specific time:
let results = run_simulation! {
until: 10.0, // Run until time = 10.0
processes {
source "source1" {}
pool "pool1" {}
}
connections {
"source1.out" -> "pool1.in" {
id: "conn1",
flow_rate: 1.0
}
}
}?;
let mut sim = simulation! {
processes {
source "source1" {}
source "source2" {}
pool "pool1" {}
}
connections {
"source1.out" -> "pool1.in" {
id: "conn1",
flow_rate: 1.0
}
"source2.out" -> "pool1.in" {
id: "conn2",
flow_rate: 2.0
}
}
}?;
let mut sim = simulation! {
processes {
source "source1" {}
pool "pool1" {
capacity: 3.0 // Pool will not accept more than 3.0 resources
}
}
connections {
"source1.out" -> "pool1.in" {
id: "conn1",
flow_rate: 1.0
}
}
}?;
This example demonstrates both delay modes in a processing chain:
let mut sim = simulation! {
processes {
source "input" {}
delay "individual_processor" {
action: DelayAction::Delay // Process each resource independently
}
delay "batch_processor" {
action: DelayAction::Queue,
release_amount: 3.0 // Process resources in batches of 3
}
drain "output" {}
}
connections {
"input.out" -> "individual_processor.in" {
id: "input_flow",
flow_rate: 1.0
}
"individual_processor.out" -> "batch_processor.in" {
id: "middle_flow",
flow_rate: 2.0 // 2 time units delay
}
"batch_processor.out" -> "output.in" {
id: "output_flow",
flow_rate: 1.0
}
}
}?;
In this example:
- Resources flow from the source to an individual processor that delays each resource by 1 time unit
- Then they pass through a batch processor that accumulates resources and releases them in groups of 3 after a 2 time unit delay
- Finally, the processed resources are consumed by the drain