Set external events

This tutorial gives an example on how to set external events for integrating unforeseen interactions of an external system in soft real-time simulation with rt_factor=1.0. A typical use case for this feature would be Human-in-the-Loop simulations to support human interactions, e.g., control actions. In mosaik, such external events can be implemented via the the asynchronous set_event method. These events will then be scheduled for the next simulation time step.

To give an example of external events in mosaik, a new scenario is created that includes a controller to set external events. In addition to the controller, a graphical user interface (GUI) is implemented and started in a subprocess for external control actions by the user.

The example code and additional requirements are shown in the following.

Requirements

First of all, we need to install some additional requirements within the virtual environment (see installation guide for setting up a virtual environment)

$ pip install pyzmq PyQt5

Set-event controller

Next, we need to create a new python module for the set-event controller, e.g., controller_set_event.py.

In the meta data dictionary of the set-event controller, we specify that this is an event-based simulator.

# controller_set_event.py

import sys
import zmq
import threading
import math

import mosaik_api_v3


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

The set-event controller subscribes to external events from the GUI via a zeromq subscriber socket using the publish-subscribe pattern. Herefore, a listener thread is created which receives external event messages from the GUI. More information about the listener thread can be found in the next section.

class Controller(mosaik_api_v3.Simulator):
    def __init__(self):
        super().__init__(META)
        self.data = {}
        self.time = 0
        self.eid = None
        self.thread = None
        self.initial_timestamp = 0
        self.once = True
        self.context = zmq.Context()

        # Subscribe to external events from the GUI
        self.subscriber = self.context.socket(zmq.SUB)
        self.subscriber.connect("tcp://localhost:5563")
        self.subscriber.setsockopt(zmq.SUBSCRIBE, b"B")

        # Listener THREAD
        self.thread = listen_to_external_events(self)

    def create(self, num, model):
        if num > 1 or self.eid is not None:
            raise RuntimeError('Can only create one instance of Controller.')

        self.eid = 'Controller_set_event'
        return [{'eid': self.eid, 'type': model}]

    def finalize(self):
        self.thread.join(0)
        sys.exit()

In order to set the event for the next time step, it is necessary to determine the current simulation time in wall clock time. For this, we need to store the initial timestamp in step once for the first simulation step.

    def step(self, time, inputs, max_advance):
        # Needed in listener thread to determine the current simulation time in wall clock time.
        if self.once:
            self.initial_timestamp = self.mosaik.world.env.now
            self.once = False

        self.time = time
        print(f"In step at time {self.time}")
        print(f"max_advance {max_advance}")

        return None

Listener thread

The listener thread can be included in the same file as the set-event controller: controller_set_event.py.

The object of the controller class needs to be passed as a parameter to the listen_to_external_events function, which is called as a thread via the defined decorator @threaded. The listener thread listens to external event messages from the GUI. Once a message arrives, the listener thread calls the set_event method to set an external event for the next simulation step in mosaik.

def threaded(fn):
    def wrapper(*args, **kwargs):
        thread = threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True)
        thread.start()
        return thread
    return wrapper
@threaded
def listen_to_external_events(controller):
    while True:
        try:
            # Receive external event message from GUI
            [address, contents] = controller.subscriber.recv_multipart(zmq.NOBLOCK)
            print(f"[{address}] {contents}")

            current_timestamp = controller.mosaik.world.env.now
            real_time = math.ceil(current_timestamp - controller.initial_timestamp)
            event_time = real_time + 1
            print(f"Current simulation time: {real_time}")

            if controller.time < event_time < controller.mosaik.world.until:
                print(f"Set external Event at time {event_time}")
                # Set external event in mosaik via asynchronous call
                controller.mosaik.set_event(event_time)

        except zmq.ZMQError as e:
            if e.errno == zmq.EAGAIN:
                # state changed since poll event
                pass
            else:
                raise

Graphical user interface

For the GUI, we create a new python module, e.g., gui_button.py.

The GUI is created with PyQt5 and provides a button to set external events in mosaik every time we click on it. To enable the set-event controller to perform this control action, a zeromq publisher socket is used to send a message to the controller’s subscriber that the button has been clicked.

GUI for setting external events in mosaik
# gui_button.py

import sys
import zmq
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow


class PushButtonWindow(QMainWindow):
    def __init__(self):
        super(PushButtonWindow,self).__init__()
        self.button = None
        self.context = zmq.Context()

        # For external events
        self.publisher = self.context.socket(zmq.PUB)
        self.publisher.bind("tcp://*:5563")

    def button_clicked(self):
        self.publisher.send_multipart([b"B", b"Push button was clicked!"])

    def create(self):
        self.setWindowTitle("MOSAIK 3.0 - External Events")

        self.button = QtWidgets.QPushButton(self)
        self.button.setText("Click me to set an external event!")
        self.button.clicked.connect(self.button_clicked)

        # Set the central widget of the Window.
        self.setCentralWidget(self.button)


def main():
    app = QApplication(sys.argv)
    window = PushButtonWindow()
    window.create()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

Scenario

Next, we need to create a new python script for the external events scenario, e.g., demo_4.py.

For this scenario, the set-event controller is added to the SIM_CONFIG of the scenario.

# demo_4.py
import subprocess

import mosaik
import mosaik.util


SIM_CONFIG = {
    'Controller': {
        'python': 'controller_set_event:Controller',
    },
}

END = 60  # 60 seconds

# Create World
world = mosaik.World(SIM_CONFIG)

The set-event controller is started and initialized. Here, an initial event is added to the set-event controller so that the controller is executed at time=0 to set the initial timestamp. This is needed for the determination of the current simulation time.

# Start simulators
controller = world.start('Controller')

# Instantiate models
external_event_controller = controller.Controller()
world.set_initial_event(external_event_controller.sid)

The GUI is started in a subprocess and must be manually closed after the simulation is completed.

# Start GUI in a subprocess
proc = subprocess.Popen(['python', 'gui_button.py'])

In order to run the simulation scenario in soft real-time, the rt_factor is set to 1.0.

# Run simulation in real-time
world.run(until=END, rt_factor=1.0)

Finally, we can run the scenario script as follows:

$ python demo_4.py

The printed output shows when the external events are triggered (button was clicked) and executed during simulation.

Starting "Controller" as "Controller-0" ...
WARNING: Controller-0 has no connections.
Starting simulation.
In step at time 0
max_advance 60
Simulation too slow for real-time factor 1.0 - 9.655498433858156e-05s behind time.
[b'B'] b'Push button was clicked!'
Current simulation time: 11
Set external Event at time 12
In step at time 12
max_advance 60
Simulation too slow for real-time factor 1.0 - 0.000688756990712136s behind time.
[b'B'] b'Push button was clicked!'
Current simulation time: 16
Set external Event at time 17
In step at time 17
max_advance 60
Simulation too slow for real-time factor 1.0 - 0.0013458110042847693s behind time.
[b'B'] b'Push button was clicked!'
Current simulation time: 26
Set external Event at time 27
In step at time 27
max_advance 60
Simulation too slow for real-time factor 1.0 - 0.0013047059765085578s behind time.
[b'B'] b'Push button was clicked!'
Current simulation time: 29
Set external Event at time 30
In step at time 30
max_advance 60
Simulation too slow for real-time factor 1.0 - 0.0019755829707719386s behind time.
[b'B'] b'Push button was clicked!'
Current simulation time: 33
Set external Event at time 34
In step at time 34
max_advance 60
Simulation too slow for real-time factor 1.0 - 0.0011994789820164442s behind time.
Simulation finished successfully.