Scheduling and simulation execution¶
When you defined your scenario and start the simulation, mosaik’s scheduler becomes active. It manages the execution of all involved simulators, keeps them in sync and handles the data-flows between them.
Mosaik runs the simulation by stepping simulators through time. The time has internally no unit attached, but by convention, seconds are used. If all simulators involved in your scenario agree on another unit (e.g., minutes or milliseconds), this can be used as well.
Anatomy of a step¶
When the simulation starts, all simulators are at time 0. Mosaik tracks the current simulation time for every simulator individually. When it asks a simulator to perform its next step, it passes its current simulation time tnow to it. After its step, the simulator returns the time at which it wants to perform its next step (tnext). Thus, a simulator’s step size doesn’t need to be constant but can vary during the simulation.
The data that a simulator computes during a step is valid for the right-open interval [tnow, tnext) as shown in the following figure.
Synchronization and data-flows¶
If there are data-flows between two simulators (because you connected some of their entities), a simulator can only perform a step if all input data has been computed.
Let’s assume we created a data-flow from a simulator A to a simulator B and B wants to perform a step from tnow(B). Mosaik determines which simulators provide input data for B. This is only A in this example. In order to provide data for B, A needs to step far enough to produce data for tnow(B), that means tnext(A) > tnow(B) as the following figure illustrates.
If this condition is met for all simulators providing input for B, mosaik collects all input data for B that is valid at tnow(B) (you could say it takes one snapshot of the global simulation state). It passes this data to B. Based upon this (and only this) data, B performs its step [tnow(B), tnext(B)).
This is relatively easy to understand if A and B have the same step size, as the following figures shows:
If B had a larger step size then A, A would produce new data while B steps. B would still only use the data that was valid at tnow(B), because it only “measures” its inputs once at the beginning of its step:
On the other hand, if A had a larger step size then B, we would reuse the same data from A multiple times as long as it is valid:
The last two examples may look like special cases, but they actually arise from the approach explained above.
How data flows through mosaik¶
After a simulator is done with its step, mosaik determines, based on the data-flows that you created in your scenario, which data other simulators need from it. It makes a get_data() API call to the simulator and stores the data that this call returns in an internal buffer. It also memorizes for which period of time this data is valid.
Before a simulator steps, mosaik determines in a similar fashion what input data the simulator needs. Mosaik checks its internal data buffer if input data from all simulators is available. If not, it waits until all simulators stepped far enough to provide that data. All input data is then passed to the inputs parameter of the step() API call.
It is important to understand that simulators don’t talk to each other directly but that all data flows through mosaik were it can be cached and managed.
Cyclic data-flows are necessary to model situations in which a control mechanism (C) controls another entity (E) based on its state, e.g. by sending commands or a schedule.
It is not possible to perform both data-flows (the state from E to C and the commands/schedule from C to E) at the same time because they depend on each other (yes, this is similar to the chicken or egg dilemma).
The cycle can be resolved by first stepping E (e.g., from t = 0 to t = 1). E’s state for that interval can then be used as input for C ’s step for the same interval. The commands/schedule that C generates for E will then be used in E’s next step.
This resolution of the cycle makes sense if you think how this would work in real life. The controller would measure the data from the controlled unit at a certain point t. It would then do some calculation which take a certain amount of time Δt which would be send to the controlled unit at t + Δt.
However, mosaik is not able to automatically resolve that cycle. That’s why you
are not allowed to
connect(E, C) and
connect(C, E) in a scenario.
Instead you have to
connect(E, C, async_requests=True) and use the
set_data() in C’s step() implementation in order to send the commands or schedule from C
You can take a look at our discussion of design decissions to learn why cyclic data-flows are handled this way.
Stepping and simulation duration¶
By now you should have a general idea of how mosaik handles data-flows between simulators. You should also have the idea that simulators only perform a step when all required input data is available. But what if they don’t need any? In this section you’ll learn about the algorithm that mosaik uses to determine whether a simulator can be stepped or not.
The general idea behind idea is laziness. A simulator will only step if it really needs to. This is usually, because someone else needs its data. This becomes problematic if your simulator is the only one in the simulation (e.g., for testing purposes) or at the end of a data-flow chain.
This is how it works:
Should there be a next step at all? *
Yes: Go to step 2.
No: Stop the simulator.
* We’ll explain how to answer this question below.
Are there simulators that need data from us?
Yes: Go to step 3.
No: Go to step 4.
Does a depending simulator require new data from us?
Yes: Go to step 4.
No: Wait until someone does. Then go to step 4.
Is all required input data from other simulators available?
Yes: Go to step 5.
No: Wait until all data is available. Then go step 5.
Collect all required input data.
Send collected input data to simulator, perform the simulation step and get the time for the next step.
Get all data from the simulator that other simulators need.
Notify simulators that already wait for that data.
So how do we determine whether a simulator must perform another step of it is done?
When we start the simulation, we pass a time unto which our simulation should
world.run(until=END)). Usually a simulator is done if the time of its
next step is larger then the value of until. This is, however, not true for
all simulators in a simulation. If no one needs the data of a simulator step,
why perform this step?
So the actual algorithm is as follows:
If a simulator has no outgoing data-flows (no other simulator needs its data) it simulates until the condition tnext > tuntil is met.
Else, if a simulator needs to provide data for other simulators, it keeps running until all of these simulators have stopped.
The algorithm explained above allows mosaik to perform as little simulation steps as possible and only perform theses steps when necessary.