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)
PyQt5 (https://pypi.org/project/PyQt5/)
$ 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 math
import sys
import threading
import mosaik_api_v3
import zmq
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 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_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__":
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.
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.
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
.
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.