Creating and running simple simulation scenarios
We will now create a simple scenario with mosaik in which we use a simple data collector to print some output from our simulation. That means, we will instantiate a few ExampleModels and a data monitor. We will then connect the model instances to that monitor and simulate that for some time.
Configuration
You should define the most important configuration values for your simulation as “constants” on top of your scenario file. This makes it easier to see what’s going on and change the parameter values.
Two of the most important parameters that you need in almost every simulation are the simulator configuration and the duration of your simulation:
# Sim config
SIM_CONFIG: mosaik.SimConfig = {
"ExampleSim": {
"python": "simulator_mosaik:ExampleSim",
},
"Collector": {
"cmd": "%(python)s collector.py %(addr)s",
},
}
END = 10 # 10 seconds
The sim config specifies which simulators are available and how to start
them. In the example above, we list our ExampleSim as well as Collector (the
names are arbitrarily chosen). For each simulator listed, we also specify how
to start it. (If you are using type checking, you can import SimConfig
from
mosaik.scenario
and change the first line to SIM_CONFIG: SimConfig = {
,
instead.)
Since our example simulator is, like mosaik, written in Python 3, mosaik can
just import it and execute it in-process. The line 'python':
'simulator_mosaik:ExampleSim'
tells mosaik to import the package
simulator_mosaik
and instantiate the class ExampleSim
from it.
The data collector will be started as external process which will communicate
with mosaik via sockets. The line 'cmd': '%(python)s collector.py %(addr)s'
tells mosaik to start the simulator by executing the command python
collector.py
. Beforehand, mosaik replaces the placeholder %(python)s
with the current python interpreter (the same as used to execute the scenario
script) and %(addr)s
with its actual socket address HOSTNAME:PORT so that
the simulator knows where to connect to.
The section about the Sim Manager explains all this in detail.
Here is the complete file of the data collector. Take care of adding it to your example:
"""
A simple data collector that prints all data when the simulation finishes.
"""
import collections
import sys
import mosaik_api_v3
META = {
"type": "event-based",
"models": {
"Monitor": {
"public": True,
"any_inputs": True,
"params": [],
"attrs": [],
},
},
}
class Collector(mosaik_api_v3.Simulator):
def __init__(self):
super().__init__(META)
self.eid = None
self.data = collections.defaultdict(lambda: collections.defaultdict(dict))
def init(self, sid, time_resolution):
return self.meta
def create(self, num, model):
if num > 1 or self.eid is not None:
raise RuntimeError("Can only create one instance of Monitor.")
self.eid = "Monitor"
return [{"eid": self.eid, "type": model}]
def step(self, time, inputs, max_advance):
data = inputs.get(self.eid, {})
for attr, values in data.items():
for src, value in values.items():
self.data[src][attr][time] = value
return None
def finalize(self):
print("Collected data:")
for sim, sim_data in sorted(self.data.items()):
print("- %s:" % sim)
for attr, values in sorted(sim_data.items()):
print(" - %s: %s" % (attr, values))
sys.stdout.flush()
if __name__ == "__main__":
mosaik_api_v3.start_simulation(Collector())
As its name suggests it collects all data it receives each step in a dictionary (including the current simulation time) and simply prints everything at the end of the simulation.
The World
The next thing we do is instantiating a World
object. This object will
hold all simulation state. It knows which simulators are available and started,
which entities exist and how they are connected. It also provides most of the
functionality that you need for modelling your scenario:
# Create World
world = mosaik.World(SIM_CONFIG)
To get access to mosaik’s world, we need to import mosaik
at the beginning
of our scenario script. We also import mosaik.util
to get access to some
helper methods later on.
# demo_1.py
import mosaik
import mosaik.util
The scenario
Before we can instantiate any simulation models, we first need to start the
respective simulators. This can be done by calling World.start
. It
takes the name of the simulator to start and, optionally, some simulator
parameters which will be passed to the simulators init()
method. So lets
start the example simulator and the data collector:
# Start simulators
examplesim = world.start("ExampleSim", eid_prefix="Model_")
collector = world.start("Collector")
We also set the eid_prefix for our example simulator. What gets returned by
World.start
is called a model factory.
We can use this factory object to create model instances within the respective
simulator. In your scenario, such an instance is represented as an
Entity
. The model factory presents the available models as if they
were classes within the factory’s namespace. So this is how we can create one
instance of our example model and one ‘Monitor’ instance:
# Instantiate models
model = examplesim.ExampleModel(init_val=2)
monitor = collector.Monitor()
The init_val parameter that we passed to ExampleModel
is the same as in
the create()
method of our Sim API implementation.
Now, we need to connect the example model to the monitor. That’s how we tell mosaik to send the outputs of the example model to the monitor.
# Connect entities
world.connect(model, monitor, "val", "delta")
The method World.connect
takes one entity pair – the source and the
destination entity, as well as a list of attributes or attribute tuples. If you
only provide single attribute names, mosaik assumes that the source and
destination use the same attribute name. If they differ, you can instead pass
a tuple like ('val_out', 'val_in')
.
Quite often, you will neither create single entities nor connect single entity pairs, but work with large(r) sets of entities. Mosaik allows you to easily create multiple entities with the same parameters at once. It also provides some utility functions for connecting sets of entities with each other. So lets create two more entities and connect them to our monitor:
# Create more entities
more_models = examplesim.ExampleModel.create(2, init_val=3)
mosaik.util.connect_many_to_one(world, more_models, monitor, "val", "delta")
Instead of instantiating the example model directly, we called its static
method create()
and passed the number of instances to it. It returns a list
of entities (two in this case). We used the utility function
mosaik.util.connect_many_to_one
to connect all of them to the
database. This function has a similar signature as World.connect
, but
the first two parameters are a world instance and a set (or list) of entities
that are all connected to the dest_entity.
Mosaik also provides the function mosaik.util.connect_randomly
. This
method randomly connects one set of entities to another set. These two methods
should cover most use cases. For more special ones, you can implement custom
functions based on the primitive World.connect
.
The simulation
In order to start the simulation, we call World.run
and specify for
how long we want our simulation to run:
# Run simulation
world.run(until=END)
Executing the scenario script will then give us the following output:
Collected data:
- ExampleSim-0.Model_0:
- delta: {0: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1}
- val: {0: 3, 1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9, 7: 10, 8: 11, 9: 12}
- ExampleSim-0.Model_1:
- delta: {0: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1}
- val: {0: 4, 1: 5, 2: 6, 3: 7, 4: 8, 5: 9, 6: 10, 7: 11, 8: 12, 9: 13}
- ExampleSim-0.Model_2:
- delta: {0: 1, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1}
- val: {0: 4, 1: 5, 2: 6, 3: 7, 4: 8, 5: 9, 6: 10, 7: 11, 8: 12, 9: 13}
Mosaik will also produce some diagnostic output along the lines of
2022-10-12 15:31:01.351 | INFO | mosaik.scenario:start:131 - Starting "ExampleSim" as "ExampleSim-0" ...
2022-10-12 15:31:01.352 | INFO | mosaik.scenario:start:131 - Starting "Collector" as "Collector-0" ...
INFO:mosaik_api_v3:Starting Collector ...
2022-10-12 15:31:01.430 | INFO | mosaik.scenario:run:381 - Starting simulation.
100%|██████████████████████████████████████| 10/10 [00:00<00:00, 1996.05steps/s]
2022-10-12 15:31:01.446 | INFO | mosaik.scenario:run:425 - Simulation finished successfully.
If you don’t want the progress bar, you can run the simulation with
world.run(until=END, print_progress=False)
instead. For even more progress bars, set print_progress='individual'
, instead.
Summary
This section introduced you to the basic of scenario creation in mosaik. For more details you can check the guide to scenarios.
For your convenience, here is the complete scenario that we created in this tutorial. You can use this for some more experiments before continuing with this tutorial:
# demo_1.py
import mosaik
import mosaik.util
# End: Imports
# Sim config
SIM_CONFIG: mosaik.SimConfig = {
"ExampleSim": {
"python": "simulator_mosaik:ExampleSim",
},
"Collector": {
"cmd": "%(python)s collector.py %(addr)s",
},
}
END = 10 # 10 seconds
# End: Sim config
# Create World
world = mosaik.World(SIM_CONFIG)
# End: Create World
# Start simulators
examplesim = world.start("ExampleSim", eid_prefix="Model_")
collector = world.start("Collector")
# End: Start simulators
# Instantiate models
model = examplesim.ExampleModel(init_val=2)
monitor = collector.Monitor()
# End: Instantiate models
# Connect entities
world.connect(model, monitor, "val", "delta")
# End: Connect entities
# Create more entities
more_models = examplesim.ExampleModel.create(2, init_val=3)
mosaik.util.connect_many_to_one(world, more_models, monitor, "val", "delta")
# End: Create more entities
# Run simulation
world.run(until=END)
The next part of the tutorial will be about integrating control mechanisms into a simulation.