Time and Pacing
A run has two clocks, and a run can follow the wall clock, ignore it, or switch from one to the other. This page shows the clock settings, the warm-up, how long a run lasts, and what the environment runs on which thread.
What It Is
Simulation time is the number of seconds SimPy has advanced, starting at 0; every record carries it as sim_ts. Logical time is the date and time a record carries as timestamp: logical_start_time plus the simulation time. factor sets how simulation time relates to the wall clock, and go_live_at switches that relation once during a run. One hour of simulation time is one hour of logical time, and takes as long on the wall clock as factor says:

The settings are the same on DynamicRealtimeEnvironment, on SimulationContext and in the simulation section of a blueprint.
How to Use It
Logical Start Time
logical_start_time is the date and time that simulation time 0 maps to; without it, the run uses the moment the environment is created. A run with a backdated start writes history:
app = SimulationContext(
sim_id="Shop",
factor=0.0,
logical_start_time=datetime.now() - timedelta(days=7), # a week of history
)
A naive datetime gives timestamps with no time zone, and a timezone-aware one gives timestamps with its offset. In a blueprint it takes now, a signed duration such as -7d, or an ISO datetime, as YAML Blueprints describes.
Factor
The wall-clock duration of a stretch of the run is its simulation duration times factor:
live = DynamicRealtimeEnvironment(factor=1.0) # 1 simulated second takes 1 real second
twice_as_fast = DynamicRealtimeEnvironment(factor=0.5) # takes half a real second
unpaced = DynamicRealtimeEnvironment(factor=0.0) # takes no real time at all
factor: 1.0follows the wall clock: a process that waits 5 simulation seconds waits 5 real seconds. It suits a live twin feeding a dashboard.factor: 0does not wait at all: the run goes as fast as the machine allows. It suits history, tests and experiments.
Go Live
factor applies until the logical clock reaches go_live_at, and from that instant the run is paced at one simulation second per real second. With a backdated logical_start_time and factor: 0, one run writes the history as fast as it can, then keeps going in real time:
go_live_at = datetime.now()
env = DynamicRealtimeEnvironment(
factor=0.0, # until go_live_at
logical_start_time=go_live_at - timedelta(days=7),
go_live_at=go_live_at, # from here, 1 simulated second per real second
)
A sink with when: history receives the records stamped before go_live_at, and one with when: live the rest, so the two halves can go to different places:

Parquet History, Kafka Live builds such a run.
Warm-up
warmup leaves the start of a run out of what it reports. It is one time per run, in simulation seconds or a duration. No sink receives a record or telemetry stamped before it, and the built-in statistics are reset once at that moment, so they describe the rest of the run. A run without it sends everything, which is what a data generator usually wants.
The warm-up is an argument of the sinks, because it is the sinks that leave records out: env.setup_egress(sinks, warmup=600). A run with no sink has nothing to leave out, and its statistics are reset at the warm-up as well when it is set through the builder.
The warm-up drops early records; go_live_at drops nothing and only decides which sink gets each one. Trusting Results shows how to choose a warm-up length.
Run Length
until is in simulation seconds. run() and a blueprint's run.until also take a duration such as "10 min", "8 hours" or "1 week"; a month counts as 30 days and a year as 365.
app.add_resource("packers", current_cap=1, max_cap=1)
app.run(until="1 week") # 604,800 simulation seconds; a number is seconds too
Without until, SimPy runs until no events remain, and a run with an arrival never runs out of events. Neither does any run with a sink or an ingress connector, even when its own work is done, because the environment's own processes repeat for as long as the run lasts. Give such a run an until, or stop it with Ctrl+C.
How It Works
What the environment holds. A DynamicRealtimeEnvironment is SimPy's RealtimeEnvironment with four additions: the registry at env.registry, the ingress connectors, the egress connectors with their buffers, and the statistics at env.stats.
Threads. A run uses three threads, with queues between them. Records flow from the processes to the sinks, and parameter changes flow from the sources to the registry:

- The SimPy thread runs the model. When a process publishes a record, the record goes into a buffer for each sink, and a full buffer is handed to that sink's queue.
- The egress thread runs the sinks' asyncio code, which drains the queues and writes to Kafka, files or databases, so network waits never stall a simulation step.
- The ingress thread runs the sources, which put
(path, value)pairs on a queue.
Processes it starts. Besides your own, the environment starts an ingress monitor, which applies queued changes every 0.1 simulation seconds; a lag monitor, which publishes system.simulation.lag_seconds every simulation second; a timed flush per sink, at a factor above 0; the statistics timer; and the warm-up reset.
Pacing and the switch. Before each event, step checks the logical clock. At the first event at or after go_live_at, it sets the factor to 1.0 and moves its two anchors, env_start and real_start, so the go-live instant counts as now; without that, the first paced event would sleep off the whole backfill. The switch happens once and always to 1.0. A go_live_at at or before logical_start_time paces the whole run, with a warning, and a run that ends before it stays unpaced. go_live_at and logical_start_time must both be naive or both timezone-aware, or the environment refuses them.
Teardown. teardown(), which run() calls in a finally block, publishes the last statistics, flushes and stops the egress connectors, then stops the ingress connectors.
SimPy and desflow. SimPy's RealtimeEnvironment takes one factor for the whole run and, with strict=True, raises an error when the model falls behind the wall clock. desflow runs without waiting at factor: 0, switches to real time at go_live_at, stamps every record with a logical time, and reports falling behind as telemetry instead of raising. SimPy and desflow compares the two.
Stability and Limits
- A run with a sink or ingress needs
until, or Ctrl+C, as Run Length explains. - At
factor: 0no timed flush runs, so records leave a buffer when it reachesbatch_sizeand at teardown, as Egress explains.LocalIngressfollows the wall clock, so its changes can land after an unpaced run has ended; a scenario follows the simulation clock.system.simulation.lag_secondsreports 0 while the run is ahead of the wall clock. warmupdrops,go_live_atroutes. Records before a warm-up are gone; records beforego_live_atgo to the sinks that take history.- A model slower than the wall clock falls behind at
factor: 1.0and does not catch up by skipping events;lag_secondsshows by how much.