Skip to content

Commit e76602a

Browse files
fix: bugfix three winding transformers in graphs (#198)
* should work * fix * first state * small cleanup * cleanup Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * useless int DCO Remediation Commit for Vincent Koppen <vincent.koppen@alliander.com> I, Vincent Koppen <vincent.koppen@alliander.com>, hereby add my Signed-off-by to this commit: b305453 I, Vincent Koppen <vincent.koppen@alliander.com>, hereby add my Signed-off-by to this commit: 9701f0c I, Vincent Koppen <vincent.koppen@alliander.com>, hereby add my Signed-off-by to this commit: affb900 I, Vincent Koppen <vincent.koppen@alliander.com>, hereby add my Signed-off-by to this commit: ed61f71 Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * make compliant Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * cleanup Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * cleanup Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * docs Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * adjust match for ruff Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * feat: add tmp_remove_branches Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * Update src/power_grid_model_ds/_core/model/graphs/models/base.py Co-authored-by: Jaap Schouten <58551444+jaapschoutenalliander@users.noreply.github.com> Signed-off-by: Vincent Koppen <53343926+vincentkoppen@users.noreply.github.com> * fix crash on raise_on_error = False Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> * refactor: simplify setup Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> --------- Signed-off-by: Vincent Koppen <vincent.koppen@alliander.com> Signed-off-by: Vincent Koppen <53343926+vincentkoppen@users.noreply.github.com> Co-authored-by: Jaap Schouten <58551444+jaapschoutenalliander@users.noreply.github.com>
1 parent 2da1faf commit e76602a

2 files changed

Lines changed: 330 additions & 9 deletions

File tree

src/power_grid_model_ds/_core/model/graphs/models/base.py

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections import Counter
77
from collections.abc import Generator
88
from contextlib import contextmanager
9+
from itertools import combinations
910
from typing import TYPE_CHECKING
1011

1112
from numpy._typing import NDArray
@@ -31,6 +32,12 @@ class BaseGraphModel(ABC):
3132
def __init__(self, active_only=False) -> None:
3233
self.active_only = active_only
3334

35+
# Three winding transformers are represented as a cycle in our graph ((1,2), (2,3) and (1,3))
36+
# This makes certain graph algorithms invalid.
37+
# With self._three_winding_nodes we keep track of the three winding transformers in the graph.
38+
# This is used to correct the graph state before we perform these graph algorithms.
39+
self._three_winding_nodes: set[tuple[int, int, int]] = set()
40+
3441
def __repr__(self) -> str:
3542
return (
3643
f"{self.__class__.__name__}(nodes={self.nr_nodes}, "
@@ -189,6 +196,7 @@ def add_branch3_array(self, branch3_array: Branch3Array) -> None:
189196
"""Add all branch3s in the branch3 array to the graph."""
190197
for branch3 in branch3_array:
191198
self.add_branch_array(branch3.as_branches())
199+
self._three_winding_nodes.add(self._get_branch3_nodes(branch3))
192200

193201
def delete_branch_array(self, branch_array: BranchArray, raise_on_fail: bool = True) -> None:
194202
"""Delete all branches in branch_array from the graph."""
@@ -200,6 +208,7 @@ def delete_branch3_array(self, branch3_array: Branch3Array, raise_on_fail: bool
200208
"""Delete all branch3s in the branch3 array from the graph."""
201209
for branch3 in branch3_array:
202210
self.delete_branch_array(branch3.as_branches(), raise_on_fail=raise_on_fail)
211+
self._three_winding_nodes.discard(self._get_branch3_nodes(branch3))
203212

204213
@contextmanager
205214
def tmp_remove_nodes(self, nodes: list[int]) -> Generator:
@@ -268,7 +277,7 @@ def get_shortest_path(self, ext_start_node_id: int, ext_end_node_id: int) -> tup
268277
internal_path, distance = self._get_shortest_path(
269278
source=self.external_to_internal(ext_start_node_id), target=self.external_to_internal(ext_end_node_id)
270279
)
271-
return self._internals_to_externals(internal_path), distance
280+
return self._to_external_path(internal_path), distance
272281
except NoPathBetweenNodes as e:
273282
raise NoPathBetweenNodes(f"No path between nodes {ext_start_node_id} and {ext_end_node_id}") from e
274283

@@ -279,12 +288,16 @@ def get_all_paths(self, ext_start_node_id: int, ext_end_node_id: int) -> list[li
279288
if ext_start_node_id == ext_end_node_id:
280289
return []
281290

282-
internal_paths = self._get_all_paths(
283-
source=self.external_to_internal(ext_start_node_id),
284-
target=self.external_to_internal(ext_end_node_id),
285-
)
291+
with self._without_three_winding_cycles() as correct_for_three_winding:
292+
internal_paths = self._get_all_paths(
293+
source=self.external_to_internal(ext_start_node_id),
294+
target=self.external_to_internal(ext_end_node_id),
295+
)
286296

287-
return [self._internals_to_externals(path) for path in internal_paths]
297+
return [
298+
self._to_external_path(internal_path=path, correct_for_three_winding=correct_for_three_winding)
299+
for path in internal_paths
300+
]
288301

289302
def get_components(self) -> list[list[int]]:
290303
"""Returns all separate components of the graph as lists
@@ -369,8 +382,13 @@ def find_fundamental_cycles(self) -> list[list[int]]:
369382
Returns:
370383
list[list[int]]: list of cycles, each cycle is a list of (external) node ids
371384
"""
372-
internal_cycles = self._find_fundamental_cycles()
373-
return [self._internals_to_externals(nodes) for nodes in internal_cycles]
385+
with self._without_three_winding_cycles() as correct_for_three_winding:
386+
internal_cycles = self._find_fundamental_cycles()
387+
388+
return [
389+
self._to_external_path(internal_path=cycle, correct_for_three_winding=correct_for_three_winding)
390+
for cycle in internal_cycles
391+
]
374392

375393
@classmethod
376394
def from_arrays(cls, arrays: "Grid", active_only=False) -> "BaseGraphModel":
@@ -393,19 +411,70 @@ def from_grid(cls, grid: "Grid", active_only=False) -> "BaseGraphModel":
393411
return new_graph
394412

395413
def _internals_to_externals(self, internal_nodes: list[int]) -> list[int]:
396-
"""Convert a list of internal nodes to external nodes"""
414+
"""Convert a list of internal node ids to external node ids"""
397415
return [self.internal_to_external(node_id) for node_id in internal_nodes]
398416

399417
def _externals_to_internals(self, external_nodes: list[int] | NDArray) -> list[int]:
400418
"""Convert a list of external nodes to internal nodes"""
401419
return [self.external_to_internal(node_id) for node_id in external_nodes]
402420

421+
@contextmanager
422+
def _without_three_winding_cycles(self) -> Generator[bool, None, None]:
423+
"""Context manager that temporarily removes cycles introduced by three winding transformers in the graph.
424+
425+
Three winding transformers are represented as a cycle. To make graph algorithms valid,
426+
we temporarily remove one branch per three winding transformer to break the cycle.
427+
The graph still has the same components after removing these branches.
428+
We just force the path through the three winding transformers.
429+
430+
NOTE: we only remove branches for a three winding transformer if all three branches are active.
431+
432+
Yields True if branches were removed (and correction for three-winding transformers is needed).
433+
"""
434+
branches_to_remove = [
435+
(group[1], group[2])
436+
for group in self._three_winding_nodes
437+
if all(self.has_branch(from_node, to_node) for from_node, to_node in combinations(group, 2))
438+
]
439+
with self.tmp_remove_branches(branches_to_remove):
440+
yield bool(branches_to_remove)
441+
442+
def _to_external_path(self, internal_path: list[int], correct_for_three_winding: bool = False) -> list[int]:
443+
"""Convert a path of internal node ids to external node ids.
444+
445+
If correct_for_three_winding is True, also removes detours through three winding transformers.
446+
This should be used together with _without_three_winding_cycles.
447+
448+
If a path contains all nodes of a three winding transformer after each other, we can remove the middle node.
449+
Since if we hadn't removed the third branch of the three winding transformer, we would have gone directly.
450+
451+
NOTE: if a node has an inactive status, we can never have a situation where
452+
we go through 3 nodes of the transformer directly after each other.
453+
So this also works for inactive three winding transformers.
454+
"""
455+
path = self._internals_to_externals(internal_path)
456+
if not correct_for_three_winding:
457+
return path
458+
459+
replacements = {frozenset(group) for group in self._three_winding_nodes}
460+
return [
461+
node
462+
for index, node in enumerate(path)
463+
if (index in (0, len(path) - 1) or frozenset([path[index - 1], node, path[index + 1]]) not in replacements)
464+
]
465+
403466
def _branch_is_relevant(self, branch: BranchArray) -> bool:
404467
"""Check if a branch is relevant"""
405468
if self.active_only:
406469
return branch.is_active.item()
407470
return True
408471

472+
def _get_branch3_nodes(self, branch3_array: Branch3Array) -> tuple[int, int, int]:
473+
"""Get the node ids of the branch3 array as a tuple"""
474+
if len(branch3_array) != 1:
475+
raise ValueError("branch3_array must be of length one element")
476+
return (branch3_array.node_1.item(), branch3_array.node_2.item(), branch3_array.node_3.item())
477+
409478
@abstractmethod
410479
def _in_branches(self, int_node_id: int) -> Generator[tuple[int, int], None, None]: ...
411480

0 commit comments

Comments
 (0)