Same-time loops

Important use cases for same-time loops can be the initialization of simulation and communication between controllers or agents. As the scenario definition has to provide initialization values for cyclic data-flows and every cyclic data-flow will lead to an incrementing simulation time, it may take some simulation steps until all simulation components are in a stable state, especially, for simulations consisting of multiple physical systems. The communication between controllers or agents usually takes place at a different time scale than the simulation of the technical systems. Thus, same-time loops can be helpful to model this behavior in a realistic way.

To give an example of same-time loops in mosaik, the previously shown scenario is extended with a master controller, which takes control over the other controllers. The communication between these two layers of controllers will take place in the same step without incrementing the simulation time. The code of the previous scenario is used as a base and extended as shown in the following.

Master controller

The master controller bases on the code of the controller of the previous scenario. The first small change for the master controller is in the meta data dictionary, where new attribute names are defined. The ‘delta_in’ represent the delta values of the controllers, which will be limited by the master controller. The results of this control function will be returned to the controllers as ‘delta_out’.

META = {
    "type": "event-based",
    "models": {
        "Agent": {
            "public": True,
            "params": [],
            "attrs": ["delta_in", "delta_out"],
        },
    },
}

__init__ is extended with self.cache for storing the inputs and self.time for storing the current simulation time, which is initialized with 0.

class Controller(mosaik_api_v3.Simulator):
    def __init__(self):
        super().__init__(META)
        self.agents = []
        self.data = {}
        self.cache = {}
        self.time = 0

The step is changed, so that first the current time is updated in the self.time variable. Also the control function is changed. The master controller gets the delta output of the other controllers as ‘delta_in’ and stores the last value of each controller in the self.cache. This is needed, because the controllers are event-based and the current values are only sent if the values changes. The control function of the master controller limits the sum of all deltas to be < 1 and > -1. If these limits are exceeded the delta of all controllers will be overwritten by the master controller with 0 and sent to the other controller as ‘delta_out’.

    def step(self, time, inputs, max_advance):
        self.time = time
        data = {}
        for agent_eid, attrs in inputs.items():
            values_dict = attrs.get("delta_in", {})
            for key, value in values_dict.items():
                self.cache[key] = value

        if sum(self.cache.values()) < -1 or sum(self.cache.values()) > 1:
            data[agent_eid] = {"delta_out": 0}

        self.data = data

        return None

Additionally, two small changes in the get_data method were done. First, the name was updated to ‘delta_out’ in the check for the correct attribute name. Second, the current time, which was stored previously in the step, is added to the output cache dictionary. This informs mosaik that the simulation should start or stay in a same-time loop if also output data for ‘delta_out’ is provided.

    def get_data(self, outputs):
        data = {}
        for agent_eid, attrs in outputs.items():
            for attr in attrs:
                if attr != "delta_out":
                    raise ValueError('Unknown output attribute "%s"' % attr)
                if agent_eid in self.data:
                    data["time"] = self.time
                    data.setdefault(agent_eid, {})[attr] = self.data[agent_eid][attr]

        return data

Controller

The controller has to be extended to handle the ‘delta_out’ from the master controller as input. If it receives an input value for the attribute ‘delta’, it will not calculate a new delta value, but use the one from the master controller.

    def step(self, time, inputs, max_advance):
        self.time = time
        data = {}
        for agent_eid, attrs in inputs.items():
            delta_dict = attrs.get("delta", {})
            if len(delta_dict) > 0:
                data[agent_eid] = {"delta": list(delta_dict.values())[0]}
                continue

The same-time loop in this scenario will always be finished after the second iteration, because the master controller will overwrite the deltas of the controller and will get back zeros as ‘delta_in’. Thus, it will produce no output in the second iteration and the same-time loop will be finished.

Scenario

This scenario is based on the previous scenario. In the following description only the changes are explained, but the full code is shown. The updated controller and the new master controller are added to the sim config of the scenario.

# demo_3.py
import mosaik
import mosaik.util

# Sim config. and other parameters
SIM_CONFIG = {
    "ExampleSim": {
        "python": "simulator_mosaik:ExampleSim",
    },
    "ExampleCtrl": {
        "python": "controller_demo_3:Controller",
    },
    "ExampleMasterCtrl": {
        "python": "controller_master:Controller",
    },
    "Collector": {
        "cmd": "%(python)s collector.py %(addr)s",
    },
}
END = 6  # 10 seconds

# Create World
world = mosaik.World(SIM_CONFIG)

The master controller is also started and initialized. The controllers get different ‘init_val’ values compared to the previous scenario. Here, it is changed to (-2, 0, -2) to have the right timing to get into the same-time loop.

# Start simulators
with world.group():
    examplesim = world.start("ExampleSim", eid_prefix="Model_")
    examplectrl = world.start("ExampleCtrl")
    examplemasterctrl = world.start("ExampleMasterCtrl")
collector = world.start("Collector")

# Instantiate models
models = [examplesim.ExampleModel(init_val=i) for i in (-2, 0, -2)]
agents = examplectrl.Agent.create(len(models))
master_agent = examplemasterctrl.Agent.create(1)
monitor = collector.Monitor()

The ‘delta’ outputs of the controllers are connected to the new master controller and the ‘delta_out’ of the master controller is connected to the respective controller. The weak=True argument defines, that the connection from the controllers to the master controller will be the first to be executed by mosaik.

# Connect entities
for model, agent in zip(models, agents):
    world.connect(model, agent, ("val", "val_in"))
    world.connect(agent, model, "delta", weak=True)

for agent in agents:
    world.connect(agent, master_agent[0], ("delta", "delta_in"))
    world.connect(master_agent[0], agent, ("delta_out", "delta"), weak=True)

mosaik.util.connect_many_to_one(world, models, monitor, "val", "delta")
mosaik.util.connect_many_to_one(world, agents, monitor, "delta")
world.connect(master_agent[0], monitor, "delta_out")

# Run simulation
world.run(until=END)

The printed output of the collector shows the states of the different simulators. The collector just shows the final result of the same-time loop and not the steps during the loop. It can be seen that the ‘delta’ of ‘Agent_1’ changes to -1 at time step 2 and at time step 4 all ‘delta’ attributes are set to 0 by the master controller.

Collected data:
- ExampleCtrl-0.Agent_0:
  - delta: {3: 0}
- ExampleCtrl-0.Agent_1:
  - delta: {2: -1, 3: 0}
- ExampleCtrl-0.Agent_2:
  - delta: {3: 0}
- ExampleMasterCtrl-0.Master_Agent_0:
  - delta_out: {3: 0}
- ExampleSim-0.Model_0:
  - delta: {0: 1, 1: 1, 2: 1, 3: 0, 4: 0, 5: 0}
  - val: {0: -1, 1: 0, 2: 2, 3: 2, 4: 2, 5: 2}
- ExampleSim-0.Model_1:
  - delta: {0: 1, 1: 1, 2: -1, 3: 0, 4: 0, 5: 0}
  - val: {0: 1, 1: 2, 2: 2, 3: 0, 4: 0, 5: 0}
- ExampleSim-0.Model_2:
  - delta: {0: 1, 1: 1, 2: 1, 3: 0, 4: 0, 5: 0}
  - val: {0: -1, 1: 0, 2: 2, 3: 2, 4: 2, 5: 2}

A visualization of the execution graph shows the data flows in the simulation. For the first two time steps, only the controllers are executed, as they do not provide any output for ‘delta’. Thus, the master controller was not stepped and the simulation was proceeded directly with the next simulation time step. At simulation time 2, the master controller is stepped, but as the sum of delta values is not exceeding the limits no control action takes place. At simulation time 4, the master controller is stepped again and this time sends back a value to the controllers to limit their ‘delta’ value. It can be seen, that the controllers are stepped a second time within the same simulation time and send data again to the master controller. After this second step of the master controller, it does not send an output again and the simulation proceeds to simulation time 5, where the same-time loop occures again.

Scheduling of demo 3

Scheduling of demo 3.