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()
PythonConnecting from outside the Simulation
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()
PythonRequesting 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.
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.
wattson_time = wattson_client.get_wattson_time()
# Optionally enable synchronization with the central time instance:
# pull: Apply changes made to the central time to your local wattson time instance
# push: When you change the local wattson time, it should be propagated to the central time instance
wattson_time.enable_synchronization(wattson_client=wattson_client, enable_pull=True, enable_push=False)
# Get current WallClock Timestamp
wall_clock_time = wattson_time.wall_clock_time()
# Get current Timestamp in simulated time
sim_clock_time = wattson_time.sim_clock_time()
# Read the speed of the simulated time
print(wattson_time.speed)
PythonUsing 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")
PythonUsing 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
...
PythonRemote Representations
While interacting with Wattson through individual Queries gives you a lot of freedom, you usually only want to interact with a relatively small subset of features on a higher level, e.g., with Network Nodes or the Physical Simulation. While these are object-oriented implementations within the Co-Simulation Controller, a client usually has no direct access to them as they run in other processes.
To allow you to interact with these object, Wattson introduces Remote Representations for various components, most prominently the RemoteNetworkEmulator and the RemotePowerGridModel. They offer you the interface of the NetworkEmulator and PowerGridModel within the Co-Simulation Controller. They automatically synchronize with the actual state of these components by using Queries. However, for the user, the interface is (nearly) identical to using a local instance of the non-remote components.
To access them, you need a connected instance of the WattsonClient.
from wattson.cosimulation.simulators.network.remote_network_emulator import RemoteNetworkEmulator
from wattson.powergrid.remote.remote_power_grid_model import RemotePowerGridModel
wattson_client = ...
remote_network_emulator = RemoteNetworkEmulator(wattson_client=wattson_client)
remote_power_grid_model = RemotePowerGridModel(wattson_client=wattson_client)
# E.g., iterate over all hosts and print their IP addresses
for host in remote_network_emulator.get_hosts():
print(f"{host.entity_id}")
for interface in host.get_interfaces():
if interface.has_ip():
print(f" {interface.ip_address_string}")
# E.g., list all loads in the power grid
for load in remote_power_grid_model.get_elements_by_type("load"):
print(f"{load.get_identifier()}")
wattson_client.stop()
PythonFor detailed information on the Remote Representations please refer to the respective documentation: