Creating and running simple simulation scenarios

We will now create a simple scenario with mosaik in which we use also use a simple data collector to get some nice 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 collector.py %(addr)s',
    },
}
END = 10 * 60  # 10 minutes

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.

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 collector.py %(addr)s' tells mosaik to start the simulator by executing the command python collector.py. Beforehand, mosaik replaces the placeholder %(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 pprint

import mosaik_api


META = {
    '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(list))
        self.step_size = None

    def init(self, sid, step_size):
        self.step_size = step_size
        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):
        data = inputs[self.eid]
        for attr, values in data.items():
            for src, value in values.items():
                self.data[src][attr].append(value)

        return time + self.step_size

    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())

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

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 HDF5 database adapter:

examplesim = world.start('ExampleSim', eid_prefix='Model_')
collector = world.start('Collector', step_size=60)

We also set the eid_prefix for our example simulator and some configuration values for the database. It will collect data every minute until the simulation ends. 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 how we can create one instance of our example model and one database instance:

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. Similarly, the database has a parameter for its filename.

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 monotir.

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 deffer, you can instead pass a tuple like ('val_out', 'val_in').

Usually, 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.util

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 (nine 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:

world.run(until=END)

Executing the scenario script will then give us the following output:

Collected data:
- ExampleSim-0.Model_0:
  - delta: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
  - val: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- ExampleSim-0.Model_1:
  - delta: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
  - val: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
- ExampleSim-0.Model_2:
  - delta: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Progress: 85.00%
Progress: 90.00%
Progress: 95.00%
Progress: 100.00%
Simulation finished successfully.

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 collector.py %(addr)s',
    },
}
END = 10 * 60  # 10 minutes

# Create World
world = mosaik.World(SIM_CONFIG)

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

# 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.