Co-simulation Interfaces¶
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 (known as “federates”) to exchange data during runtime and stay synchronized in time such that together they act as one large simulation, or “federation”.
The HELICS interface for PyDSS is built to reduce the complexity of setting up large-scale co-simulation scenarios. The user defines publications and subscriptions to exchange data with external federates.
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 configured using the simulation.toml file.
The following attributes can be configured:
- 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: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='mainbroker', alias='Broker', alias_priority=2, title='broker', description='Broker name')] = 'mainbroker' (alias 'Broker')¶
Broker name
- field broker_port: Annotated[int, FieldInfo(annotation=NoneType, required=False, default=0, alias='Broker port', alias_priority=2, title='broker_port', description='Broker port')] = 0 (alias 'Broker port')¶
Broker port
- field co_simulation_mode: Annotated[bool, FieldInfo(annotation=NoneType, required=False, default=False, alias='Co-simulation Mode', alias_priority=2, title='co_simulation_mode', description='Set to true to enable the HELICS interface for co-simulation.')] = False (alias 'Co-simulation Mode')¶
Set to true to enable the HELICS interface for co-simulation.
- field core_type: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='zmq', alias='Core type', alias_priority=2, title='core_type', description='Core type to be use for communication')] = 'zmq' (alias 'Core type')¶
Core type to be use for communication
- field error_tolerance: Annotated[float, FieldInfo(annotation=NoneType, required=False, default=0.0001, alias='Error tolerance', alias_priority=2, title='error_tolerance', description='Error tolerance')] = 0.0001 (alias 'Error tolerance')¶
Error tolerance
- field federate_name: Annotated[str, FieldInfo(annotation=NoneType, required=False, default='pydss', alias='Federate name', alias_priority=2, title='federate_name', description='Name of the federate')] = 'pydss' (alias 'Federate name')¶
Name of the federate
- field iterative_mode: Annotated[bool, FieldInfo(annotation=NoneType, required=False, default=False, alias='Iterative Mode', alias_priority=2, title='iterative_mode', description='Iterative mode')] = False (alias 'Iterative Mode')¶
Iterative mode
- field logging_level: Annotated[int, FieldInfo(annotation=NoneType, required=False, default=5, alias='Helics logging level', alias_priority=2, title='logging_level', description='Logging level for the federate. Refer to HELICS documentation.')] = 5 (alias 'Helics logging level')¶
Logging level for the federate. Refer to HELICS documentation.
- Validated by:
- field max_co_iterations: Annotated[int, FieldInfo(annotation=NoneType, required=False, default=15, alias='Max co-iterations', alias_priority=2, title='max_co_iterations', description='Max number of co-simulation iterations')] = 15 (alias 'Max co-iterations')¶
Max number of co-simulation iterations
- Validated by:
- field time_delta: Annotated[float, FieldInfo(annotation=NoneType, required=False, default=0.01, alias='Time delta', alias_priority=2, title='time_delta', description='The property controlling the minimum time delta for a federate.')] = 0.01 (alias 'Time delta')¶
The property controlling the minimum time delta for a federate.
- field uninterruptible: Annotated[bool, FieldInfo(annotation=NoneType, required=False, default=True, alias='Uninterruptible', alias_priority=2, title='uninterruptible', description='Can the federate be interrupted')] = True (alias 'Uninterruptible')¶
Can the federate be interrupted
- validator check_logging_level » logging_level[source]¶
- validator check_max_co_iterations » max_co_iterations[source]¶
co_simulation_mode: Set totrueto enable the HELICS interface (default:false).federate_name: Required to identify a federate in a co-simulation with many federates.Additional settings for convergence, timing, and iteration can be configured here.
For more information on these values, refer to the HELICS documentation.
Once the HELICS co-simulation interface has been enabled, the next step is to set up
publications and subscriptions for data exchange with external federates.
PyDSS enables zero-code setup of these modules. Each scenario can have its own publication
and subscription definitions, managed by files in the ExportLists directory.
Publication tags (names) follow this convention:
<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 simulation.toml file.
Setting up Publications¶
Publications (data sent to external federates) can be configured using Exports.toml.
This file is also used to define export variables for a simulation scenario. By setting the
publish attribute to true, PyDSS automatically sets up a HELICS publication.
The file supports multiple filtering options including regex to publish only what is needed.
examples:
The following example sets up publications for all PV system powers in the model.
Setting publish to false will still write the data to the HDF5 store, but
will not publish it on the HELICS interface.
[[PVSystems]]
property = "Powers"
sample_interval = 1
publish = true
store_values_type = "all"
There are two options to filter and publish a subset of elements. You can use the
name_regexes attribute to filter elements matching regex expressions, or use the
names attribute to explicitly list elements.
Filtering using regex expressions:
[[PVSystems]]
property = "Powers"
name_regexes = [".*pvgnem.*"]
sample_interval = 1
publish = true
store_values_type = "all"
Filtering using explicit element names:
[[PVSystems]]
property = "Powers"
sample_interval = 1
names = ["PVSystems.pv1", "PVSystems.pv2"]
publish = true
store_values_type = "all"
Setting up Subscriptions¶
Subscriptions (data received from external federates) are configured using
Subscriptions.toml in the ExportLists directory for a given scenario.
Valid subscriptions should conform to the 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¶
When setting up subscriptions, note that the subscription tag is generated by the external
federate and must be known before configuration. In the example below, values received from
subscription tag test.load1.power are used to update the kw property of load
Load.mpx000635970. The multiplier property can scale values before they update the model.
example
[[subscriptions]]
model = "Load.mpx000635970"
property = "kw"
id = "test.load1.power"
unit = "kW"
subscribe = true
data_type = "double"
multiplier = 1
A complete example is available in examples/external_interfaces/.
The Socket Interface¶
The socket interface is implemented as a PyDSS controller
(pydss.pyControllers.Controllers.SocketController.SocketController).
It is well suited for situations where an existing external controller needs to be integrated
into the simulation environment — for example, integrating a controller for thermostatically
controlled loads implemented in Modelica or Python. This allows integration without modifying
the external controller.
The socket interface is also useful for hardware-in-the-loop simulations, integrating the
simulation engine with actual hardware. Interfaces for Modbus-TCP and DNP3 communications
have been developed and tested with PyDSS.
A minimal socket example is provided in examples/external_interfaces/pydss_project.
The socket scenario defines the socket controller in its pyControllerList folder.
The controller configuration specifies the element to control, the socket connection details,
and the data to exchange:
["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))