Wattson’s Co-Simulation Coordination#
The Co-Simulation Controller in Wattson manages the Co-Simulation and coordination of multiple simulators as well as their individual components. For a unified interface, Wattson relies on Queries that are sent to the Co-Simulation Controller and that are then handled either directly by it or passed to other simulators to handle. Queries can have a side-effect, e.g., they can change the topology of the network. After the Query has been implemented, a Response is returned to the querying entity.
Queries & Responses#
A Query (WattsonQuery
) is an object that has a type (string) and some data (usually a dictionary).
Queries are sent by entities, e.g., simulators, network nodes or the user of Wattson, and forwarded to the Co-Simulation Controller. Every component, e.g., the WattsonNetworkEmulator or the PhysicalSimulator, register to the Co-Simulation Controller as a Query Handler. This allows the Controller to forward queries to these handlers.
The type serves as an identifier for the desired functionality and also allows the Co-Simulation Controller to correctly route the Query to the respective component. Each Query Handler states whether they are willing to handle the respective type. Each handler can use arbitrary types, e.g., the NetworkEmulator supports the get-nodes query type which is used to return a list of all network nodes to the querying entity. For further routing refinements, the WattsonQuery
can be used as a parent class to define own query classes, e.g., the WattsonNetworkQuery
.
The data allows to pass options to the query handler to specify what exactly should be done or returned. The data is not standardized but follows the convention that it should be a dictionary with fixed key names. For instance, the NetworkEmulator supports the query of type remove-node to remove a network node from the emulated network. Each node is identified by a unique entityId
. Hence, to specify which node to remove, the data of the query is a dictionary with {"entity_id": $ENTITY_ID_OF_NODE}
.
To each Query, a respective Response (WattsonResponse
) is returned. It indicates whether the query has been handled successfully and can contain additional data, e.g., the list of nodes following the get-nodes example from above.
The overall flow is visualized in the following diagram.
Sometimes, the implementation of a query can take longer than a few milliseconds. To avoid the simulation from blocking and the client from waiting indefinitely, the QueryHandler can return an Response Promise (WattsonResponsePromise
). Such a promise is still a response, but it does not contain a success status nor data yet. It just informs the querying entity that the query is being handled (asynchronously). The querying entity can wait for the promise to be resolved, i.e., to turn into a “real” Response, by polling the resolved status of the Response Promise (is_resolved()
), by waiting for the promise to resolve (resolve(timeout)
) or by setting a Callback to be called once the response is ready (on_resolve(callback)
).
For instance, the network emulator could be instructed to create several new network nodes which might take a few seconds. Hence, the NetworkEmulator receives the Query, decides to handle it asynchronously in a separate thread, and immediately returns the Response Promise. As soon as the thread finishes the task, it resolves the pending promise which is sent to the querying entity. I.e., the ResponsePromise returns a usual Response which has the success status as well as any additional data.
Notifications#
Similar to Queries, Notifications allow entities to communicate with each other. They can be sent by every component (e.g., a Network Node, the NetworkEmulator and PhysicalSimulator, the Co-Simulation Controller or the User). Each notification has a topic and optional data as well as a list of recipients. When a notification is sent by an entity, all other entities receive this notification and can either use it or discard the information. I.e., entities can filter Notifications by the recipient list and the topic. In contrast to a Query, a Notification has no Response.
The topic indicates the content of the notification. It can be related to the simulation as such as well as to aspects of the system under test. E.g., the Co-Simulation Controller sends a simulation-start notification to all clients when the simulation starts, i.e., it informs about the simulation as such.
In contrast, the NetworkEmulator issues a Notification when the emulated network topology changes, i.e., it informs about an event in the system under test. Similarly, the PowerGridSimulator informs all subscribed clients about changed measurements as well as the state of the simulation itself.
Events#
Events are “stateful” notifications. Each event has a name and a state (i.e., it occurred or it did not yet occur). Wattson Events can be seen as a multi-entity implementation of the default threading.Event
in Python.
Events are centrally managed by the Co-Simulation Controller and dynamically created when needed. By default, no event occured. Each entity can query the state of an event and change it, i.e., an Event can be set or cleared. Each entity is informed about the change of the event state. In contrast to Notifications, events are persistent. When Entity A sets an Event before Entity B even joins the Co-Simulation, Entity B is still synchronized to the Event. By default, Notifications are not persisted (although topics can be marked to be kept).
Wattson Client#
The implementation of the previously explained concepts is handled by the Wattson Server (which is part of the Co-Simulation Controller) and the Wattson Client - which can be used by every entity in the simulation. Every Host in the emulated ICT network is also part of a dedicated Management Network, a dedicated (switched) subnet that allows all entities to communicate with the Controller.
External processes, i.e., entities that are not part of the network emulation, can also communicate with the simulation. Here, the WattsonClient joins a dedicated network namespace to allow the communication even outside of the actual simulation.
By default, the Wattson Server listens on the IP address 10.0.0.1
(and 127.0.0.1
) which is reachable by all network hosts.
Connecting from within the Simulation#
from wattson.cosimulation.control.interface.wattson_client import WattsonClient
from wattson.cosimulation.control.messages.wattson_query import WattsonQuery
from wattson.cosimulation.control.messages.wattson_query_type import WattsonQueryType
# Create client and connect
wattson_client = WattsonClient(wattson_socket_ip="10.0.0.1", name="custom-client")
wattson_client.require_connection()
wattson_client.register()
# Send an ECHO query ("ping")
query = WattsonQuery(query_type=WattsonQueryType.ECHO)
response = wattson_client.query(query=query)
print(response.is_successful())
wattson_client.stop()
Connecting from outside the Simulation#
Important
From outside the simulation, you have to run Python with super user privileges. The client has to move to a different networking namespace which requires these elevated privileges.
from wattson.cosimulation.control.interface.wattson_client import WattsonClient
from wattson.cosimulation.control.messages.wattson_query import WattsonQuery
from wattson.cosimulation.control.messages.wattson_query_type import WattsonQueryType
# Create client and connect
## This line is different from the "within the Simulation" snippet.
wattson_client = WattsonClient(namespace="auto", name="custom-client")
wattson_client.require_connection()
wattson_client.register()
# Send an ECHO query ("ping")
query = WattsonQuery(query_type=WattsonQueryType.ECHO)
response = wattson_client.query(query=query)
print(response.is_successful())
wattson_client.stop()
Requesting the WattsonTime#
Wattson offers a synchronized time for all simulators to use. By default, this is just the actual wall clock time. For some simulations, you might want to have the simulation run faster than real-time, e.g., you want to simulate a full day of power grid activity in 2 hours.
For this, Wattson offers the WattsonTime that translates between the wall clock and the simulated clock.
Important
Note that usually, the NetworkEmulation has to run in real time and does not respect the simulated clock!
To use the WattsonTime in your component, you can request it via the WattsonClient.
Using Notifications#
You can subscribe to individual topics or to the catch-all topic.
Note that, in the following example, a notification with topic "my-topic"
will trigger both callbacks!
from wattson.cosimulation.control.messages.wattson_notification import WattsonNotification
from wattson.cosimulation.control.messages.wattson_notification_topic import WattsonNotificationTopic
wattson_client = ...
# Callback for all notification
def notification_handler(notification: WattsonNotification):
print("Got notification")
print(f"Topic: {notification.topic}")
print(repr(notification.data))
def single_topic_handler(notification: WattsonNotification):
print("Got notification (specific)")
print(f"Topic: {notification.topic}")
print(repr(notification.data))
# "*" serves as a catch-all
wattson_client.subscribe(topic="*", callback=notification_handler)
# Specific topic
wattson_client.subscribe(topic="my-topic", callback=single_topic_handler)
# Trigger a notification yourself
my_notification = WattsonNotification(
notification_topic="my-topic",
notification_data={
"some": "data"
}
)
# Optionally: Restrict recipient
my_notification.recipients = ["client-1", "client-2", ...]
# Send Notification
wattson_client.notify(notification=my_notification)
# For notifications with activated history, you can request the history
notifications = wattson_client.get_notification_history(topic="my-topic")
Using Events#
Similar to Notifications, Events can be queried and modified with the WattsonClient.
from wattson.cosimulation.control.interface.wattson_event import WattsonEvent
wattson_client = ...
# Wait for an event (analog to threading.Event.wait(timeout))
if wattson_client.event_wait("my-event", timeout=10):
print("Event occured")
else:
print("Timeout while waiting for my-event")
# Check if event is set
if wattson_client.event_is_set("my-event"):
pass
# Set event
wattson_client.event_set("my-event")
# Clear event
wattson_client.event_clear("my-event")
# Typical Use Case: Wait for the Simulation to be started
if not wattson_client.event_wait(WattsonEvent.START, timeout=30):
print("Timeout during simulation startup")
return
## Execute code that depends on a running simulation
...