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. and other parameters
SIM_CONFIG = {
    '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:

"""
A simple data collector that prints all data when the simulation finishes.

"""
import collections

import mosaik_api


META = {
    'type': 'event-based',
    'models': {
        'Monitor': {
            'public': True,
            'any_inputs': True,
            'params': [],
            'attrs': [],
        },
    },
}


class Collector(mosaik_api.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))


if __name__ == '__main__':
    mosaik_api.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:

import mosaik
import mosaik.util
# Create World
world = mosaik.World(SIM_CONFIG)

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:

import mosaik
import mosaik.util
# 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: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

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


# Sim config. and other parameters
SIM_CONFIG = {
    'ExampleSim': {
        'python': 'simulator_mosaik:ExampleSim',
    },
    'Collector': {
        'cmd': '%(python)s collector.py %(addr)s',
    },
}
END = 10  # 10 seconds

# Create World
world = mosaik.World(SIM_CONFIG)

# Start simulators
examplesim = world.start('ExampleSim', eid_prefix='Model_')
collector = world.start('Collector')

# Instantiate models
model = examplesim.ExampleModel(init_val=2)
monitor = collector.Monitor()

# Connect entities
world.connect(model, monitor, 'val', 'delta')

# Create more entities
more_models = examplesim.ExampleModel.create(2, init_val=3)
mosaik.util.connect_many_to_one(world, more_models, monitor, 'val', 'delta')

# Run simulation
world.run(until=END)

The next part of the tutorial will be about integrating control mechanisms into a simulation.