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(())
}
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
}
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
6BC7
}
}
}?;