-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Input format for components #31
Changes from all commits
144616d
1a66ac9
4e156f0
27e8377
c7ccaa6
0153969
c888296
8b1872e
23c31d8
c778b1f
551d4e6
c25a2d0
c4579d0
22a62d2
2e77a30
c3413e8
f038183
f537fd1
1eb5c2e
b1c7a1e
f0a9bcf
aff0de8
336336d
c93c6ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,5 @@ pytest-cov | |
pre-commit~=3.5.0 | ||
types-PyYAML~=6.0.12.12 | ||
antlr4-tools~=0.2.1 | ||
pandas~=2.0.3 | ||
pandas-stubs<=2.0.3 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Copyright (c) 2024, RTE (https://www.rte-france.com) | ||
# | ||
# See AUTHORS.txt | ||
# | ||
# This Source Code Form is subject to the terms of the Mozilla Public | ||
# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
# file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
# | ||
# SPDX-License-Identifier: MPL-2.0 | ||
# | ||
# This file is part of the Antares project. | ||
import typing | ||
from typing import List, Optional | ||
|
||
from pydantic import BaseModel, Field | ||
from yaml import safe_load | ||
|
||
|
||
def parse_yaml_components(input_components: typing.TextIO) -> "InputComponents": | ||
tree = safe_load(input_components) | ||
return InputComponents.model_validate(tree["study"]) | ||
|
||
|
||
# Design note: actual parsing and validation is delegated to pydantic models | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note : C++ has no pydantic. What does this do for us ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, pydantic is used here to convert from a dictionary/list representation to object oriented representation, including validation of inputs. Depending on available libs on c++ side, we will most likely need to implement more things by ourselves (c++ does not have reflection so it cannot really automate this kind of object construction, unlike python/java). |
||
def _to_kebab(snake: str) -> str: | ||
return snake.replace("_", "-") | ||
|
||
|
||
class InputPortConnections(BaseModel): | ||
component1: str | ||
port_1: str | ||
component2: str | ||
port_2: str | ||
|
||
|
||
class InputComponentParameter(BaseModel): | ||
name: str | ||
type: str | ||
value: Optional[float] = None | ||
timeseries: Optional[str] = None | ||
|
||
|
||
class InputComponent(BaseModel): | ||
id: str | ||
model: str | ||
parameters: Optional[List[InputComponentParameter]] = None | ||
|
||
|
||
class InputComponents(BaseModel): | ||
nodes: List[InputComponent] = Field(default_factory=list) | ||
components: List[InputComponent] = Field(default_factory=list) | ||
connections: List[InputPortConnections] = Field(default_factory=list) | ||
|
||
class Config: | ||
alias_generator = _to_kebab |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
# Copyright (c) 2024, RTE (https://www.rte-france.com) | ||
# | ||
# See AUTHORS.txt | ||
# | ||
# This Source Code Form is subject to the terms of the Mozilla Public | ||
# License, v. 2.0. If a copy of the MPL was not distributed with this | ||
# file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
# | ||
# SPDX-License-Identifier: MPL-2.0 | ||
# | ||
# This file is part of the Antares project. | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from typing import Dict, Iterable, List, Optional | ||
|
||
import pandas as pd | ||
|
||
from andromede.model import Model | ||
from andromede.model.library import Library | ||
from andromede.study import ( | ||
Component, | ||
ConstantData, | ||
DataBase, | ||
Network, | ||
Node, | ||
PortRef, | ||
PortsConnection, | ||
) | ||
from andromede.study.data import ( | ||
AbstractDataStructure, | ||
TimeScenarioIndex, | ||
TimeScenarioSeriesData, | ||
load_ts_from_txt, | ||
) | ||
from andromede.study.parsing import ( | ||
InputComponent, | ||
InputComponents, | ||
InputPortConnections, | ||
) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class NetworkComponents: | ||
components: Dict[str, Component] | ||
nodes: Dict[str, Component] | ||
connections: List[PortsConnection] | ||
|
||
|
||
def network_components( | ||
components_list: Iterable[Component], | ||
nodes: Iterable[Component], | ||
connections: Iterable[PortsConnection], | ||
) -> NetworkComponents: | ||
return NetworkComponents( | ||
components=dict((m.id, m) for m in components_list), | ||
nodes=dict((n.id, n) for n in nodes), | ||
connections=list(connections), | ||
) | ||
|
||
|
||
def resolve_components_and_cnx( | ||
input_comp: InputComponents, library: Library | ||
) -> NetworkComponents: | ||
""" | ||
Resolves: | ||
- components to be used for study | ||
- connections between components""" | ||
components_list = [_resolve_component(library, m) for m in input_comp.components] | ||
nodes = [_resolve_component(library, n) for n in input_comp.nodes] | ||
all_components: List[Component] = components_list + nodes | ||
connections = [] | ||
for cnx in input_comp.connections: | ||
resolved_cnx = _resolve_connections(cnx, all_components) | ||
connections.append(resolved_cnx) | ||
|
||
return network_components(components_list, nodes, connections) | ||
|
||
|
||
def _resolve_component(library: Library, component: InputComponent) -> Component: | ||
model = library.models[component.model] | ||
|
||
return Component( | ||
model=model, | ||
id=component.id, | ||
) | ||
|
||
|
||
def _resolve_connections( | ||
connection: InputPortConnections, | ||
all_components: List[Component], | ||
) -> PortsConnection: | ||
cnx_component1 = connection.component1 | ||
cnx_component2 = connection.component2 | ||
port1 = connection.port_1 | ||
port2 = connection.port_2 | ||
|
||
component_1 = _get_component_by_id(all_components, cnx_component1) | ||
component_2 = _get_component_by_id(all_components, cnx_component2) | ||
assert component_1 is not None and component_2 is not None | ||
port_ref_1 = PortRef(component_1, port1) | ||
port_ref_2 = PortRef(component_2, port2) | ||
|
||
return PortsConnection(port_ref_1, port_ref_2) | ||
|
||
|
||
def _get_component_by_id( | ||
all_components: List[Component], component_id: str | ||
) -> Optional[Component]: | ||
components_dict = {component.id: component for component in all_components} | ||
return components_dict.get(component_id) | ||
|
||
|
||
def consistency_check( | ||
input_components: Dict[str, Component], input_models: Dict[str, Model] | ||
) -> bool: | ||
""" | ||
Checks if all components in the Components instances have a valid model from the library. | ||
Returns True if all components are consistent, raises ValueError otherwise. | ||
""" | ||
model_ids_set = input_models.keys() | ||
for component_id, component in input_components.items(): | ||
if component.model.id not in model_ids_set: | ||
raise ValueError( | ||
f"Error: Component {component_id} has invalid model ID: {component.model.id}" | ||
) | ||
return True | ||
|
||
|
||
def build_network(comp_network: NetworkComponents) -> Network: | ||
network = Network("study") | ||
|
||
for node_id, node in comp_network.nodes.items(): | ||
node = Node(model=node.model, id=node_id) | ||
network.add_node(node) | ||
|
||
for component_id, component in comp_network.components.items(): | ||
network.add_component(component) | ||
|
||
for connection in comp_network.connections: | ||
network.connect(connection.port1, connection.port2) | ||
return network | ||
|
||
|
||
def build_data_base( | ||
input_comp: InputComponents, timeseries_dir: Optional[Path] | ||
) -> DataBase: | ||
database = DataBase() | ||
for comp in input_comp.components: | ||
for param in comp.parameters or []: | ||
param_value = _evaluate_param_type( | ||
param.type, param.value, param.timeseries, timeseries_dir | ||
) | ||
database.add_data(comp.id, param.name, param_value) | ||
|
||
return database | ||
|
||
|
||
def _evaluate_param_type( | ||
param_type: str, | ||
param_value: Optional[float], | ||
timeseries_name: Optional[str], | ||
timeseries_dir: Optional[Path], | ||
) -> AbstractDataStructure: | ||
if param_type == "constant" and param_value is not None: | ||
return ConstantData(float(param_value)) | ||
|
||
elif param_type == "timeseries": | ||
return TimeScenarioSeriesData(load_ts_from_txt(timeseries_name, timeseries_dir)) | ||
|
||
raise ValueError(f"Data should be either constant or timeseries ") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bit strange. Why add the prefix here ?
I understand that we don't want ".txt" in the yaml files, but this adds implicit knowledge (the file XXX.txt must be referenced as XXX in the yaml).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to hand different extensions and that the final user should only put timeseries file name in the .yaml file