skills/simpy/references/real-time.md
Verified 2026-07-23 against SimPy 4.1.2.
simpy.rt.RealtimeEnvironment retains the Event/Process API but delays event
processing so simulation time tracks wall-clock time.
from simpy.rt import RealtimeEnvironment
env = RealtimeEnvironment(
initial_time=0,
factor=0.1,
strict=True,
)
initial_time: starting simulation clock.factor: wall-clock seconds per simulation time unit; must be positive.
1.0: one simulation unit takes one second;0.1: one simulation unit takes 0.1 seconds;60.0: one simulation unit takes one minute.strict=True: raise RuntimeError when processing falls more than the allotted
factor behind.strict=False suppresses deadline failure. It permits drift; it does not make an
overloaded simulation accurately synchronized.
import time
from simpy.rt import RealtimeEnvironment
def ticker(env, count):
for index in range(count):
before = time.monotonic()
yield env.timeout(1)
elapsed = time.monotonic() - before
print(index, env.now, elapsed)
env = RealtimeEnvironment(factor=0.05, strict=True)
process = env.process(ticker(env, count=3))
env.run(until=process)
Use time.monotonic() for elapsed wall time. Calendar time can jump.
Callbacks and generator code execute synchronously on the simulation thread. Slow computation, blocking I/O, logging, garbage collection, OS scheduling, and loaded CI hosts all consume the real-time budget.
import time
import simpy.rt
def slow(env):
time.sleep(0.02)
yield env.timeout(1)
env = simpy.rt.RealtimeEnvironment(factor=0.01, strict=True)
env.process(slow(env))
try:
env.run()
except RuntimeError:
print("simulation missed its real-time budget")
This sleep() intentionally demonstrates a missed deadline. Do not put blocking
sleep in ordinary SimPy process logic to represent simulated delay; use
env.timeout().
Real-time execution is useful when wall-clock synchronization is intrinsic:
It is usually inappropriate for Monte Carlo replications: normal Environment
runs faster, is less affected by host load, and preserves the same model-time
logic.
SimPy itself is single-threaded and does not make external I/O asynchronous. Integrating devices or services requires an explicit adapter and failure model. Document:
The bundled skill CLIs intentionally provide no device, network, plugin, or external-service integration.
Choose a real origin and compare expected elapsed wall time with actual monotonic elapsed time:
import time
origin_real = time.monotonic()
origin_sim = env.now
def drift_seconds(env, factor):
expected = (env.now - origin_sim) * factor
actual = time.monotonic() - origin_real
return actual - expected
Define sign, sampling instant, percentile, maximum allowed lag, and platform before testing. A mean near zero can hide large deadline misses.
Environment.time.monotonic() and broad platform-aware bounds.strict=True deadline detection and intentional strict=False lag.Example tolerant assertion:
start = time.monotonic()
env = RealtimeEnvironment(factor=0.02, strict=False)
env.run(until=env.timeout(2))
elapsed = time.monotonic() - start
assert elapsed >= 0.02
assert elapsed < 1.0
The upper bound is deliberately generous; tune it for controlled hardware, not a busy shared runner.
Real-time environments inherit Environment.run() semantics:
until can run forever;until excludes normal events at the boundary;until returns the Event value;peek()/step() remain available.Always bound real-time runs. A tiny factor combined with a huge event count can still consume substantial CPU, and a huge factor can make a short model wait for a long wall duration.
strict=False can accumulate unbounded lag.See sources.md for the 4.1.2 real-time topical guide and simpy.rt API.