diff --git a/pyproject.toml b/pyproject.toml index 3fb1e8be..3e8347df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ dependencies = [ "pydantic>=2.0", "antares_craft>=0.3", "anytree>=2.12", - "pypsa", ] classifiers = [ # Classifiers here: https://pypi.org/classifiers/ diff --git a/requirements-dev.txt b/requirements-dev.txt index 8efc1733..d256bdef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -261,8 +261,6 @@ pyproj==3.7.1 # via # -r requirements.txt # geopandas -pypsa==0.34.1 - # via -r requirements.txt pytest==7.0.1 # via # -r requirements-dev.in diff --git a/requirements.in b/requirements.in index 78a4b282..db8b30ab 100644 --- a/requirements.in +++ b/requirements.in @@ -5,5 +5,4 @@ antlr4-python3-runtime==4.13.1 PyYAML~=6.0.1 pydantic antares_craft>=0.3 -anytree==2.12.1 -pypsa \ No newline at end of file +anytree==2.12.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bf4b7ca2..8fcd7bbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -140,8 +140,6 @@ pyparsing==3.2.3 # via matplotlib pyproj==3.7.1 # via geopandas -pypsa==0.34.1 - # via -r requirements.in python-dateutil==2.9.0.post0 # via # matplotlib diff --git a/src/gems/libs/pypsa_models/pypsa_models.yml b/src/gems/libs/pypsa_models/pypsa_models.yml deleted file mode 100644 index 02b9bfed..00000000 --- a/src/gems/libs/pypsa_models/pypsa_models.yml +++ /dev/null @@ -1,507 +0,0 @@ -# Copyright (c) 2025, 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. - -# Library of models for the emulation of PyPSA -# References for PyPSA -# T. Brown, J. Hörsch, D. Schlachtberger, PyPSA: Python for Power System Analysis, 2018, Journal of Open Research Software, 6(1), arXiv:1707.09913, DOI:10.5334/jors.188 -# https://pypsa.readthedocs.io/en/latest/ - -library: - id: pypsa_models - description: PyPSA model library - #https://pypsa.readthedocs.io/en/latest/user-guide/components.html# - - port-types: - - id: flow - description: A port which transfers power flow - fields: - - id: flow - - id: emission - description: A port which accounts for CO2 emissions - fields: - - id: emission - - models: - - id: bus - #Model for PyPSA bus: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#bus - parameters: - - id: v_nom - time-dependent: false - scenario-dependent: false - #- id: type # This parameter is not used in the Gems model - - id: x - time-dependent: false - scenario-dependent: false - - id: y - time-dependent: false - scenario-dependent: false - #- id : carrier # This parameter is not used in the Gems model - #- id : unit # This parameter is not used in the Gems model - #- id : location # This parameter is not used in the Gems model - - id: v_mag_pu_set - time-dependent: true - scenario-dependent: false - - id: v_mag_pu_min - time-dependent: false - scenario-dependent: false - - id: v_mag_pu_max - time-dependent: false - scenario-dependent: false - ports: - - id: p_balance_port - type: flow - - id: q_balance_port - type: flow - binding-constraints: - - id: p_balance - expression: sum_connections(p_balance_port.flow) = 0 - - id: q_balance - expression: sum_connections(q_balance_port.flow) = 0 - - - id: load - #Model for PyPSA load: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#load - parameters: - #- id : carrier # This parameter is not used in the Gems model - #- id : type # This parameter is not used in the Gems model - - id: p_set - time-dependent: true - scenario-dependent: false # Since PyPSA v0.x does not manage simultaneous scenarios at this point - - id: q_set - time-dependent: true - scenario-dependent: false # Since PyPSA v0.x does not manage simultaneous scenarios at this point - - id: sign #default value = -1 - time-dependent: false - scenario-dependent: false - #- id: active #Parameter that is not instantiated: only active components are built by the data converter. - ports: - - id: p_balance_port - type: flow - - id: q_balance_port - type: flow - port-field-definitions: - - port: p_balance_port - field: flow - definition: sign * p_set - - port: q_balance_port - field: flow - definition: sign * q_set - - - id: generator - #Model for PyPSA generator: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#generator - parameters: - #- id: control #Parameter not instantiated for now - #- id: type # This parameter is not used in the Gems model - #- id: p_nom_mod #Parameter not instantiated for now - #- id: p_nom_extendable #Parameter that is not used in Gems model: p_nom_min and p_nom_max are used to possibly fix the value of p_nom - - id: p_nom_min - time-dependent: false - scenario-dependent: false - - id: p_nom_max - time-dependent: false - scenario-dependent: false - - id: p_min_pu - time-dependent: true - scenario-dependent: false - - id: p_max_pu - time-dependent: true - scenario-dependent: false - #- id: p_set #Parameter not instantiated for now - #- id: q_set #Parameter not instantiated for now - - id: e_sum_min - time-dependent: false - scenario-dependent: false - - id: e_sum_max - time-dependent: false - scenario-dependent: false - - id: sign #default value = 1 - time-dependent: false - scenario-dependent: false - #- id: carrier # This parameter is not used in the Gems model - - id: marginal_cost - time-dependent: true - scenario-dependent: false - #- id: marginal_cost_quadratic #Parameter not instantiated for now - #- id: active #Parameter that is not instantiated: only active components are built by the data converter. - #- id: build_year #Parameter not instantiated for now - #- id: lifetime #Parameter not instantiated for now - - id: capital_cost - time-dependent: false - scenario-dependent: false - - id: efficiency - time-dependent: true - scenario-dependent: false - #- id: committable #Parameter not instantiated for now - #- id: start_up_cost #Parameter not instantiated for now - #- id: shut_down_cost #Parameter not instantiated for now - #- id: stand_by_cost #Parameter not instantiated for now - #- id: min_up_time #Parameter not instantiated for now - #- id: min_down_time #Parameter not instantiated for now - #- id: up_time_before #Parameter not instantiated for now - #- id: down_time_before #Parameter not instantiated for now - #- id: ramp_limit_up #Parameter not instantiated for now - #- id: ramp_limit_down #Parameter not instantiated for now - #- id: ramp_limit_start_up #Parameter not instantiated for now - #- id: ramp_limit_shut_down #Parameter not instantiated for now - #- id: weight #Parameter not instantiated for now - ##Additional parameters that are not stored in the PyPSA object, but in a Carrier object - - id: emission_factor - time-dependent: false - scenario-dependent: false - - variables: - - id: p_nom - lower-bound: p_nom_min - upper-bound: p_nom_max - time-dependent: false - scenario-dependent: false - - id: p - #- id: q - ports: - - id: p_balance_port - type: flow - - id: emission_port - type: emission - #- id: q_balance_port - # type: flow - port-field-definitions: - - port: p_balance_port - field: flow - definition: p*sign - - port: emission_port - field: emission - definition: p*emission_factor/efficiency - #- port: q_balance_port - # field: flow - # definition: q*sign - constraints: - - id: min_dispatch - expression: p >=p_nom * p_min_pu - - id: max_dispatch - expression: p <=p_nom * p_max_pu - - id: min_production - expression: sum(p) >= e_sum_min - - id: max_production - expression: sum(p) <= e_sum_max - objective-contributions: - - id: obj - expression: expec(sum(marginal_cost * p)) + p_nom * capital_cost - - - id: link - #Model for PyPSA link: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#link - parameters: - #- id: type # This parameter is not used in the Gems model - #- id: carrier # This parameter is not used in the Gems model - - id: efficiency - time-dependent: true - scenario-dependent: false - #- id: active #Parameter that is not instantiated: only active components are built by the data converter. - #- id: build_year #Parameter not instantiated for now - #- id: lifetime #Parameter not instantiated for now - #- id: p_nom_mod #Parameter not instantiated for now - #- id: p_nom_extendable #Parameter that is not instantiated: p_nom_min and p_nom_max are used to possibly fix the value of p_nom - - id: p_nom_min - time-dependent: false - scenario-dependent: false - - id: p_nom_max - time-dependent: false - scenario-dependent: false - #- id: p_set #Parameter not instantiated for now - - id: p_min_pu - time-dependent: true - scenario-dependent: false - - id: p_max_pu - time-dependent: true - scenario-dependent: false - - id: capital_cost - time-dependent: false - scenario-dependent: false - - id: marginal_cost - time-dependent: true - scenario-dependent: false - #- id: marginal_cost_quadratic #Parameter not instantiated for now - #- id: stand_by_cost #Parameter not instantiated for now - #- id: length #Parameter not instantiated for now - #- id: terrain_factor #Parameter not instantiated for now - #- id: committable #Parameter not instantiated for now - #- id: start_up_cost #Parameter not instantiated for now - #- id: shut_down_cost #Parameter not instantiated for now - #- id: min_up_time #Parameter not instantiated for now - #- id: min_down_time #Parameter not instantiated for now - #- id: up_time_before #Parameter not instantiated for now - #- id: down_time_before #Parameter not instantiated for now - #- id: ramp_limit_up #Parameter not instantiated for now - #- id: ramp_limit_down #Parameter not instantiated for now - #- id: ramp_limit_start_up #Parameter not instantiated for now - #- id: ramp_limit_shut_down #Parameter not instantiated for now - variables: - - id: p_nom - time-dependent: false - scenario-dependent: false - lower-bound: p_nom_min - upper-bound: p_nom_max - - id: p0 - #- id: p_nom_opt - #- id: status - #- id: start_up - #- id: shut_down - ports: - - id: p0_port - type: flow - #- id: q_0_port - # type: flow - - id: p1_port - type: flow - #- id: q_1_port - # type: flow - port-field-definitions: - - port: p0_port - field: flow - definition: -p0 - - port: p1_port - field: flow - definition: efficiency * p0 - #- port: q_balance_port - # field: flow - # definition: q - constraints: - - id: p0_upper - expression: p0 <= p_max_pu * p_nom - - id: p0_lower - expression: p0 >= p_min_pu * p_nom - objective-contributions: - - id: obj - expression: expec(sum(marginal_cost * p0)) + capital_cost * p_nom - - - - id: storage_unit - #Model for PyPSA storage unit: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#storage-unit - parameters: - #- id: control #Parameter not instantiated for now - #- id: type # This parameter is not used in the Gems model - #- id: p_nom_mod #Parameter not instantiated for now - #- id: p_nom_extendable #Parameter that is not instantiated: p_nom_min and p_nom_max are used to possibly fix the value of p_nom - - id: p_nom_min - time-dependent: false - scenario-dependent: false - - id: p_nom_max - time-dependent: false - scenario-dependent: false - - id: p_min_pu - time-dependent: true - scenario-dependent: false - - id: p_max_pu - time-dependent: true - scenario-dependent: false - #- id: p_set #Parameter not instantiated for now - #- id: q_set #Parameter not instantiated for now - - id: sign #default value = 1 - time-dependent: false - scenario-dependent: false - #- id: carrier # This parameter is not used in the Gems model - - id: spill_cost #Parameter not instantiated for now - time-dependent: true - scenario-dependent: false - - id: marginal_cost - time-dependent: true - scenario-dependent: false - #- id: marginal_cost_quadratic #Parameter not instantiated for now - - id: marginal_cost_storage - time-dependent: true - scenario-dependent: false - - id: capital_cost - time-dependent: false - scenario-dependent: false - #- id: active #Parameter that is not instantiated: only active components are built by the data converter. - #- id: build_year #Parameter not instantiated for now - #- id: lifetime #Parameter not instantiated for now - #- id: state_of_charge_initial #Parameter not instantiated for now - #- id: state_of_charge_initial_per_period #Parameter not instantiated for now - #- id: state_pf_charge_set #Parameter not instantiated for now - #- id: cyclic_state_of_charge #Parameter not instantiated for now - #- id: cyclic_state_of_charge_per_period #Parameter not instantiated for now - - id: max_hours - time-dependent: false - scenario-dependent: false - - id: efficiency_store - time-dependent: true - scenario-dependent: false - - id: efficiency_dispatch - time-dependent: true - scenario-dependent: false - - id: standing_loss - time-dependent: true - scenario-dependent: false - - id: inflow - time-dependent: true - scenario-dependent: false - ##Additional parameters that are not stored in the PyPSA object, but in a Carrier object - - id: emission_factor - time-dependent: false - scenario-dependent: false - variables: - - id: p_nom - time-dependent: false - scenario-dependent: false - lower-bound: p_nom_min - upper-bound: p_nom_max - - id: p_store - lower-bound: 0 - upper-bound: p_max_pu * p_nom_max - - id: p_dispatch - lower-bound: 0 - upper-bound: p_max_pu * p_nom_max - - id: state_of_charge - lower-bound: 0 - upper-bound: max_hours * p_nom_max - - id: spill - lower-bound: 0 - ports: - - id: p_balance_port - type: flow - - id: emission_port - type: emission - port-field-definitions: - - port: p_balance_port - field: flow - definition: p_dispatch - p_store - - port: emission_port - field: emission - definition: emission_factor * 0 #Since we assume here cyclity of StorageUnits. In the future, for non-cyclic Store: (e[-1] - e[T-1])*emission_factor - constraints: - - id: p_store_upper - expression: p_store <= p_max_pu * p_nom - - id: p_dispatch_upper - expression: p_dispatch <= p_max_pu * p_nom - - id: state_of_charge_upper - expression: state_of_charge <= max_hours * p_nom - - id: state_of_charge_balance - expression: state_of_charge = (1- standing_loss) * state_of_charge[t-1] + efficiency_store * p_store - p_dispatch / efficiency_dispatch + inflow - spill - objective-contributions: - - id: obj - expression: expec(sum(marginal_cost * p_dispatch + spill_cost*spill + marginal_cost_storage * state_of_charge)) + capital_cost * p_nom - - - id: store - #Model for PyPSA store: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#store - parameters: - #- id: type # This parameter is not used in the Gems model - #- id: carrier # This parameter is not used in the Gems model - #- id: e_nom_mod #Parameter not instantiated for now - #- id: e_nom_extendable #Parameter that is not instantiated: e_nom_min and e_nom_max are used to possibly fix the value of e_nom - - id: e_nom_min - time-dependent: false - scenario-dependent: false - - id: e_nom_max - time-dependent: false - scenario-dependent: false - - id: e_min_pu - time-dependent: true - scenario-dependent: false - - id: e_max_pu - time-dependent: true - scenario-dependent: false - #- id: e_initial #Parameter not instantiated for now - #- id: e_initial_per_period #Parameter not instantiated for now - #- id: e_cyclic #Parameter not instantiated for now - #- id: e_cyclic_per_period #Parameter not instantiated for now - #- id: p_set #Parameter not instantiated for now - #- id: q_set #Parameter not instantiated for now - - id: sign #default value = 1 - time-dependent: false - scenario-dependent: false - - id: marginal_cost - time-dependent: true - scenario-dependent: false - #- id: marginal_cost_quadratic #Parameter not instantiated for now - - id: marginal_cost_storage - time-dependent: true - scenario-dependent: false - - id: capital_cost - time-dependent: false - scenario-dependent: false - - id: standing_loss - time-dependent: true - scenario-dependent: false - #- id: active #Parameter that is not instantiated: only active components are built by the data converter. - #- id: build_year #Parameter not instantiated for now - #- id: lifetime #Parameter not instantiated for now - ##Additional parameters that are not stored in the PyPSA object, but in a Carrier object - - id: emission_factor - time-dependent: false - scenario-dependent: false - variables: - - id: e_nom - time-dependent: false - scenario-dependent: false - lower-bound: e_nom_min - upper-bound: e_nom_max - - id: e - - id: p - ports: - - id: p_balance_port - type: flow - - id: emission_port - type: emission - port-field-definitions: - - port: p_balance_port - field: flow - definition: p - - port: emission_port - field: emission - definition: emission_factor * 0 #Since we assume here cyclity of stores. In the future, for non-cyclic Store: (e[-1] - e[T-1])*emission_factor - constraints: - - id: e_upper - expression: e <= e_max_pu * e_nom - - id: e_lower - expression: e >= e_min_pu * e_nom - - id: energy_balance - expression: e = (1 - standing_loss) * e[t-1] - p - objective-contributions: - - id: obj - expression: expec(sum(marginal_cost * p + marginal_cost_storage * e)) + capital_cost * e_nom - - - -#### Models for PyPSA global constraint: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#global-constraint ### - - - id: global_constraint_co2_max - #Model for PyPSA global constraint: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#global-constraint - # Case CO2, <= - parameters: - #-type = Primary energy - #-carrier_attribute = CO2_emissions - #sense = <= - - id: quota - time-dependent: false - scenario-dependent: false - ports: - - id: emission_port - type: emission - binding-constraints: - - id: constraint_expression - expression: sum(sum_connections(emission_port.emission)) <= quota - - - id: global_constraint_co2_eq - #Model for PyPSA global constraint: https://pypsa.readthedocs.io/en/latest/user-guide/components.html#global-constraint - # Case CO2, == - parameters: - #-type = Primary energy - #-carrier_attribute = CO2_emissions - #sense = == - - id: quota - time-dependent: false - scenario-dependent: false - ports: - - id: emission_port - type: emission - binding-constraints: - - id: constraint_expression - expression: sum(sum_connections(emission_port.emission)) = quota diff --git a/src/gems/pypsa_converter/__init__.py b/src/gems/pypsa_converter/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/gems/pypsa_converter/models/__init__.py b/src/gems/pypsa_converter/models/__init__.py deleted file mode 100644 index 29887278..00000000 --- a/src/gems/pypsa_converter/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .modified_base_model import ModifiedBaseModel diff --git a/src/gems/pypsa_converter/models/gems_system_yaml_schema/__init__.py b/src/gems/pypsa_converter/models/gems_system_yaml_schema/__init__.py deleted file mode 100644 index 844a751d..00000000 --- a/src/gems/pypsa_converter/models/gems_system_yaml_schema/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .gems_component import GemsComponent -from .gems_component_parameter import GemsComponentParameter -from .gems_port_connection import GemsPortConnection -from .gems_system import GemsSystem diff --git a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_area_connection.py b/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_area_connection.py deleted file mode 100644 index 33e56f74..00000000 --- a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_area_connection.py +++ /dev/null @@ -1,7 +0,0 @@ -from ..modified_base_model import ModifiedBaseModel - - -class GemsAreaConnection(ModifiedBaseModel): - component: str - port: str - area: str diff --git a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_component.py b/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_component.py deleted file mode 100644 index c14e9bb7..00000000 --- a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_component.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional, List -from ..modified_base_model import ModifiedBaseModel -from .gems_component_parameter import GemsComponentParameter - - -class GemsComponent(ModifiedBaseModel): - id: str - model: str - scenario_group: Optional[str] = None - parameters: Optional[List[GemsComponentParameter]] = None diff --git a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_component_parameter.py b/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_component_parameter.py deleted file mode 100644 index c2a0a60a..00000000 --- a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_component_parameter.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional, Union -from ..modified_base_model import ModifiedBaseModel - - -class GemsComponentParameter(ModifiedBaseModel): - id: str - time_dependent: bool = False - scenario_dependent: bool = False - value: Union[float, str] - scenario_group: Optional[str] = None diff --git a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_port_connection.py b/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_port_connection.py deleted file mode 100644 index 6b4bc786..00000000 --- a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_port_connection.py +++ /dev/null @@ -1,8 +0,0 @@ -from ..modified_base_model import ModifiedBaseModel - - -class GemsPortConnection(ModifiedBaseModel): - component1: str - port1: str - component2: str - port2: str diff --git a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_system.py b/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_system.py deleted file mode 100644 index 9cb5d9c0..00000000 --- a/src/gems/pypsa_converter/models/gems_system_yaml_schema/gems_system.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import List, Optional -from pydantic import Field -from ..modified_base_model import ModifiedBaseModel -from .gems_component import GemsComponent -from .gems_port_connection import GemsPortConnection -from .gems_area_connection import GemsAreaConnection - - -class GemsSystem(ModifiedBaseModel): - id: Optional[str] = None - model_libraries: Optional[str] = None # Parsed but unused for n - components: List[GemsComponent] = Field(default_factory=list) - connections: Optional[List[GemsPortConnection]] = None - area_connections: Optional[List[GemsAreaConnection]] = None - nodes: Optional[List[GemsComponent]] = [] diff --git a/src/gems/pypsa_converter/models/modified_base_model.py b/src/gems/pypsa_converter/models/modified_base_model.py deleted file mode 100644 index 9e220762..00000000 --- a/src/gems/pypsa_converter/models/modified_base_model.py +++ /dev/null @@ -1,8 +0,0 @@ -from pydantic import BaseModel - - -class ModifiedBaseModel(BaseModel): - class Config: - alias_generator = lambda snake: snake.replace("_", "-") - extra = "forbid" - populate_by_name = True diff --git a/src/gems/pypsa_converter/models/pypsa_model_schema/__init__.py b/src/gems/pypsa_converter/models/pypsa_model_schema/__init__.py deleted file mode 100644 index 970e415a..00000000 --- a/src/gems/pypsa_converter/models/pypsa_model_schema/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .pypsa_component_data import PyPSAComponentData -from .pypsa_global_constraint_data import PyPSAGlobalConstraintData diff --git a/src/gems/pypsa_converter/models/pypsa_model_schema/pypsa_component_data.py b/src/gems/pypsa_converter/models/pypsa_model_schema/pypsa_component_data.py deleted file mode 100644 index b6699084..00000000 --- a/src/gems/pypsa_converter/models/pypsa_model_schema/pypsa_component_data.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import dataclass -import pandas as pd - - -@dataclass -class PyPSAComponentData: - pypsa_model_id: str - constant_data: pd.DataFrame - time_dependent_data: dict[str, pd.DataFrame] - gems_model_id: str - pypsa_params_to_gems_params: dict[str, str] - pypsa_params_to_gems_connections: dict[str, tuple[str, str]] - - def check_params_consistency(self) -> None: - for key in self.pypsa_params_to_gems_params: - self._check_key_in_constant_data(key) - for key in self.pypsa_params_to_gems_connections: - self._check_key_in_constant_data(key) - - def _check_key_in_constant_data(self, key: str) -> None: - if key not in self.constant_data.columns: - raise ValueError( - f"Parameter {key} not available in constant data, defining all available parameters for model {self.pypsa_model_id}" - ) diff --git a/src/gems/pypsa_converter/models/pypsa_model_schema/pypsa_global_constraint_data.py b/src/gems/pypsa_converter/models/pypsa_model_schema/pypsa_global_constraint_data.py deleted file mode 100644 index 89c45505..00000000 --- a/src/gems/pypsa_converter/models/pypsa_model_schema/pypsa_global_constraint_data.py +++ /dev/null @@ -1,13 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class PyPSAGlobalConstraintData: - pypsa_name: str - # pypsa_investment_period - pypsa_carrier_attribute: str - pypsa_sense: str - pypsa_constant: float - gems_model_id: str # gems model for this GlobalConstraint - gems_port_id: str # gems port for this GlobalConstraint - gems_components_and_ports: list[tuple[str, str]] diff --git a/src/gems/pypsa_converter/parsing.py b/src/gems/pypsa_converter/parsing.py deleted file mode 100644 index 7ed5e91b..00000000 --- a/src/gems/pypsa_converter/parsing.py +++ /dev/null @@ -1,91 +0,0 @@ -# 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 argparse -import os -from dataclasses import dataclass -from pathlib import Path -from typing import List, TextIO - -import pandas as pd -from yaml import safe_load - -from gems.pypsa_converter.models.gems_system_yaml_schema import GemsSystem - - -def parse_yaml_components(input_study: TextIO) -> GemsSystem: - tree = safe_load(input_study) - return GemsSystem.model_validate(tree["system"]) - - -def parse_scenario_builder(file: Path) -> pd.DataFrame: - sb = pd.read_csv(file, names=("name", "year", "scenario")) - sb.rename(columns={0: "name", 1: "year", 2: "scenario"}) - return sb - - -@dataclass(frozen=True) -class ParsedArguments: - models_path: List[Path] - components_path: Path - timeseries_path: Path - duration: int - nb_scenarios: int - - -def parse_cli() -> ParsedArguments: - parser = argparse.ArgumentParser() - parser.add_argument( - "--study", type=Path, help="path to the root directory of the study" - ) - parser.add_argument( - "--models", nargs="+", type=Path, help="list of path to model file, *.yml" - ) - parser.add_argument( - "--component", type=Path, help="path to the component file, *.yml" - ) - parser.add_argument( - "--timeseries", type=Path, help="path to the timeseries directory" - ) - parser.add_argument( - "--duration", type=int, help="duration of the simulation", default=1 - ) - parser.add_argument( - "--scenario", type=int, help="number of scenario of the simulation", default=1 - ) - - args = parser.parse_args() - - if args.study: - if args.models or args.component or args.timeseries: - parser.error( - "--study flag can't be use with --models, --component and --timeseries" - ) - - components_path = args.study / "input" / "components" / "components.yml" - timeseries_dir = args.study / "input" / "components" / "series" - model_paths = [ - args.study / "input" / "models" / file - for file in os.listdir(args.study / "input" / "models") - ] - - else: - if not args.models or not args.component: - parser.error("--models and --component must be entered") - - components_path = args.component - timeseries_dir = args.timeseries - model_paths = args.models - - return ParsedArguments( - model_paths, components_path, timeseries_dir, args.duration, args.scenario - ) diff --git a/src/gems/pypsa_converter/pypsa_converter.py b/src/gems/pypsa_converter/pypsa_converter.py deleted file mode 100644 index 10d0920d..00000000 --- a/src/gems/pypsa_converter/pypsa_converter.py +++ /dev/null @@ -1,566 +0,0 @@ -# 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 logging -from math import inf -from pathlib import Path - -import pandas as pd -from pypsa import Network - -from gems.pypsa_converter.utils import any_to_float -from gems.pypsa_converter.models.pypsa_model_schema import ( - PyPSAComponentData, - PyPSAGlobalConstraintData, -) - -from gems.pypsa_converter.models.gems_system_yaml_schema import ( - GemsComponent, - GemsComponentParameter, - GemsPortConnection, - GemsSystem, -) - - -class PyPSAStudyConverter: - def __init__( - self, - pypsa_network: Network, - logger: logging.Logger, - system_dir: Path, - series_dir: Path, - series_file_format: str, - ): - """ - Initialize processor - """ - self.logger = logger - self.system_dir = system_dir - self.series_dir = series_dir - self.pypsa_network = pypsa_network - self.pypsalib_id = "pypsa_models" - self.null_carrier_id = "null" - self.system_name = pypsa_network.name - self.series_file_format = series_file_format - - self.pypsa_components = [ - "buses", - "loads", - "generators", - "stores", - "storage_units", - "links", - "lines", - "transformers", - ] - self._pypsa_network_assertion() - self._pypsa_network_preprocessing() - self._preprocess_pypsa_components("loads", False, "/") - self._preprocess_pypsa_components("generators", True, "p_nom") - self._preprocess_pypsa_components("stores", True, "e_nom") - self._preprocess_pypsa_components("storage_units", True, "p_nom") - self._preprocess_pypsa_components("links", True, "p_nom") - - self.pypsa_components_data: dict[str, PyPSAComponentData] = {} - self._register_pypsa_components() - self.pypsa_globalconstraints_data: dict[str, PyPSAGlobalConstraintData] = {} - self._register_pypsa_globalconstraints() - - def _pypsa_network_assertion(self) -> None: - """Assertion function to keep trace of the limitations of the converter""" - assert len(self.pypsa_network.investment_periods) == 0 - assert (self.pypsa_network.snapshot_weightings.values == 1.0).all() - ### PyPSA components : Generators - if not (all((self.pypsa_network.generators["marginal_cost_quadratic"] == 0))): - raise ValueError(f"Converter supports only Generators with linear cost") - if not (all((self.pypsa_network.generators["active"] == 1))): - raise ValueError(f"Converter supports only Generators with active = 1") - if not (all((self.pypsa_network.generators["committable"] == False))): - raise ValueError( - f"Converter supports only Generators with commitable = False" - ) - ### PyPSA components : Loads - if not (all((self.pypsa_network.loads["active"] == 1))): - raise ValueError(f"Converter supports only Loads with active = 1") - ### PyPSA components : Links - if not (all((self.pypsa_network.links["active"] == 1))): - raise ValueError(f"Converter supports only Links with active = 1") - ### PyPSA components : Lines - if not len(self.pypsa_network.lines) == 0: - raise ValueError(f"Converter does not support Lines yet") - ### PyPSA components : Storage Units - if not (all((self.pypsa_network.links["active"] == 1))): - raise ValueError(f"Converter supports only Storage Units with active = 1") - if not (all((self.pypsa_network.storage_units["sign"] == 1))): - raise ValueError(f"Converter supports only Storage Units with sign = 1") - if not (all((self.pypsa_network.storage_units["cyclic_state_of_charge"] == 1))): - raise ValueError( - f"Converter supports only Storage Units with cyclic_state_of_charge" - ) - if not ( - all((self.pypsa_network.storage_units["marginal_cost_quadratic"] == 0)) - ): - raise ValueError(f"Converter supports only Storage Units with linear cost") - ### PyPSA components : Stores - if not (all((self.pypsa_network.links["active"] == 1))): - raise ValueError(f"Converter supports only Stores with active = 1") - if not (all((self.pypsa_network.stores["sign"] == 1))): - raise ValueError(f"Converter supports only Stores with sign = 1") - if not (all((self.pypsa_network.stores["e_cyclic"] == 1))): - raise ValueError(f"Converter supports only Stores with e_cyclic = True") - if not (all((self.pypsa_network.stores["marginal_cost_quadratic"] == 0))): - raise ValueError(f"Converter supports only Stores with linear cost") - ### PyPSA components : GlobalConstraint - for pypsa_model_id in self.pypsa_network.global_constraints.index: - assert ( - self.pypsa_network.global_constraints.loc[pypsa_model_id, "type"] - == "primary_energy" - ) - assert ( - self.pypsa_network.global_constraints.loc[ - pypsa_model_id, "carrier_attribute" - ] - == "co2_emissions" - ) - - def _rename_buses(self) -> None: - ### Rename PyPSA buses, to delete spaces - if len(self.pypsa_network.buses) > 0: - self.pypsa_network.buses.index = self.pypsa_network.buses.index.str.replace( - " ", "_" - ) - for _, val in self.pypsa_network.buses_t.items(): - val.columns = val.columns.str.replace(" ", "_") - ### Update the 'bus' columns for the different types of PyPSA components - for component_type in self.pypsa_components: - df = getattr(self.pypsa_network, component_type) - if len(df) > 0: - for col in ["bus", "bus0", "bus1"]: - if col in df.columns: - df[col] = df[col].str.replace(" ", "_") - - def _pypsa_network_preprocessing(self) -> None: - ###Add fictitious carrier - self.pypsa_network.add( - "Carrier", - self.null_carrier_id, - co2_emissions=0, - max_growth=any_to_float(inf), - ) - self.pypsa_network.carriers[ - "carrier" - ] = self.pypsa_network.carriers.index.values - self._rename_buses() - - def _rename_pypsa_components(self, component_type: str) -> None: - df = getattr(self.pypsa_network, component_type) - if len(df) == 0: - return - ### Rename PyPSA components, to make sure that the names are uniques (used as id in the Gems model) - prefix = component_type[:-1] - df.index = prefix + "_" + df.index.str.replace(" ", "_") - dictionnary = getattr(self.pypsa_network, component_type + "_t") - for _, val in dictionnary.items(): - val.columns = prefix + "_" + val.columns.str.replace(" ", "_") - - def _fix_capacities(self, component_type: str, capa_str: str) -> None: - df = getattr(self.pypsa_network, component_type) - if len(df) == 0: - return - ### Adding min and max capacities to non-extendable objects - for field in [capa_str + "_min", capa_str + "_max"]: - df.loc[df[capa_str + "_extendable"] == False, field] = df[capa_str] - df.loc[df[capa_str + "_extendable"] == False, "capital_cost"] = 0.0 - - def _preprocess_pypsa_components( - self, component_type: str, extendable: bool, capa_str: str - ) -> None: - ### Handling PyPSA objects without carriers - df = getattr(self.pypsa_network, component_type) - for comp in df.index: - if len(df.loc[comp, "carrier"]) == 0: - df.loc[comp, "carrier"] = self.null_carrier_id - setattr( - self.pypsa_network, - component_type, - df.join( - self.pypsa_network.carriers, - on="carrier", - how="left", - rsuffix="_carrier", - ), - ) - self._rename_pypsa_components(component_type) - if extendable: - self._fix_capacities(component_type, capa_str) - - def _register_pypsa_components(self) -> None: - ### PyPSA components : Generators - self._register_pypsa_components_of_given_model( - "generators", - self.pypsa_network.generators, - self.pypsa_network.generators_t, - "generator", - { - "p_nom_min": "p_nom_min", - "p_nom_max": "p_nom_max", - "p_min_pu": "p_min_pu", - "p_max_pu": "p_max_pu", - "marginal_cost": "marginal_cost", - "capital_cost": "capital_cost", - "e_sum_min": "e_sum_min", - "e_sum_max": "e_sum_max", - "sign": "sign", - "efficiency": "efficiency", - "co2_emissions": "emission_factor", - }, - {"bus": ("p_balance_port", "p_balance_port")}, - ) - ### PyPSA components : Loads - self._register_pypsa_components_of_given_model( - "loads", - self.pypsa_network.loads, - self.pypsa_network.loads_t, - "load", - { - "p_set": "p_set", - "q_set": "q_set", - "sign": "sign", - }, - {"bus": ("p_balance_port", "p_balance_port")}, - ) - ### PyPSA components : Buses - self._register_pypsa_components_of_given_model( - "buses", - self.pypsa_network.buses, - self.pypsa_network.buses_t, - "bus", - { - "v_nom": "v_nom", - "x": "x", - "y": "y", - "v_mag_pu_set": "v_mag_pu_set", - "v_mag_pu_min": "v_mag_pu_min", - "v_mag_pu_max": "v_mag_pu_max", - }, - {}, - ) - ### PyPSA components : Links - self._register_pypsa_components_of_given_model( - "links", - self.pypsa_network.links, - self.pypsa_network.links_t, - "link", - { - "efficiency": "efficiency", - "p_nom_min": "p_nom_min", - "p_nom_max": "p_nom_max", - "p_min_pu": "p_min_pu", - "p_max_pu": "p_max_pu", - "marginal_cost": "marginal_cost", - "capital_cost": "capital_cost", - }, - { - "bus0": ("p0_port", "p_balance_port"), - "bus1": ("p1_port", "p_balance_port"), - }, - ) - ### PyPSA components : Storage Units - self._register_pypsa_components_of_given_model( - "storage_units", - self.pypsa_network.storage_units, - self.pypsa_network.storage_units_t, - "storage_unit", - { - "p_nom_min": "p_nom_min", - "p_nom_max": "p_nom_max", - "p_min_pu": "p_min_pu", - "p_max_pu": "p_max_pu", - "sign": "sign", - "efficiency_store": "efficiency_store", - "efficiency_dispatch": "efficiency_dispatch", - "standing_loss": "standing_loss", - "max_hours": "max_hours", - "marginal_cost": "marginal_cost", - "capital_cost": "capital_cost", - "marginal_cost_storage": "marginal_cost_storage", - "spill_cost": "spill_cost", - "inflow": "inflow", - "co2_emissions": "emission_factor", - }, - {"bus": ("p_balance_port", "p_balance_port")}, - ) - ### PyPSA components : Stores - self._register_pypsa_components_of_given_model( - "stores", - self.pypsa_network.stores, - self.pypsa_network.stores_t, - "store", - { - "sign": "sign", - "e_nom_min": "e_nom_min", - "e_nom_max": "e_nom_max", - "e_min_pu": "e_min_pu", - "e_max_pu": "e_max_pu", - "standing_loss": "standing_loss", - "marginal_cost": "marginal_cost", - "capital_cost": "capital_cost", - "marginal_cost_storage": "marginal_cost_storage", - "co2_emissions": "emission_factor", - }, - {"bus": ("p_balance_port", "p_balance_port")}, - ) - - def _add_contributors_to_globalconstraints( - self, gems_components_and_ports: list[tuple[str, str]], component_type: str - ) -> list[tuple[str, str]]: - df = getattr(self.pypsa_network, component_type) - gems_components_and_ports += [ - (comp, "emission_port") - for comp in df[df["carrier"] != self.null_carrier_id].index - ] - return gems_components_and_ports - - def _register_pypsa_globalconstraints(self) -> None: - gems_components_and_ports: list[tuple[str, str]] = [] - for component_type in ["generators", "stores", "storage_units"]: - gems_components_and_ports = self._add_contributors_to_globalconstraints( - gems_components_and_ports, component_type - ) - - for pypsa_model_id in self.pypsa_network.global_constraints.index: - name, sense, carrier_attribute = ( - pypsa_model_id, - self.pypsa_network.global_constraints.loc[pypsa_model_id, "sense"], - self.pypsa_network.global_constraints.loc[ - pypsa_model_id, "carrier_attribute" - ], - ) - if carrier_attribute == "co2_emissions" and sense == "<=": - self.pypsa_globalconstraints_data[ - pypsa_model_id - ] = PyPSAGlobalConstraintData( - name, - carrier_attribute, - sense, - self.pypsa_network.global_constraints.loc[ - pypsa_model_id, "constant" - ], - "global_constraint_co2_max", - "emission_port", - gems_components_and_ports, - ) - elif carrier_attribute == "co2_emissions" and sense == "==": - self.pypsa_globalconstraints_data[ - pypsa_model_id - ] = PyPSAGlobalConstraintData( - name, - carrier_attribute, - sense, - self.pypsa_network.global_constraints.loc[ - pypsa_model_id, "constant" - ], - "global_constraint_co2_eq", - "emission_port", - gems_components_and_ports, - ) - else: - raise ValueError("Type of GlobalConstraint not supported.") - - def _register_pypsa_components_of_given_model( - self, - pypsa_model_id: str, - constant_data: pd.DataFrame, - time_dependent_data: dict[str, pd.DataFrame], - gems_model_id: str, - pypsa_params_to_gems_params: dict[str, str], - pypsa_params_to_gems_connections: dict[str, tuple[str, str]], - ) -> None: - if pypsa_model_id in self.pypsa_components_data: - raise ValueError(f"{pypsa_model_id} already registered !") - - self.pypsa_components_data[pypsa_model_id] = PyPSAComponentData( - pypsa_model_id, - constant_data, - time_dependent_data, - gems_model_id, - pypsa_params_to_gems_params, - pypsa_params_to_gems_connections, - ) - - def to_gems_study(self) -> GemsSystem: - """Main function, to export PyPSA as Gems system""" - - self.logger.info("Study conversion started") - list_components, list_connections = [], [] - - for pypsa_components_data in self.pypsa_components_data.values(): - components, connections = self._convert_pypsa_components_of_given_model( - pypsa_components_data - ) - list_components.extend(components) - list_connections.extend(connections) - - for pypsa_global_constraint_data in self.pypsa_globalconstraints_data.values(): - ( - components, - connections, - ) = self._convert_pypsa_globalconstraint_of_given_model( - pypsa_global_constraint_data - ) - list_components.extend(components) - list_connections.extend(connections) - - return GemsSystem( - nodes=[], components=list_components, connections=list_connections - ) - - def _convert_pypsa_components_of_given_model( - self, pypsa_components_data: PyPSAComponentData - ) -> tuple[list[GemsComponent], list[GemsPortConnection]]: - """ - Generic function to handle the different PyPSA classes - - """ - - self.logger.info( - f"Creating objects of type: {pypsa_components_data.gems_model_id}. " - ) - - # We test whether the keys of the conversion dictionary are allowed in the PyPSA model : all authorized parameters are columns in the constant data frame (even though they are specified as time-varying values in the time-varying data frame) - pypsa_components_data.check_params_consistency() - - # List of params that may be time-dependent in the pypsa model, among those we want to keep - time_dependent_params = set( - pypsa_components_data.pypsa_params_to_gems_params - ).intersection(set(pypsa_components_data.time_dependent_data.keys())) - # Save time series and memorize the time-dependent parameters - comp_param_to_timeseries_name = self._write_and_register_timeseries( - pypsa_components_data.time_dependent_data, time_dependent_params - ) - - connections = self._create_gems_connections( - pypsa_components_data.constant_data, - pypsa_components_data.pypsa_params_to_gems_connections, - ) - - components = self._create_gems_components( - pypsa_components_data.constant_data, - pypsa_components_data.gems_model_id, - pypsa_components_data.pypsa_params_to_gems_params, - comp_param_to_timeseries_name, - ) - return components, connections - - def _convert_pypsa_globalconstraint_of_given_model( - self, pypsa_gc_data: PyPSAGlobalConstraintData - ) -> tuple[list[GemsComponent], list[GemsPortConnection]]: - self.logger.info( - f"Creating PyPSA GlobalConstraint of type: {pypsa_gc_data.gems_model_id}. " - ) - components = [ - GemsComponent( - id=pypsa_gc_data.pypsa_name, - model=f"{self.pypsalib_id}.{pypsa_gc_data.gems_model_id}", - parameters=[ - GemsComponentParameter( - id="quota", - time_dependent=False, - scenario_dependent=False, - value=pypsa_gc_data.pypsa_constant, - ) - ], - ) - ] - connections = [] - for component_id, port_id in pypsa_gc_data.gems_components_and_ports: - connections.append( - GemsPortConnection( - component1=pypsa_gc_data.pypsa_name, - port1=pypsa_gc_data.gems_port_id, - component2=component_id, - port2=port_id, - ) - ) - - return components, connections - - def _write_and_register_timeseries( - self, - time_dependent_data: dict[str, pd.DataFrame], - time_dependent_params: set[str], - ) -> dict[tuple[str, str], str]: - comp_param_to_timeseries_name = dict() - for param in time_dependent_params: - param_df = time_dependent_data[param] - for component in param_df.columns: - timeseries_name = self.system_name + "_" + component + "_" + param - comp_param_to_timeseries_name[(component, param)] = timeseries_name - param_df[[component]].to_csv( - self.series_dir / Path(timeseries_name + self.series_file_format), - index=False, - header=False, - ) - - return comp_param_to_timeseries_name - - def _create_gems_components( - self, - constant_data: pd.DataFrame, - gems_model_id: str, - pypsa_params_to_gems_params: dict[str, str], - comp_param_to_timeseries_name: dict[tuple[str, str], str], - ) -> list[GemsComponent]: - components = [] - for component in constant_data.index: - components.append( - GemsComponent( - id=component, - model=f"{self.pypsalib_id}.{gems_model_id}", - parameters=[ - GemsComponentParameter( - id=pypsa_params_to_gems_params[param], - time_dependent=(component, param) - in comp_param_to_timeseries_name, - scenario_dependent=False, - value=( - comp_param_to_timeseries_name[(component, param)] - if (component, param) in comp_param_to_timeseries_name - else any_to_float(constant_data.loc[component, param]) - ), - ) - for param in pypsa_params_to_gems_params - ], - ) - ) - return components - - def _create_gems_connections( - self, - constant_data: pd.DataFrame, - pypsa_params_to_gems_connections: dict[str, tuple[str, str]], - ) -> list[GemsPortConnection]: - connections = [] - for bus_id, ( - model_port, - bus_port, - ) in pypsa_params_to_gems_connections.items(): - buses = constant_data[bus_id].values - for component_id, component in enumerate(constant_data.index): - connections.append( - GemsPortConnection( - component1=buses[component_id], - port1=bus_port, - component2=component, - port2=model_port, - ) - ) - return connections diff --git a/src/gems/pypsa_converter/utils.py b/src/gems/pypsa_converter/utils.py deleted file mode 100644 index e98f0685..00000000 --- a/src/gems/pypsa_converter/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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 typing import Any - -import numpy as np -import yaml -from pydantic import BaseModel - -PYPSA_CONVERTER_MAX_FLOAT = 100000000000 - - -def any_to_float(el: Any) -> float: - """Auxiliary function for type consistency""" - try: - return max( - min(float(el), PYPSA_CONVERTER_MAX_FLOAT), PYPSA_CONVERTER_MAX_FLOAT * -1 - ) - except: - raise TypeError(f"Could not convert {el} to float") - - -def transform_to_yaml(model: BaseModel, output_path: str) -> None: - with open(output_path, "w", encoding="utf-8") as yaml_file: - yaml.dump( - {"system": model.model_dump(by_alias=True, exclude_unset=True)}, - yaml_file, - allow_unicode=True, - ) diff --git a/tests/e2e/models/systems/pypsa_basic_system.yml b/tests/e2e/models/systems/pypsa_basic_system.yml deleted file mode 100644 index 0cde8588..00000000 --- a/tests/e2e/models/systems/pypsa_basic_system.yml +++ /dev/null @@ -1,125 +0,0 @@ -# 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. - -# Basic study to test yml model library for PyPSA emulation -# -# - -system: - model-libraries: pypsa_models - - components: - - - id: pypsatown - model: pypsa_models.bus - parameters: - - id: v_nom - time-dependent: false - scenario-dependent: false - value: 1.0 - - id: x - time-dependent: false - scenario-dependent: false - value: 0.0 - - id: y - time-dependent: false - scenario-dependent: false - value: 0.0 - - id: v_mag_pu_set - time-dependent: false - scenario-dependent: false - value: 1.0 - - id: v_mag_pu_min - time-dependent: false - scenario-dependent: false - value: 1.0 - - id: v_mag_pu_max - time-dependent: false - scenario-dependent: false - value: 1.0 - - - id: pypsaload - model: pypsa_models.load - parameters: - - id: p_set - time-dependent: true - scenario-dependent: false - value: basic_load - - id: q_set - time-dependent: false - scenario-dependent: false - value: 0 - - id: sign - time-dependent: false - scenario-dependent: false - value: -1 - - id: active - time-dependent: false - scenario-dependent: false - value: 1 - - id: pypsagenerator - model: pypsa_models.generator - parameters: - - id: p_nom_min - time-dependent: false - scenario-dependent: false - value: 200.0 - - id: p_nom_max - time-dependent: false - scenario-dependent: false - value: 200.0 - - id: marginal_cost - time-dependent: false - scenario-dependent: false - value: 50.0 - - id: capital_cost - time-dependent: false - scenario-dependent: false - value: 0.0 - - id: p_min_pu - time-dependent: false - scenario-dependent: false - value: 0.0 - - id: p_max_pu - time-dependent: false - scenario-dependent: false - value: 1.0 - - id: sign - time-dependent: false - scenario-dependent: false - value: 1 - - id: e_sum_min - time-dependent: false - scenario-dependent: false - value: 0.0 - - id: e_sum_max - time-dependent: false - scenario-dependent: false - value: 100000000 - - id: efficiency - time_dependent: false - scenario_dependent: false - value: 1 - - id: emission_factor - time_dependent: false - scenario_dependent: false - value: 0 - connections: - - component1: pypsatown - port1: p_balance_port - component2: pypsaload - port2: p_balance_port - - - component1: pypsatown - port1: p_balance_port - component2: pypsagenerator - port2: p_balance_port \ No newline at end of file diff --git a/tests/e2e/models/test_pypsa_models.py b/tests/e2e/models/test_pypsa_models.py deleted file mode 100644 index 88eeb44d..00000000 --- a/tests/e2e/models/test_pypsa_models.py +++ /dev/null @@ -1,85 +0,0 @@ -# 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 math -from pathlib import Path - -import pytest - -from gems.model.parsing import parse_yaml_library -from gems.model.resolve_library import resolve_library -from gems.simulation.optimization import build_problem -from gems.simulation.time_block import TimeBlock -from gems.study.parsing import parse_yaml_components -from gems.study.resolve_components import build_data_base, build_network, resolve_system - - -@pytest.fixture -def data_dir() -> Path: - return Path(__file__).parent - - -@pytest.fixture -def systems_dir(data_dir: Path) -> Path: - return data_dir / "systems" - - -@pytest.fixture -def series_dir(data_dir: Path) -> Path: - return data_dir / "series" - - -@pytest.mark.parametrize( - "system_file, timespan, target_value, relative_accuracy", - [ - ( - "pypsa_basic_system.yml", - 2, - 7500, - 1e-6, - ), - ], -) -def test_model_behaviour( - system_file: str, - systems_dir: Path, - series_dir: Path, - timespan: float, - target_value: float, - relative_accuracy: float, -) -> None: - scenarios = 1 - - with open(systems_dir / system_file) as compo_file: - input_component = parse_yaml_components(compo_file) - - with open("src/gems/libs/pypsa_models/pypsa_models.yml") as lib_file1: - input_libraries = [parse_yaml_library(lib_file1)] - - result_lib = resolve_library(input_libraries) - components_input = resolve_system(input_component, result_lib) - database = build_data_base(input_component, Path(series_dir)) - network = build_network(components_input) - problem = build_problem( - network, - database, - TimeBlock(1, [i for i in range(0, timespan)]), - scenarios, - ) - status = problem.solver.Solve() - print(problem.solver.Objective().Value()) - assert status == problem.solver.OPTIMAL - assert math.isclose( - target_value, - problem.solver.Objective().Value(), - rel_tol=relative_accuracy, - ) diff --git a/tests/pypsa_converter/__init__.py b/tests/pypsa_converter/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/pypsa_converter/conftest.py b/tests/pypsa_converter/conftest.py deleted file mode 100644 index ba1740b1..00000000 --- a/tests/pypsa_converter/conftest.py +++ /dev/null @@ -1,40 +0,0 @@ -# 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 pathlib import Path - -import pytest - - -@pytest.fixture -def data_dir(tmp_path: Path) -> Path: - return tmp_path - - -@pytest.fixture -def results_dir(data_dir: Path) -> Path: - results_dir = data_dir / "results" - results_dir.mkdir(parents=True, exist_ok=True) - return results_dir - - -@pytest.fixture -def systems_dir(data_dir: Path) -> Path: - systems_dir = data_dir / "systems" - systems_dir.mkdir(parents=True, exist_ok=True) - return systems_dir - - -@pytest.fixture -def series_dir(data_dir: Path) -> Path: - series_dir = data_dir / "series" - series_dir.mkdir(parents=True, exist_ok=True) - return series_dir diff --git a/tests/pypsa_converter/pypsa_input_files/base_s_4_elec.nc b/tests/pypsa_converter/pypsa_input_files/base_s_4_elec.nc deleted file mode 100644 index 86fd0515..00000000 Binary files a/tests/pypsa_converter/pypsa_input_files/base_s_4_elec.nc and /dev/null differ diff --git a/tests/pypsa_converter/pypsa_input_files/base_s_6_elec_lvopt_.nc b/tests/pypsa_converter/pypsa_input_files/base_s_6_elec_lvopt_.nc deleted file mode 100644 index d9fff143..00000000 Binary files a/tests/pypsa_converter/pypsa_input_files/base_s_6_elec_lvopt_.nc and /dev/null differ diff --git a/tests/pypsa_converter/pypsa_input_files/simple.nc b/tests/pypsa_converter/pypsa_input_files/simple.nc deleted file mode 100644 index abb83668..00000000 Binary files a/tests/pypsa_converter/pypsa_input_files/simple.nc and /dev/null differ diff --git a/tests/pypsa_converter/systems/pypsa_study.yml b/tests/pypsa_converter/systems/pypsa_study.yml deleted file mode 100644 index 19090881..00000000 --- a/tests/pypsa_converter/systems/pypsa_study.yml +++ /dev/null @@ -1,237 +0,0 @@ -system: - components: - - id: generator_Generator_pypsagenerator - model: pypsa_models.generator - parameters: - - id: p_nom_min - scenario-dependent: false - time-dependent: false - value: 200.0 - - id: p_nom_max - scenario-dependent: false - time-dependent: false - value: 200.0 - - id: p_min_pu - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: p_max_pu - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: marginal_cost - scenario-dependent: false - time-dependent: false - value: 50.0 - - id: capital_cost - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: e_sum_min - scenario-dependent: false - time-dependent: false - value: -100000000000.0 - - id: e_sum_max - scenario-dependent: false - time-dependent: false - value: 100000000000.0 - - id: sign - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: efficiency - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: emission_factor - scenario-dependent: false - time-dependent: false - value: 10.0 - - id: generator_Generator_pypsagenerator2 - model: pypsa_models.generator - parameters: - - id: p_nom_min - scenario-dependent: false - time-dependent: false - value: 200.0 - - id: p_nom_max - scenario-dependent: false - time-dependent: false - value: 200.0 - - id: p_min_pu - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: p_max_pu - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: marginal_cost - scenario-dependent: false - time-dependent: false - value: 40.0 - - id: capital_cost - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: e_sum_min - scenario-dependent: false - time-dependent: false - value: -100000000000.0 - - id: e_sum_max - scenario-dependent: false - time-dependent: false - value: 100000000000.0 - - id: sign - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: efficiency - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: emission_factor - scenario-dependent: false - time-dependent: false - value: 20.0 - - id: generator_Generator_pypsagenerator3_emissions_free - model: pypsa_models.generator - parameters: - - id: p_nom_min - scenario-dependent: false - time-dependent: false - value: 10.0 - - id: p_nom_max - scenario-dependent: false - time-dependent: false - value: 10.0 - - id: p_min_pu - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: p_max_pu - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: marginal_cost - scenario-dependent: false - time-dependent: false - value: 50.0 - - id: capital_cost - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: e_sum_min - scenario-dependent: false - time-dependent: false - value: -100000000000.0 - - id: e_sum_max - scenario-dependent: false - time-dependent: false - value: 100000000000.0 - - id: sign - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: efficiency - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: emission_factor - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: load_Load_pypsaload - model: pypsa_models.load - parameters: - - id: p_set - scenario-dependent: false - time-dependent: true - value: Demo_load_Load_pypsaload_p_set - - id: q_set - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: sign - scenario-dependent: false - time-dependent: false - value: -1.0 - - id: load_Load_pypsaload2 - model: pypsa_models.load - parameters: - - id: p_set - scenario-dependent: false - time-dependent: true - value: Demo_load_Load_pypsaload2_p_set - - id: q_set - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: sign - scenario-dependent: false - time-dependent: false - value: -1.0 - - id: pypsatown - model: pypsa_models.bus - parameters: - - id: v_nom - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: x - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: y - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: v_mag_pu_set - scenario-dependent: false - time-dependent: false - value: 1.0 - - id: v_mag_pu_min - scenario-dependent: false - time-dependent: false - value: 0.0 - - id: v_mag_pu_max - scenario-dependent: false - time-dependent: false - value: 100000000000.0 - - id: co2_budget - model: pypsa_models.global_constraint_co2_max - parameters: - - id: quota - scenario-dependent: false - time-dependent: false - value: 10000000000.0 - connections: - - component1: pypsatown - component2: generator_Generator_pypsagenerator - port1: p_balance_port - port2: p_balance_port - - component1: pypsatown - component2: generator_Generator_pypsagenerator2 - port1: p_balance_port - port2: p_balance_port - - component1: pypsatown - component2: generator_Generator_pypsagenerator3_emissions_free - port1: p_balance_port - port2: p_balance_port - - component1: pypsatown - component2: load_Load_pypsaload - port1: p_balance_port - port2: p_balance_port - - component1: pypsatown - component2: load_Load_pypsaload2 - port1: p_balance_port - port2: p_balance_port - - component1: co2_budget - component2: generator_Generator_pypsagenerator - port1: emission_port - port2: emission_port - - component1: co2_budget - component2: generator_Generator_pypsagenerator2 - port1: emission_port - port2: emission_port - nodes: [] diff --git a/tests/pypsa_converter/test_advanced_pypsa_cases.py b/tests/pypsa_converter/test_advanced_pypsa_cases.py deleted file mode 100644 index 52531d00..00000000 --- a/tests/pypsa_converter/test_advanced_pypsa_cases.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright (c) 2025, 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. - -""" -Script to convert a PyPSA study, loaded from NetCDF file, to Gems format and run it. - -This script loads a PyPSA study using the load_pypsa_study function, -converts it to Gems format, and runs the converted study. -""" - -import logging -import math -import os -from pathlib import Path - -from pypsa import Network - -from gems.model.parsing import parse_yaml_library -from gems.model.resolve_library import resolve_library -from gems.pypsa_converter.utils import transform_to_yaml -from gems.study.resolve_components import resolve_system -from tests.pypsa_converter.utils import build_problem_from_system, convert_pypsa_network - - -def load_pypsa_study(file: str, load_scaling: float) -> Network: - """ - Load a PyPSA study from a NetCDF file, preparing it for analysis or manipulation. - - This function loads a PyPSA network from a predefined NetCDF file located in the - pypsa_input_files directory. It uses a relative path to avoid hardcoding the - absolute path. - - Returns: - pypsa.Network: A PyPSA network object loaded from the NetCDF file, - containing all components and settings from the dataset. - """ - import os - from pathlib import Path - - import pypsa - - # Get the directory of the current file - current_dir = Path(__file__).parent - - # Define the relative path to the input file - input_file = current_dir / "pypsa_input_files" / file - - # Load the PyPSA network from the file - network = pypsa.Network(input_file) - - # Scale the load to make the test case feasible - network = scale_load(network, load_scaling) - - return network - - -def extend_quota(network: Network) -> Network: - # Temporary function, used while the GlobalConstraint model is not implemented yet. - # Set the CO2 bound to very large value - network.global_constraints["constant"][0] = 10000000000 - return network - - -def scale_load(network: Network, factor: float) -> Network: - network.loads_t["p_set"] *= factor - return network - - -def replace_lines_by_links(network: Network) -> Network: - """ - Replace lines in a PyPSA network with equivalent links. - - This function converts transmission lines to links, which allows for more - flexible modeling of power flow constraints. Each line is replaced with - two links (one for each direction) to maintain bidirectional flow capability. - - Args: - network (pypsa.Network): The PyPSA network to modify - - Returns: - pypsa.Network: The modified network with lines replaced by links - """ - - # Create a copy of the lines DataFrame to iterate over - lines = network.lines.copy() - - # For each line, create two links (one for each direction) - for idx, line in lines.iterrows(): - # Get line parameters - bus0 = line["bus0"] - bus1 = line["bus1"] - s_nom = line["s_nom"] - efficiency = 1.0 - - # Add forward link - network.add( - "Link", - f"{idx}-link-{bus0}-{bus1}", - bus0=bus0, - bus1=bus1, - p_min_pu=-1, - p_max_pu=1, - p_nom=s_nom, # Use line capacity as link capacity - efficiency=efficiency, - ) - network.remove("Line", lines.index) - return network - - -def pypsa_gemspy_benchmark( - file: str, load_scaling: float, activate_quota: bool -) -> None: - """ - Main function to convert a PyPSA study to Gems format and run it. - """ - # Set up logger - logger = logging.getLogger(__name__) - - # Define directories for systems and series - current_dir = Path(__file__).parent - systems_dir = current_dir / "systems" - series_dir = current_dir / "series" - - # Create directories if they don't exist - systems_dir.mkdir(exist_ok=True) - series_dir.mkdir(exist_ok=True) - - # Load the PyPSA study - logger.info("Loading PyPSA study...") - pypsa_network = load_pypsa_study(file, load_scaling) - logger.info( - f"Loaded PyPSA network with {len(pypsa_network.buses)} buses and {len(pypsa_network.generators)} generators" - ) - logger.info(f"Replacing {len(pypsa_network.lines)} Lines by links") - pypsa_network = replace_lines_by_links(pypsa_network) - if not (activate_quota): - pypsa_network = extend_quota(pypsa_network) - - # Get the number of timesteps - T = len(pypsa_network.snapshots) - logger.info(f"Number of timesteps: {T}") - # Convert to Gems System - logger.info("Converting PyPSA network to Gems format...") - input_system_from_pypsa_converter = convert_pypsa_network( - pypsa_network.copy(), systems_dir, series_dir, ".txt" - ) - - # Save the InputSystem to YAML - system_filename = "pypsa_study.yml" - logger.info(f"Saving Gems system to {systems_dir / system_filename}...") - transform_to_yaml( - model=input_system_from_pypsa_converter, - output_path=systems_dir / system_filename, - ) - - # Load the model library - logger.info("Loading model library...") - # Get the path to the project root by going up two levels from the current directory - project_root = Path(__file__).parents[2] - pypsa_models_path = project_root / "src/gems/libs/pypsa_models/pypsa_models.yml" - logger.info(f"Loading PyPSA models from {pypsa_models_path}...") - with open(pypsa_models_path) as lib_file: - input_libraries = [parse_yaml_library(lib_file)] - result_lib = resolve_library(input_libraries) - - # Resolve the system - logger.info("Resolving the system...") - resolved_system = resolve_system(input_system_from_pypsa_converter, result_lib) - - # Build and solve the optimization problem - logger.info("Building the optimization problem...") - problem = build_problem_from_system( - resolved_system, input_system_from_pypsa_converter, series_dir, T - ) - - logger.info("Solving the optimization problem...") - # Solve the problem - problem.solver.EnableOutput() - status = problem.solver.Solve() - - # Log the results - if status == problem.solver.OPTIMAL: - logger.info("Optimization problem solved successfully!") - logger.info(f"Objective value: {problem.solver.Objective().Value()}") - else: - logger.error(f"Failed to solve optimization problem. Status: {status}") - - # Optimize PyPSA network - logger.info("Solving PyPSA network after line to link...") - pypsa_network.optimize() - logger.info(f"PyPSA objective value: {pypsa_network.objective}") - assert math.isclose( - pypsa_network.objective + pypsa_network.objective_constant, - problem.solver.Objective().Value(), - rel_tol=1e-6, - ) - - -def test_case_gemspy() -> None: - pypsa_gemspy_benchmark("base_s_4_elec.nc", 0.4, True) - pypsa_gemspy_benchmark("base_s_6_elec_lvopt_.nc", 0.3, True) - pypsa_gemspy_benchmark("simple.nc", 1.0, False) diff --git a/tests/pypsa_converter/test_pypsa_converter.py b/tests/pypsa_converter/test_pypsa_converter.py deleted file mode 100644 index 9a0c838d..00000000 --- a/tests/pypsa_converter/test_pypsa_converter.py +++ /dev/null @@ -1,711 +0,0 @@ -# Copyright (c) 2025, 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 math -from pathlib import Path - -import pypsa -import pytest - -from gems.model.parsing import parse_yaml_library -from gems.model.resolve_library import resolve_library -from gems.pypsa_converter.utils import transform_to_yaml -from gems.study.parsing import parse_yaml_components -from gems.study.resolve_components import resolve_system -from tests.pypsa_converter.utils import build_problem_from_system, convert_pypsa_network - - -def test_load_gen(systems_dir: Path, series_dir: Path) -> None: - # Function to test the behaviour of Generator with "p_nom_extendable = False" - T = 10 - n1 = pypsa.Network(name="Demo", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", "pypsaload", bus="pypsatown", p_set=[i * 10 for i in range(T)], q_set=0 - ) - n1.add("Load", "pypsaload2", bus="pypsatown", p_set=100, qset=0) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator2", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=40, # €/MWh - p_nom=50, # MW - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test(n1, n1.objective, "test_load_gen.yml", systems_dir, series_dir) - - -@pytest.mark.parametrize( - "capital_cost, p_nom_min,p_nom_max", - [ - (100.0, 0, 5), - (1.0, 0, 5), - (1.0, 0, 100), - (0.1, 0, 100), - (100.0, 10, 50), - (100.0, 50, 50), - ], -) -def test_load_gen_ext( - systems_dir: Path, - series_dir: Path, - capital_cost: float, - p_nom_min: float, - p_nom_max: float, -) -> None: - # Function to test the behaviour of Generator with "p_nom_extendable = True" - T = 10 - n1 = pypsa.Network(name="Demo", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", "pypsaload", bus="pypsatown", p_set=[i * 10 for i in range(T)], q_set=0 - ) - n1.add("Load", "pypsaload2", bus="pypsatown", p_set=100, qset=0) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator2", - bus="pypsatown", - p_nom_extendable=True, - marginal_cost=10, # €/MWh - capital_cost=capital_cost, # €/MWh - p_nom_min=p_nom_min, # MW - p_nom_max=p_nom_max, # MW - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_load_gen_ext.yml", systems_dir, series_dir - ) - - -@pytest.mark.parametrize( - "ratio, sense", - [(0, "<="), (0.2, "<="), (0.5, "<="), (1.0, "<="), (0.5, "=="), (0.2, "==")], -) -def test_load_gen_emissions( - systems_dir: Path, series_dir: Path, ratio: float, sense: str -) -> None: - # Testing PyPSA Generators with CO2 constraints - T, min_emissions, max_emissions = 10, 10, 20 - n1 = pypsa.Network(name="Demo", snapshots=[i for i in range(T)]) - n1.add("Carrier", "fictive_fuel_one", co2_emissions=min_emissions) - n1.add("Carrier", "fictive_fuel_two", co2_emissions=max_emissions) - n1.add("Bus", "pypsatown", v_nom=1) - load1 = [i * 10 for i in range(T)] - n1.add("Load", "pypsaload", bus="pypsatown", p_set=load1, q_set=0) - load2 = [100 for i in range(T)] - n1.add("Load", "pypsaload2", bus="pypsatown", p_set=load2, qset=0) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - carrier="fictive_fuel_one", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator2", - bus="pypsatown", - carrier="fictive_fuel_two", - p_nom_extendable=False, - marginal_cost=40, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator3_emissions_free", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=10, # MW - ) - quota = (ratio * min_emissions + (1 - ratio) * max_emissions) * ( - sum(load1) + sum(load2) - ) - n1.add("GlobalConstraint", name="co2_budget", sense=sense, constant=quota) - n1.optimize() - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_load_gen_emissions.yml", systems_dir, series_dir - ) - - -def test_load_gen_pmin(systems_dir: Path, series_dir: Path) -> None: - # Testing pmin_pu and pmax_pu parameters for Generator component - - # Building the PyPSA test problem - T = 10 - n1 = pypsa.Network(name="Demo", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - - n1.add("Load", "pypsaload2", bus="pypsatown", p_set=100, qset=0) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator2", - bus="pypsatown", - pmin_pu=0.1, - pmax_pu=[0.8 + 0.1 * i for i in range(T)], - p_nom_extendable=False, - marginal_cost=10, # €/MWh - p_nom=50, # MW - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_load_gen_pmin.yml", systems_dir, series_dir - ) - - -def test_load_gen_sum(systems_dir: Path, series_dir: Path) -> None: - # Testing e_sum parameters for Generator component - - # Building the PyPSA test problem - T = 10 - n1 = pypsa.Network(name="Demo", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - - n1.add("Load", "pypsaload2", bus="pypsatown", p_set=100, qset=0) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator2", - bus="pypsatown", - e_sum_max=200, - p_nom_extendable=False, - marginal_cost=10, # €/MWh - p_nom=50, # MW - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_load_gen_sum.yml", systems_dir, series_dir - ) - - -def test_load_gen_link(systems_dir: Path, series_dir: Path) -> None: - T = 10 - n1 = pypsa.Network(name="Demo2", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", "pypsaload", bus="pypsatown", p_set=[i * 10 for i in range(T)], q_set=0 - ) - n1.add("Load", "pypsaload2", bus="pypsatown", p_set=100, qset=0) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator2", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=40, # €/MWh - p_nom=50, # MW - ) - n1.add("Bus", "paris", v_nom=1) - n1.add("Load", "parisload", bus="paris", p_set=200, qset=0) - n1.add( - "Generator", - "pypsagenerator3", - bus="paris", - p_nom_extendable=False, - marginal_cost=200, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Link", - "link-paris-pypsatown", - bus0="pypsatown", - bus1="paris", - efficiency=0.9, - marginal_cost=0.5, - p_nom=50, - p_min_pu=-1, - p_max_pu=1, - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_load_gen_link.yml", systems_dir, series_dir - ) - - -@pytest.mark.parametrize( - "capital_cost, p_nom_min,p_nom_max", - [ - (100.0, 0, 50), - (1.0, 0, 50), - (1.0, 0, 100), - (0.1, 0, 100), - (100.0, 10, 50), - (100.0, 50, 50), - ], -) -def test_load_gen_link_ext( - systems_dir: Path, - series_dir: Path, - capital_cost: float, - p_nom_min: float, - p_nom_max: float, -) -> None: - T = 10 - n1 = pypsa.Network(name="Demo2", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", "pypsaload", bus="pypsatown", p_set=[i * 10 for i in range(T)], q_set=0 - ) - n1.add("Load", "pypsaload2", bus="pypsatown", p_set=100, qset=0) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Generator", - "pypsagenerator2", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=40, # €/MWh - p_nom=50, # MW - ) - n1.add("Bus", "paris", v_nom=1) - n1.add("Load", "parisload", bus="paris", p_set=200, qset=0) - n1.add( - "Generator", - "pypsagenerator3", - bus="paris", - p_nom_extendable=False, - marginal_cost=200, # €/MWh - p_nom=200, # MW - ) - n1.add( - "Link", - "link-paris-pypsatown", - bus0="pypsatown", - bus1="paris", - efficiency=0.9, - marginal_cost=0.5, - p_nom_min=p_nom_min, - p_nom_max=p_nom_max, - p_nom_extendable=True, - capital_cost=capital_cost, - p_min_pu=-1, - p_max_pu=1, - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_load_gen_link_ext.yml", systems_dir, series_dir - ) - - -@pytest.mark.parametrize( - "state_of_charge_initial, standing_loss,efficiency_store,inflow_factor", - [ - (100.0, 0.01, 0.99, 1e-6), - (100.0, 0.01, 0.99, 1), - (0.0, 0.01, 0.98, 1), - (0.0, 0.05, 0.9, 1), - (0.0, 0.05, 0.9, 4), - ], -) -def test_storage_unit( - systems_dir: Path, - series_dir: Path, - state_of_charge_initial: float, - standing_loss: float, - efficiency_store: float, - inflow_factor: float, -) -> None: - # Building the PyPSA test problem with a storage unit - T = 20 - n1 = pypsa.Network(name="Demo3", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", - "pypsaload", - bus="pypsatown", - p_set=[ - 100, - 160, - 100, - 70, - 90, - 30, - 0, - 150, - 200, - 10, - 0, - 0, - 200, - 240, - 0, - 0, - 20, - 50, - 60, - 50, - ], - q_set=0, - ) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=150.0, # MW - ) - n1.add( - "StorageUnit", - "pypsastorage", - bus="pypsatown", - p_nom=100, # MW - max_hours=4, # Hours of storage at full output - efficiency_store=efficiency_store, - efficiency_dispatch=0.85, - standing_loss=standing_loss, - state_of_charge_initial=state_of_charge_initial, - marginal_cost=10.0, # €/MWh - marginal_cost_storage=1.5, # €/MWh - spill_cost=100.0, # €/MWh - p_min_pu=-1, - p_max_pu=1, - inflow=[i * inflow_factor for i in range(T)], - cyclic_state_of_charge=True, - cyclic_state_of_charge_per_period=True, - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_storage_unit.yml", systems_dir, series_dir - ) - - -@pytest.mark.parametrize( - "state_of_charge_initial, standing_loss,efficiency_store,inflow_factor", - [ - (100.0, 0.01, 0.99, 1e-6), - (100.0, 0.01, 0.99, 1), - (0.0, 0.01, 0.98, 1), - (0.0, 0.05, 0.9, 1), - (0.0, 0.05, 0.9, 4), - ], -) -def test_storage_unit_ext( - systems_dir: Path, - series_dir: Path, - state_of_charge_initial: float, - standing_loss: float, - efficiency_store: float, - inflow_factor: float, -) -> None: - # Function to test the StorageUnit Components with "p_nom_extendable = True" - - # Building the PyPSA test problem with a storage unit - T = 20 - n1 = pypsa.Network(name="Demo3", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", - "pypsaload", - bus="pypsatown", - p_set=[ - 100, - 160, - 100, - 70, - 90, - 30, - 0, - 150, - 200, - 10, - 0, - 0, - 200, - 240, - 0, - 0, - 20, - 50, - 60, - 50, - ], - q_set=0, - ) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=150.0, # MW - ) - n1.add( - "StorageUnit", - "pypsastorage", - bus="pypsatown", - p_nom_min=100, # MW - p_nom_max=150, # MW - p_nom_extendable=True, - capital_cost=1, - max_hours=4, # Hours of storage at full output - efficiency_store=efficiency_store, - efficiency_dispatch=0.85, - standing_loss=standing_loss, - state_of_charge_initial=state_of_charge_initial, - marginal_cost=10.0, # €/MWh - marginal_cost_storage=1.5, # €/MWh - spill_cost=100.0, # €/MWh - p_min_pu=-1, - p_max_pu=1, - inflow=inflow_factor, - cyclic_state_of_charge=True, - cyclic_state_of_charge_per_period=True, - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test( - n1, n1.objective, "test_storage_unit.yml", systems_dir, series_dir - ) - - -@pytest.mark.parametrize( - "e_initial, standing_loss", - [ - (50.0, 0.1), - (0.0, 0.01), - (0.0, 0.05), - ], -) -def test_store( - systems_dir: Path, series_dir: Path, e_initial: float, standing_loss: float -) -> None: - # Building the PyPSA test problem with a store - T = 20 - - n1 = pypsa.Network(name="StoreDemo", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", - "pypsaload", - bus="pypsatown", - p_set=[ - 100, - 160, - 100, - 70, - 90, - 30, - 0, - 150, - 200, - 10, - 0, - 0, - 200, - 240, - 0, - 0, - 20, - 50, - 60, - 50, - ], - q_set=0, - ) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=50, # €/MWh - p_nom=150.0, # MW - ) - n1.add( - "Store", - "pypsastore", - bus="pypsatown", - e_nom=200, # MWh - e_initial=e_initial, - standing_loss=standing_loss, # 1% loss per hour - marginal_cost=10.0, # €/MWh - marginal_cost_storage=1.5, # €/MWh - e_cyclic=True, - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test(n1, n1.objective, "test_store.yml", systems_dir, series_dir) - - -def test_store_ext(systems_dir: Path, series_dir: Path) -> None: - # Building the PyPSA test problem with a store - T = 20 - - n1 = pypsa.Network(name="StoreDemo", snapshots=[i for i in range(T)]) - n1.add("Bus", "pypsatown", v_nom=1) - n1.add( - "Load", - "pypsaload", - bus="pypsatown", - p_set=[ - 100, - 160, - 100, - 70, - 90, - 30, - 0, - 150, - 200, - 10, - 0, - 0, - 200, - 240, - 0, - 0, - 20, - 50, - 60, - 50, - ], - q_set=0, - ) - n1.add( - "Generator", - "pypsagenerator", - bus="pypsatown", - p_nom_extendable=False, - marginal_cost=[i for i in range(T)], # €/MWh - p_nom=150.0, # MW - ) - n1.add( - "Store", - "pypsastore", - bus="pypsatown", - e_nom_min=10.0, # MWh - e_nom_max=1000.0, # MWh - e_nom_extendable=True, - e_initial=100.0, - capital_cost=10, - standing_loss=0.1, # 1% loss per hour - marginal_cost=1.0, # €/MWh - marginal_cost_storage=1.5, # €/MWh - e_cyclic=True, - ) - n1.optimize() - - # Testing the PyPSA_to_Gems converter - run_conversion_test(n1, n1.objective, "test_store_ext.yml", systems_dir, series_dir) - - -def run_conversion_test( - pypsa_network: pypsa.Network, - target_value: float, - system_filename: str, - systems_dir: Path, - series_dir: Path, -) -> None: - T = len(pypsa_network.timesteps) - - # Conversion to Gems System - input_system_from_pypsa_converter = convert_pypsa_network( - pypsa_network, systems_dir, series_dir, ".txt" - ) - - # Loading the model library - with open("src/gems/libs/pypsa_models/pypsa_models.yml") as lib_file: - input_libraries = [parse_yaml_library(lib_file)] - result_lib = resolve_library(input_libraries) - - # Approach 1 : Comparing PyPSA result with Gems result using the InputSystem directly - resolved_system_from_pypsa_converter = resolve_system( - input_system_from_pypsa_converter, result_lib - ) - - # Approcach 2 : Saving the InputSystem to yaml, reading it the yaml and loading the InputSystem - transform_to_yaml( - model=input_system_from_pypsa_converter, - output_path=systems_dir / system_filename, - ) - with open(systems_dir / system_filename) as system_file: - input_system_from_yaml = parse_yaml_components(system_file) - resolved_system_from_yaml = resolve_system(input_system_from_yaml, result_lib) - - # Testing both InputSystem objects - for resolved_system, input_system in [ - (resolved_system_from_pypsa_converter, input_system_from_pypsa_converter), - (resolved_system_from_yaml, input_system_from_yaml), - ]: - problem = build_problem_from_system( - resolved_system, input_system, series_dir, T - ) - status = problem.solver.Solve() - print(problem.solver.Objective().Value()) - assert status == problem.solver.OPTIMAL - assert math.isclose( - problem.solver.Objective().Value(), target_value, rel_tol=1e-6 - ) diff --git a/tests/pypsa_converter/utils.py b/tests/pypsa_converter/utils.py deleted file mode 100644 index 4b7bea14..00000000 --- a/tests/pypsa_converter/utils.py +++ /dev/null @@ -1,58 +0,0 @@ -import logging -from pathlib import Path - -from pypsa import Network - -from gems.pypsa_converter.pypsa_converter import PyPSAStudyConverter -from gems.simulation.optimization import OptimizationProblem, build_problem -from gems.simulation.time_block import TimeBlock -from gems.study.parsing import InputSystem -from gems.study.resolve_components import System, build_data_base, build_network - - -def convert_pypsa_network( - pypsa_network: Network, systems_dir: Path, series_dir: Path, series_file_format: str -) -> InputSystem: - """ - Convert a PyPSA network to an Gems InputSystem. - - Args: - pypsa_network: The PyPSA network to convert - systems_dir: Directory to store system files - series_dir: Directory to store time series data - - Returns: - InputSystem: The converted Gems InputSystem - """ - logger = logging.getLogger(__name__) - converter = PyPSAStudyConverter( - pypsa_network, logger, systems_dir, series_dir, series_file_format - ) - input_system_from_pypsa_converter = converter.to_gems_study() - return input_system_from_pypsa_converter - - -def build_problem_from_system( - resolved_system: System, input_system: InputSystem, series_dir: Path, timesteps: int -) -> OptimizationProblem: - """ - Build an optimization problem from a resolved system. - - Args: - resolved_system: The resolved Gems system - input_system: The input system - series_dir: Directory containing time series data - timesteps: Number of timesteps in the simulation - - Returns: - OptimizationProblem: The built optimization problem - """ - database = build_data_base(input_system, Path(series_dir)) - network = build_network(resolved_system) - problem = build_problem( - network, - database, - TimeBlock(1, [i for i in range(timesteps)]), - 1, - ) - return problem diff --git a/tests/unittests/simulation/test_simulation_table.py b/tests/unittests/simulation/test_simulation_table.py deleted file mode 100644 index 5ffb351a..00000000 --- a/tests/unittests/simulation/test_simulation_table.py +++ /dev/null @@ -1,67 +0,0 @@ -# Standard library imports -from pathlib import Path - -# Third-party imports -import pandas as pd -import pytest - -# Local application/library imports -from gems.model.parsing import parse_yaml_library -from gems.model.resolve_library import resolve_library -from gems.simulation import OutputValues, TimeBlock, build_problem -from gems.simulation.simulation_table import ( - SimulationTableBuilder, - SimulationTableWriter, -) -from gems.study.parsing import parse_yaml_components -from gems.study.resolve_components import build_data_base, build_network, resolve_system - - -@pytest.mark.parametrize("scenario_count", [1, 3]) -def test_pypsa_model_simulation_table(tmp_path: Path, scenario_count: int) -> None: - pypsa_library_file = Path("src/gems/libs/pypsa_models/pypsa_models.yml") - pypsa_model_file = Path("tests/pypsa_converter/systems/pypsa_study.yml") - database_path = Path("tests/pypsa_converter/series") - - # --- Load PyPSA library --- - with pypsa_library_file.open() as lib_file: - input_library_obj = parse_yaml_library(lib_file) - - # Resolve the library - resolved_lib = resolve_library([input_library_obj]) - - # --- Load PyPSA model --- - with pypsa_model_file.open() as model_file: - input_system = parse_yaml_components(model_file) - - # Resolve system and database - components_input = resolve_system(input_system, resolved_lib) - database = build_data_base(input_system, database_path) - network = build_network(components_input) - - # --- Build and solve optimization problem --- - time_block = TimeBlock(1, list(range(5))) # adjust time steps as needed - problem = build_problem(network, database, time_block, 1) - - status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL, "Problem did not solve optimally" - - # --- Extract output values --- - results = OutputValues(problem) - - # --- Build simulation table --- - builder = SimulationTableBuilder() - sim_df = builder.build(results) - # --- Write CSV using writer --- - writer = SimulationTableWriter(sim_df) - - csv_path = writer.write_csv( - tmp_path, simulation_id=builder.simulation_id, optim_nb=1 - ) - - # --- Assertions --- - assert csv_path.exists(), "Simulation table CSV not created" - assert not sim_df.empty, "Simulation table dataframe is empty" - - # Optional: print first few rows - print(sim_df.head())