Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
1327f49
start writing the code
MartinBelthle Jun 23, 2026
16e0a76
fixing
MartinBelthle Jun 23, 2026
d9482cc
c
MartinBelthle Jun 23, 2026
ddbfe0a
c
MartinBelthle Jun 23, 2026
0bee06f
c
MartinBelthle Jun 23, 2026
46644d2
start writing test to ensure nothing broke
MartinBelthle Jun 23, 2026
69d9e3e
continue
MartinBelthle Jun 23, 2026
219af13
implement deletion
MartinBelthle Jun 23, 2026
03f5acb
add empty skeleton
MartinBelthle Jun 23, 2026
6957f2f
adapt study converter
MartinBelthle Jun 23, 2026
a67e302
brevet
MartinBelthle Jun 23, 2026
a186dfd
fixing mypy
MartinBelthle Jun 23, 2026
486d6e6
fixing mypy issues
MartinBelthle Jun 23, 2026
6febe37
write code
MartinBelthle Jun 23, 2026
0ed7d8b
c
MartinBelthle Jun 23, 2026
f44798a
create one command
MartinBelthle Jun 24, 2026
ced952b
introduce second command
MartinBelthle Jun 24, 2026
d415ecf
fixing ruff and mypy
MartinBelthle Jun 24, 2026
ec5a90c
a
MartinBelthle Jun 24, 2026
ad88bf9
typo
MartinBelthle Jun 24, 2026
1d887ce
fix copilot issue
MartinBelthle Jun 24, 2026
f10e606
clean thermal in fs mode
MartinBelthle Jun 24, 2026
ad3af53
write deletion code even if it does not work for now
MartinBelthle Jun 24, 2026
b8093c9
bien
MartinBelthle Jun 24, 2026
98c7792
add alembic migration
MartinBelthle Jun 25, 2026
639b1a5
start creating skeletons
MartinBelthle Jun 25, 2026
b6a3e7f
add in memory code
MartinBelthle Jun 25, 2026
bdf5aa2
c
MartinBelthle Jun 25, 2026
58c30b6
start filling
MartinBelthle Jun 25, 2026
e59ff97
add UTs for merge function
MartinBelthle Jun 26, 2026
42372f1
simplify a bit the function
MartinBelthle Jun 26, 2026
fa4b169
fixing mypy issues
MartinBelthle Jun 26, 2026
df24615
add unicity verification
MartinBelthle Jun 26, 2026
bf2b6c6
start renaming as symmetries would we merged
MartinBelthle Jun 26, 2026
1b11cf9
f
MartinBelthle Jun 26, 2026
03f6f17
c
MartinBelthle Jun 26, 2026
811b1cf
c
MartinBelthle Jun 26, 2026
1185cd7
c
MartinBelthle Jun 26, 2026
c749c4c
continue
MartinBelthle Jun 26, 2026
0df636d
introduce symmetries_check
MartinBelthle Jun 26, 2026
b6a183b
reuse other exception
MartinBelthle Jun 26, 2026
c3c8a92
finalize DB implem
MartinBelthle Jun 26, 2026
2339ff4
no mypy issue
MartinBelthle Jun 26, 2026
05e81f2
study converter
MartinBelthle Jun 26, 2026
4f9cf90
c
MartinBelthle Jun 26, 2026
0198e5d
mama
MartinBelthle Jun 26, 2026
496a99c
put code in common
MartinBelthle Jun 26, 2026
a903b7c
implement the deletion
MartinBelthle Jun 26, 2026
9958c18
add commen
MartinBelthle Jun 26, 2026
4e8dd54
change serialization method
MartinBelthle Jun 26, 2026
293eb0b
finalize dao code
MartinBelthle Jun 26, 2026
01c71c6
fix thermal removal issues
MartinBelthle Jun 26, 2026
f708b85
remove method
MartinBelthle Jun 26, 2026
cda36b7
add 2nd command
MartinBelthle Jun 26, 2026
a21d421
implement manager
MartinBelthle Jun 26, 2026
144954c
remove deletion code
MartinBelthle Jun 29, 2026
8eca53c
build the managers
MartinBelthle Jun 29, 2026
749fd35
introduce 2nd endpoint
MartinBelthle Jun 29, 2026
22d93c7
Merge branch 'dev' into feat/support-thermal-reserves
MartinBelthle Jun 29, 2026
e16bbf6
remove cmd
MartinBelthle Jun 29, 2026
36b34bc
start rewriting code for new endpoints
MartinBelthle Jun 29, 2026
fbfe261
typo
MartinBelthle Jun 29, 2026
0d1689e
ice
MartinBelthle Jun 29, 2026
419258a
create new cmd
MartinBelthle Jun 29, 2026
94b9cae
fix
MartinBelthle Jun 29, 2026
32d79ee
f
MartinBelthle Jun 29, 2026
813acce
continue refactoring
MartinBelthle Jun 29, 2026
4be569f
c
MartinBelthle Jun 29, 2026
40ae3ca
c
MartinBelthle Jun 29, 2026
90b225d
c
MartinBelthle Jun 29, 2026
bf693b3
fix import issue
MartinBelthle Jun 30, 2026
73695a9
fix DB code issue
MartinBelthle Jun 30, 2026
ae34556
start rewriting
MartinBelthle Jun 30, 2026
4737d6e
type ignore
MartinBelthle Jun 30, 2026
8b576e8
not done yet
MartinBelthle Jun 30, 2026
5633129
no mypy issue anymroe
MartinBelthle Jun 30, 2026
c7c9a4a
Merge branch 'dev' into feat/support-thermal-reserves
MartinBelthle Jun 30, 2026
6ed6a48
adapt new code
MartinBelthle Jun 30, 2026
15fde8e
add comment
MartinBelthle Jun 30, 2026
f1954fb
continue
MartinBelthle Jun 30, 2026
9662b68
add certifications endpoint
MartinBelthle Jun 30, 2026
af738f0
typo
MartinBelthle Jun 30, 2026
bb26626
fix issue
MartinBelthle Jun 30, 2026
5adcf18
implement the DB code
MartinBelthle Jun 30, 2026
c6ed3f9
start writing cmd UT
MartinBelthle Jun 30, 2026
37bb4fb
finalize set up code
MartinBelthle Jun 30, 2026
ce300e8
typo
MartinBelthle Jun 30, 2026
9137227
continue
MartinBelthle Jun 30, 2026
28482a4
continue
MartinBelthle Jun 30, 2026
f368bcc
c
MartinBelthle Jun 30, 2026
056a4f9
fix fs behavior
MartinBelthle Jun 30, 2026
b70af86
finalize error case test
MartinBelthle Jun 30, 2026
ba7feb8
fix fs code
MartinBelthle Jun 30, 2026
9d3a27c
add first logic test
MartinBelthle Jun 30, 2026
418a278
c
MartinBelthle Jun 30, 2026
3f862fa
fix
MartinBelthle Jun 30, 2026
5839007
finalize UT
MartinBelthle Jun 30, 2026
f27f992
fix src code
MartinBelthle Jun 30, 2026
e7616e1
continue
MartinBelthle Jun 30, 2026
1cf4e17
error test ok on DB but not on FS
MartinBelthle Jun 30, 2026
69c15c1
error test ok
MartinBelthle Jun 30, 2026
7062904
start real test
MartinBelthle Jun 30, 2026
85e522e
fix certification order inside db code
MartinBelthle Jun 30, 2026
87f5181
forgot to save the yaml content
MartinBelthle Jun 30, 2026
8a495ee
fix parsing code
MartinBelthle Jun 30, 2026
f8c068d
test okay for FS but not for DB
MartinBelthle Jun 30, 2026
983adc1
fix db code
MartinBelthle Jun 30, 2026
4d023e6
start writing command factory test
MartinBelthle Jun 30, 2026
8c033cd
test is ok
MartinBelthle Jun 30, 2026
5228595
set up the FS test
MartinBelthle Jun 30, 2026
2e239c8
start writing test
MartinBelthle Jun 30, 2026
2365156
handle error case
MartinBelthle Jun 30, 2026
fe2da43
rename
MartinBelthle Jun 30, 2026
cd4a774
c
MartinBelthle Jun 30, 2026
a7bbf97
finalize UT for FS
MartinBelthle Jun 30, 2026
93993bb
Merge branch 'dev' into feat/support-thermal-reserves
MartinBelthle Jul 1, 2026
a8d56d2
fix alembic migration merge
MartinBelthle Jul 1, 2026
14ebfcc
remove merge_symmetries method
MartinBelthle Jul 3, 2026
68d51aa
add sorting as a validator
MartinBelthle Jul 3, 2026
a5cf7a3
rename files
MartinBelthle Jul 3, 2026
0a04bd8
write UT that currently fails
MartinBelthle Jul 3, 2026
f3275ac
continue
MartinBelthle Jul 3, 2026
e808df1
now test is passing
MartinBelthle Jul 3, 2026
5ebdf6c
make test storage mode parametrized
MartinBelthle Jul 3, 2026
f43d321
fix tets
MartinBelthle Jul 3, 2026
31151ab
move test
MartinBelthle Jul 3, 2026
81373e8
TDD
MartinBelthle Jul 3, 2026
525e87b
simplify exception raised
MartinBelthle Jul 3, 2026
53fd2b1
introduce common code
MartinBelthle Jul 3, 2026
f4eef45
continue
MartinBelthle Jul 3, 2026
8850a44
test is ok for both storage modes
MartinBelthle Jul 3, 2026
000607d
add deletion tests
MartinBelthle Jul 3, 2026
efbe2e6
add the same test for symmetries
MartinBelthle Jul 3, 2026
ed76d49
fix code to make test work
MartinBelthle Jul 3, 2026
3bb5d5f
Merge branch 'dev' into feat/support-thermal-reserves
MartinBelthle Jul 3, 2026
317bbb7
fix failing test
MartinBelthle Jul 3, 2026
c2edf21
resolve conflicts with dev
MartinBelthle Jul 9, 2026
6fd2bb2
fix src code
MartinBelthle Jul 9, 2026
868bb00
start building big pydantic model
MartinBelthle Jul 9, 2026
2faa37f
continue moving code
MartinBelthle Jul 9, 2026
9e0307f
error cases test is ok
MartinBelthle Jul 9, 2026
0b69634
add another parsing test
MartinBelthle Jul 9, 2026
e10e00d
create structure
MartinBelthle Jul 9, 2026
7c2a1bb
c
MartinBelthle Jul 9, 2026
96a8e6c
c
MartinBelthle Jul 9, 2026
e50362c
one less error
MartinBelthle Jul 9, 2026
57f8ad8
c
MartinBelthle Jul 9, 2026
fdf0a48
start fixing
MartinBelthle Jul 9, 2026
cd1afd8
no mypy issue
MartinBelthle Jul 9, 2026
b08fb49
typo
MartinBelthle Jul 9, 2026
98791f4
c
MartinBelthle Jul 9, 2026
70b7136
test is now ok
MartinBelthle Jul 9, 2026
38695bf
Merge branch 'dev' into feat/support-thermal-reserves
MartinBelthle Jul 9, 2026
06db558
make free function from static method
MartinBelthle Jul 9, 2026
bbff505
document the design notes
MartinBelthle Jul 9, 2026
719491b
fix symmetries deletion test
MartinBelthle Jul 9, 2026
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
62 changes: 62 additions & 0 deletions alembic/versions/665f7b1d7575_add_thermal_reserve_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""add_thermal_reserve_tables

Revision ID: 665f7b1d7575
Revises: 80fdf2408ede
Create Date: 2026-06-23 13:41:03.310148

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '665f7b1d7575'
down_revision = '80fdf2408ede'
branch_labels = None
depends_on = None


def upgrade() -> None:
# Certifications
op.create_table(
"thermal_reserve_certifications",
sa.Column("study_id", sa.String(36), nullable=False),
sa.Column("area_id", sa.String(255), nullable=False),
sa.Column("thermal_id", sa.String(255), nullable=False),
sa.Column("reserve_id", sa.String(255), nullable=False),
sa.Column("max_power", sa.Float(), nullable=False),
sa.Column("max_power_off", sa.Float(), nullable=False),
sa.Column("participation_cost", sa.Float(), nullable=False),
sa.Column("participation_cost_off", sa.Float(), nullable=False),
sa.ForeignKeyConstraint(
["study_id", "area_id", "thermal_id"],
["thermal_cluster.study_id", "thermal_cluster.area_id", "thermal_cluster.thermal_id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["study_id", "area_id", "reserve_id"],
["reserve_definition.study_id", "reserve_definition.area_id", "reserve_definition.reserve_id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("study_id", "area_id", "thermal_id", "reserve_id"),
)

# Symmetries
op.create_table(
"thermal_reserve_symmetries",
sa.Column("study_id", sa.String(36), nullable=False),
sa.Column("area_id", sa.String(255), nullable=False),
sa.Column("thermal_id", sa.String(255), nullable=False),
sa.Column("symmetries", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["study_id", "area_id", "thermal_id"],
["thermal_cluster.study_id", "thermal_cluster.area_id", "thermal_cluster.thermal_id"],
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("study_id", "area_id", "thermal_id"),
)


def downgrade() -> None:
op.drop_table("thermal_reserve_certifications")
op.drop_table("thermal_reserve_symmetries")
12 changes: 12 additions & 0 deletions antarest/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ def __init__(self, area_id: str, reserve_id: str):
super().__init__(HTTPStatus.NOT_FOUND, msg)


class ReserveDefinitionsNotFound(HTTPException):
def __init__(self, invalid_reserve_ids: dict[str, set[str]]):
msg = f"Reserve definitions not found: {invalid_reserve_ids}"
super().__init__(HTTPStatus.NOT_FOUND, msg)


# ============================================================
# Duplicate (409)
# ============================================================
Expand Down Expand Up @@ -857,3 +863,9 @@ class SevenZipNotSupportedOnThisMachine(Exception):
def __init__(self) -> None:
msg = "7z command line is not supported on this machine. Consider installing it if you want to unarchive these files."
super().__init__(msg)


class ThermalReserveCertificationNotFound(HTTPException):
def __init__(self, area_id: str, thermal_id: str, reserve_ids: set[str]):
msg = f"Certifications for reserve(s) '{reserve_ids}' on thermal cluster '{thermal_id}' not found in area '{area_id}'"
super().__init__(HTTPStatus.NOT_FOUND, msg)
58 changes: 58 additions & 0 deletions antarest/study/business/areas/reserve_certification_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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 antarest.study.business.model.thermal_reserve_certification_model import (
ReserveCertifications,
)
from antarest.study.business.study_interface import StudyInterface
from antarest.study.storage.variantstudy.model.command.icommand import ICommand
from antarest.study.storage.variantstudy.model.command.replace_thermal_reserve_certifications import (
ReplaceThermalReserveCertifications,
)
from antarest.study.storage.variantstudy.model.command_context import CommandContext


class ReserveCertificationsManager:
def __init__(self, command_context: CommandContext) -> None:
self._command_context = command_context

def get_certifications(self, study: StudyInterface, area_id: str) -> ReserveCertifications:
thermals = study.get_study_dao().get_thermal_reserve_certifications(area_id)
return ReserveCertifications(thermals=thermals)

def set_certifications(
self,
study: StudyInterface,
area_id: str,
data: ReserveCertifications,
) -> ReserveCertifications:
commands: list[ICommand] = []

# Thermals part
if data.thermals:
Comment thread
MartinBelthle marked this conversation as resolved.
command = ReplaceThermalReserveCertifications(
area_id=area_id,
certifications=data.thermals,
study_version=study.version,
command_context=self._command_context,
)
commands.append(command)

# St-Storage part
# todo

# Hydro part
# todo

# Apply the modifications
if commands:
study.add_commands(commands)
return data
38 changes: 38 additions & 0 deletions antarest/study/business/areas/reserve_symmetries_management.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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 antarest.study.business.model.reserve_symmetries_model import ReserveSymmetries
from antarest.study.business.study_interface import StudyInterface
from antarest.study.dao.common import ThermalId
from antarest.study.storage.variantstudy.model.command.replace_thermal_reserve_symmetries import (
ReplaceThermalReserveSymmetries,
)
from antarest.study.storage.variantstudy.model.command_context import CommandContext


class ReserveSymmetriesManager:
def __init__(self, command_context: CommandContext) -> None:
self._command_context = command_context

def get_thermal_symmetries(self, study: StudyInterface, area_id: str) -> dict[ThermalId, ReserveSymmetries]:
return study.get_study_dao().get_thermal_reserve_symmetries(area_id)

def set_thermal_symmetries(
self, study: StudyInterface, area_id: str, data: dict[ThermalId, ReserveSymmetries]
) -> dict[ThermalId, ReserveSymmetries]:
command = ReplaceThermalReserveSymmetries(
area_id=area_id,
symmetries=data,
command_context=self._command_context,
study_version=study.version,
)
study.add_commands([command])
return data
28 changes: 28 additions & 0 deletions antarest/study/business/model/reserve_symmetries_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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 typing import Annotated, TypeAlias

from pydantic import BeforeValidator

from antarest.study.business.model.reserve_definition_model import ReserveDefinitionId


def _symmetry_validator(data: list[str]) -> list[str]:
if len(data) < 2:
raise ValueError(f"Reserve symmetries should have at least 2 elements, and was {data}")
if len(set(data)) != len(data):
raise ValueError(f"Reserve symmetries should not contain duplicates, and was {data}")
return sorted(data)


ReserveSymmetry: TypeAlias = Annotated[list[ReserveDefinitionId], BeforeValidator(_symmetry_validator)]
ReserveSymmetries: TypeAlias = list[ReserveSymmetry]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 typing import Annotated, TypeAlias

from pydantic import ConfigDict, Field
from pydantic.alias_generators import to_camel

from antarest.core.serde import AntaresBaseModel
from antarest.study.business.model.reserve_definition_model import ReserveDefinitionId

Cost = Annotated[float, Field(ge=0)]
Power = Annotated[float, Field(ge=0)]

##########################
# Thermal part
##########################


class ThermalReserveCertification(AntaresBaseModel):
model_config = ConfigDict(alias_generator=to_camel, extra="forbid", populate_by_name=True)

max_power: Power = 0.0
max_power_off: Power = 0.0
participation_cost: Cost = 0.0
participation_cost_off: Cost = 0.0


ThermalId: TypeAlias = str
ThermalReserveCertificationMapping = dict[ReserveDefinitionId, dict[ThermalId, ThermalReserveCertification]]


##########################
# Whole model
##########################


class ReserveCertifications(AntaresBaseModel):
model_config = ConfigDict(alias_generator=to_camel, extra="forbid", populate_by_name=True)

thermals: ThermalReserveCertificationMapping
36 changes: 36 additions & 0 deletions antarest/study/dao/api/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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 antarest.study.business.model.reserve_definition_model import ReserveDefinitionId
from antarest.study.business.model.reserve_symmetries_model import ReserveSymmetries


def remove_reserve_symmetries_by_cascade(
symmetries_dict: dict[str, ReserveSymmetries], reserve_ids_to_remove: set[ReserveDefinitionId]
) -> dict[str, ReserveSymmetries] | None:
"""
When removing a reserve, we should also remove it from the symmetries.

Returns:
The updated symmetries dictionary or None if no symmetries were updated.
"""
should_update_symmetries = False
for symmetries in symmetries_dict.values():
for i, symmetry in enumerate(symmetries):
symmetries[i] = [reserve_id for reserve_id in symmetry if reserve_id not in reserve_ids_to_remove]
if len(symmetries[i]) != len(symmetry):
should_update_symmetries = True
if len(symmetries[i]) == 1:
# We only have one reserve left in the symmetry, we should remove it
symmetries[i] = []
if should_update_symmetries:
return symmetries_dict
return None
49 changes: 49 additions & 0 deletions antarest/study/dao/api/reserve_certification_dao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 abc import ABC, abstractmethod

from antarest.study.business.model.thermal_reserve_certification_model import (
ThermalReserveCertificationMapping,
)
from antarest.study.dao.common import AreaId


class ReadOnlyReserveCertificationDao(ABC):
@abstractmethod
def get_all_thermal_reserve_certifications(self) -> dict[AreaId, ThermalReserveCertificationMapping]:
"""
Returns the thermal reserve certifications of the whole study.

Design notes:
- If an area has no certification, it won't be present in the returned data.
- If a thermal cluster has no certification, it also won't be present in the returned data.

"""
raise NotImplementedError()

@abstractmethod
def get_thermal_reserve_certifications(self, area_id: AreaId) -> ThermalReserveCertificationMapping:
raise NotImplementedError()


class ReserveCertificationDao(ReadOnlyReserveCertificationDao):
@abstractmethod
def save_thermal_reserve_certifications(self, data: dict[AreaId, ThermalReserveCertificationMapping]) -> None:
"""
Replace the thermal reserve certifications with the given one.

Design notes:
- If an area is absent from the given data, its certifications are not modified.
- If a thermal cluster is absent from in the given data, its certifications will be removed.

"""
raise NotImplementedError()
Loading
Loading