From a8fb852c537fb4f5e9a17034c87f0d0878a90522 Mon Sep 17 00:00:00 2001 From: LNavick Date: Wed, 1 Oct 2025 11:34:16 +0300 Subject: [PATCH 1/2] feature: flowcnec limit consistency check --- rao/crac/builder.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/rao/crac/builder.py b/rao/crac/builder.py index dcc3aa2..e33bdac 100644 --- a/rao/crac/builder.py +++ b/rao/crac/builder.py @@ -34,6 +34,30 @@ def crac(self): def crac_pprint(self): return print(json.dumps(self.crac, indent=2)) + def perform_cnec_consistency_check(self): + + # Find the flowCnec thresholds + flow_cnecs = list(getattr(self._crac, "flowCnecs", [])) + kept = [] + + for cnec in flow_cnecs: + cnec_name = getattr(cnec, "name", None) + + thresholds = getattr(cnec, "thresholds", []) or [] + if not isinstance(thresholds, list): + thresholds = [thresholds] + + # Flag FlowCNEC as removed if limits min=0, max=0 + removed = any((getattr(th, "min", None) == 0) and (getattr(th, "max", None) == 0) for th in thresholds) + + if removed: + logger.warning(f"CNEC {cnec_name} removed from the CRAC file due to missing limits") + else: + kept.append(cnec) + + # Keep only consistent flowCnecs + setattr(self._crac, "flowCnecs", kept) + def get_limits(self): if self.network is None: @@ -385,6 +409,7 @@ def build_crac(self, contingency_ids: list | None = None): self.process_cnecs() self.process_remedial_actions() self.update_limits_from_network() + self.perform_cnec_consistency_check() return self.crac From b2035c8cea398825dd72216f39ef15cb1d90977c Mon Sep 17 00:00:00 2001 From: LNavick Date: Wed, 15 Oct 2025 09:17:25 +0300 Subject: [PATCH 2/2] feature: #37 topological actions assigned virtual cost values during CRAC building --- rao/crac/builder.py | 23 +++++++++++++++++++---- rao/crac/models.py | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/rao/crac/builder.py b/rao/crac/builder.py index e33bdac..5fb037c 100644 --- a/rao/crac/builder.py +++ b/rao/crac/builder.py @@ -37,15 +37,15 @@ def crac_pprint(self): def perform_cnec_consistency_check(self): # Find the flowCnec thresholds - flow_cnecs = list(getattr(self._crac, "flowCnecs", [])) - kept = [] + flow_cnecs = list(getattr(self._crac, "flowCnecs", [ ])) + kept = [ ] for cnec in flow_cnecs: cnec_name = getattr(cnec, "name", None) - thresholds = getattr(cnec, "thresholds", []) or [] + thresholds = getattr(cnec, "thresholds", [ ]) or [ ] if not isinstance(thresholds, list): - thresholds = [thresholds] + thresholds = [ thresholds ] # Flag FlowCNEC as removed if limits min=0, max=0 removed = any((getattr(th, "min", None) == 0) and (getattr(th, "max", None) == 0) for th in thresholds) @@ -324,6 +324,9 @@ def _get_opposite_terminal_connection_value(value: str): logger.warning(f"Not supported by CRAC builder, ignoring remedial action") continue + # Set base activation cost for all remedial actions + data['virtual_cost'] = 0 + # Create network elements property modification actions = [] for action in data.to_dict("records"): @@ -358,6 +361,17 @@ def _get_opposite_terminal_connection_value(value: str): logger.warning(f"Grid state alteration type is not supported: {action_type}") continue + # Set non-reserve topology actions virtual cost higher than reserve topology actions + if action_type == 'TopologyAction' and directions.iloc[0] == 'none': + data['virtual_cost'] = 50 + + logger.debug(f"Assigning virtual cost of {data['virtual_cost'].iloc[0]} to non-reserve topology action {data['IdentifiedObject.name_GridStateAlterationRemedialAction'].iloc[0]}") + # TODO [TEMPORARY] remove extra RA checking for direction kind == up once all RA directions are semantically aligned + elif action_type == 'TopologyAction' and directions.iloc[0] == 'up': + data['virtual_cost'] = 50 + + logger.debug(f"Assigning virtual cost of {data['virtual_cost'].iloc[0]} to non-reserve topology action {data['IdentifiedObject.name_GridStateAlterationRemedialAction'].iloc[0]} with direction {directions.iloc[0]}" ) + # TODO [TEMPORARY] - perform consistency check of action (not optimal doing one by one) if element_id not in self.network.ID.values: logger.warning(f"Alteration equipment of remedial action does not exist in network model: {action['IdentifiedObject.name_GridStateAlteration']}") @@ -376,6 +390,7 @@ def _get_opposite_terminal_connection_value(value: str): id=data['IdentifiedObject.mRID_GridStateAlterationRemedialAction'].iloc[0], name=data['IdentifiedObject.name_GridStateAlterationRemedialAction'].iloc[0], operator=data['RemedialAction.RemedialActionSystemOperator'].iloc[0], + activationCost=data['virtual_cost'].iloc[0], onInstantUsageRules=[ { "usageMethod": "available", diff --git a/rao/crac/models.py b/rao/crac/models.py index 871fcd0..44f20cb 100644 --- a/rao/crac/models.py +++ b/rao/crac/models.py @@ -79,6 +79,7 @@ class NetworkAction(BaseModel): id: str name: str operator: str + activationCost: int onInstantUsageRules: List[Dict] terminalsConnectionActions: Optional[List[TerminalsAction]] = None shuntCompensatorPositionActions: Optional[List[ShuntCompensatorPositionAction]] = None