The HELICS interface#
Hierarchical Engine for Large-scale Infrastructure Co-Simulation (HELICS) provides an open-source, general-purpose, modular, highly-scalable co-simulation framework that runs cross-platform (Linux, Windows, and Mac OS X). It is not a modeling tool by itself, but rather an integration tool that enables multiple existing simulation tools (and/or multiple instances of the same tool), known as “federates,” to exchange data during runtime and stay synchronized in time such that together they act as one large simulation, or “federation”. This enables bringing together established (or new/emerging) off-the-shelf tools from multiple domains to form a complex software-simulation without having to change the individual tools (known as “black-box” modeling). All that is required is for someone to write a thin interface layer for each tool that interfaces with existing simulation time control and data value updating, such as through an existing scripting interface. Moreover, the HELICS community has a growing ecosystem of established interfaces for popular tools, such that many users can simply mix and match existing tools with their own data and run complex co-simulations with minimal coding. More information on HELICS can be found here (https://github.com/GMLC-TDC/HELICS).
The HELICS interface for PyDSS is built to reduce complexity of setting up large scale cosimulation scenarios. The user is required to publications and suscriptions.
A minimal HELICS example is availble in the examples
folder (top directory of the repository). Enabling the HELICS interface requires user to define additional parammeters in the scenario TOML file.
Interface overview#
The HELICS interface can be enabled and setup using the simultion.toml file.
Following attributes can be configured for the HELICS interface.
- pydantic model PyDSS.simulation_input_models.HelicsModel[source]#
Defines the user inputs for HELICS.
Show JSON schema
{ "title": "InputsBaseModel", "description": "Defines the user inputs for HELICS.", "type": "object", "properties": { "Co-simulation Mode": { "default": false, "description": "Set to true to enable the HELICS interface for co-simulation.", "title": "co_simulation_mode", "type": "boolean" }, "Iterative Mode": { "default": false, "description": "Iterative mode", "title": "iterative_mode", "type": "boolean" }, "Error tolerance": { "default": 0.0001, "description": "Error tolerance", "title": "error_tolerance", "type": "number" }, "Max co-iterations": { "default": 15, "description": "Max number of co-simulation iterations", "title": "max_co_iterations", "type": "integer" }, "Broker": { "default": "mainbroker", "description": "Broker name", "title": "broker", "type": "string" }, "Broker port": { "default": 0, "description": "Broker port", "title": "broker_port", "type": "integer" }, "Federate name": { "default": "PyDSS", "description": "Name of the federate", "title": "federate_name", "type": "string" }, "Time delta": { "default": 0.01, "description": "The property controlling the minimum time delta for a federate.", "title": "time_delta", "type": "number" }, "Core type": { "default": "zmq", "description": "Core type to be use for communication", "title": "core_type", "type": "string" }, "Uninterruptible": { "default": true, "description": "Can the federate be interrupted", "title": "uninterruptible", "type": "boolean" }, "Helics logging level": { "default": 5, "description": "Logging level for the federate. Refer to HELICS documentation.", "title": "logging_level", "type": "integer" } }, "additionalProperties": false }
- Config:
title: str = InputsBaseModel
str_strip_whitespace: bool = True
validate_assignment: bool = True
validate_default: bool = True
extra: str = forbid
use_enum_values: bool = False
populate_by_name: bool = True
- Fields:
- Validators:
- field broker: str = 'mainbroker' (alias 'Broker')#
Broker name
- field broker_port: int = 0 (alias 'Broker port')#
Broker port
- field co_simulation_mode: bool = False (alias 'Co-simulation Mode')#
Set to true to enable the HELICS interface for co-simulation.
- field core_type: str = 'zmq' (alias 'Core type')#
Core type to be use for communication
- field error_tolerance: float = 0.0001 (alias 'Error tolerance')#
Error tolerance
- field federate_name: str = 'PyDSS' (alias 'Federate name')#
Name of the federate
- field iterative_mode: bool = False (alias 'Iterative Mode')#
Iterative mode
- field logging_level: int = 5 (alias 'Helics logging level')#
Logging level for the federate. Refer to HELICS documentation.
- Validated by:
- field max_co_iterations: int = 15 (alias 'Max co-iterations')#
Max number of co-simulation iterations
- Validated by:
- field time_delta: float = 0.01 (alias 'Time delta')#
The property controlling the minimum time delta for a federate.
- field uninterruptible: bool = True (alias 'Uninterruptible')#
Can the federate be interrupted
- validator check_logging_level » logging_level[source]#
- validator check_max_co_iterations » max_co_iterations[source]#
- model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#
A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
“co_simulation_mode” : Set to ‘true’ to enable the HELICS interface. By default it is set to ‘false’
“federate_name” : Required to identify a federate in a cosimulation with a large number of federates.
Additional attribution pertaining to convergence, timing and iteration can be configured here
Default values for additional simulation settings are as follows. For more information on how to appropriately set these values please look at HELICS documentaion
Once the HELICS co-simulation interface has been enabled, the next step is to set up publications
and subscriptions
to set up information exchange with external federates.
PyDSS enables zero code setup of these modules. Each scenario can have its publlcation and subscription defination and is managed by two file in the ExportLists
directory for a given scenario.
publication tags (names) follow the following convertion
<federate name>.<object type>.<object name>.<object property>
examples,
federate1.Circuit.70008.TotalPower
federate1.Load.load_1.VoltageMagAng
where federate name
is defined in the project’s settings.toml
file
Setting up publications#
Publications (information communicated to external federates) can be set up by using the Exports.toml
. This file is also used to define export varibles
for a simulation scenario. By setting the publish
attribute to true
, enable automated setup of a HELICS publication.
The file enables users to use multiple filtering options such as regex operation etc. to only pushlish what is reuired for a given use case.
examples:
The following code block will setup publications for all PV systems powers in a given model.
Setting the publish
attribute to false
will allow the data to be writtin to the h5 store,
but the data will not be published on the helics interface.
[[PVSystems]]
property = "Powers"
sample_interval = 1
publish = true
store_values_type = "all"
Users tave two options to filter and setup publication for a subset of object type (in this case PV systems).
User are able to use tag attribute name_regexes
to filter PV systems matching a given list of regex expressions.
Alternately, users can use names
attribute to explicitly define objects whos property they want publiched on the HELICS interface.
Filtering using regex expressions
[[PVSystems]]
property = "Powers"
name_regexes = [".*pvgnem.*"]
sample_interval = 1
publish = true
store_values_type = "all"
Filtering using explicitly list model names
[[PVSystems]]
property = "Powers"
sample_interval = 1
names = ["PVSystems.pv1", "PVSystems.pv2"]
publish = true
store_values_type = "all"
Setting up subscriptions#
Subscriptions (information ingested from external federates) can be set up
using the Subscriptions.toml
in the ExportLists
directory for a given scenario.
Valis subscriptions should confine to teh following model
- pydantic model PyDSS.helics_interface.Subscription[source]#
Show JSON schema
{ "title": "Subscription", "type": "object", "properties": { "model": { "title": "Model", "type": "string" }, "property": { "title": "Property", "type": "string" }, "id": { "title": "Id", "type": "string" }, "unit": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "default": null, "title": "Unit" }, "subscribe": { "default": true, "title": "Subscribe", "type": "boolean" }, "data_type": { "$ref": "#/$defs/DataType" }, "multiplier": { "default": 1.0, "title": "Multiplier", "type": "number" }, "object": { "default": null, "title": "Object" }, "states": { "default": [ 0.0, 0.0, 0.0, 0.0, 0.0 ], "items": { "anyOf": [ { "type": "number" }, { "type": "integer" }, { "type": "boolean" } ] }, "title": "States", "type": "array" }, "sub": { "default": null, "title": "Sub" } }, "$defs": { "DataType": { "enum": [ "double", "vector", "string", "boolean", "integer" ], "title": "DataType", "type": "string" } }, "required": [ "model", "property", "id", "data_type" ] }
- Config:
arbitrary_types_allowed: bool = True
- Fields:
- field data_type: DataType [Required]#
- field id: str [Required]#
- field model: str [Required]#
- field multiplier: float = 1.0#
- field object: Any = None#
- field property: str [Required]#
- field states: List[float | int | bool] = [0.0, 0.0, 0.0, 0.0, 0.0]#
- field sub: Any = None#
- field subscribe: bool = True#
- field unit: str | None = None#
- model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#
A dictionary of computed field names and their corresponding ComputedFieldInfo objects.
When setting up subscriptions it is important to understand that the subscription tag is generated by
the external federate and should be known before setting up the subscriptions. In the example below, values recieved from
subscription tag test.load1.power
are used to update the kw
property of load Load.mpx000635970
. multiplier
property can be used to
scale values before they are used to update the coupled model.
example
[[subscriptions]]
model = "Load.mpx000635970"
property = "kw"
id = "test.load1.power"
unit = "kW"
subscribe = true
data_type = "double"
multiplier = 1
Within the example folder the project named external interfaces provides an example usage of all three interafces.
The socket interface#
The socket interface is implemented as a PyDSS pyController. Implmentation details and expected inputs are detailed here: PyDSS.pyControllers.Controllers.SocketController.SocketController
.
The socket controller is well suited in situatons where an existing controller needs to be integrated to the simulation environment.
An exmaple of this would be integrating a controller for thermostatically controlled loads implemeted in say Modelica or Python.
This allows user to integrate controller, without making changes to the implemented controller. With a little effort,
the same controller can be implemented as a pyController object in PyDSS.
The socket interface in PyDSS also come in handy, when setting up a hardware-in-loop type simulations and integrating the simulation
engine with actual hardware. Interfaces similar to raw socket implementations have been developed (to be open-sourced at a later time)
for Modbus-TCP and DNP3 communcations have developed and tested with PyDSS with sucess. A minimal socket interfacing example has
been provided as a PyDSS project in ~PyDSS/examples/external_interfaces. Within the folder,
~/PyDSS/examples/external_interfaces/pydss_project a scenario called ‘socket’ has been defined. Socket
controller definations have been detailed with the ‘pyControllerList’ folder. An example of input requirements can be studied below.
This example will publish voltage magnitude
(see Even set in Index) and real power
for load Load.mpx000635970
in the model. Subscribed
values will be used to update the kW
property of the coupled load (Load.mpx000635970 in this case)
["Load.mpx000635970"]
IP = "127.0.0.1"
Port = 5001
Encoding = false
Buffer = 1024
Index = "Even,Even"
Inputs = "VoltagesMagAng,Powers"
Outputs = "kW"
Finally, the minimal example below shows how to retrive data from the sockets and return new values for parameters defined in the definations file.
# first of all import the socket library
import socket
import struct
# next create a socket object
sockets = []
for i in range(2):
s = socket.socket()
s.bind(('127.0.0.1', 5001 + i))
s.listen(5)
sockets.append(s)
while True:
# Establish connection with client.
conns = []
for s in sockets:
c, addr = s.accept()
conns.append(c)
while True:
for c in conns: #Reading data from all ports
Data = c.recv(1024)
if Data: #Creating a list of doubles from the recieved byte stream
numDoubles = int(len(Data) / 8)
tag = str(numDoubles) + 'd'
Data = list(struct.unpack(tag, Data))
for c , v in zip(conns, [5, 3]): #Writing data to all ports
values = [v]
c.sendall(struct.pack('%sd' % len(values), *values))