From 176aff1133ffc7262a1ff912870fccab0f88c542 Mon Sep 17 00:00:00 2001 From: Joseph McKinsey Date: Mon, 2 Feb 2026 15:16:04 -0700 Subject: [PATCH] Add a lot of docstrings to satisfy ruff --- docs/conf.py | 2 + .../componentframework/basic_component.py | 4 +- .../componentframework/mock_component.py | 58 ++++++++- .../system_configuration.py | 25 +++- .../wiring_diagram_utils.py | 3 + src/oedisi/tools/api.py | 1 + src/oedisi/tools/broker_utils.py | 6 +- src/oedisi/tools/cli_tools.py | 23 +++- src/oedisi/tools/metrics.py | 2 +- src/oedisi/tools/pausing_broker.py | 6 + src/oedisi/tools/testing_broker.py | 8 ++ src/oedisi/types/common.py | 10 ++ src/oedisi/types/data_types.py | 120 ++++++++++++------ src/oedisi/types/generate_schema.py | 2 + 14 files changed, 220 insertions(+), 50 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 66aa918..525cf7d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,5 @@ +"""Sphinx documentation configuration for OEDISI.""" + # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full diff --git a/src/oedisi/componentframework/basic_component.py b/src/oedisi/componentframework/basic_component.py index 8780ab1..a8517a8 100644 --- a/src/oedisi/componentframework/basic_component.py +++ b/src/oedisi/componentframework/basic_component.py @@ -34,17 +34,19 @@ class ComponentDescription(BaseModel): def types_to_dict(types: list[AnnotatedType]): + """Convert list of annotated types to dictionary keyed by port name.""" return {t.port_name: t for t in types} def component_from_json(filepath, type_checker): + """Load component description from JSON file and create component type.""" with open(filepath) as f: comp_desc = ComponentDescription.model_validate(json.load(f)) return basic_component(comp_desc, type_checker) def basic_component(comp_desc: ComponentDescription, type_checker): - """Uses data in component_definition to create a new component type. + """Create a new component type from component definition data. Parameters ---------- diff --git a/src/oedisi/componentframework/mock_component.py b/src/oedisi/componentframework/mock_component.py index 6347b5d..b429482 100644 --- a/src/oedisi/componentframework/mock_component.py +++ b/src/oedisi/componentframework/mock_component.py @@ -1,4 +1,5 @@ -""" +"""Mock component and federate for testing HELICS simulations. + MockComponent and MockFederate allow you to instantiate a mock component with a specified set of inputs and outputs. The parameters dictionary should contain a list under "inputs" and "outputs". During implementation, @@ -23,6 +24,27 @@ class MockComponent(system_configuration.ComponentType): + """Mock component for testing HELICS-based simulations. + + Provides a configurable mock component with dynamic inputs and outputs + for use in testing and validation scenarios. + + Parameters + ---------- + name : str + Name of the mock component. + parameters : dict[str, dict[str, str]] + Configuration parameters containing "inputs" and "outputs" keys. + directory : str + Working directory for component configuration files. + host : str, optional + Host address (not used in mock implementation). + port : int, optional + Port number (not used in mock implementation). + comp_type : str, optional + Component type identifier (not used in mock implementation). + """ + def __init__( self, name, @@ -40,6 +62,13 @@ def __init__( self.process_parameters(parameters) def process_parameters(self, parameters): + """Process and configure component parameters. + + Parameters + ---------- + parameters : dict[str, dict[str, str]] + Configuration dictionary with "inputs" and "outputs" keys. + """ self._dynamic_inputs = { name: AnnotatedType(type="", port_id=name) for name in parameters["inputs"] } @@ -50,6 +79,13 @@ def process_parameters(self, parameters): self.generate_helics_config(parameters["outputs"]) def generate_helics_config(self, outputs): + """Generate HELICS configuration file for the mock component. + + Parameters + ---------- + outputs : dict[str, str] + Mapping of output port names to HELICS data types. + """ helics_config = { "name": self._name, "core_type": "zmq", @@ -63,27 +99,39 @@ def generate_helics_config(self, outputs): json.dump(helics_config, f) def generate_input_mapping(self, links): + """Generate input mapping file for subscriptions. + + Parameters + ---------- + links : dict + Mapping of local input port names to HELICS subscription keys. + """ with open(os.path.join(self._directory, "input_mapping.json"), "w") as f: json.dump(links, f) @property def dynamic_inputs(self): + """Dynamic input ports.""" return self._dynamic_inputs @property def dynamic_outputs(self): + """Dynamic output ports.""" return self._dynamic_outputs @property def execute_function(self): + """Path to mock component execution script.""" return self._execute_function def get_default_value(date_type: h.HelicsDataType): + """Return pi value for mock publications.""" return 3.1415926536 def destroy_federate(fed): + """Disconnect and free a HELICS federate.""" _ = h.helicsFederateDisconnect(fed) h.helicsFederateFree(fed) h.helicsCloseLibrary() @@ -91,7 +139,14 @@ def destroy_federate(fed): class MockFederate: + """Mock HELICS federate for testing simulations. + + Loads configuration and subscriptions from files, then publishes + test values during simulation. + """ + def __init__(self): + """Initialize mock federate from HELICS and input mapping configs.""" logger.info(f"Current Working Directory: {os.path.abspath(os.curdir)}") self.fed = h.helicsCreateValueFederateFromConfig("helics_config.json") logger.info(f"Created federate {self.fed.name}") @@ -105,6 +160,7 @@ def __init__(self): logging.info("Loaded all subscriptions from file") def run(self): + """Execute simulation, publishing values for 100 seconds.""" self.fed.enter_executing_mode() logger.info("Entered HELICS execution mode") diff --git a/src/oedisi/componentframework/system_configuration.py b/src/oedisi/componentframework/system_configuration.py index 7c7ebe2..57b97df 100644 --- a/src/oedisi/componentframework/system_configuration.py +++ b/src/oedisi/componentframework/system_configuration.py @@ -81,25 +81,31 @@ def __init__( @abstractmethod def generate_input_mapping(self, links: dict[str, str]): + """Generate input mapping from link target ports to HELICS subscription keys.""" pass @property @abstractmethod def execute_function(self): + """Command to execute the component federate.""" pass @property @abstractmethod def dynamic_inputs(self): + """Dictionary of dynamic input port names to types.""" pass @property @abstractmethod def dynamic_outputs(self): + """Dictionary of dynamic output port names to types.""" pass class Link(BaseModel): + """Connection between component ports in wiring diagram.""" + source: str source_port: str target: str @@ -107,10 +113,13 @@ class Link(BaseModel): class Port(BaseModel): + """Port identifier for creating links between components.""" + name: str port_name: str def connect(self, port: "Port"): + """Create link from this port to target port.""" return Link( source=self.name, source_port=self.port_name, @@ -130,6 +139,7 @@ class Component(BaseModel): parameters: dict[str, Any] def port(self, port_name: str): + """Create Port object for connecting this component.""" return Port(name=self.name, port_name=port_name) @field_validator("image", mode="before") @@ -142,6 +152,8 @@ def validate_image(cls, v, info: ValidationInfo): class ComponentStruct(BaseModel): + """Component with its associated links for multi-container configuration.""" + component: Component links: list[Link] @@ -154,6 +166,7 @@ class WiringDiagram(BaseModel): links: list[Link] def clean_model(self, target_directory="."): + """Remove component directories, log files, and stray broker processes.""" for component in self.components: to_delete = os.path.join(target_directory, component.name) log_file = os.path.join(target_directory, component.name + ".log") @@ -170,7 +183,6 @@ def clean_model(self, target_directory="."): else: os.remove(log_file) - # TODO: Check for any processes using the HELICS port and kill them too for proc in psutil.process_iter(): if proc.name() == "helics_broker": proc.kill() @@ -186,6 +198,7 @@ def check_component_names(cls, components): @field_validator("links") @classmethod def check_link_names(cls, links, info: ValidationInfo): + """Validate that link source and target components exist.""" if "components" in info.data: components = info.data["components"] names = set(map(lambda c: c.name, components)) @@ -194,13 +207,16 @@ def check_link_names(cls, links, info: ValidationInfo): return links def add_component(self, c: Component): + """Add component to wiring diagram.""" self.components.append(c) def add_link(self, link: Link): + """Add link to wiring diagram.""" self.links.append(link) @classmethod def empty(cls, name="unnamed"): + """Create empty wiring diagram with no components or links.""" return cls(name=name, components=[], links=[]) @@ -214,6 +230,7 @@ class Federate(BaseModel): def get_federates_conn_info(wiring_diagram: WiringDiagram): + """Get connection information string for all federates.""" data = "" for component in wiring_diagram.components: data += f" {component.host} {component.port}" @@ -274,6 +291,7 @@ def initialize_federates( def get_link_map(wiring_diagram: WiringDiagram): + """Create mapping from component names to their incoming links.""" link_map = defaultdict(list) for link in wiring_diagram.links: link_map[link.target].append(link) @@ -300,7 +318,7 @@ class RunnerConfig(BaseModel): def bad_compatability_checker(type1, type2): - """Basic compatability checker that says all types are compatible.""" + """Return True for all type pairs (no type checking).""" return True @@ -310,8 +328,7 @@ def generate_runner_config( compatibility_checker=bad_compatability_checker, target_directory=".", ): - """Brings together a `WiringDiagram` and a dictionary of `ComponentTypes` - to create a helics run configuration. + """Create HELICS run configuration from wiring diagram and component types. Parameters ---------- diff --git a/src/oedisi/componentframework/wiring_diagram_utils.py b/src/oedisi/componentframework/wiring_diagram_utils.py index 83ca8cc..9dabb34 100644 --- a/src/oedisi/componentframework/wiring_diagram_utils.py +++ b/src/oedisi/componentframework/wiring_diagram_utils.py @@ -26,6 +26,7 @@ def get_graph(wiring_diagram: WiringDiagram): def plot_graph_matplotlib(wiring_diagram: WiringDiagram): + """Plot wiring diagram using matplotlib with spring layout.""" import matplotlib.pyplot as plt import networkx as nx @@ -53,6 +54,7 @@ def plot_graph_matplotlib(wiring_diagram: WiringDiagram): def get_graph_renderer(G): # noqa: N803 + """Create bokeh graph renderer with styling for interactive visualization.""" import networkx as nx from bokeh.plotting import from_networkx from bokeh.models import Circle, EdgesOnly, MultiLine @@ -80,6 +82,7 @@ def get_graph_renderer(G): # noqa: N803 def plot_graph_bokeh(wiring_diagram: WiringDiagram): + """Plot wiring diagram using bokeh for interactive visualization.""" from bokeh.models import ( BoxSelectTool, HoverTool, diff --git a/src/oedisi/tools/api.py b/src/oedisi/tools/api.py index e69de29..c38d53a 100644 --- a/src/oedisi/tools/api.py +++ b/src/oedisi/tools/api.py @@ -0,0 +1 @@ +"""API utilities for OEDISI.""" diff --git a/src/oedisi/tools/broker_utils.py b/src/oedisi/tools/broker_utils.py index 10aa68d..4ace561 100644 --- a/src/oedisi/tools/broker_utils.py +++ b/src/oedisi/tools/broker_utils.py @@ -1,3 +1,5 @@ +"""Utilities for HELICS broker time data management.""" + from pydantic import BaseModel @@ -10,7 +12,7 @@ class TimeData(BaseModel): def pprint_time_data(time_data): - """A table would be better somehow, but which should be the columns.""" + """Pretty print time data for a federate.""" print( f""" Name : {time_data.name} @@ -21,6 +23,7 @@ def pprint_time_data(time_data): def parse_time_data(response): + """Parse broker response into list of TimeData objects.""" time_data = [] for core in response["cores"]: for fed in core["federates"]: @@ -36,5 +39,6 @@ def parse_time_data(response): def get_time_data(broker): + """Query broker for global time data and parse into TimeData objects.""" # Use global time debugging? return parse_time_data(broker.query("broker", "global_time")) diff --git a/src/oedisi/tools/cli_tools.py b/src/oedisi/tools/cli_tools.py index 5b6bb85..2a54269 100644 --- a/src/oedisi/tools/cli_tools.py +++ b/src/oedisi/tools/cli_tools.py @@ -1,3 +1,5 @@ +"""CLI tools for building and running OEDISI simulations.""" + from typing import Any from pathlib import Path from uuid import uuid4 @@ -38,15 +40,17 @@ @click.group() def cli(): + """OEDISI CLI for building and running simulations.""" pass def bad_type_checker(type, x): - """Does not check types.""" + """Return True for all types (no type checking).""" return True def get_basic_component(filename): + """Load basic component from component definition file.""" # before, the runner would use the directory given _in_ the component description # which may be inaccurate with open(filename) as f: @@ -159,6 +163,7 @@ def build( def validate_optional_inputs(wiring_diagram: WiringDiagram, component_dict_of_files: dict): + """Validate required host and container_port for multi-container.""" for component in wiring_diagram.components: assert hasattr(component, "host"), ( f"host parameter required for component {component.name} " @@ -171,6 +176,7 @@ def validate_optional_inputs(wiring_diagram: WiringDiagram, component_dict_of_fi def drop_null_values(model: Any) -> dict: + """Remove null values from dict and convert snake_case to camelCase.""" clean_model = {} assert isinstance(model, dict), "input to this function should be a dict" for k, v in model.items(): @@ -202,6 +208,7 @@ def create_kubernetes_deployment( broker_port: int, simulation_id: str, ): + """Create Kubernetes deployment YAML files for wiring diagram components.""" kube_folder = os.path.join(target_directory, "kubernetes") if not os.path.exists(kube_folder): os.makedirs(kube_folder, exist_ok=True) @@ -240,6 +247,7 @@ def create_kubernetes_deployment( def create_single_kubernetes_deyployment( component: Component, kube_folder: Path | str, simulation_id: str ): + """Create Kubernetes pod YAML file for a single component.""" kube_network_svc = f"{KUBERNETES_SERVICE_PREFIX}-{simulation_id}".lower() fixed_container_name = component.name.replace("_", "-") my_container = client.V1Container( @@ -275,6 +283,7 @@ def create_single_kubernetes_deyployment( def edit_docker_file(file_path, component: Component): + """Generate Dockerfile for component with OEDISI and component dependencies.""" dir_path = os.path.abspath(os.path.join(file_path, os.pardir)) server_file = os.path.join(dir_path, "server.py") assert os.path.exists(server_file), ( @@ -287,7 +296,8 @@ def edit_docker_file(file_path, component: Component): f.write("RUN apt-get update\n") f.write("RUN apt-get install -y git ssh\n") - # TODO: This works for now. Should be removed when a tagged release is available + # TODO(jmckinse): Remove when tagged release is available + # https://github.com/openEDI/oedisi/issues/75 f.write("RUN git clone https://github.com/openEDI/oedisi.git /oedisi\n") f.write("RUN pip install /oedisi \n") @@ -303,6 +313,7 @@ def edit_docker_file(file_path, component: Component): def edit_docker_files(wiring_diagram: WiringDiagram, component_types: dict): + """Generate Dockerfiles for all unique component types in wiring diagram.""" parsed_components = [] for component in wiring_diagram.components: if component.type not in parsed_components: @@ -319,6 +330,7 @@ def create_docker_compose_file( component_types: dict, simulation_id: str, ): + """Create docker-compose.yml configuration for multi-container simulation.""" config = {"services": {}, "networks": {}} config["services"][f"{APP_NAME}_{BROKER_SERVICE}"] = { @@ -369,7 +381,7 @@ def create_docker_compose_file( help="Location of helics run json. Usually build/system_runner.json", ) def run(runner): - """Calls out to helics run command. + """Run HELICS simulation using helics run command. Examples:: @@ -386,7 +398,9 @@ def run(runner): help="Location of helics run json. Usually build/system_runner.json", ) def run_with_pause(runner): - """Helics broker is run in the foreground, and we allow user input + """Run HELICS simulation with interactive time barrier control. + + Helics broker is run in the foreground, and we allow user input to block time. Examples:: @@ -438,6 +452,7 @@ def run_with_pause(runner): help="Use the flag to launch in a kubernetes pod. ", ) def run_mc(runner, kubernetes, docker_compose): + """Run multi-container simulation using docker-compose or Kubernetes.""" assert os.path.exists(runner), f"The provied path {runner} does not exist." file_name = Path(runner).name.lower() os.system("docker system prune --all") diff --git a/src/oedisi/tools/metrics.py b/src/oedisi/tools/metrics.py index b8b83ba..7180244 100644 --- a/src/oedisi/tools/metrics.py +++ b/src/oedisi/tools/metrics.py @@ -1,4 +1,4 @@ -"""Metrics for comparing voltages and power flows from algorithms: +"""Metrics for comparing voltages and power flows from algorithms. For voltages: - Mean absolute relative error. diff --git a/src/oedisi/tools/pausing_broker.py b/src/oedisi/tools/pausing_broker.py index b92ba38..60650b6 100644 --- a/src/oedisi/tools/pausing_broker.py +++ b/src/oedisi/tools/pausing_broker.py @@ -1,3 +1,5 @@ +"""Pausing broker for interactive HELICS simulations.""" + import time import click @@ -6,10 +8,14 @@ class PausingBroker: + """Interactive HELICS broker with user-controlled time barriers.""" + def __init__(self, n): + """Initialize pausing broker with n federates.""" self.initstring = f"-f {n} --name=mainbroker" def run(self): + """Run broker with interactive time barrier control.""" self.broker = h.helicsCreateBroker("zmq", "", self.initstring) print("Setting time barrier to 0.0") h.helicsBrokerSetTimeBarrier(self.broker, 0.0) diff --git a/src/oedisi/tools/testing_broker.py b/src/oedisi/tools/testing_broker.py index 6d78135..a4ebc46 100644 --- a/src/oedisi/tools/testing_broker.py +++ b/src/oedisi/tools/testing_broker.py @@ -1,3 +1,5 @@ +"""Testing utilities for HELICS broker and federate graph management.""" + import time from typing import cast @@ -5,6 +7,7 @@ def get_inputs_outputs(graph_dict): + """Extract input and output mappings from federate graph dictionary.""" federate_inputs = {} federate_outputs = {} federate_id_handle2key = {} @@ -34,10 +37,14 @@ def get_inputs_outputs(graph_dict): class TestingBroker: + """HELICS broker for testing federate graph connections.""" + def __init__(self, n): + """Initialize testing broker with n federates.""" self.initstring = f"-f {n} --name=mainbroker" def run(self): + """Run broker and extract data flow graph.""" time.sleep(2) self.broker = h.helicsCreateBroker("zmq", "", self.initstring) h.helicsBrokerSetTimeBarrier(self.broker, 0.0) @@ -50,6 +57,7 @@ def run(self): return get_inputs_outputs(graph_dict) def wait_until_connected(self): + """Wait until all federates have initialized and connected.""" print("Waiting for initialization") while True: time.sleep(2) diff --git a/src/oedisi/types/common.py b/src/oedisi/types/common.py index 4cd3dd9..d9f0193 100644 --- a/src/oedisi/types/common.py +++ b/src/oedisi/types/common.py @@ -1,3 +1,5 @@ +"""Common types and constants for OEDISI components.""" + from pydantic import BaseModel from enum import Enum import warnings @@ -10,11 +12,15 @@ class DefaultFileNames(str, Enum): + """Standard filenames for component configuration files.""" + INPUT_MAPPING = "input_mapping.json" STATIC_INPUTS = "static_inputs.json" class BrokerConfig(BaseModel): + """Configuration for HELICS broker connection.""" + broker_port: int = 23404 broker_ip: str = "127.0.0.1" api_port: int = 12345 @@ -23,6 +29,8 @@ class BrokerConfig(BaseModel): class HealthCheck(BaseModel): + """Health check response for component status.""" + hostname: str host_ip: str @@ -41,5 +49,7 @@ def __init__(self, **data): class ServerReply(BaseModel): + """Standard server response message.""" + detail: str action: str | None = None diff --git a/src/oedisi/types/data_types.py b/src/oedisi/types/data_types.py index cc5796e..fe4ae6c 100644 --- a/src/oedisi/types/data_types.py +++ b/src/oedisi/types/data_types.py @@ -1,17 +1,18 @@ +"""Power system data types for OEDISI measurements and control.""" + from __future__ import annotations import datetime from enum import Enum from pydantic import model_validator, BaseModel, RootModel, Field ### Supporting Functions ### -# TODO: Connect with CIM values - Complex = tuple[float, float] class StateArray(BaseModel): - """ + """Base class for power system equipment state arrays. + Extended by classes: "SwitchStates", "CapacitorStates", @@ -25,19 +26,26 @@ class StateArray(BaseModel): class SwitchStates(StateArray): + """Switch state data for power system equipment.""" + pass class CapacitorStates(StateArray): + """Capacitor state data for power system equipment.""" + pass class RegulatorStates(StateArray): + """Voltage regulator state data for power system equipment.""" + pass class CostArray(BaseModel): - """ + """Base class for cost-related data arrays. + Extended by classes: "RealCostFunctions", "ReactiveCostFunctions", @@ -54,27 +62,38 @@ class CostArray(BaseModel): class RealCostFunctions(CostArray): + """Real power cost functions for equipment.""" + pass class ReactiveCostFunctions(CostArray): + """Reactive power cost functions for equipment.""" + pass class RealWholesalePrices(CostArray): + """Real power wholesale price data for equipment.""" + pass class ReactiveWholesalePrices(CostArray): + """Reactive power wholesale price data for equipment.""" + pass class OperationalCosts(CostArray): + """Operational cost data for equipment.""" + pass class MeasurementArray(BaseModel): - """ + """Base class for measurement data arrays. + Extended by classes: "BusArray", "EquipmentArray", @@ -90,136 +109,151 @@ class MeasurementArray(BaseModel): class BusArray(MeasurementArray): - """ - Extended by classes: - "VoltagesMagnitude", - "VoltagesAngle", - "VoltagesReal", - "VoltagesImaginary". - """ + """Measurements for or at power system buses (primarily voltages).""" pass class EquipmentArray(MeasurementArray): - """ - Extended by classes: - "SolarIrradiances", - "Temperatures", - "WindSpeeds", - "StatesOfCharge", - "CurrentsMagnitude", - "CurrentsAngle", - "CurrentsReal", - "CurrentsImaginary", - "ImpedanceMagnitude", - "ImpedanceAngle", - "ImpedanceReal", - "ImpedanceImaginary",. - """ + """Measurements at equipment nodes (currents, impedances, environmental).""" pass class EquipmentNodeArray(MeasurementArray): - """ - Primary key is ids + equipment_ids. - - - ids corresponding node id, so "113.1", "113.2", "113.3" - - equipment_id corresponds to PVSystem.113 - - Extended by classes: - "PowersMagnitude", - "PowersAngle", - "PowersReal", - "PowersImaginary", + """Power measurements at equipment nodes (primary key: ids + equipment_ids). + Primary key is ids + equipment_ids where ids correspond to node ids + (e.g., "113.1", "113.2", "113.3") and equipment_ids correspond to + equipment identifiers (e.g., PVSystem.113). """ equipment_ids: list[str] class VoltagesMagnitude(BusArray): + """Voltage magnitude measurements at buses.""" + units: str = "V" class VoltagesAngle(BusArray): + """Voltage angle measurements at buses.""" + units: str = "radians" class VoltagesReal(BusArray): + """Real component of voltage measurements at buses.""" + units: str = "V" class VoltagesImaginary(BusArray): + """Imaginary component of voltage measurements at buses.""" + units: str = "V" class CurrentsMagnitude(EquipmentArray): + """Current magnitude measurements at equipment.""" + units: str = "A" class CurrentsAngle(EquipmentArray): + """Current angle measurements at equipment.""" + units: str = "radians" class CurrentsReal(EquipmentArray): + """Real component of current measurements at equipment.""" + units: str = "A" class CurrentsImaginary(EquipmentArray): + """Imaginary component of current measurements at equipment.""" + units: str = "A" class ImpedanceReal(EquipmentArray): + """Real component of impedance measurements at equipment.""" + units: str = "Ohm" class ImpedanceImaginary(EquipmentArray): + """Imaginary component of impedance measurements at equipment.""" + units: str = "Ohm" class ImpedanceMagnitude(EquipmentArray): + """Impedance magnitude measurements at equipment.""" + units: str = "Ohm" class ImpedanceAngle(EquipmentArray): + """Impedance angle measurements at equipment.""" + units: str = "radians" class PowersMagnitude(EquipmentNodeArray): + """Power magnitude (apparent power) measurements at equipment nodes.""" + units: str = "kVA" class PowersAngle(EquipmentNodeArray): + """Power angle measurements at equipment nodes.""" + units: str = "radians" class PowersReal(EquipmentNodeArray): + """Real power measurements at equipment nodes.""" + units: str = "kW" class PowersImaginary(EquipmentNodeArray): + """Reactive power measurements at equipment nodes.""" + units: str = "kVAR" class SolarIrradiances(EquipmentArray): + """Solar irradiance measurements at equipment.""" + units: str = "kW/m^2" class Temperatures(EquipmentArray): + """Temperature measurements at equipment.""" + units: str = "C" class WindSpeeds(EquipmentArray): + """Wind speed measurements at equipment.""" + units: str = "m/s" class StatesOfCharge(EquipmentArray): + """State of charge measurements for energy storage equipment.""" + units: str = "percent" class Topology(BaseModel): + """Power system network topology with admittance and injection data.""" + admittance: AdmittanceSparse | AdmittanceMatrix injections: Injection incidences: IncidenceList | None = None @@ -229,27 +263,37 @@ class Topology(BaseModel): class Incidence(BaseModel): + """Incidence relationships between equipment in the power system.""" + from_equipment: list[str] to_equipment: list[str] equipment_type: list[str] | None = None class IncidenceList(Incidence): + """Incidence relationships with associated identifiers.""" + ids: list[str] class AdmittanceSparse(Incidence): + """Sparse representation of network admittance matrix.""" + admittance_list: list[Complex] units: str = "S" class AdmittanceMatrix(BaseModel): + """Dense representation of network admittance matrix.""" + admittance_matrix: list[list[Complex]] ids: list[str] units: str = "S" class Injection(BaseModel): + """Current and power injections at network nodes.""" + # Shouldn't these be equipment arrays? current_real: CurrentsReal = Field( default_factory=lambda: CurrentsReal(values=[], ids=[], units="A") diff --git a/src/oedisi/types/generate_schema.py b/src/oedisi/types/generate_schema.py index 38fded9..d3d84ed 100644 --- a/src/oedisi/types/generate_schema.py +++ b/src/oedisi/types/generate_schema.py @@ -1,3 +1,5 @@ +"""Generate JSON schemas for all OEDISI data types.""" + from .data_types import * import os import logging