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))