Declarative API
The SimulationContext acts as the entry point and configuration builder for standard Dynamic DES simulations. It implements the Builder Pattern to construct the simulation environment and register resources, statistical samplers, and ingress/egress connectors.
Example
The Standard API uses the SimulationContext builder to configure the twin's resources, distributions, ingress/egress parameters, and I/O connectors.
All execution logic is declared using clean Python decorators:
from dynamic_des import SimulationContext, ConsoleEgress
app = (
SimulationContext(sim_id="Line_A", factor=1.0)
.add_resource("lathe", current_cap=2, max_cap=5)
.add_arrival("standard", dist="exponential", rate=1.0)
.add_service("milling", dist="normal", mean=3.0, std=0.5)
.add_egress(ConsoleEgress())
)
@app.arrival_loop("standard")
def generate(context: SimulationContext):
task_id = 0
while True:
yield context.wait_for_arrival("standard")
context.spawn(run_task(task_id))
task_id += 1
@app.task(service_id="milling", resource_id="lathe")
def run_task(task_id: int):
# This function is automatically wrapped with:
# 1. Emission of a "queued" event to Kafka/Console.
# 2. Block until the "lathe" resource is acquired.
# 3. Emission of a "started" event.
# 4. Yield of the "milling" timeout (sampled from the distribution).
# 5. Emission of a "finished" event with the dictionary returned below.
return {"part_id": task_id}
Builder Lifecycle
Creating a simulation with SimulationContext follows a strict two-phase lifecycle to guarantee thread safety and determinism:
1. Builder Phase (Configure)
└── Register resources, distributions, and ingress/egress
2. Compilation Phase (app.run)
├── Instantiate DynamicRealtimeEnvironment
├── Start background network/egress threads
└── Boot the SimPy clock and event loop
- Builder Phase (Pre-Compilation): You register resources, services, and connectors. Everything is held as passive configuration data structures (
SimParameter,DistributionConfig, etc.) in memory. - Compilation & Execution Phase (Post-Compilation): The moment you call
app.run(...), the builder compiles the configuration. It instantiates theDynamicRealtimeEnvironment, boots background network connector threads, and starts the SimPy event loop.
Architectural Guarantee: You cannot call runtime helpers like
context.spawn(),context.get_resource(), orcontext.envduring the Builder Phase. Attempting to do so will raise aRuntimeError. This prevents partial state leaks and guarantees that the environment clock is strictly controlled.
Builder API Reference
The fluent builder API allows chaining configurations:
Resource Configuration
.add_resource(name, current_cap, max_cap): Stages a discrete token-based resource (e.g. machines, work areas)..add_container(name, current_cap, max_cap): Stages a continuous fluid-like container (e.g. storage tanks, battery charge levels)..add_variable(name, value): Stages generic variables or physical parameters (e.g. system conveyor speeds).
Clock and Pacing
SimulationContext(sim_id, factor=1.0, random_seed=None, logical_start_time=None):factorsets the pacing (0.0 runs unpaced),random_seedseeds the sharedSampler, andlogical_start_timesets the instant that simulation time 0 maps to in every record'stimestamp.SimulationContext(..., go_live_at): Logical instant, a datetime on the same clock aslogical_start_time, at whichfactorgives way to real-time pacing. One run can therefore backfill unpaced and then tail live. See Backfill Then Go Live in One Run.
Statistical Profiles
.add_arrival(name, dist, rate, mean, std): Configures an inter-arrival time distribution..add_service(name, dist, mean, std, rate): Configures a task processing duration distribution.
Decorators
@app.task(service_id, resource_id): Wraps a function into a task that emitsqueued, waits for the resource, emitsstarted, waits a time sampled from the service, and emits the finished event. The finished event's value is exactly the dictionary the function returns, so returnstatusorpath_idyourself if consumers need them.@app.arrival_loop(arrival_id): Starts a generator function with the context when the run starts, typically a loop overcontext.wait_for_arrival(arrival_id).@app.telemetry_loop(interval): Calls a function with the context everyintervalsimulation seconds.
Processes
.add_process(func, **kwargs): Starts a generator function when the run starts, called asfunc(context, **kwargs). Use it for a process that is not an arrival or telemetry loop, such as a drift engine. It is the builder form ofcontext.spawn(), which only works once the run has started..compile_parameters(): Returns theSimParameterthatrun()registers, so the registry paths a configuration creates can be checked before the run. Registry paths lists them.
Connectors & Ingestion
.add_ingress(provider): Attaches an ingress connector (e.g.LocalIngressorKafkaIngress) to stream live configuration updates into the switchboard..add_egress(provider, when=None, batch_size=None, flush_interval=None): Attaches an egress connector (e.g.ConsoleEgressorKafkaEgress) to publish event and telemetry streams. Every attached provider receives every record, so a stream sink and a lake sink can be written in one pass. Passwhento give a provider a predicate and route records instead, for example the hot tail to Kafka and cold history to Parquet. Passbatch_sizeorflush_intervalto give one provider its own cadence, so a stream sink can flush small and often while a lake sink writes large files..with_batching(batch_size, flush_interval): Sets the default queue batching size and flush timeout for highly efficient I/O. Every provider uses these unlessadd_egressoverrides them..with_batching(..., max_queued_batches, drain_stall_seconds): Bounds the egress queue and sets how long teardown keeps waiting for it. The queue is bounded so a sink that cannot keep up slows the simulation instead of building a backlog, and teardown drains until the queue stops shrinking rather than abandoning it on a fixed deadline. A sink that stops consuming altogether raisesRuntimeErrorrather than losing events silently.
Building from YAML
SimulationContext.from_yaml(path) builds a context from a YAML blueprint. The file is validated first, and every error names the file and line. The context is then built with the builder methods above, in the order a script calls them, so the result is an ordinary SimulationContext.
app = SimulationContext.from_yaml("examples/yaml/local.yaml")
app.run() # uses run.until from the file
run() calls the blueprint's run.before functions first, and uses run.until when no until is passed. The ddes run command does the same from a shell.