Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/oedisi/componentframework/basic_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import json
import os
from shutil import copytree
from . import system_configuration
from .system_configuration import AnnotatedType
from pydantic import BaseModel
from typing import Any

from pydantic import BaseModel, Field

from . import system_configuration
from .system_configuration import AnnotatedType, ComponentCapabilities
from oedisi.types.helics_config import HELICSFederateConfig


class ComponentDescription(BaseModel):
"""Component description for simple ComponentType.
Expand All @@ -24,13 +27,16 @@ class ComponentDescription(BaseModel):
List of input types. Typically subscriptions.
dynamic_outputs :
List of output types. Typically publications.
capabilities :
Component capability declarations for build-time validation.
"""

directory: str
execute_function: str
static_inputs: list[AnnotatedType]
dynamic_inputs: list[AnnotatedType]
dynamic_outputs: list[AnnotatedType]
capabilities: ComponentCapabilities = Field(default_factory=ComponentCapabilities)


def types_to_dict(types: list[AnnotatedType]):
Expand Down Expand Up @@ -64,17 +70,18 @@ class BasicComponent(system_configuration.ComponentType):
_dynamic_inputs = types_to_dict(comp_desc.dynamic_inputs)
_dynamic_outputs = types_to_dict(comp_desc.dynamic_outputs)
_static_inputs = types_to_dict(comp_desc.static_inputs)
_capabilities = comp_desc.capabilities

def __init__(
self,
name,
base_config: HELICSFederateConfig,
parameters: dict[str, Any],
directory: str,
host: str,
port: int,
comp_type: str,
):
self._name = name
self._base_config = base_config
self._directory = directory
self._parameters = parameters
self.check_parameters(parameters)
Expand All @@ -95,9 +102,13 @@ def copy_code_into_directory(self):
copytree(self._origin_directory, self._directory, dirs_exist_ok=True)

def generate_parameter_config(self):
self._parameters["name"] = self._name
if self.broker_config_support:
config = self._base_config.to_dict().update(self._parameters)
else: # Backwards compatible behavior where we ignore extra information.
config = self._parameters
config["name"] = self._base_config.name
with open(os.path.join(self._directory, "static_inputs.json"), "w") as f:
json.dump(self._parameters, f)
json.dump(config, f)

def generate_input_mapping(self, links):
with open(os.path.join(self._directory, "input_mapping.json"), "w") as f:
Expand All @@ -115,4 +126,8 @@ def dynamic_outputs(self):
def execute_function(self):
return self._execute_function

@property
def broker_config_support(self):
return self._capabilities.broker_config

return BasicComponent
8 changes: 5 additions & 3 deletions src/oedisi/componentframework/mock_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import logging
import json
import os

from . import system_configuration
from .system_configuration import AnnotatedType
from oedisi.types.helics_config import HELICSFederateConfig


logger = logging.getLogger(__name__)
Expand All @@ -25,14 +27,14 @@
class MockComponent(system_configuration.ComponentType):
def __init__(
self,
name,
base_config: HELICSFederateConfig,
parameters: dict[str, dict[str, str]],
directory: str,
host: str | None = None,
port: int | None = None,
comp_type: str | None = None,
):
self._name = name
self._base_config = base_config
self._directory = directory
self._execute_function = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "mock_component.sh"
Expand All @@ -51,7 +53,7 @@ def process_parameters(self, parameters):

def generate_helics_config(self, outputs):
helics_config = {
"name": self._name,
"name": self._base_config.name,
"core_type": "zmq",
"period": 1,
"log_level": "warning",
Expand Down
86 changes: 73 additions & 13 deletions src/oedisi/componentframework/system_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@

from pydantic import field_validator, BaseModel, ValidationInfo
from oedisi.types.common import DOCKER_HUB_USER, APP_NAME
from oedisi.types.helics_config import HELICSFederateConfig, SharedFederateConfig


class ComponentCapabilities(BaseModel):
"""Component capability declarations for build-time validation.

Parameters
----------
version :
Capabilities schema version.
broker_config :
Whether this component supports receiving federate_config in static_inputs.json.
If True, the component can be used with WiringDiagram.shared_helics_config.
"""

version: str = "1.0"
broker_config: bool = False


class AnnotatedType(BaseModel):
Expand Down Expand Up @@ -67,10 +84,12 @@ class ComponentType(ABC):
to run the component.
"""

_capabilities: ComponentCapabilities = ComponentCapabilities()

@abstractmethod
def __init__(
self,
name: str,
base_config: HELICSFederateConfig,
parameters: dict[str, dict[str, str]],
directory: str,
host: str | None = None,
Expand Down Expand Up @@ -128,6 +147,7 @@ class Component(BaseModel):
container_port: int | None = None
image: str = ""
parameters: dict[str, Any]
helics_config_override: SharedFederateConfig | None = None

def port(self, port_name: str):
return Port(name=self.name, port_name=port_name)
Expand All @@ -147,11 +167,25 @@ class ComponentStruct(BaseModel):


class WiringDiagram(BaseModel):
"""Cosimulation configuration. This may end up wrapped in another interface."""
"""Cosimulation configuration. This may end up wrapped in another interface.

Parameters
----------
name :
Name of the simulation.
components :
List of components in the simulation.
links :
List of links connecting component ports.
shared_helics_config :
Optional shared federate configuration applied to all components.
Per-component values (name, core_name) are derived automatically.
"""

name: str
components: list[Component]
links: list[Link]
shared_helics_config: SharedFederateConfig | None = None

def clean_model(self, target_directory="."):
for component in self.components:
Expand Down Expand Up @@ -201,7 +235,7 @@ def add_link(self, link: Link):

@classmethod
def empty(cls, name="unnamed"):
return cls(name=name, components=[], links=[])
return cls(name=name, components=[], links=[], shared_helics_config=None)


class Federate(BaseModel):
Expand Down Expand Up @@ -234,8 +268,34 @@ def initialize_federates(
if not os.path.exists(directory):
os.makedirs(directory)
component_type = component_types[component.type]

# Generate per-component federate config
federate_config = None
if component.helics_config_override is not None:
logging.warning(
f"Component '{component.name}' uses federate_config_override. "
"Per-component overrides can cause subtle timing issues."
)
federate_config = component.helics_config_override.to_federate_config(
name=component.name
)
elif wiring_diagram.shared_helics_config is not None:
federate_config = wiring_diagram.shared_helics_config.to_federate_config(
name=component.name
)

# Validate broker config support
if federate_config is not None and not component_type._capabilities.broker_config:
raise ValueError(
f"Component '{component.name}' (type: {component.type}) does not support "
'HELICS configuration. Add \'"capabilities": {"broker_config": true}\' '
"to the component's component_definition.json file."
)
elif federate_config is None:
federate_config = HELICSFederateConfig(name=component.name)

initialized_component = component_type(
component.name,
federate_config,
component.parameters,
directory,
component.host,
Expand All @@ -247,17 +307,17 @@ def initialize_federates(
for link in wiring_diagram.links:
source_types = components[link.source].dynamic_outputs
target_types = components[link.target].dynamic_inputs
assert (
link.source_port in source_types
), f"{link.source} does not have {link.source_port}"
assert (
link.target_port in target_types
), f"{link.target} does not have dynamic input {link.target_port}"
assert link.source_port in source_types, (
f"{link.source} does not have {link.source_port}"
)
assert link.target_port in target_types, (
f"{link.target} does not have dynamic input {link.target_port}"
)
source_type = source_types[link.source_port]
target_type = target_types[link.target_port]
assert compatability_checker(
source_type, target_type
), f"{source_type} is not compatible with {target_type}"
assert compatability_checker(source_type, target_type), (
f"{source_type} is not compatible with {target_type}"
)

federates = []
for name, component in components.items():
Expand Down
15 changes: 15 additions & 0 deletions src/oedisi/tools/cli_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ def build(
click.echo(f"Building system in {target_directory}")

if multi_container:
# Validate no broker overrides in multicontainer mode
if wiring_diagram.shared_helics_config is not None:
raise ValueError(
"Multicontainer builds do not support 'shared_helics_config'. "
"Broker configuration is controlled by the broker service at runtime."
)

for component in wiring_diagram.components:
if component.helics_config_override is not None:
raise ValueError(
f"Component '{component.name}' has 'helics_config_override'. "
"Multicontainer builds do not support per-component HELICS overrides. "
"Broker configuration is controlled by the broker service at runtime."
)

if simulation_id is None:
simulation_id = str(uuid4())
click.echo(f"Simulation ID: {simulation_id}")
Expand Down
12 changes: 12 additions & 0 deletions src/oedisi/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
__version__ = "3.0.1"

from oedisi.types.helics_config import (
HELICSBrokerConfig,
HELICSFederateConfig,
SharedFederateConfig,
)

__all__ = [
"HELICSBrokerConfig",
"HELICSFederateConfig",
"SharedFederateConfig",
]
Loading