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
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to GemsPy are documented here.

## [Unreleased]

### Added

- **System components: `properties`** - introduces optional `properties` on components in `system.yml` (a list of `id`/`value` pairs). These are normalized into a `dict[str, str]` on the resolved `Component` (duplicate ids raise a `ValueError`).
- **Model schema: `taxonomy-category`** - introduces optional `taxonomy-category` on models in library YAML files, exposed as `ModelSchema.taxonomy_category`.
- **Taxonomy check** - new `gems.model.taxonomy` module with `load_taxonomy(path)` and `check_library_against_taxonomy(library, taxonomy)`. Validates that every model declaring a `taxonomy-category` references a category that exists in the taxonomy file, and exposes all port IDs required by that category. Taxonomy classes (`TaxonomyItem`, `TaxonomyCategory`, `Taxonomy`) mirror the structure defined in GEMS-ViewsBuilder.

## [0.1.0] - 2026-04-30

### Study folder structure
Expand Down
4 changes: 4 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ library:

- id: generator
description: A basic generator model
taxonomy-category: production
parameters:
- id: marginal_cost
time-dependent: false
Expand Down Expand Up @@ -92,6 +93,9 @@ system:
components:
- id: G1
model: basic.generator
properties:
- id: technology
value: nuclear
parameters:
- id: marginal_cost
time-dependent: false
Expand Down
22 changes: 22 additions & 0 deletions docs/user-guide/inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ from `input/data-series/modeler-scenariobuilder.dat` (if present).
Use the lower-level functions when you want to load files individually or build
parts of the study from in-memory data.

---

## System YAML: optional component `properties`

In `system.yml`, each component may define an optional `properties` section as a
list of id/value pairs:

~~~ yaml
system:
components:
- id: nuclear_1
model: basic.generator
properties:
- id: technology
value: nuclear
- id: company
value: rhonepower
~~~

At resolution time (`resolve_system` / `load_study`), this list is normalized into
a `dict[str, str]` stored on the resolved `Component`. Duplicate ids are rejected.

### Loading the library and the system

~~~ python
Expand Down
1 change: 1 addition & 0 deletions src/gems/model/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class ExtraOutputSchema(ModifiedBaseModel):

class ModelSchema(ModifiedBaseModel):
id: str
taxonomy_category: Optional[str] = None
parameters: List[ParameterSchema] = Field(default_factory=list)
variables: List[VariableSchema] = Field(default_factory=list)
ports: List[ModelPortSchema] = Field(default_factory=list)
Expand Down
91 changes: 91 additions & 0 deletions src/gems/model/taxonomy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright (c) 2026, 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, field
from pathlib import Path
from typing import Dict, List, Optional

import yaml
from pydantic import Field

from gems.model.parsing import LibrarySchema
from gems.utils import ModifiedBaseModel


class TaxonomyItem(ModifiedBaseModel):
id: str


class TaxonomyCategory(ModifiedBaseModel):
id: str
parent_category: Optional[str] = None
variables: List[TaxonomyItem] = Field(default_factory=list)
parameters: List[TaxonomyItem] = Field(default_factory=list)
ports: List[TaxonomyItem] = Field(default_factory=list)
constraints: List[TaxonomyItem] = Field(default_factory=list)
extra_outputs: List[TaxonomyItem] = Field(default_factory=list)
properties: List[TaxonomyItem] = Field(default_factory=list)


class TaxonomyData(ModifiedBaseModel):
id: str
description: str = ""
categories: List[TaxonomyCategory] = Field(default_factory=list)


@dataclass
class Taxonomy:
id: str
description: str = ""
categories: List[TaxonomyCategory] = field(default_factory=list)


def load_taxonomy(taxonomy_file: Path) -> Taxonomy:
with open(taxonomy_file, encoding="utf-8") as f:
raw = yaml.safe_load(f)
if "taxonomy" not in raw:
raise ValueError(f"Missing 'taxonomy' key at root of {taxonomy_file}")
data = TaxonomyData.model_validate(raw["taxonomy"])
return Taxonomy(
id=data.id, description=data.description, categories=data.categories
)


def check_library_against_taxonomy(library: LibrarySchema, taxonomy: Taxonomy) -> None:
"""
Validates that every model declaring a taxonomy_category:
1. References a category that exists in the taxonomy.
2. Exposes all port IDs listed in that taxonomy category.

Raises ValueError describing the first violation found.
"""
categories: Dict[str, TaxonomyCategory] = {c.id: c for c in taxonomy.categories}

for model_schema in library.models:
cat_id = model_schema.taxonomy_category
if cat_id is None:
continue

if cat_id not in categories:
raise ValueError(
f"Model '{model_schema.id}' references taxonomy category '{cat_id}' "
f"which does not exist in taxonomy '{taxonomy.id}'."
)

category = categories[cat_id]
model_port_ids = {p.id for p in model_schema.ports}
missing = sorted({item.id for item in category.ports} - model_port_ids)
if missing:
raise ValueError(
f"Model '{model_schema.id}' (taxonomy-category: '{cat_id}') is missing "
f"port(s) required by the taxonomy: {missing}."
)
6 changes: 6 additions & 0 deletions src/gems/study/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,17 @@ class ComponentParameterSchema(ModifiedBaseModel):
scenario_group: Optional[str] = None


class ComponentPropertySchema(ModifiedBaseModel):
id: str
value: str


class ComponentSchema(ModifiedBaseModel):
id: str
model: str
scenario_group: Optional[str] = None
parameters: Optional[List[ComponentParameterSchema]] = None
properties: Optional[List[ComponentPropertySchema]] = None


class SystemSchema(ModifiedBaseModel):
Expand Down
29 changes: 25 additions & 4 deletions src/gems/study/resolve_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union

import pandas as pd

from gems.model import Model
from gems.model.library import Library
from gems.study import (
Component,
ConstantData,
DataBase,
PortRef,
PortsConnection,
System,
)
from gems.study.data import (
Expand All @@ -33,10 +30,33 @@
dataframe_to_time_series,
load_ts_from_file,
)
from gems.study.parsing import ComponentSchema, PortConnectionsSchema, SystemSchema
from gems.study.parsing import (
ComponentPropertySchema,
ComponentSchema,
PortConnectionsSchema,
SystemSchema,
)
from gems.study.scenario_builder import ScenarioBuilder


def _resolve_properties_raw_to_dict(
raw: Optional[List[ComponentPropertySchema]],
component_id: str,
) -> Dict[str, str]:
"""Turn parsed ``properties`` (list of ``ComponentPropertySchema``) into ``Component``'s dict."""
if raw is None:
return {}
properties: Dict[str, str] = {}
for item in raw:
k = item.id
if k in properties:
raise ValueError(
f"Component {component_id!r}: duplicate properties id {k!r}"
)
properties[k] = item.value
return properties


def resolve_system(input_system: SystemSchema, libraries: dict[str, Library]) -> System:
"""
Resolves:
Expand Down Expand Up @@ -68,6 +88,7 @@ def _resolve_component(
model=model,
id=component.id,
scenario_group=component.scenario_group,
properties=_resolve_properties_raw_to_dict(component.properties, component.id),
)


Expand Down
13 changes: 11 additions & 2 deletions src/gems/study/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Component:
model: Model
id: str
scenario_group: Optional[str] = None
properties: Dict[str, str] = field(default_factory=dict)

def is_variable_in_model(self, var_id: str) -> bool:
return var_id in self.model.variables.keys()
Expand All @@ -42,9 +43,17 @@ def replicate(self, /, **changes: Any) -> "Component":


def create_component(
model: Model, id: str, scenario_group: Optional[str] = None
model: Model,
id: str,
scenario_group: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> Component:
return Component(model=model, id=id, scenario_group=scenario_group)
return Component(
model=model,
id=id,
scenario_group=scenario_group,
properties=properties or {},
)


@dataclass(frozen=True)
Expand Down
16 changes: 16 additions & 0 deletions tests/unittests/lib_parsing/test_lib_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ def test_binary_variable_parsing() -> None:
assert on_off.data_type == ValueType.BINARY


def test_model_taxonomy_category_parses() -> None:
yaml_content = """
library:
id: taxo_lib
models:
- id: bus
taxonomy-category: balance
parameters:
- id: v_nom
time-dependent: false
scenario-dependent: false
"""
input_lib = parse_yaml_library(io.StringIO(yaml_content))
assert input_lib.models[0].taxonomy_category == "balance"


def test_library_error_parsing(libs_dir: Path) -> None:
lib_file = libs_dir / "model_port_definition_ko.yml"

Expand Down
Loading
Loading