Skip to content

Commit

Permalink
fix(commands): Scenario Builder rules are updated when a BC is remove…
Browse files Browse the repository at this point in the history
…d or updated
  • Loading branch information
laurent-laporte-pro committed May 31, 2024
1 parent 8c3bc8d commit e0c82b4
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import typing
import typing as t
from abc import ABCMeta

Expand Down Expand Up @@ -292,7 +293,13 @@ def validates_and_fills_matrices(
self.equal_term_matrix = self.get_corresponding_matrices(self.equal_term_matrix, time_step, version, create)

def apply_binding_constraint(
self, study_data: FileStudy, binding_constraints: t.Dict[str, t.Any], new_key: str, bd_id: str
self,
study_data: FileStudy,
binding_constraints: t.Dict[str, t.Any],
new_key: str,
bd_id: str,
*,
old_groups: t.Optional[t.Set[str]] = None,
) -> CommandOutput:
version = study_data.config.version

Expand Down Expand Up @@ -337,11 +344,20 @@ def apply_binding_constraint(
binding_constraints,
["input", "bindingconstraints", "bindingconstraints"],
)

if version >= 870:
# When all BC of a given group are removed, the group should be removed from the scenario builder
old_groups = old_groups or set()
new_groups = {bd.get("group", DEFAULT_GROUP).lower() for bd in binding_constraints.values()}
removed_groups = old_groups - new_groups
remove_bc_from_scenario_builder(study_data, removed_groups)

if self.values:
if not isinstance(self.values, str): # pragma: no cover
raise TypeError(repr(self.values))
if version < 870:
study_data.tree.save(self.values, ["input", "bindingconstraints", bd_id])

for matrix_term, matrix_name, matrix_alias in zip(
[self.less_term_matrix, self.equal_term_matrix, self.greater_term_matrix],
TERM_MATRICES,
Expand Down Expand Up @@ -444,3 +460,21 @@ def match(self, other: "ICommand", equal: bool = False) -> bool:
if not equal:
return self.name == other.name
return super().match(other, equal)


def remove_bc_from_scenario_builder(study_data: FileStudy, removed_groups: typing.Set[str]) -> None:
"""
Update the scenario builder by removing the rows that correspond to the BC groups to remove.
NOTE: this update can be very long if the scenario builder configuration is large.
"""
rulesets = study_data.tree.get(["settings", "scenariobuilder"])

for ruleset in rulesets.values():
for key in list(ruleset):
# The key is in the form "symbol,group,year"
symbol, *parts = key.split(",")
if symbol == "bc" and parts[0] in removed_groups:
del ruleset[key]

study_data.tree.save(rulesets, ["settings", "scenariobuilder"])
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import typing
from typing import Any, Dict, List, Tuple

from antarest.core.model import JSON
from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput
from antarest.study.storage.variantstudy.model.command.create_binding_constraint import DEFAULT_GROUP
from antarest.study.storage.variantstudy.model.command.create_binding_constraint import (
DEFAULT_GROUP,
remove_bc_from_scenario_builder,
)
from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand
from antarest.study.storage.variantstudy.model.model import CommandDTO

Expand All @@ -30,24 +32,6 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput,
study_data.bindings.remove(next(iter([bind for bind in study_data.bindings if bind.id == self.id])))
return CommandOutput(status=True), {}

# noinspection PyMethodMayBeStatic
def _remove_bc_from_scenario_builder(self, study_data: FileStudy, removed_groups: typing.Set[str]) -> None:
"""
Update the scenario builder by removing the rows that correspond to the BC groups to remove.
NOTE: this update can be very long if the scenario builder configuration is large.
"""
rulesets = study_data.tree.get(["settings", "scenariobuilder"])

for ruleset in rulesets.values():
for key in list(ruleset):
# The key is in the form "symbol,group,year"
symbol, *parts = key.split(",")
if symbol == "bc" and parts[0] in removed_groups:
del ruleset[key]

study_data.tree.save(rulesets, ["settings", "scenariobuilder"])

def _apply(self, study_data: FileStudy) -> CommandOutput:
if self.id not in [bind.id for bind in study_data.config.bindings]:
return CommandOutput(status=False, message=f"Binding constraint not found: '{self.id}'")
Expand All @@ -73,7 +57,7 @@ def _apply(self, study_data: FileStudy) -> CommandOutput:
old_groups = {bd.get("group", DEFAULT_GROUP).lower() for bd in binding_constraints.values()}
new_groups = {bd.get("group", DEFAULT_GROUP).lower() for bd in new_binding_constraints.values()}
removed_groups = old_groups - new_groups
self._remove_bc_from_scenario_builder(study_data, removed_groups)
remove_bc_from_scenario_builder(study_data, removed_groups)

return self._apply_config(study_data.config)[0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput
from antarest.study.storage.variantstudy.model.command.create_binding_constraint import (
DEFAULT_GROUP,
TERM_MATRICES,
AbstractBindingConstraintCommand,
create_binding_constraint_config,
Expand Down Expand Up @@ -52,6 +53,9 @@ def _find_binding_config(self, binding_constraints: Mapping[str, JSON]) -> Optio
def _apply(self, study_data: FileStudy) -> CommandOutput:
binding_constraints = study_data.tree.get(["input", "bindingconstraints", "bindingconstraints"])

# When all BC of a given group are removed, the group should be removed from the scenario builder
old_groups = {bd.get("group", DEFAULT_GROUP).lower() for bd in binding_constraints.values()}

index_and_cfg = self._find_binding_config(binding_constraints)
if index_and_cfg is None:
return CommandOutput(
Expand Down Expand Up @@ -80,7 +84,7 @@ def _apply(self, study_data: FileStudy) -> CommandOutput:
term_ids = {k for k in updated_cfg if "%" in k or "." in k}
binding_constraints[index] = {k: v for k, v in updated_cfg.items() if k not in term_ids}

return super().apply_binding_constraint(study_data, binding_constraints, index, self.id)
return super().apply_binding_constraint(study_data, binding_constraints, index, self.id, old_groups=old_groups)

def to_dto(self) -> CommandDTO:
matrices = ["values"] + TERM_MATRICES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
BindingConstraintFrequency,
BindingConstraintOperator,
)
from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.business.command_extractor import CommandExtractor
from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter
Expand Down Expand Up @@ -38,12 +39,12 @@ def test_manage_binding_constraint(empty_study: FileStudy, command_context: Comm
area1 = "area1"
area2 = "area2"
cluster = "cluster"
CreateArea.parse_obj({"area_name": area1, "command_context": command_context}).apply(empty_study)
CreateArea.parse_obj({"area_name": area2, "command_context": command_context}).apply(empty_study)
CreateLink.parse_obj({"area1": area1, "area2": area2, "command_context": command_context}).apply(empty_study)
CreateCluster.parse_obj(
{"area_id": area1, "cluster_name": cluster, "parameters": {}, "command_context": command_context}
).apply(empty_study)
CreateArea(area_name=area1, command_context=command_context).apply(empty_study)
CreateArea(area_name=area2, command_context=command_context).apply(empty_study)
CreateLink(area1=area1, area2=area2, command_context=command_context).apply(empty_study)
CreateCluster(area_id=area1, cluster_name=cluster, parameters={}, command_context=command_context).apply(
empty_study
)

output = CreateBindingConstraint(
name="BD 1",
Expand All @@ -70,8 +71,12 @@ def test_manage_binding_constraint(empty_study: FileStudy, command_context: Comm
else:
matrix_links = [
# fmt: off
"bd 1_lt.txt.link", "bd 1_eq.txt.link", "bd 1_gt.txt.link",
"bd 2_lt.txt.link", "bd 2_eq.txt.link", "bd 2_gt.txt.link",
"bd 1_lt.txt.link",
"bd 1_eq.txt.link",
"bd 1_gt.txt.link",
"bd 2_lt.txt.link",
"bd 2_eq.txt.link",
"bd 2_gt.txt.link",
# fmt: on
]
for matrix_link in matrix_links:
Expand Down Expand Up @@ -204,6 +209,60 @@ def test_manage_binding_constraint(empty_study: FileStudy, command_context: Comm
assert rulesets == {"Default Ruleset": {}}


@pytest.mark.parametrize("empty_study", ["empty_study_870.zip"], indirect=True)
def test_scenario_builder(empty_study: FileStudy, command_context: CommandContext):
"""
Test that the scenario builder is updated when a binding constraint group is renamed or removed
"""
# This test requires a study with version >= 870, which support "scenarised" binding constraints.
assert empty_study.config.version >= 870

# Create two areas and a link between them:
areas = {name: transform_name_to_id(name) for name in ["Area X", "Area Y"]}
for area in areas.values():
output = CreateArea(area_name=area, command_context=command_context).apply(empty_study)
assert output.status, output.message
output = CreateLink(area1=areas["Area X"], area2=areas["Area Y"], command_context=command_context).apply(
empty_study
)
assert output.status, output.message
link_id = f"{areas['Area X']}%{areas['Area Y']}"

# Create a binding constraint in a specific group:
bc_group = "Group 1"
output = CreateBindingConstraint(
name="BD 1",
enabled=False,
time_step=BindingConstraintFrequency.DAILY,
operator=BindingConstraintOperator.BOTH,
coeffs={link_id: [0.3]},
group=bc_group,
command_context=command_context,
).apply(empty_study)
assert output.status, output.message

# Create a rule in the scenario builder for the binding constraint group:
output = UpdateScenarioBuilder(
data={"Default Ruleset": {f"bc,{bc_group.lower()},0": 1}}, # group name in lowercase
command_context=command_context,
).apply(study_data=empty_study)
assert output.status, output.message

# Here, we have a binding constraint between "Area X" and "Area Y" in the group "Group 1"
# and a rule in the scenario builder for this group.
# If we update the group name in the BC, the scenario builder should be updated
output = UpdateBindingConstraint(
id="bd 1",
group="Group 2",
command_context=command_context,
).apply(empty_study)
assert output.status, output.message

# Check the BC rule is removed from the scenario builder
rulesets = empty_study.tree.get(["settings", "scenariobuilder"])
assert rulesets == {"Default Ruleset": {}}


def test_match(command_context: CommandContext):
values = default_bc_weekly_daily.tolist()
base = CreateBindingConstraint(
Expand Down

0 comments on commit e0c82b4

Please sign in to comment.