From cbae933404d63f7e4c191315031deeb2722dc041 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Thu, 19 Jun 2025 12:36:30 +1000 Subject: [PATCH 1/5] use sets, hash and compare only the MRID Signed-off-by: Max Chesterfield --- .../network/tracing/networktrace/network_trace_tracker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py index f14c41728..f82d06af4 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py @@ -13,7 +13,7 @@ class NetworkTraceTracker: Internal class that tracks visited state of a Terminal's Phase in a Network Trace """ def __init__(self): - self._visited = list() + self._visited = set() def has_visited(self, terminal: Terminal, phases: Set[SinglePhaseKind]=None) -> bool: """Returns True if this Terminal's Phase has been visited, False otherwise""" @@ -23,7 +23,7 @@ def visit(self, terminal: Terminal, phases: Set[SinglePhaseKind]=None) -> bool: """Marks this Terminal's Phase as visited""" key = self._get_key(terminal, phases) if key not in self._visited: - self._visited.append(self._get_key(terminal, phases)) + self._visited.add(self._get_key(terminal, phases)) return True return False @@ -34,6 +34,6 @@ def clear(self): @staticmethod def _get_key(terminal: Terminal, phases: Set[SinglePhaseKind]) -> Any: if phases: - return terminal, phases + return terminal.mrid, phases else: - return terminal + return terminal.mrid From 86cf3548f21243d68182c7c5143fcbf00e7265d5 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Thu, 19 Jun 2025 12:43:13 +1000 Subject: [PATCH 2/5] sets cant be hashed, use frozenset instead Signed-off-by: Max Chesterfield --- .../network/tracing/networktrace/network_trace.py | 6 +++--- .../network/tracing/networktrace/network_trace_step.py | 8 ++++---- .../network/tracing/networktrace/network_trace_tracker.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py index 20dd1ac1a..33ad59dd5 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -5,7 +5,7 @@ from collections.abc import Callable from functools import singledispatchmethod -from typing import TypeVar, Union, Generic, Set, Type, Generator +from typing import TypeVar, Union, Generic, Set, Type, Generator, FrozenSet from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment @@ -295,7 +295,7 @@ def create_new_this(self) -> 'NetworkTrace[T]': def start_nominal_phase_path(phases: PhaseCode) -> Set[NominalPhasePath]: return {NominalPhasePath(it, it) for it in phases.single_phases} if phases and phases.single_phases else set() - def has_visited(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: + def has_visited(self, terminal: Terminal, phases: FrozenSet[SinglePhaseKind]) -> bool: parent = self.parent while parent is not None: if parent._tracker.has_visited(terminal, phases): @@ -303,7 +303,7 @@ def has_visited(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: parent = parent.parent return self._tracker.has_visited(terminal, phases) - def visit(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: + def visit(self, terminal: Terminal, phases: FrozenSet[SinglePhaseKind]) -> bool: if self.parent and self.parent.has_visited(terminal, phases): return False return self._tracker.visit(terminal, phases) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step.py index 5b469db9b..005a518a7 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Set, Generic, TypeVar, TYPE_CHECKING, Optional, List +from typing import Set, Generic, TypeVar, TYPE_CHECKING, Optional, List, FrozenSet from zepben.evolve.services.network.tracing.connectivity.nominal_phase_path import NominalPhasePath @@ -50,10 +50,10 @@ class Path: traversed_ac_line_segment: Optional[AcLineSegment] = field(default=None) nominal_phase_paths: Optional[Set[NominalPhasePath]] = field(default_factory=set) - def to_phases_set(self) -> Set[SinglePhaseKind]: + def to_phases_set(self) -> FrozenSet[SinglePhaseKind]: if len(self.nominal_phase_paths) == 0: - return set() - return set(map(lambda it: it.to_phase, self.nominal_phase_paths)) + return frozenset() + return frozenset(map(lambda it: it.to_phase, self.nominal_phase_paths)) @property def from_equipment(self) -> ConductingEquipment: diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py index f82d06af4..b766660bd 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_tracker.py @@ -2,7 +2,7 @@ # 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 https://mozilla.org/MPL/2.0/. -from typing import Set, Any +from typing import Set, Any, FrozenSet from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind @@ -15,11 +15,11 @@ class NetworkTraceTracker: def __init__(self): self._visited = set() - def has_visited(self, terminal: Terminal, phases: Set[SinglePhaseKind]=None) -> bool: + def has_visited(self, terminal: Terminal, phases: FrozenSet[SinglePhaseKind]=None) -> bool: """Returns True if this Terminal's Phase has been visited, False otherwise""" return self._get_key(terminal, phases) in self._visited - def visit(self, terminal: Terminal, phases: Set[SinglePhaseKind]=None) -> bool: + def visit(self, terminal: Terminal, phases: FrozenSet[SinglePhaseKind]=None) -> bool: """Marks this Terminal's Phase as visited""" key = self._get_key(terminal, phases) if key not in self._visited: @@ -32,7 +32,7 @@ def clear(self): self._visited.clear() @staticmethod - def _get_key(terminal: Terminal, phases: Set[SinglePhaseKind]) -> Any: + def _get_key(terminal: Terminal, phases: FrozenSet[SinglePhaseKind]) -> Any: if phases: return terminal.mrid, phases else: From b38e1f2dd6ff850329ee8df67bb1117f01b02aff Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Thu, 19 Jun 2025 12:43:28 +1000 Subject: [PATCH 3/5] lets make sure this bug never returns Signed-off-by: Max Chesterfield --- .../services/network/tracing/networktrace/test_network_trace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index 3967d64ae..2b5109a00 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -190,7 +190,7 @@ async def test_can_run_large_branching_traces(self): builder.from_junction(num_terminals=1) \ .to_acls() - for i in range(500): + for i in range(1000): builder.to_junction(mrid=f'junc-{i}', num_terminals=3) \ .to_acls(mrid=f'acls-{i}-top') \ .from_acls(mrid=f'acls-{i}-bottom') \ From e7127fe12bbc78ec447a3de90048a08e7e9efbcd Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Thu, 19 Jun 2025 12:48:37 +1000 Subject: [PATCH 4/5] Update changelog Signed-off-by: Max Chesterfield --- changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.md b/changelog.md index a6cfb13f0..386a90396 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,8 @@ * `RemoveDirection` has been removed. It did not work reliably with dual fed networks with loops. You now need to clear direction using the new `ClearDirection` and reapply directions where appropriate using `SetDirection`. * `Cut` supports adding a maximum of 2 terminals. +* `NetworkTraceTracker` now uses a `set` to track visited objects, if you were using unhashable objects this will need to be addressed. + ### New Features @@ -39,6 +41,8 @@ * `NetworkTrace`/`Traversal` now correctly respects `can_stop_on_start_item` when providing multiple start items. * `AssignToFeeders`/`AssignToLvFeeders` now finds back-fed equipment correctly * `AssignToFeeders` and `AssignToLvFeeders` will now associate `PowerElectronicUnits` with their `powerElectronicsConnection` `Feeder`/`LvFeeder`. +* `NetworkTrace` no longer hashes the entire terminal object passed in, and will only reference the mRID in `self.tracker`, Exponentially large execution + times should now be fixed. From 704183c584ae2283dc5587c1b34f9dafce9806d4 Mon Sep 17 00:00:00 2001 From: Anthony Charlton Date: Thu, 19 Jun 2025 18:58:10 +1000 Subject: [PATCH 5/5] Removed internal detail from changelog that has no external impact (unless people are hacking into our internals, gross) Signed-off-by: Anthony Charlton --- changelog.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/changelog.md b/changelog.md index 386a90396..b91960d8e 100644 --- a/changelog.md +++ b/changelog.md @@ -5,13 +5,11 @@ * `AcLineSegment` supports adding a maximum of 2 terminals. Mid-span terminals are no longer supported and models should migrate to using `Clamp`. * `Clamp` supports only adding a single terminal. * `FeederDirectionStateOperations` have been reworked to take `NetworkStateOperators` as a parameter. -* `RemoveDirection` has been removed. It did not work reliably with dual fed networks with loops. You now need to clear direction using the new -`ClearDirection` and reapply directions where appropriate using `SetDirection`. +* `RemoveDirection` has been removed. It did not work reliably with dual fed networks with loops. You now need to clear direction using the new `ClearDirection` + and reapply directions where appropriate using `SetDirection`. * `Cut` supports adding a maximum of 2 terminals. * `NetworkTraceTracker` now uses a `set` to track visited objects, if you were using unhashable objects this will need to be addressed. - - ### New Features * Added `ClearDirection` that clears feeder directions. @@ -24,9 +22,8 @@ * You can now add sites to the `TestNetworkBuilder` via `addSite`. * You can now add busbar sections natively with `from_busbar_section` and `to_busbar_section` * The prefix for generated mRIDs for "other" equipment can be specified with the `default_mrid_prefix` argument in `from_other` and `to_other`. -* When processing feeder assignments, all LV feeders belonging to a dist substation site will now be considered energized when the site is energized by a feeder. - - +* When processing feeder assignments, all LV feeders belonging to a dist substation site will now be considered energized when the site is energized by a + feeder. ### Fixes * When finding `LvFeeders` in the `Site` we will now exclude `LvFeeders` that start with an open `Switch` @@ -41,10 +38,6 @@ * `NetworkTrace`/`Traversal` now correctly respects `can_stop_on_start_item` when providing multiple start items. * `AssignToFeeders`/`AssignToLvFeeders` now finds back-fed equipment correctly * `AssignToFeeders` and `AssignToLvFeeders` will now associate `PowerElectronicUnits` with their `powerElectronicsConnection` `Feeder`/`LvFeeder`. -* `NetworkTrace` no longer hashes the entire terminal object passed in, and will only reference the mRID in `self.tracker`, Exponentially large execution - times should now be fixed. - - ### Notes * None. @@ -99,7 +92,8 @@ ## [0.44.0] - 2025-01-24 ### Breaking Changes -* `GrpcChannelBuilder.build()` now accepts a `timeout_seconds` argument. This is the timeout used for each connection attempt so the total amount of time the connection test may take to fail can be greater than `timeout_seconds`. +* `GrpcChannelBuilder.build()` now accepts a `timeout_seconds` argument. This is the timeout used for each connection attempt so the total amount of time the + connection test may take to fail can be greater than `timeout_seconds`. ### New Features * Added the following new CIM classes: @@ -135,9 +129,10 @@ * Removed `ProcessingPaused` current state response message as this functionality won't be supported. * `QueryNetworkStateClient.get_current_states` now returns a `CurrentStateEventBatch` rather than just the events themselves. * `QueryNetworkStateService.on_get_current_states` must now return a stream of `CurrentStateEventBatch` rather than just the events themselves. -* `AcLineSegment.per_length_sequence_impedance` has been corrected to `per_length_impedance`. This has been done in a non-breaking way, however the public +* `AcLineSegment.per_length_sequence_impedance` has been corrected to `per_length_impedance`. This has been done in a non-breaking way, however the public resolver `Resolvers.per_length_sequence_impedance` is now `Resolvers.per_length_impedance`, correctly reflecting the CIM relationship. -* Removed `get_current_equipment_for_feeder` implementation for `NetworkConsumerClient` as its functionality is now incorporated in `get_equipment_for_container`. +* Removed `get_current_equipment_for_feeder` implementation for `NetworkConsumerClient` as its functionality is now incorporated in + `get_equipment_for_container`. ### New Features * Added `BatchNotProcessed` current state response. This is used to indicate a batch has been ignored, rather than just returning a `BatchSuccessful`.