ICoSimulatorStep Method |
Namespace: Demo3D.EventQueue.CoSimulation
CoSimulatorStatus Step( ModelTime currentTime, ModelTime stepBeginTime, ModelTime step )
Demo3D moves the simulation forward in steps. It first waits for an amount of real time, and then 'pulls' the model forward an equivalent amount of model time. Effectively real time advances ahead of model time. (The alternative is to move the simulation forward by one step, and then wait for a corresponding amount of real time.)
We advance real time ahead of model time in order to support Interrupted. An interrupt occurs to handle unsolicited external events. These events need to cause a simulation step to occur immediately, at a model time that (as closely as possible) corresponds to the actual real event time.
The consequence is that a simulator should process the requested step time. We allow the simulator to control the step size of the next step by setting MinStep and MaxStep (and in some cases using GetNextStepSize(ModelTime)).
Normally (the way FMI would have it), every simulator performs every step every time. They agree up-front on the step size, and then every simulator is expected to execute the requested step size. But sometimes we can't do that while also remaining in sync with real time. The problem is that some simulators can take a long time even to start a step (not run it, just start/trigger it). The model is forced to stall if the time taken starting a step exceeds the step size time. The only option is to increase the step size so that the step size experienced by the slow simulator exceeds its trigger time (and then hope that it can execute its step faster than real time in whatever time it has remaining).
The FMI way would mean that we'd also have to increase the step size of all the other simulators too. But that results in a choppy model. We really want physics steps to be no more than 10ms; some simulators really can't handle 10ms steps.
Other simulators are unable to execute exactly step. They may be forced to execute more or less time than requested.
The solution is a variable, unsynchronised step size - each simulator stepping forward independently of each other, synchronised to model time (which may itself be synchronised to real time), and using the model as the repository for exchanged information:
A step begins at stepBeginTime and steps forward by step. At the end of the step, the time will be "stepEndTime". The step is called at currentTime. (Normally currentTime will be the same as stepBeginTime, but it isn't always depending on how the simulator is set up to run.)
If you set CanRunEarly then MinStep and MaxStep (and GetNextStepSize(ModelTime)) are advisory, and the next step called may have a step that's shorter than that requested. If other simulators require a shorter next step, then the simulation master may execute smaller steps involving those simulators before calling this simulator again, and under those circumstances currentTime will be greater than stepBeginTime.
If the model is tracking real time and is running slow, then it may execute the next step with a longer step than requested, though never longer than MaxStep.
Regardless of the step size, the simulator can request to be called again early by raising Interrupted, but the timing of the next step can't be guaranteed. It should only be used to process unsolicited external events.
The Problems With Variable Length Steps.
Step may take a long time to execute; if we're asking for 'x' milliseconds, then it may take as much as 'x' milliseconds to execute. And 'x' itself is a variable length of time (each step can be a different length).
Since we advance real time and then 'pull' the model forward, this means that we execute step N while we're sleeping for step N+1. If there's a big difference in step size between step N (the one we've just slept and are about to execute) and step N+1 (the one we're about to sleep for) then we may legitimately take longer executing the step N than step N+1 is expecting to sleep. This will lead to cosimulation master introducing a pause even though nothing is wrong.
For example, step N is 100ms, and step N+1 is 10ms Model Simulator ----------------------------------------------- + sleep 100ms + sleep 10ms step 100ms + oh, simulator is still running pause the model while we wait
The reason we 'pull the model behind' is so we can fit real time external events into the simulation at the correct point in model time.This isn't just for accuracy of modeling, but in order to handle simulations that are primarily driven by external events. Imagine a Sim3D model where the next event is 10 seconds away. We sleep for 10s but a message arrives from a WMS after 150ms. For this to work we must 'pull' the simulation behind us, rather than have real time and model time run in parallel.
The alternative approach ('pushing the model ahead'), executes steps and then sleeps for the equivalent real time. But this means that for very long steps we'd have to wait to insert external events. We could limit the step size to a small enough unit to allow external events to be inserted in a 'reasonable' time. The problem with this is that it would mean always executing short steps, and that's inefficient (some simulators just can't, and run terribly badly if we try, or overrun massively).
A resolution for 'pull behind' is to try to keep consecutive step sizes the same (per simulator) so that we never find we're executing a longer (previous) step than the length of the (next) step we're about to sleep.
So for simulators that never run flat out (always run with a small time multiplier), it's helpful to keep MinStep and MaxStep close together.