From 43457bacf69c2ae74923e9479b83c2a09020d863 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Sun, 18 May 2025 21:46:51 +1000 Subject: [PATCH 01/47] . Signed-off-by: Max Chesterfield --- test/cim/cim_creators.py | 6 +++++- test/cim/iec61970/base/core/test_terminal.py | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/test/cim/cim_creators.py b/test/cim/cim_creators.py index ff72ccb26..d49ffc91b 100644 --- a/test/cim/cim_creators.py +++ b/test/cim/cim_creators.py @@ -738,13 +738,17 @@ def create_substation(include_runtime: bool = True): def create_terminal(include_runtime: bool = True): + runtime = { + "traced_phases": builds(TracedPhases) + } if include_runtime else {} return builds( Terminal, **create_ac_dc_terminal(include_runtime), conducting_equipment=sampled_conducting_equipment(include_runtime), connectivity_node=builds(ConnectivityNode, **create_identified_object(include_runtime)), phases=sampled_phase_code(), - sequence_number=integers(min_value=MIN_SEQUENCE_NUMBER, max_value=MAX_SEQUENCE_NUMBER) + sequence_number=integers(min_value=MIN_SEQUENCE_NUMBER, max_value=MAX_SEQUENCE_NUMBER), + **runtime ) diff --git a/test/cim/iec61970/base/core/test_terminal.py b/test/cim/iec61970/base/core/test_terminal.py index 46425c36e..fdc3cd6ff 100644 --- a/test/cim/iec61970/base/core/test_terminal.py +++ b/test/cim/iec61970/base/core/test_terminal.py @@ -23,7 +23,7 @@ } # noinspection PyArgumentList -terminal_args = [*ac_dc_terminal_args, ConductingEquipment(), PhaseCode.XYN, TracedPhases, 1, FeederDirection.UPSTREAM, FeederDirection.DOWNSTREAM, +terminal_args = [*ac_dc_terminal_args, ConductingEquipment(), PhaseCode.XYN, TracedPhases(1), 1, FeederDirection.UPSTREAM, FeederDirection.DOWNSTREAM, ConnectivityNode()] @@ -36,6 +36,7 @@ def test_terminal_constructor_default(): assert t.sequence_number == 0 assert t.normal_feeder_direction == FeederDirection.NONE assert t.current_feeder_direction == FeederDirection.NONE + assert t.traced_phases == TracedPhases() assert not t.connectivity_node @@ -47,6 +48,7 @@ def test_terminal_constructor_kwargs(conducting_equipment, phases, sequence_numb sequence_number=sequence_number, normal_feeder_direction=normal_feeder_direction, current_feeder_direction=current_feeder_direction, + traced_phases= traced_phases, connectivity_node=connectivity_node, **kwargs) @@ -56,6 +58,7 @@ def test_terminal_constructor_kwargs(conducting_equipment, phases, sequence_numb assert t.sequence_number == sequence_number assert t.normal_feeder_direction == normal_feeder_direction assert t.current_feeder_direction == current_feeder_direction + assert t.traced_phases == TracedPhases() assert t.connectivity_node == connectivity_node From e007fb43de69657a8c1a4108b9b7669a7f486c1f Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Sun, 18 May 2025 22:35:00 +1000 Subject: [PATCH 02/47] more tests, slight optimisation Signed-off-by: Max Chesterfield --- .../tracing/networktrace/network_trace.py | 16 ++-------- .../networktrace/network_trace_tracker.py | 6 ++-- .../networktrace/test_network_trace.py | 31 ++++++++++++------- 3 files changed, 25 insertions(+), 28 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 21c6ec908..139a773bc 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -200,22 +200,10 @@ def start_nominal_phase_path(phases: PhaseCode) -> list[NominalPhasePath]: return [NominalPhasePath(it, it) for it in phases.single_phases] if phases and phases.single_phases else [] def has_visited(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: - parent = self.parent - while parent is not None: - if parent._tracker.has_visited(terminal, phases): - return True - parent = parent.parent - - return self._tracker.has_visited(terminal, phases) + return self._tracker.has_visited(terminal, phases) or (self.parent and self.parent.has_visited(terminal, phases)) def visit(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: - parent = self.parent - while parent is not None: - if parent._tracker.has_visited(terminal, phases): - return False - parent = parent.parent - - return self._tracker.visit(terminal, phases) + return not (self.parent and self.parent.has_visited(terminal, phases)) and self._tracker.visit(terminal, phases) def to_network_trace_queue_condition(queue_condition: QueueCondition[NetworkTraceStep[T]], step_type: NetworkTraceStep.Type, override_step_type: bool): 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 d0677c154..46d0f2e62 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 @@ -32,7 +32,7 @@ def clear(self): @staticmethod def _get_key(terminal: Terminal, phases: Set[SinglePhaseKind]) -> Any: - if phases and len(phases) < 1: - return terminal - else: + if phases: return terminal, phases + else: + return terminal diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index 733a02847..2c2753d99 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -2,27 +2,36 @@ # 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/. +import sys + import pytest from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.testing.test_network_builder import TestNetworkBuilder + class TestNetworkTrace: - @pytest.mark.skip @pytest.mark.asyncio async def test_can_run_large_branching_traces(self): - builder = TestNetworkBuilder() - network = builder.network + try: + sys.setrecursionlimit(100000) # need to bump this for this test, we're going 1000+ recursive calls deep + + builder = TestNetworkBuilder() + network = builder.network + + builder.from_junction(num_terminals=1) \ + .to_acls() - builder.from_junction(num_terminals=1) \ - .to_acls() + 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') \ + .connect(f'junc-{i}', f'acls-{i}-bottom', 2, 1) - for i in range(250): - builder.to_junction(mrid=f'junc-{i}', num_terminals=3) \ - .to_acls(mrid=f'acls-{i}-top') \ - .from_acls(mrid=f'acls-{i}-bottom') \ - .connect(f'junc-{i}', f'acls-{i}-bottom', 2, 1) + await Tracing.network_trace_branching().run(network['j0'].get_terminal_by_sn(1)) - await Tracing.network_trace_branching().run(network['j0'].get_terminal_by_sn(1)) \ No newline at end of file + except Exception as e: + sys.setrecursionlimit(1000) # back to default + raise e From dc7870a7ab41f83613393e1bef0d3b9764ab49cd Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Mon, 19 May 2025 17:46:52 +1000 Subject: [PATCH 03/47] Add step path provider, and some tests also polished off a few other bits and pieces Signed-off-by: Max Chesterfield --- .../sqlite/network/network_cim_reader.py | 2 +- .../model/cim/iec61970/base/core/terminal.py | 2 +- .../cim/iec61970/base/wires/aclinesegment.py | 1 + .../model/cim/iec61970/base/wires/clamp.py | 2 + .../model/cim/iec61970/base/wires/cut.py | 2 + .../networktrace/network_trace_step.py | 42 ++- .../network_trace_step_path_provider.py | 246 ++++++++++++++++++ src/zepben/evolve/types.py | 5 +- .../test_network_trace_step_path_provider.py | 163 ++++++++++++ 9 files changed, 448 insertions(+), 17 deletions(-) create mode 100644 src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py create mode 100644 test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py diff --git a/src/zepben/evolve/database/sqlite/network/network_cim_reader.py b/src/zepben/evolve/database/sqlite/network/network_cim_reader.py index e00bbe5c1..daccaa0e7 100644 --- a/src/zepben/evolve/database/sqlite/network/network_cim_reader.py +++ b/src/zepben/evolve/database/sqlite/network/network_cim_reader.py @@ -1,4 +1,4 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # 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/. diff --git a/src/zepben/evolve/model/cim/iec61970/base/core/terminal.py b/src/zepben/evolve/model/cim/iec61970/base/core/terminal.py index b903a7091..8063dfdcc 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/evolve/model/cim/iec61970/base/core/terminal.py @@ -174,6 +174,6 @@ def is_feeder_head_terminal(self): def has_connected_busbars(self): try: - return any(it != self and it.conducting_equipment is BusbarSection for it in self.connectivity_node.terminals) == True + return any(it != self and isinstance(it.conducting_equipment, BusbarSection) for it in self.connectivity_node.terminals) == True except AttributeError: return False diff --git a/src/zepben/evolve/model/cim/iec61970/base/wires/aclinesegment.py b/src/zepben/evolve/model/cim/iec61970/base/wires/aclinesegment.py index dca7c7ec5..bf3677d31 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/wires/aclinesegment.py +++ b/src/zepben/evolve/model/cim/iec61970/base/wires/aclinesegment.py @@ -64,6 +64,7 @@ class AcLineSegment(Conductor): However, boundary lines may have slightly different BaseVoltage.nominalVoltages and variation is allowed. Larger voltage difference in general requires use of an equivalent branch. """ + max_terminals = 2 per_length_impedance: Optional[PerLengthImpedance] = None """A `zepben.evolve.PerLengthImpedance` describing this AcLineSegment""" diff --git a/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py b/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py index 92d6597a1..4f9f7b18e 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py @@ -25,3 +25,5 @@ class Clamp(ConductingEquipment): ac_line_segment: Optional[AcLineSegment] = None """The line segment to which the clamp is connected.""" + + max_terminals = 1 diff --git a/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py b/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py index 5ee09bc75..0ac3f06e4 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py @@ -24,6 +24,8 @@ class Cut(Switch): be connected at them. """ + max_terminals = 2 + length_from_terminal_1: Optional[float] = None """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" 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 2fb4d9e87..01b02ea5e 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,14 +6,15 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Set, Generic, TypeVar, TYPE_CHECKING, List +from typing import Set, Generic, TypeVar, TYPE_CHECKING -from zepben.evolve import SinglePhaseKind from zepben.evolve.services.network.tracing.connectivity.nominal_phase_path import NominalPhasePath if TYPE_CHECKING: from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment + from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment + from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind T = TypeVar('T') @@ -33,20 +34,21 @@ class Path: """ Represents the path taken in a network trace step, detailing the transition from one terminal to another. - A limitation of the network trace is that all terminals must have associated conducting equipment. This means that if the [fromTerminal] - or [toTerminal] have `null` conducting equipment an [IllegalStateException] will be thrown. + A limitation of the network trace is that all terminals must have associated conducting equipment. This means that if the `from_terminal` + or `to_terminal` have `None` conducting equipment an [IllegalStateException] will be thrown. - `fromTerminal` The terminal that was stepped from. - `toTerminal` The terminal that was stepped to. - `nominalPhasePaths` A list of nominal phase paths traced in this step. If this is empty, phases have been ignored. - `fromEquipment` The conducting equipment associated with the [fromTerminal]. - `toEquipment` The conducting equipment associated with the [toTerminal]. - `tracedInternally` `true` if the from and to terminals belong to the same equipment; `false` otherwise. - `tracedExternally` `true` if the from and to terminals belong to different equipment; `false` otherwise. + No validation is done on the `traversed_ac_line_segment` against the `from_terminal` and `to_terminal`. It assumes the creator knows what they are doing + and thus avoids the overhead of validation as this class will have lots if instances created as part of a [NetworkTrace]. + + :param from_terminal: The terminal that was stepped from. + :param to_terminal: The terminal that was stepped to. + :param traversed_ac_line_segment: If the from_terminal and to_terminal path was via an `AcLineSegment`, this is the segment that was traversed + :param nominal_phase_paths: A list of nominal phase paths traced in this step. If this is empty, phases have been ignored. """ from_terminal: Terminal to_terminal: Terminal - nominal_phase_paths: List[NominalPhasePath] = field(default_factory=list) + traversed_ac_line_segment: AcLineSegment = field(default=None) + nominal_phase_paths: Set[NominalPhasePath] = field(default_factory=set) def to_phases_set(self) -> Set[SinglePhaseKind]: if len(self.nominal_phase_paths) == 0: @@ -56,6 +58,9 @@ def to_phases_set(self) -> Set[SinglePhaseKind]: @property def from_equipment(self) -> ConductingEquipment: + """ + The conducting equipment associated with `self.from_terminal`. + """ ce = self.from_terminal.conducting_equipment if not ce: raise AttributeError("Network trace does not support terminals that do not have conducting equipment") @@ -63,6 +68,9 @@ def from_equipment(self) -> ConductingEquipment: @property def to_equipment(self) -> ConductingEquipment: + """ + The conducting equipment associated with `self.to_terminal`. + """ ce = self.to_terminal.conducting_equipment if not ce: raise AttributeError("Network trace does not support terminals that do not have conducting equipment") @@ -70,12 +78,22 @@ def to_equipment(self) -> ConductingEquipment: @property def traced_internally(self) -> bool: + """ + `True` if the from and to terminals belong to the same equipment; `False` otherwise. + """ return self.from_equipment == self.to_equipment @property def traced_externally(self) -> bool: + """ + `True` if the from and to terminals belong to different equipment; `False` otherwise. + """ return not self.traced_internally + @property + def did_traverse_ac_line_segment(self) -> bool: + return self.traversed_ac_line_segment is not None + Type = Enum('Type', ('ALL', 'INTERNAL', 'EXTERNAL')) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py new file mode 100644 index 000000000..3593e612c --- /dev/null +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -0,0 +1,246 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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/. + +import itertools +import sys +from typing import Generator, Optional, Callable, Iterable + +from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp +from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection +from zepben.evolve.model.cim.iec61970.base.wires.cut import Cut +from zepben.evolve.services.network.tracing.connectivity.terminal_connectivity_connected import TerminalConnectivityConnected +from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal +from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep +from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import InServiceStateOperators +from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment + +PathFactory = Callable[[Terminal, AcLineSegment], Optional[NetworkTraceStep.Path]] + + + +class NetworkTraceStepPathProvider: + def __init__(self, state_operators: InServiceStateOperators): + self.state_operators = state_operators + + def next_paths(self, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: + path_factory = self._create_path_with_phases_factory(path) if path.nominal_phase_paths else self._create_path_factory(path) + + def _get_next_paths(): + to_equipment = path.to_equipment + if isinstance(to_equipment, AcLineSegment): + return self._next_paths_from_ac_line_segment(to_equipment, path, path_factory) + elif isinstance(to_equipment, BusbarSection): + return self._next_paths_from_busbar(path, path_factory) + elif isinstance(to_equipment, Clamp): + return self._next_paths_from_clamp(to_equipment, path, path_factory) + elif isinstance(to_equipment, Cut): + return self._next_paths_from_cut(to_equipment, path, path_factory) + else: + if path.traced_internally: + return self._next_external_paths(path, path_factory) + else: + return seq_term_map_to_path(path.to_terminal.other_terminals(), path_factory) + + return (p for p in _get_next_paths() if p and self.state_operators.is_in_service(p.to_terminal.conducting_equipment)) + + def _create_path_factory(self, path: NetworkTraceStep.Path) -> PathFactory: + def path_factory(next_terminal: Terminal, traversed: AcLineSegment): + return NetworkTraceStep.Path(path.to_terminal, next_terminal, traversed) + return path_factory + + def _create_path_with_phases_factory(self, path: NetworkTraceStep.Path) -> PathFactory: + phase_paths = set(p.to_phase for p in path.nominal_phase_paths) + next_from_terminal = path.to_terminal + + def path_factory(next_terminal: Terminal, traversed: AcLineSegment): + next_paths = TerminalConnectivityConnected().terminal_connectivity(next_from_terminal, next_terminal, phase_paths) + if next_paths.nominal_phase_paths: + return NetworkTraceStep.Path(next_from_terminal, next_terminal, traversed, set(next_paths.nominal_phase_paths)) + return path_factory + + def _next_paths_from_ac_line_segment(self, segment: AcLineSegment, path: NetworkTraceStep.Path, path_factory: PathFactory + ) -> Generator[NetworkTraceStep.Path, None, None]: + #If the current path traversed the segment, we need to step externally from the segment terminal. + #Otherwise, we traverse the segment + if path.traced_internally or path.did_traverse_ac_line_segment: + return self._next_external_paths(path, path_factory) + else: + if path.to_terminal.sequence_number == 1: + return self._acls_traverse_from_terminal(segment, + path.to_terminal, + length_from_T1=0.0, + towards_segment_T2=True, + can_stop_at_cut_at_same_position=True, + cut_at_same_position_from_terminal_number=1, + path_factory=path_factory) + else: + return self._acls_traverse_from_terminal(segment, + path.to_terminal, + length_from_T1=acls_length_or_max(segment), + towards_segment_T2=False, + can_stop_at_cut_at_same_position=True, + cut_at_same_position_from_terminal_number=2, + path_factory=path_factory) + + def _next_paths_from_busbar(self, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: + return seq_term_map_to_path((t for t in path.to_terminal.connected_terminals() + # We don't go back to the terminal we came from as we already visited it to get to this busbar. + if t != path.from_terminal + # We don't step to terminals that are busbars as they would have been returned at the same time this busbar step was. + and not isinstance(t.conducting_equipment, BusbarSection)) + , path_factory) + + def _next_paths_from_clamp(self, clamp: Clamp, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Iterable[NetworkTraceStep.Path]: + # If the current path was from traversing an AcLineSegment, we need to step externally to other equipment. + # Otherwise, we need to traverse the segment both ways. + if path.did_traverse_ac_line_segment: + return self._next_external_paths(path, path_factory) + else: + # Because we consider clamps at the same position as a cut on the terminal 1 side, we do not stop at cuts at the same position when + # traversing towards t1, but we do when traversing towards t2. + if not clamp.ac_line_segment: + return set() + + next_paths_towards_T1 = self._acls_traverse_from_terminal(clamp.ac_line_segment, + path.to_terminal, + length_from_T1=clamp.length_from_terminal_1 or 0.0, + towards_segment_T2=False, + can_stop_at_cut_at_same_position=False, + cut_at_same_position_from_terminal_number=1, + path_factory=path_factory) + + next_paths_towards_T2 = self._acls_traverse_from_terminal(clamp.ac_line_segment, + path.to_terminal, + length_from_T1=clamp.length_from_terminal_1 or 0.0, + towards_segment_T2=True, + can_stop_at_cut_at_same_position=True, + cut_at_same_position_from_terminal_number=1, + path_factory=path_factory) + + return set(itertools.chain(next_paths_towards_T1, next_paths_towards_T2)) + + def _next_paths_from_cut(self, cut: Cut, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Iterable[NetworkTraceStep.Path]: + # If the current path was from traversing an AcLineSegment, we need to step externally to other equipment. + # Else we need to traverse the segment. + next_terminals = ( + self._next_external_paths(path, path_factory) if path.did_traverse_ac_line_segment else + self._acls_traverse_from_terminal(cut.ac_line_segment, + path.to_terminal, + length_from_T1=cut.length_from_terminal_1 or 0.0, + towards_segment_T2=path.to_terminal.sequence_number != 1, + can_stop_at_cut_at_same_position=False, + cut_at_same_position_from_terminal_number=path.to_terminal.sequence_number, + path_factory=path_factory + ) if cut.ac_line_segment else [] + ) + # If the current path traced internally, we need to also return the external terminals + # Else we need to step internally to the Cut's other terminal. + if path.traced_internally: + # traversedAcLineSegment and tracedInternally should never both be true, so we should never get external terminals twice + return set(itertools.chain(next_terminals, self._next_external_paths(path, path_factory))) + else: + other_terminal = cut.get_terminal_by_sn(2 if path.to_terminal.sequence_number == 1 else 1) + return set(itertools.chain(next_terminals, seq_term_map_to_path(other_terminal, path_factory))) + + + def _next_external_paths(self, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: + #Busbars are only modelled with a single terminal. So if we find any we need to step to them before the + #other (non busbar) equipment connected to the same connectivity node. Once the busbar has been + #visited we then step to the other non busbar terminals connected to the same connectivity node. + #If there are no busbars we can just step to all other connected terminals. + if isinstance(path.to_equipment, BusbarSection): + return self._next_paths_from_busbar(path, path_factory) + elif path.to_terminal.has_connected_busbars(): + return seq_term_map_to_path((t for t in path.to_terminal.connected_terminals() if isinstance(t.conducting_equipment, BusbarSection)), path_factory) + else: + return seq_term_map_to_path(path.to_terminal.connected_terminals(), path_factory) + + def _acls_traverse_from_terminal(self, + acls: AcLineSegment, + from_terminal: Terminal, + length_from_T1: float, + towards_segment_T2: bool, + can_stop_at_cut_at_same_position: bool, + cut_at_same_position_from_terminal_number: int, + path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: + """ + This returns terminals found traversing along an AcLineSegment from any terminal "on" the segment. Terminals considered on the segment are any clamp + or cut terminals that belong to the segment as well as the segment's own terminals. When traversing the segment, the traversal stops + at and returns the next cut terminal found along the segment plus any clamp terminals it found between the fromTerminal and the cut terminal. + If there are no cuts on the segment the terminal, the other end of the segment is returned along with all clamp terminals. + To determine order of terminals on the segment, `lengthFromTerminal1` is used for cuts and clamps. When this property is null a default value of 0.0 is + assumed, effectively placing it at the start of the segment. Terminal 1 on the segment is deemed at 0.0 and Terminal 2 is deemed at + [AcLineSegment.length] or [Double.MAX_VALUE] if the length or the segment is `None`. + + This algorithm assumes AcLineSegments have exactly 2 terminals, cuts have exactly 2 terminals and clamps have exactly 1 terminal. + + If there is a cut and a clamp at the exact same length on the segment, it is assumed the clamp is on the terminal 1 side of the cut. This is so you do not + get the clamp twice when traversing a segment from one end to the other. As a clamp can't technically be in the exact same spot as a cut, you should + realistically model this either attaching the equipment attached by the clamp to the appropriate cut terminal, or, place a clamp at a length that is + not exactly the same as the cut. This would yield more accurate and deterministic behaviour. + + :param from_terminal: The terminal on the segment to traverse from. This could either be a segment terminal, or a terminal from any cut or clamp on the segment. + :param length_from_T1: The length from terminal 1 the fromTerminal is. + :param towards_segment_T2: Use `true` if the segment should be traversed towards terminal 2, otherwise `False` to traverse towards terminal 1 + """ + # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! + # We also make sure we filter out the cut or the clamp we are starting at, so we don't compare it in our checks + filter_func = lambda it: it != from_terminal.conducting_equipment and self.state_operators.is_in_service(it) + cuts = list(filter(filter_func, acls.cuts)) + clamps = list(filter(filter_func, acls.clamps)) + + # Can do a simple return if we don't need to do any special cuts/clamps processing + if not(any((cuts, clamps))): + yield from seq_term_map_to_path(from_terminal.other_terminals(), path_factory, acls) + + cuts_at_same_position = list(filter(lambda it: it.length_from_T1_or_0 == length_from_T1, cuts)) + stop_at_cuts_at_same_position = can_stop_at_cut_at_same_position and cuts_at_same_position + + def next_cut_length_from_terminal_1_func(): + cut_length_generator = (it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 > length_from_T1) + if stop_at_cuts_at_same_position: + return length_from_T1 + elif towards_segment_T2: + return min(cut_length_generator, default=None) + else: + return max(cut_length_generator, default=None) + + next_cut_length_from_terminal_1 = next_cut_length_from_terminal_1_func() + + next_cuts = [it for it in cuts if it.length_from_T1_or_0 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 else [] + + next_terminal_length_from_terminal_1 = next_cut_length_from_terminal_1 or acls_length_or_max(acls) if towards_segment_T2 else 0.0 + + def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: + if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_T2: + return lambda it: length_from_T1 < it.length_from_T1_or_0 < next_terminal_length_from_terminal_1 + elif towards_segment_T2: + return lambda it: length_from_T1 < it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 + elif next_terminal_length_from_terminal_1 == 0.0 and not next_cuts: + return lambda it: next_terminal_length_from_terminal_1 < it.length_from_T1_or_0 < length_from_T1 + else: + return lambda it: length_from_T1 >= it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 + clamps_before_next_terminal = filter(clamps_before_next_terminal_filter(), clamps) + + next_stop_terminals = [] if stop_at_cuts_at_same_position else ( + it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts + ) if next_cuts else (it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts) + + next_terminals = ( + (it.get_terminal(cut_at_same_position_from_terminal_number) for it in cuts_at_same_position), + (it.get_terminal(1) for it in clamps_before_next_terminal), + next_stop_terminals + ) + + for generator in next_terminals: + yield from seq_term_map_to_path(generator, path_factory, acls) + +def seq_term_map_to_path(terms: Iterable[Terminal], path_factory: PathFactory, traversed_acls: AcLineSegment=None + ) -> Generator[NetworkTraceStep.Path, None, None]: + for terminal in terms: + if terminal is not None: + yield path_factory(terminal, traversed_acls) + +acls_length_or_max = lambda acls: acls.length or sys.float_info.max \ No newline at end of file diff --git a/src/zepben/evolve/types.py b/src/zepben/evolve/types.py index e62d59ab7..bf5ccc9c6 100644 --- a/src/zepben/evolve/types.py +++ b/src/zepben/evolve/types.py @@ -14,8 +14,7 @@ T = TypeVar("T") -__all__ = ["OpenTest", "PhaseSelector", "DirectionSelector"] +__all__ = ["OpenTest"] OpenTest = Callable[[ConductingEquipment, Optional[SinglePhaseKind]], bool] -PhaseSelector = Callable[[Terminal], PhaseStatus] -DirectionSelector = Callable[[Terminal], DirectionStatus] + diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py new file mode 100644 index 000000000..149d71e91 --- /dev/null +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -0,0 +1,163 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 Generator, Tuple, Iterable + +from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode +from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind +from zepben.evolve.services.network.network_service import NetworkService +from zepben.evolve import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker +from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider + +Path = NetworkTraceStep.Path +SPK = SinglePhaseKind + +class TestNetworkTraceStepPathProvider: + path_provider = NetworkTraceStepPathProvider(NetworkStateOperators.NORMAL) + + def test_current_external_path_steps_internally(self): + # + # 2 + # 1--c0--2 1 j1 + # 3 + # + network = (TestNetworkBuilder() + .from_acls() # c0 + .to_junction(num_terminals=3) # j1 + ).network + + c0 = network['c0'] + j1 = network['j1'] + + current_path = Path(c0[2], j1[1]) + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (Path(j1[1], j1[2]), Path(j1[1], j1[3]))) + + def test_current_internal_path_steps_externally(self): + # + # 1 j0 21--c1--2 + # 1 + # c2 + # 2 + # + network = (TestNetworkBuilder() + .from_junction() # j0 + .to_acls() # c1 + .from_acls() # c2 + .connect('j0', 'c2', 2, 1) + ).network + + j0 = network['j0'] + c1 = network['c1'] + c2 = network['c2'] + + current_path = Path(j0[1], j0[2]) + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (Path(j0[2], c1[1]), Path(j0[2], c2[1]))) + + def test_only_steps_to_in_service_equipment(self): + # + # 1 j0 21--c1--2 + # 1 + # c2 (not in service) + # 2 + # + network = (TestNetworkBuilder() + .from_junction() # j0 + .to_acls() # c1 + .from_acls() # c2 + .connect('j0', 'c2', 2, 1) + ).network + + network['c2'].normally_in_service = False + + j0 = network['j0'] + c1 = network['c1'] + + current_path = Path(j0[1], j0[2]) + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (Path(j0[2], c1[1]), )) + + def test_only_includes_followed_phases(self): + # + # 2 (A) + # 1--c0--21 tx1 3 (B) + # 4 (C) + # + network = (TestNetworkBuilder() + .from_acls() # c0 + .to_power_transformer([PhaseCode.ABC, PhaseCode.A, PhaseCode.B, PhaseCode.C]) + ).network + + c0 = network['c0'] + tx1 = network['tx1'] + + current_path = Path(c0[2], tx1[1], None, {NominalPhasePath(SPK.A, SPK.A), NominalPhasePath(SPK.B, SPK.B)}) + next_paths = self.path_provider.next_paths(current_path) + + # Should not contain tx1-t4 because its not in the phase paths + _verify_paths(next_paths, [ + Path(tx1[1], tx1[2], None, {NominalPhasePath(SPK.A, SPK.A)}), + Path(tx1[1], tx1[3], None, {NominalPhasePath(SPK.B, SPK.B)})]) + + def test_stepping_externally_to_connectivity_node_with_busbars_only_goes_to_busbars(self): + network = self._busbar_network() + + b0 = network['b0'] + bbs1 = network['bbs1'] + bbs2 = network['bbs2'] + + current_path = Path(b0[1], b0[2]) + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (Path(b0[2], bbs1[1]), Path(b0[2], bbs2[1]))) + + def _busbar_network(self) -> NetworkService: + # 1 + # b0 + # bbs1 1-2-1 bbs2 + # -----| |----- + # 1 1 1 1 + # b3 b4 b5 b6 + # 2 2 2 2 + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_busbar_section() # bbs1 + .branch_from('b0', 2) + .to_busbar_section() # bbs2 + .branch_from('bbs1', 1) + .to_breaker() # b3 + .branch_from('bbs1', 1) + .to_breaker() # b4 + .branch_from('bbs2', 1) + .to_breaker() # b5 + .branch_from('bbs2', 1) + .to_breaker() # b6 + ).network + + bbs1 = network['bbs1'] + bbs2 = network['bbs2'] + b0: Breaker = network['b0'] + b3 = network['b3'] + b4 = network['b4'] + b5 = network['b5'] + b6 = network['b6'] + + b0_terms = list(b0[2].connectivity_node.terminals) + for term in (b0[2], bbs1[1], bbs2[1], b3[1], b4[1], b5[1], b6[1]): + assert term in b0_terms + + return network + + +def _verify_paths(in_paths: Generator[NetworkTraceStep.Path, None, None], in_expected: Iterable[Path], check_length=True): + paths = list(in_paths) + expected = list(in_expected) + for path in expected: + assert path in paths + if check_length: + assert len(paths) == len(expected) From 7c4b0337a31f825e59305b5eb0a79ff700627be7 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Mon, 19 May 2025 17:47:20 +1000 Subject: [PATCH 04/47] be helpful if the TestNetworkBuilder knew what a Busbar was Signed-off-by: Max Chesterfield --- .../evolve/testing/test_network_builder.py | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/zepben/evolve/testing/test_network_builder.py b/src/zepben/evolve/testing/test_network_builder.py index 1ef758be3..67a2311fe 100644 --- a/src/zepben/evolve/testing/test_network_builder.py +++ b/src/zepben/evolve/testing/test_network_builder.py @@ -13,7 +13,7 @@ from zepben.evolve import ConductingEquipment, NetworkService, PhaseCode, EnergySource, AcLineSegment, Breaker, Junction, Terminal, Feeder, LvFeeder, \ PowerTransformerEnd, PowerTransformer, EnergyConsumer, \ - PowerElectronicsConnection + PowerElectronicsConnection, BusbarSection def null_action(_): @@ -343,6 +343,51 @@ def to_energy_consumer( self._current = it return self + def from_busbar_section( + self, + nominal_phases: PhaseCode=PhaseCode.ABC, + mrid: str=None, + action: Callable[[BusbarSection], None]=null_action + ) -> 'TestNetworkBuilder': + """ + Start a new network island from a `BusbarSection`, updating the network pointer to the new `BusbarSection`. + + :param nominal_phases: The nominal phases for the new `BusbarSection`. + :param mrid: Optional mRID for the new `BusbarSection`. + :param action: An action that accepts the new `BusbarSection` to allow for additional initialisation. + + :return: This `TestNetworkBuilder` to allow for fluent use. + """ + it = self._create_busbar_section(mrid, nominal_phases) + action(it) + self._current = it + return self + + def to_busbar_section( + self, + nominal_phases: PhaseCode = PhaseCode.ABC, + mrid: str = None, + connectivity_node_mrid: Optional[str] = None, + action: Callable[[BusbarSection], None] = null_action + ) -> 'TestNetworkBuilder': + """ + + Add a new `BusbarSection` to the network and connect it to the current network pointer, updating the network pointer to the new `BusbarSection`. + + :param nominal_phases: The nominal phases for the new `BusbarSection`. + :param mrid: Optional mRID for the new `BusbarSection`. + :param connectivity_node_mrid: Optional id of the connectivity node used to connect this `BusbarSection` to the previous item. Will only be used + if the previous item is not already connected. + :param action: An action that accepts the new `BusbarSection` to allow for additional initialisation. + + :return: This `TestNetworkBuilder` to allow for fluent use. + """ + it = self._create_busbar_section(mrid, nominal_phases) + self._connect(self._current, it, connectivity_node_mrid) + action(it) + self._current = it + return self + def from_other( self, creator: Union[OtherCreator, Type[ConductingEquipment]], @@ -555,6 +600,13 @@ def _create_junction(self, mrid: Optional[str], nominal_phases: PhaseCode, num_t self.network.add(j) return j + def _create_busbar_section(self, mrid: Optional[str], nominal_phases: PhaseCode) -> BusbarSection: + b = BusbarSection(mrid=self._next_id(mrid, 'bbs')) + self._add_terminal(b, 1, nominal_phases) + + self.network.add(b) + return b + def _create_power_electronics_connection(self, mrid: Optional[str], nominal_phases: PhaseCode, num_terminals: Optional[int]) -> PowerElectronicsConnection: pec = PowerElectronicsConnection(mrid=self._next_id(mrid, "pec")) for i in range(1, (num_terminals if num_terminals is not None else 2) + 1): From 3b69b626c8105a32e72c47787715d0588f2c2113 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 20 May 2025 15:35:15 +1000 Subject: [PATCH 05/47] stashing changes Signed-off-by: Max Chesterfield --- .../base/core/conducting_equipment.py | 31 ++++- .../model/cim/iec61970/base/wires/cut.py | 2 +- .../services/network/network_service.py | 25 +++- .../network_trace_step_path_provider.py | 64 ++++----- .../test_network_trace_step_path_provider.py | 126 +++++++++++++++++- 5 files changed, 200 insertions(+), 48 deletions(-) diff --git a/src/zepben/evolve/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/evolve/model/cim/iec61970/base/core/conducting_equipment.py index 7e58e84eb..66cc722f1 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/evolve/model/cim/iec61970/base/core/conducting_equipment.py @@ -6,7 +6,7 @@ from __future__ import annotations import sys -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, Generator, TYPE_CHECKING, Union from zepben.evolve.model.cim.iec61970.base.core.base_voltage import BaseVoltage from zepben.evolve.model.cim.iec61970.base.core.equipment import Equipment @@ -80,12 +80,31 @@ def num_terminals(self): """ return len(self._terminals) + def get_terminal(self, identifier: Union[int, str]): + """ + Get the `Terminal` for this `ConductingEquipment` identified by `mrid` or `sequence_number` + + :param identifier: the mRID of the required `Terminal`, or the `sequence_number` of the terminal in relation + to this `ConductingEquipment` + :return: The `Terminal` with the specified `mrid` if it exists + + Raises `KeyError` if `mrid` wasn't present. + Raises `TypeError` if the identifier wasn't a recognised type + """ + if isinstance(identifier, int): + return self.get_terminal_by_sn(identifier) + elif isinstance(identifier, str): + return self.get_terminal_by_mrid(identifier) + raise TypeError(f'`identifier` parameter not a recognised type: {type(identifier)}') + def get_terminal_by_mrid(self, mrid: str) -> Terminal: """ Get the `Terminal` for this `ConductingEquipment` identified by `mrid` - `mrid` the mRID of the required `Terminal` - Returns The `Terminal` with the specified `mrid` if it exists + :param mrid: the mRID of the required `Terminal` + + :return: The `Terminal` with the specified `mrid` if it exists + Raises `KeyError` if `mrid` wasn't present. """ return get_by_mrid(self._terminals, mrid) @@ -94,8 +113,10 @@ def get_terminal_by_sn(self, sequence_number: int): """ Get the `Terminal` on this `ConductingEquipment` by its `sequence_number`. - `sequence_number` The `sequence_number` of the `Terminal` in relation to this `ConductingEquipment`. - Returns The `Terminal` on this `ConductingEquipment` with sequence number `sequence_number` + :param sequence_number: The `sequence_number` of the `Terminal` in relation to this `ConductingEquipment`. + + :return: The `Terminal` on this `ConductingEquipment` with sequence number `sequence_number` + Raises IndexError if no `Terminal` was found with sequence_number `sequence_number`. """ for term in self._terminals: diff --git a/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py b/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py index 0ac3f06e4..5a46816d4 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py @@ -26,7 +26,7 @@ class Cut(Switch): max_terminals = 2 - length_from_terminal_1: Optional[float] = None + length_from_terminal_1: Optional[float] = 0.0 """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" ac_line_segment: Optional[AcLineSegment] = None diff --git a/src/zepben/evolve/services/network/network_service.py b/src/zepben/evolve/services/network/network_service.py index abaaf38ab..06d25a88b 100644 --- a/src/zepben/evolve/services/network/network_service.py +++ b/src/zepben/evolve/services/network/network_service.py @@ -18,6 +18,7 @@ from zepben.evolve.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.evolve.model.cim.iec61970.infiec61970.feeder.lv_feeder import LvFeeder from zepben.evolve.model.cim.iec61970.base.core.equipment_container import Feeder +from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal from zepben.evolve.model.cim.iec61970.base.core.connectivity_node import ConnectivityNode from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode @@ -26,7 +27,7 @@ from zepben.evolve.services.network.tracing.connectivity.terminal_connectivity_connected import TerminalConnectivityConnected if TYPE_CHECKING: - from zepben.evolve import Terminal, SinglePhaseKind, ConnectivityResult, Measurement, ConductingEquipment + from zepben.evolve import SinglePhaseKind, ConnectivityResult, Measurement, ConductingEquipment logger = logging.getLogger(__name__) TRACED_NETWORK_FILE = str(Path.home().joinpath(Path("traced.json"))) @@ -153,13 +154,26 @@ def remove_measurement(self, measurement) -> bool: self._remove_measurement_index(measurement) return self.remove(measurement) + def connect(self, terminal: Terminal, to: Union[str, Terminal]) -> bool: + """ + Connect a `Terminal` to either a `Terminal` or `ConnectivityNode` depending on the type of `to` + + :return: the boolean result of the action + """ + if isinstance(to, Terminal): + return self.connect_terminals(terminal, to) + elif isinstance(to, str): + return self.connect_by_mrid(terminal, to) + else: + raise TypeError(f'to parameter not a recognised type: {type(to)}') + def connect_by_mrid(self, terminal: Terminal, connectivity_node_mrid: str) -> bool: """ Connect a `Terminal` to the `ConnectivityNode` with mRID `connectivity_node_mrid` `terminal` The `Terminal` to connect. `connectivity_node_mrid` The mRID of the `ConnectivityNode`. Will be created in the `Network` if it doesn't already exist. - Returns True if the connection was made or already existed, False if `Terminal` was already connected to a + :return: `True` if the connection was made or already existed, `False` if `Terminal` was already connected to a different `ConnectivityNode` """ if not connectivity_node_mrid: @@ -175,7 +189,7 @@ def connect_by_mrid(self, terminal: Terminal, connectivity_node_mrid: str) -> bo def connect_terminals(self, terminal1: Terminal, terminal2: Terminal) -> bool: """ Connect two `Terminal`s - Returns True if the `Terminal`s could be connected, False otherwise. + :return: `True` if the `Terminal` could be connected, `False` otherwise. """ status = _attempt_to_reuse_connection(terminal1, terminal2) if status == ProcessStatus.PROCESSED: @@ -200,7 +214,7 @@ def disconnect(self, terminal: Terminal): """ Disconnect a `Terminal`` from its `ConnectivityNode`. Will also remove the `ConnectivityNode` from this `Network` if it no longer has any terminals. - `terminal` The `Terminal` to disconnect. + :param terminal: The `Terminal` to disconnect. """ cn = terminal.connectivity_node if cn is None: @@ -214,7 +228,8 @@ def disconnect_by_mrid(self, connectivity_node_mrid: str): """ Disconnect a `ConnectivityNode` from this `Network`. Will disconnect all ``Terminal`s from the `ConnectivityNode` - `connectivity_node_mrid` The mRID of the `ConnectivityNode` to disconnect. + :param connectivity_node_mrid: The mRID of the `ConnectivityNode` to disconnect. + Raises `KeyError` if there is no `ConnectivityNode` for `connectivity_node_mrid` """ cn = self._connectivity_nodes[connectivity_node_mrid] diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index 3593e612c..40477f1f4 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -5,7 +5,7 @@ import itertools import sys -from typing import Generator, Optional, Callable, Iterable +from typing import Generator, Optional, Callable, Iterable, List from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection @@ -105,7 +105,7 @@ def _next_paths_from_clamp(self, clamp: Clamp, path: NetworkTraceStep.Path, path next_paths_towards_T1 = self._acls_traverse_from_terminal(clamp.ac_line_segment, path.to_terminal, - length_from_T1=clamp.length_from_terminal_1 or 0.0, + length_from_T1=clamp.length_from_terminal_1, towards_segment_T2=False, can_stop_at_cut_at_same_position=False, cut_at_same_position_from_terminal_number=1, @@ -113,7 +113,7 @@ def _next_paths_from_clamp(self, clamp: Clamp, path: NetworkTraceStep.Path, path next_paths_towards_T2 = self._acls_traverse_from_terminal(clamp.ac_line_segment, path.to_terminal, - length_from_T1=clamp.length_from_terminal_1 or 0.0, + length_from_T1=clamp.length_from_terminal_1, towards_segment_T2=True, can_stop_at_cut_at_same_position=True, cut_at_same_position_from_terminal_number=1, @@ -128,7 +128,7 @@ def _next_paths_from_cut(self, cut: Cut, path: NetworkTraceStep.Path, path_facto self._next_external_paths(path, path_factory) if path.did_traverse_ac_line_segment else self._acls_traverse_from_terminal(cut.ac_line_segment, path.to_terminal, - length_from_T1=cut.length_from_terminal_1 or 0.0, + length_from_T1=cut.length_from_terminal_1, towards_segment_T2=path.to_terminal.sequence_number != 1, can_stop_at_cut_at_same_position=False, cut_at_same_position_from_terminal_number=path.to_terminal.sequence_number, @@ -157,14 +157,16 @@ def _next_external_paths(self, path: NetworkTraceStep.Path, path_factory: PathFa else: return seq_term_map_to_path(path.to_terminal.connected_terminals(), path_factory) - def _acls_traverse_from_terminal(self, - acls: AcLineSegment, - from_terminal: Terminal, - length_from_T1: float, - towards_segment_T2: bool, - can_stop_at_cut_at_same_position: bool, - cut_at_same_position_from_terminal_number: int, - path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: + def _acls_traverse_from_terminal( + self, + acls: AcLineSegment, + from_terminal: Terminal, + length_from_T1: float, + towards_segment_T2: bool, + can_stop_at_cut_at_same_position: bool, + cut_at_same_position_from_terminal_number: int, + path_factory: PathFactory + ) -> Generator[NetworkTraceStep.Path, None, None]: """ This returns terminals found traversing along an AcLineSegment from any terminal "on" the segment. Terminals considered on the segment are any clamp or cut terminals that belong to the segment as well as the segment's own terminals. When traversing the segment, the traversal stops @@ -185,21 +187,21 @@ def _acls_traverse_from_terminal(self, :param length_from_T1: The length from terminal 1 the fromTerminal is. :param towards_segment_T2: Use `true` if the segment should be traversed towards terminal 2, otherwise `False` to traverse towards terminal 1 """ + # Can do a simple return if we don't need to do any special cuts/clamps processing + if not(any((acls.cuts, acls.clamps))): + yield from seq_term_map_to_path(from_terminal.other_terminals(), path_factory, acls) + # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! # We also make sure we filter out the cut or the clamp we are starting at, so we don't compare it in our checks filter_func = lambda it: it != from_terminal.conducting_equipment and self.state_operators.is_in_service(it) - cuts = list(filter(filter_func, acls.cuts)) - clamps = list(filter(filter_func, acls.clamps)) + cuts: List[Cut] = list(filter(filter_func, acls.cuts)) + clamps: List[Clamp] = list(filter(filter_func, acls.clamps)) - # Can do a simple return if we don't need to do any special cuts/clamps processing - if not(any((cuts, clamps))): - yield from seq_term_map_to_path(from_terminal.other_terminals(), path_factory, acls) - - cuts_at_same_position = list(filter(lambda it: it.length_from_T1_or_0 == length_from_T1, cuts)) - stop_at_cuts_at_same_position = can_stop_at_cut_at_same_position and cuts_at_same_position + cuts_at_same_position = list(filter(lambda it: it.length_from_terminal_1 == length_from_T1, cuts)) + stop_at_cuts_at_same_position = bool(can_stop_at_cut_at_same_position and cuts_at_same_position) def next_cut_length_from_terminal_1_func(): - cut_length_generator = (it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 > length_from_T1) + cut_length_generator = (it.length_from_terminal_1 for it in cuts if it.length_from_terminal_1 > length_from_T1) if stop_at_cuts_at_same_position: return length_from_T1 elif towards_segment_T2: @@ -209,24 +211,26 @@ def next_cut_length_from_terminal_1_func(): next_cut_length_from_terminal_1 = next_cut_length_from_terminal_1_func() - next_cuts = [it for it in cuts if it.length_from_T1_or_0 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 else [] + next_cuts = [it for it in cuts if it.length_from_terminal_1 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 else [] - next_terminal_length_from_terminal_1 = next_cut_length_from_terminal_1 or acls_length_or_max(acls) if towards_segment_T2 else 0.0 + next_terminal_length_from_terminal_1 = next_cut_length_from_terminal_1 or (acls_length_or_max(acls) if towards_segment_T2 else 0.0) def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_T2: - return lambda it: length_from_T1 < it.length_from_T1_or_0 < next_terminal_length_from_terminal_1 + return lambda it: length_from_T1 <= it.length_from_terminal_1 <= next_terminal_length_from_terminal_1 elif towards_segment_T2: - return lambda it: length_from_T1 < it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 - elif next_terminal_length_from_terminal_1 == 0.0 and not next_cuts: - return lambda it: next_terminal_length_from_terminal_1 < it.length_from_T1_or_0 < length_from_T1 + return lambda it: it.length_from_terminal_1 > length_from_T1 and it.length_from_terminal_1 <= next_terminal_length_from_terminal_1 + elif (next_terminal_length_from_terminal_1 == 0.0) and not len(next_cuts) == 0: + return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_terminal_1 <= length_from_T1 else: - return lambda it: length_from_T1 >= it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 - clamps_before_next_terminal = filter(clamps_before_next_terminal_filter(), clamps) + return lambda it: it.length_from_terminal_1 <= length_from_T1 and it.length_from_terminal_1 > next_terminal_length_from_terminal_1 + _filter = clamps_before_next_terminal_filter() + + clamps_before_next_terminal = filter(_filter, clamps) next_stop_terminals = [] if stop_at_cuts_at_same_position else ( it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts - ) if next_cuts else (it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts) + ) if next_cuts else list(acls.get_terminal(2 if towards_segment_T2 else 1)) next_terminals = ( (it.get_terminal(cut_at_same_position_from_terminal_number) for it in cuts_at_same_position), diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 149d71e91..4f6e46114 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -7,7 +7,8 @@ from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.evolve.services.network.network_service import NetworkService -from zepben.evolve import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker +from zepben.evolve import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker, AcLineSegment, Clamp, Cut, \ + ConductingEquipment from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider Path = NetworkTraceStep.Path @@ -116,6 +117,34 @@ def test_stepping_externally_to_connectivity_node_with_busbars_only_goes_to_busb _verify_paths(next_paths, (Path(b0[2], bbs1[1]), Path(b0[2], bbs2[1]))) + def test_steppiong_externally_from_busbars_does_not_step_to_busbars_or_original_from_terminal(self): + network = self._busbar_network() + + bbs1 = network['bbs1'] + b0= network['b0'] + b3 = network['b3'] + b4 = network['b4'] + b5 = network['b5'] + b6 = network['b6'] + + current_path = Path(b0[2], bbs1[1]) + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (Path(bbs1[1], b3[1]), Path(bbs1[1], b4[1]), Path(bbs1[1], b5[1]), Path(bbs1[1], b6[1]))) + + def test_traversing_segment_with_clamps_from_t1_includes_all_clamp_steps(self): + network = self._acls_with_clamps_network() + + breaker = network['b0'] + segment: AcLineSegment = network['c1'] + clamp1 = network['c1-clamp1'] + clamp2 = network['c1-clamp2'] + + current_path = Path(breaker[2], segment[1]) + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (Path(segment[1], clamp1[1]), Path(segment[1], clamp2[1]), Path(segment[1], segment[2]))) + def _busbar_network(self) -> NetworkService: # 1 # b0 @@ -153,11 +182,94 @@ def _busbar_network(self) -> NetworkService: return network + def _acls_with_clamps_network(self): + # + # clamp1 + # 1 + # 1 b0 21 ---*---c1---*---21 b2 2 + # 1 + # clamp2 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_breaker() # b2 + ).network + segment: AcLineSegment = network['c1'] + + _segment_with_clamp(network, segment, 1.0) + _segment_with_clamp(network, segment, 2.0) + + return network + + def _acls_with_clamps_and_cuts_network(self): + # + # 2 2 + # c3 2 c7 2 + # 1 c5 1 c9 + # 1 clamp1 1 1 clamp3 1 + # | | | | + # 1 b0 21--*--*1 cut1 2*--*--c1--*--*1 cut2 2*--*--21 b2 2 + # | | | | + # 1 1 clamp2 1 1 clamp4 + # c4 1 c8 1 + # 2 c6 2 c10 + # 2 2 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_breaker() # b2 + .to_acls() # c3 + .to_acls() # c4 + .to_acls() # c5 + .to_acls() # c6 + .to_acls() # c7 + .to_acls() # c8 + .to_acls() # c9 + .to_acls() # c10 + ).network + + segment: AcLineSegment = network['c1'] + + clamp1 = _segment_with_clamp(network, segment, 1.0) + cut1 = _segment_with_cut(network, segment, 2.0) + clamp2 = _segment_with_clamp(network, segment, 3.0) + clamp3 = _segment_with_clamp(network, segment, 4.0) + cut2 = _segment_with_cut(network, segment, 5.0) + clamp4 = _segment_with_clamp(network, segment, 6.0) + + network.connect(clamp1[1], network.get('c3', ConductingEquipment)[1]) + network.connect(cut1[1], network.get('c4', ConductingEquipment)[1]) + network.connect(cut1[2], network.get('c5', ConductingEquipment)[1]) + network.connect(clamp2[1], network.get('c6', ConductingEquipment)[1]) + network.connect(clamp3[1], network.get('c7', ConductingEquipment)[1]) + network.connect(cut2[1], network.get('c8', ConductingEquipment)[1]) + network.connect(cut2[2], network.get('c9', ConductingEquipment)[1]) + network.connect(clamp4[1], network.get('c10', ConductingEquipment)[1]) + + return network + +def _segment_with_clamp(network: NetworkService, segment: AcLineSegment, length_from_terminal1: float) -> Clamp: + clamp = Clamp(mrid=f'clamp{segment.num_clamps() + 1}') + clamp.add_terminal(Terminal(mrid=f'{clamp.mrid}-t1')) + clamp.length_from_terminal_1 = length_from_terminal1 + + segment.add_clamp(clamp) + network.add(clamp) + return clamp + +def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_from_terminal1: float) -> Cut: + cut = Cut(mrid=f'cut{segment.num_cuts() + 1}', length_from_terminal_1=length_from_terminal1) + cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t1')) + cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t2')) + + segment.add_cut(cut) + network.add(cut) + return cut + def _verify_paths(in_paths: Generator[NetworkTraceStep.Path, None, None], in_expected: Iterable[Path], check_length=True): - paths = list(in_paths) - expected = list(in_expected) - for path in expected: - assert path in paths - if check_length: - assert len(paths) == len(expected) + paths = sorted(list(in_paths), key=lambda p: (p.from_terminal, p.to_terminal)) + expected = sorted(in_expected, key=lambda p: (p.from_terminal, p.to_terminal)) + assert paths == expected From 9b9ce0d1f189fdfc17aa15141c9baae46bfb260e Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 21 May 2025 00:18:12 +1000 Subject: [PATCH 06/47] NetworkTraceStepPathProvider seems to do the needful Signed-off-by: Max Chesterfield --- .../model/cim/iec61970/base/wires/clamp.py | 4 + .../model/cim/iec61970/base/wires/cut.py | 6 +- .../networktrace/network_trace_step.py | 2 - .../network_trace_step_path_provider.py | 64 +- test/cim/cim_creators.py | 14 +- .../test_network_trace_step_path_provider.py | 578 +++++++++++++++++- 6 files changed, 601 insertions(+), 67 deletions(-) diff --git a/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py b/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py index 4f9f7b18e..8d0a0c915 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py @@ -27,3 +27,7 @@ class Clamp(ConductingEquipment): """The line segment to which the clamp is connected.""" max_terminals = 1 + + @property + def length_from_T1_or_0(self) -> float: + return self.length_from_terminal_1 or 0.0 diff --git a/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py b/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py index 5a46816d4..149d98bb2 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/evolve/model/cim/iec61970/base/wires/cut.py @@ -26,8 +26,12 @@ class Cut(Switch): max_terminals = 2 - length_from_terminal_1: Optional[float] = 0.0 + length_from_terminal_1: Optional[float] = None """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" ac_line_segment: Optional[AcLineSegment] = None """The line segment to which the cut is applied.""" + + @property + def length_from_T1_or_0(self) -> float: + return self.length_from_terminal_1 or 0.0 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 01b02ea5e..b3f132c00 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 @@ -55,7 +55,6 @@ def to_phases_set(self) -> Set[SinglePhaseKind]: return set() return set(map(lambda it: it.to_phase, self.nominal_phase_paths)) - @property def from_equipment(self) -> ConductingEquipment: """ @@ -94,7 +93,6 @@ def traced_externally(self) -> bool: def did_traverse_ac_line_segment(self) -> bool: return self.traversed_ac_line_segment is not None - Type = Enum('Type', ('ALL', 'INTERNAL', 'EXTERNAL')) def __init__(self, path: Path, num_terminal_steps: int, num_equipment_steps: int, data: T): diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index 40477f1f4..f1a04ece9 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -5,7 +5,7 @@ import itertools import sys -from typing import Generator, Optional, Callable, Iterable, List +from typing import Generator, Optional, Callable, Iterable, List, Union from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection @@ -46,7 +46,7 @@ def _get_next_paths(): return (p for p in _get_next_paths() if p and self.state_operators.is_in_service(p.to_terminal.conducting_equipment)) def _create_path_factory(self, path: NetworkTraceStep.Path) -> PathFactory: - def path_factory(next_terminal: Terminal, traversed: AcLineSegment): + def path_factory(next_terminal: Terminal, traversed: AcLineSegment) -> NetworkTraceStep.Path: return NetworkTraceStep.Path(path.to_terminal, next_terminal, traversed) return path_factory @@ -105,7 +105,7 @@ def _next_paths_from_clamp(self, clamp: Clamp, path: NetworkTraceStep.Path, path next_paths_towards_T1 = self._acls_traverse_from_terminal(clamp.ac_line_segment, path.to_terminal, - length_from_T1=clamp.length_from_terminal_1, + length_from_T1=clamp.length_from_T1_or_0, towards_segment_T2=False, can_stop_at_cut_at_same_position=False, cut_at_same_position_from_terminal_number=1, @@ -113,36 +113,36 @@ def _next_paths_from_clamp(self, clamp: Clamp, path: NetworkTraceStep.Path, path next_paths_towards_T2 = self._acls_traverse_from_terminal(clamp.ac_line_segment, path.to_terminal, - length_from_T1=clamp.length_from_terminal_1, + length_from_T1=clamp.length_from_T1_or_0, towards_segment_T2=True, can_stop_at_cut_at_same_position=True, cut_at_same_position_from_terminal_number=1, path_factory=path_factory) - return set(itertools.chain(next_paths_towards_T1, next_paths_towards_T2)) + return itertools.chain(next_paths_towards_T1, next_paths_towards_T2) def _next_paths_from_cut(self, cut: Cut, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Iterable[NetworkTraceStep.Path]: # If the current path was from traversing an AcLineSegment, we need to step externally to other equipment. # Else we need to traverse the segment. next_terminals = ( self._next_external_paths(path, path_factory) if path.did_traverse_ac_line_segment else - self._acls_traverse_from_terminal(cut.ac_line_segment, + (self._acls_traverse_from_terminal(cut.ac_line_segment, path.to_terminal, - length_from_T1=cut.length_from_terminal_1, + length_from_T1=cut.length_from_T1_or_0, towards_segment_T2=path.to_terminal.sequence_number != 1, can_stop_at_cut_at_same_position=False, cut_at_same_position_from_terminal_number=path.to_terminal.sequence_number, path_factory=path_factory - ) if cut.ac_line_segment else [] + ) if cut.ac_line_segment else []) ) # If the current path traced internally, we need to also return the external terminals # Else we need to step internally to the Cut's other terminal. if path.traced_internally: # traversedAcLineSegment and tracedInternally should never both be true, so we should never get external terminals twice - return set(itertools.chain(next_terminals, self._next_external_paths(path, path_factory))) + return itertools.chain(next_terminals, self._next_external_paths(path, path_factory)) else: other_terminal = cut.get_terminal_by_sn(2 if path.to_terminal.sequence_number == 1 else 1) - return set(itertools.chain(next_terminals, seq_term_map_to_path(other_terminal, path_factory))) + return itertools.chain(next_terminals, seq_term_map_to_path(other_terminal, path_factory)) def _next_external_paths(self, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: @@ -188,7 +188,7 @@ def _acls_traverse_from_terminal( :param towards_segment_T2: Use `true` if the segment should be traversed towards terminal 2, otherwise `False` to traverse towards terminal 1 """ # Can do a simple return if we don't need to do any special cuts/clamps processing - if not(any((acls.cuts, acls.clamps))): + if not(any((list(acls.cuts), list(acls.clamps)))): yield from seq_term_map_to_path(from_terminal.other_terminals(), path_factory, acls) # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! @@ -197,40 +197,47 @@ def _acls_traverse_from_terminal( cuts: List[Cut] = list(filter(filter_func, acls.cuts)) clamps: List[Clamp] = list(filter(filter_func, acls.clamps)) - cuts_at_same_position = list(filter(lambda it: it.length_from_terminal_1 == length_from_T1, cuts)) + cuts_at_same_position = list(filter(lambda it: it.length_from_T1_or_0 == length_from_T1, cuts)) stop_at_cuts_at_same_position = bool(can_stop_at_cut_at_same_position and cuts_at_same_position) def next_cut_length_from_terminal_1_func(): - cut_length_generator = (it.length_from_terminal_1 for it in cuts if it.length_from_terminal_1 > length_from_T1) if stop_at_cuts_at_same_position: return length_from_T1 elif towards_segment_T2: - return min(cut_length_generator, default=None) + return min((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 > length_from_T1), default=None) else: - return max(cut_length_generator, default=None) + return max((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 < length_from_T1), default=None) next_cut_length_from_terminal_1 = next_cut_length_from_terminal_1_func() - next_cuts = [it for it in cuts if it.length_from_terminal_1 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 else [] + next_cuts = [it for it in cuts if it.length_from_T1_or_0 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 is not None else [] + + def next_term_length_from_term_1_func(): + if next_cut_length_from_terminal_1 is not None: + return next_cut_length_from_terminal_1 + elif towards_segment_T2: + return acls_length_or_max(acls) + else: + return 0.0 - next_terminal_length_from_terminal_1 = next_cut_length_from_terminal_1 or (acls_length_or_max(acls) if towards_segment_T2 else 0.0) + next_terminal_length_from_terminal_1 = next_term_length_from_term_1_func() def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_T2: - return lambda it: length_from_T1 <= it.length_from_terminal_1 <= next_terminal_length_from_terminal_1 + return lambda it: length_from_T1 <= it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 elif towards_segment_T2: - return lambda it: it.length_from_terminal_1 > length_from_T1 and it.length_from_terminal_1 <= next_terminal_length_from_terminal_1 - elif (next_terminal_length_from_terminal_1 == 0.0) and not len(next_cuts) == 0: - return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_terminal_1 <= length_from_T1 + return lambda it: it.length_from_T1_or_0 > length_from_T1 and it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 + elif (next_terminal_length_from_terminal_1 == 0.0) and len(next_cuts) == 0: + return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_T1_or_0 <= length_from_T1 else: - return lambda it: it.length_from_terminal_1 <= length_from_T1 and it.length_from_terminal_1 > next_terminal_length_from_terminal_1 + return lambda it: it.length_from_T1_or_0 <= length_from_T1 and it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 _filter = clamps_before_next_terminal_filter() clamps_before_next_terminal = filter(_filter, clamps) next_stop_terminals = [] if stop_at_cuts_at_same_position else ( it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts - ) if next_cuts else list(acls.get_terminal(2 if towards_segment_T2 else 1)) + ) if next_cuts else [acls.get_terminal(2 if towards_segment_T2 else 1)] next_terminals = ( (it.get_terminal(cut_at_same_position_from_terminal_number) for it in cuts_at_same_position), @@ -241,10 +248,13 @@ def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: for generator in next_terminals: yield from seq_term_map_to_path(generator, path_factory, acls) -def seq_term_map_to_path(terms: Iterable[Terminal], path_factory: PathFactory, traversed_acls: AcLineSegment=None +def seq_term_map_to_path(terms: Union[Terminal, Iterable[Terminal]], path_factory: PathFactory, traversed_acls: AcLineSegment=None ) -> Generator[NetworkTraceStep.Path, None, None]: - for terminal in terms: - if terminal is not None: - yield path_factory(terminal, traversed_acls) + if isinstance(terms, Iterable): + for terminal in terms: + if terminal is not None: + yield path_factory(terminal, traversed_acls) + else: + yield path_factory(terms, traversed_acls) acls_length_or_max = lambda acls: acls.length or sys.float_info.max \ No newline at end of file diff --git a/test/cim/cim_creators.py b/test/cim/cim_creators.py index d49ffc91b..87dabf96a 100644 --- a/test/cim/cim_creators.py +++ b/test/cim/cim_creators.py @@ -1026,9 +1026,11 @@ def create_power_electronics_wind_unit(include_runtime: bool = True): def create_ac_line_segment(include_runtime: bool = True): + args = create_conductor(include_runtime) + args["terminals"] = lists(builds(Terminal, **create_identified_object(include_runtime)), min_size=1, max_size=1) return builds( AcLineSegment, - **create_conductor(include_runtime), + **args, per_length_impedance=builds(PerLengthSequenceImpedance, **create_identified_object(include_runtime)) ) @@ -1049,20 +1051,24 @@ def create_busbar_section(include_runtime: bool = True): def create_clamp(include_runtime: bool = True): + args = create_conducting_equipment(include_runtime) + args["terminals"] = lists(builds(Terminal, **create_identified_object(include_runtime)), min_size=1, max_size=1) return builds( Clamp, - **create_conducting_equipment(include_runtime), + **args, length_from_terminal_1=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), ac_line_segment=builds(AcLineSegment, **create_identified_object(include_runtime)) ) def create_cut(include_runtime: bool = True): + args = create_switch(include_runtime) + args["terminals"] = lists(builds(Terminal, **create_identified_object(include_runtime)), min_size=1, max_size=2) return builds( Cut, - **create_switch(include_runtime), + **args, length_from_terminal_1=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - ac_line_segment=builds(AcLineSegment, **create_identified_object(include_runtime)) + ac_line_segment=builds(AcLineSegment, **create_identified_object(include_runtime)), ) diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 4f6e46114..59114b61e 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -2,7 +2,9 @@ # 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 Generator, Tuple, Iterable +from typing import Generator, Tuple, Iterable, Optional + +from pytest_subtests.plugin import subtests from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind @@ -11,7 +13,29 @@ ConductingEquipment from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider -Path = NetworkTraceStep.Path +class PathTerminal(Terminal): + def __add__(self, other: Terminal) -> NetworkTraceStep.Path: + """ + allows shorthand notation to create a NetworkTraceStep.Path between 2 terminals. Eg: j0[1]+c1[1] + """ + return NetworkTraceStep.Path(self, other, None) + + def __sub__(self, other: Terminal) -> NetworkTraceStep.Path: + """ + allows shorthand notation to create a NetworkTraceStep.Path that traversed an AcLineSegment betweenm 2 terminals. Eg: c1[1]-clamp1[1] + """ + def traversed_ce(ce): + if isinstance(ce, AcLineSegment): + return ce + elif isinstance(ce, (Clamp, Cut)): + return ce.ac_line_segment + else: + raise TypeError('Did not traverse') + return NetworkTraceStep.Path(self, other, traversed_ce(self.conducting_equipment)) + +Terminal.__add__ = PathTerminal.__add__ +Terminal.__sub__ = PathTerminal.__sub__ + SPK = SinglePhaseKind class TestNetworkTraceStepPathProvider: @@ -31,10 +55,10 @@ def test_current_external_path_steps_internally(self): c0 = network['c0'] j1 = network['j1'] - current_path = Path(c0[2], j1[1]) + current_path = c0[2] + j1[1] next_paths = self.path_provider.next_paths(current_path) - _verify_paths(next_paths, (Path(j1[1], j1[2]), Path(j1[1], j1[3]))) + _verify_paths(next_paths, (j1[1] + j1[2], j1[1] + j1[3])) def test_current_internal_path_steps_externally(self): # @@ -54,10 +78,10 @@ def test_current_internal_path_steps_externally(self): c1 = network['c1'] c2 = network['c2'] - current_path = Path(j0[1], j0[2]) + current_path = j0[1] + j0[2] next_paths = self.path_provider.next_paths(current_path) - _verify_paths(next_paths, (Path(j0[2], c1[1]), Path(j0[2], c2[1]))) + _verify_paths(next_paths, (j0[2] + c1[1], j0[2] + c2[1])) def test_only_steps_to_in_service_equipment(self): # @@ -78,10 +102,10 @@ def test_only_steps_to_in_service_equipment(self): j0 = network['j0'] c1 = network['c1'] - current_path = Path(j0[1], j0[2]) + current_path = j0[1] + j0[2] next_paths = self.path_provider.next_paths(current_path) - _verify_paths(next_paths, (Path(j0[2], c1[1]), )) + _verify_paths(next_paths, (j0[2] + c1[1], ) ) def test_only_includes_followed_phases(self): # @@ -97,13 +121,13 @@ def test_only_includes_followed_phases(self): c0 = network['c0'] tx1 = network['tx1'] - current_path = Path(c0[2], tx1[1], None, {NominalPhasePath(SPK.A, SPK.A), NominalPhasePath(SPK.B, SPK.B)}) + current_path = NetworkTraceStep.Path(c0[2], tx1[1], None, {NominalPhasePath(SPK.A, SPK.A), NominalPhasePath(SPK.B, SPK.B)}) next_paths = self.path_provider.next_paths(current_path) # Should not contain tx1-t4 because its not in the phase paths _verify_paths(next_paths, [ - Path(tx1[1], tx1[2], None, {NominalPhasePath(SPK.A, SPK.A)}), - Path(tx1[1], tx1[3], None, {NominalPhasePath(SPK.B, SPK.B)})]) + NetworkTraceStep.Path(tx1[1], tx1[2], None, {NominalPhasePath(SPK.A, SPK.A)}), + NetworkTraceStep.Path(tx1[1], tx1[3], None, {NominalPhasePath(SPK.B, SPK.B)})]) def test_stepping_externally_to_connectivity_node_with_busbars_only_goes_to_busbars(self): network = self._busbar_network() @@ -112,10 +136,10 @@ def test_stepping_externally_to_connectivity_node_with_busbars_only_goes_to_busb bbs1 = network['bbs1'] bbs2 = network['bbs2'] - current_path = Path(b0[1], b0[2]) + current_path = b0[1] + b0[2] next_paths = self.path_provider.next_paths(current_path) - _verify_paths(next_paths, (Path(b0[2], bbs1[1]), Path(b0[2], bbs2[1]))) + _verify_paths(next_paths, (b0[2] + bbs1[1], b0[2] + bbs2[1])) def test_steppiong_externally_from_busbars_does_not_step_to_busbars_or_original_from_terminal(self): network = self._busbar_network() @@ -127,23 +151,434 @@ def test_steppiong_externally_from_busbars_does_not_step_to_busbars_or_original_ b5 = network['b5'] b6 = network['b6'] - current_path = Path(b0[2], bbs1[1]) + current_path = b0[2] + bbs1[1] next_paths = self.path_provider.next_paths(current_path) - _verify_paths(next_paths, (Path(bbs1[1], b3[1]), Path(bbs1[1], b4[1]), Path(bbs1[1], b5[1]), Path(bbs1[1], b6[1]))) + _verify_paths(next_paths, (bbs1[1] + b3[1], bbs1[1] + b4[1], bbs1[1] + b5[1], bbs1[1] + b6[1])) def test_traversing_segment_with_clamps_from_t1_includes_all_clamp_steps(self): network = self._acls_with_clamps_network() breaker = network['b0'] segment: AcLineSegment = network['c1'] - clamp1 = network['c1-clamp1'] - clamp2 = network['c1-clamp2'] + clamp1 = network['clamp1'] + clamp2 = network['clamp2'] + + current_path = breaker[2] + segment[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (segment[1] - clamp1[1], segment[1] - clamp2[1], segment[1] - segment[2])) + + def test_traversing_segment_with_clamps_from_t2_includes_all_clamp_steps(self): + network = self._acls_with_clamps_network() + + breaker = network['b2'] + segment: AcLineSegment = network['c1'] + clamp1 = network['clamp1'] + clamp2 = network['clamp2'] + + current_path = breaker[1] + segment[2] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (segment[2] - clamp2[1], segment[2] - clamp1[1], segment[2] - segment[1])) + + def test_non_traverse_step_to_segment_t1_traverses_towards_t2_stopping_at_cut(self): + network = self._acls_with_clamps_and_cuts_network() + + b0 = network['b0'] + segment = network['c1'] + clamp1 = network['clamp1'] + cut1 = network['cut1'] + + current_path = b0[2] + segment[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (segment[1] - clamp1[1], segment[1] - cut1[1])) + + def test_non_traverse_step_to_segment_t2_traverses_towards_t1_stopping_at_cut(self): + network = self._acls_with_clamps_and_cuts_network() + + b2 = network['b2'] + segment = network['c1'] + clamp4 = network['clamp4'] + cut2 = network['cut2'] + + current_path = b2[1] + segment[2] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (segment[2] - clamp4[1], segment[2] - cut2[2])) + + def test_traverse_step_to_cut_t1_steps_externally_and_across_cut(self): + network = self._acls_with_clamps_and_cuts_network() + + segment = network['c1'] + cut1 = network['cut1'] + c4 = network['c4'] + + current_path = segment[1] - cut1[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (cut1[1] + cut1[2], cut1[1] + c4[1])) + + def test_traverse_step_to_cut_t2_steps_externally_and_across_cut(self): + network = self._acls_with_clamps_and_cuts_network() + + segment = network['c1'] + cut2 = network['cut2'] + c9 = network['c9'] + + current_path = segment[2] - cut2[2] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (cut2[2] + cut2[1], cut2[2] + c9[1])) + + def test_non_traverse_step_to_cut_t1_traverses_segment_towards_t1_and_internally_through_cut_to_t2(self): + network = self._acls_with_clamps_and_cuts_network() + + segment = network['c1'] + clamp1 = network['clamp1'] + cut1 = network['cut1'] + c4 = network['c4'] + + current_path = c4[1] + cut1[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (cut1[1] - clamp1[1], cut1[1] - segment[1], cut1[1] + cut1[2])) + + def test_non_traverse_step_to_cut_t2_traverses_segment_towards_t2_and_internally_through_cut_to_t1(self): + network = self._acls_with_clamps_and_cuts_network() + + segment = network['c1'] + clamp4 = network['clamp4'] + cut2 = network['cut2'] + c9 = network['c9'] + + current_path = c9[1] + cut2[2] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (cut2[2] - clamp4[1], cut2[2] - segment[2], cut2[2] + cut2[1])) + + def test_non_traverse_step_to_clamp_traverses_segment_in_both_directions(self): + network = self._acls_with_clamps_and_cuts_network() + + segment = network['c1'] + clamp1 = network['clamp1'] + cut1 = network['cut1'] + c3 = network['c3'] + + current_path = c3[1] + clamp1[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (clamp1[1] - segment[1], clamp1[1] - cut1[1])) + + def test_traverse_step_to_clamp_traces_externally_and_does_not_traverse_back_along_segment(self): + network = self._acls_with_clamps_and_cuts_network() + + segment = network['c1'] + clamp1 = network['clamp1'] + c3 = network['c3'] + + current_path = segment[1] - clamp1[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (clamp1[1] + c3[1], )) + + def test_non_traverse_step_to_clamp_between_cuts_traverses_segment_both_ways_stopping_at_cuts(self): + network = self._acls_with_clamps_and_cuts_network() - current_path = Path(breaker[2], segment[1]) + c6 = network['c6'] + clamp2 = network['clamp2'] + clamp3 = network['clamp3'] + cut1 = network['cut1'] + cut2 = network['cut2'] + + current_path = c6[1] + clamp2[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (clamp2[1] - cut1[2], clamp2[1] - clamp3[1], clamp2[1] - cut2[1])) + + def test_non_traverse_external_step_to_cut_t2_between_cuts_traverses_segment_towards_t2_stopping_at_next_cut_and_steps_internally_to_cut_t1(self): + network = self._acls_with_clamps_and_cuts_network() + + c5 = network['c5'] + clamp2 = network['clamp2'] + clamp3 = network['clamp3'] + cut1 = network['cut1'] + cut2 = network['cut2'] + + current_path = c5[1] + cut1[2] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (cut1[2] - clamp2[1], cut1[2] - clamp3[1], cut1[2] - cut2[1], cut1[2] + cut1[1])) + + def test_non_traverse_external_step_to_cut_t1_between_cuts_traverses_segment_towards_t1_stopping_at_next_cut_and_steps_internally_to_cut_t2(self): + network = self._acls_with_clamps_and_cuts_network() + + c8 = network['c8'] + clamp2 = network['clamp2'] + clamp3 = network['clamp3'] + cut1 = network['cut1'] + cut2 = network['cut2'] + + current_path = c8[1] + cut2[1] next_paths = self.path_provider.next_paths(current_path) - _verify_paths(next_paths, (Path(segment[1], clamp1[1]), Path(segment[1], clamp2[1]), Path(segment[1], segment[2]))) + _verify_paths(next_paths, (cut2[1] - clamp3[1], cut2[1] - clamp2[1], cut2[1] - cut1[2], cut2[1] + cut2[2])) + + def test_internal_step_to_cut_t2_between_cuts_steps_externally_and_traverses_segment_towards_t2_stopping_at_the_next_cut(self): + network = self._acls_with_clamps_and_cuts_network() + + c5 = network['c5'] + clamp2 = network['clamp2'] + clamp3 = network['clamp3'] + cut1 = network['cut1'] + cut2 = network['cut2'] + + current_path = cut1[1] + cut1[2] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (cut1[2] - clamp2[1], cut1[2] - clamp3[1], cut1[2] - cut2[1], cut1[2] + c5[1])) + + def test_internal_step_to_cut_t1_between_cuts_steps_externally_and_traverses_segment_towards_t1_stopping_at_the_next_cut(self): + network = self._acls_with_clamps_and_cuts_network() + + c8 = network['c8'] + clamp2 = network['clamp2'] + clamp3 = network['clamp3'] + cut1 = network['cut1'] + cut2 = network['cut2'] + + current_path = cut2[2] + cut2[1] + next_paths = self.path_provider.next_paths(current_path) + + _verify_paths(next_paths, (cut2[1] - clamp2[1], cut2[1] - clamp3[1], cut2[1] - cut1[2], cut2[1] + c8[1])) + + def test_traverse_with_cut_with_known_length_from_t1_does_not_return_clamp_with_known_length_from_t1(self, subtests): + # + # (Cut with null length is treated as 0.0 + # 1 b0 21*1 cut1 2*-c1-*-21 b2 2 + # 1 + # clamp1 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_breaker() # b2 + ).network + + c1 = network['c1'] + b0 = network['b0'] + b2 = network['b2'] + + clamp = _segment_with_clamp(network, c1, 1.0) + cut = _segment_with_cut(network, c1, None) + + with subtests.test('Traverse from T1 towards T2'): + current_path = b0[2] + c1[1] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[1] - cut[1], )) + + with subtests.test('Traverse from T2 towards T1'): + current_path = b2[1] + c1[2] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[2] - clamp[1], c1[2] - cut[2])) + + def test_multiple_cuts_at_same_positions_step_to_all_cuts_at_that_position(self, subtests): + # + # *1 cut2 2* + # 1 b0 21-c1-*1 cut1 2*-c1-*-21 b2 2 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_breaker() # b2 + ).network + + c1 = network['c1'] + b0 = network['b0'] + b2 = network['b2'] + + cut1 = _segment_with_cut(network, c1, 1.0) + cut2 = _segment_with_cut(network, c1, 1.0) + + with subtests.test('Traverse from T1 towards T2 should have both cuts t1'): + current_path = b0[2] + c1[1] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[1] - cut1[1], c1[1] - cut2[1])) + + with subtests.test('Traverse from T2 towards T1 should have both cuts t2'): + current_path = b2[1] + c1[2] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[2] - cut1[2], c1[2] - cut2[2])) + + with subtests.test('Internal step on cut1 t1 to t2 has cut2.t2 and traverses towards segment T2'): + current_path = cut1[1] + cut1[2] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (cut1[2] - c1[2], cut1[2] - cut2[2])) + + with subtests.test('Internal step on cut1 t2 to t1 traverses towards segment T2'): + current_path = cut1[2] + cut1[1] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (cut1[1] - c1[1], cut1[1] - cut2[1])) + + def test_cut_and_clamp_without_length_only_returns_clamp_on_T1_side_of_cut(self, subtests): + # + # 1 b0 21*1 cut1 2*-c1-*-21 b2 2 + # 1 + # clamp1 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_breaker() # b2 + ).network + + c1 = network['c1'] + b0 = network['b0'] + b2 = network['b2'] + + clamp = _segment_with_clamp(network, c1, None) + cut = _segment_with_cut(network, c1, None) + + with subtests.test('Traverse from T1 towards T2'): + current_path = b0[2] + c1[1] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[1] - cut[1], c1[1] - clamp[1])) + + with subtests.test('Traverse from T2 towards T1'): + current_path = b2[1] + c1[2] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[2] - cut[2], )) + + with subtests.test('Internally stepped on cut T1 to T2, traverse towards c1.t2'): + current_path = cut[1] + cut[2] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (cut[2] - c1[2], )) + + with subtests.test('Internally stepped on cut T2 to T2, traverse towards c1.t1'): + current_path =cut[2] + cut[1] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (cut[1] - c1[1], cut[1] - clamp[1])) + + def test_multiple_clamps_at_same_position_does_not_return_the_other_clamps_more_then_once(self): + # (Cut with None length is treated as 0.0 + # clamp2 + # 1 + # 1 b0 21*1 cut1 2*-c1-*-21 b2 2 + # 1 + # clamp1 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_breaker() # b2 + ).network + + c1 = network['c1'] + + clamp1 = _segment_with_clamp(network, c1, None) + clamp2 = _segment_with_clamp(network, c1, None) + cut = _segment_with_cut(network, c1, None) + + next_paths = self.path_provider.next_paths(clamp1[1] + clamp1[1]) + _verify_paths(next_paths, (clamp1[1] - c1[1], clamp1[1] - clamp2[1], clamp1[1] - cut[1])) + + def test_unrealistic_cuts_and_clamps_network_doesnt_break_the_pathing_algorith(self, subtests): + network = self._acls_with_clamps_and_cuts_at_same_position_network() + + b0 = network['b0'] + b2 = network['b2'] + c1 = network['c1'] + clamp1 = network['clamp1'] + clamp2 = network['clamp2'] + clamp3 = network['clamp3'] + clamp4 = network['clamp4'] + clamp5 = network['clamp5'] + clamp6 = network['clamp6'] + cut1 = network['cut1'] + cut2 = network['cut2'] + cut3 = network['cut3'] + cut4 = network['cut4'] + cut5 = network['cut5'] + cut6 = network['cut6'] + cClamp1 = network['c-clamp1'] + cCut1t1 = network['c-cut1t1'] + cCut1t2 = network['c-cut1t2'] + cClamp3 = network['c-clamp3'] + cCut3t1 = network['c-cut3t1'] + cCut3t2 = network['c-cut3t2'] + cClamp5 = network['c-clamp5'] + cCut5t1 = network['c-cut5t1'] + cCut5t2 = network['c-cut5t2'] + + with subtests.test("traverse from c1.t1 should get clamps at start and stop at both cuts at start"): + next_paths = self.path_provider.next_paths(b0[2] + c1[1]) + _verify_paths(next_paths, (c1[1] - clamp1[1], c1[1] - clamp2[1], c1[1] - cut1[1], c1[1] - cut2[1])) + + with subtests.test('traverse from clamp1.t1 should traverse to other clamp at start, stop at both cuts at start and c1.t1'): + next_paths = self.path_provider.next_paths(cClamp1[1] + clamp1[1]) + _verify_paths(next_paths, (clamp1[1] - clamp2[1], clamp1[1] - c1[1], clamp1[1] - cut1[1], clamp1[1] - cut2[1])) + + with subtests.test("traverse from cut1.t1 (external) should traverse to cut2.t1, clamps at start, c1.t1 and internally step to cut1.t2"): + next_paths = self.path_provider.next_paths(cCut1t1[1] + cut1[1]) + _verify_paths(next_paths, (cut1[1] - cut2[1], cut1[1] - clamp1[1], cut1[1] - clamp2[1], cut1[1] - c1[1], cut1[1] + cut1[2])) + + with subtests.test("traverse from cut1.t1 (internal) should traverse to cut2.t1, clamps at start, c1.t1 and step to cCut1"): + next_paths = self.path_provider.next_paths(cut1[2] + cut1[1]) + _verify_paths(next_paths, (cut1[1] - cut2[1], cut1[1] - clamp1[1], cut1[1] - clamp2[1], cut1[1] - c1[1], cut1[1] + cCut1t1[1])) + + with subtests.test("traverse from cut1.t2 (external) should traverse to cut2.t2, middle cuts, middle clamps, internally step to c1.t1"): + next_paths = self.path_provider.next_paths(cCut1t2[1] + cut1[2]) + _verify_paths(next_paths, (cut1[2] - cut2[2], cut1[2] - clamp3[1], cut1[2] - clamp4[1], cut1[2] - cut3[1], cut1[2] - cut4[1], cut1[2] + cut1[1])) + + with subtests.test("traverse from cut1.t2 (internal) should traverse to cut2.t2, middle cuts, middle clamps and externally to cCut1t2"): + next_paths = self.path_provider.next_paths(cut1[1] + cut1[2]) + _verify_paths(next_paths, (cut1[2] - cut2[2], cut1[2] - clamp3[1], cut1[2] - clamp4[1], cut1[2] - cut3[1], cut1[2] - cut4[1], cut1[2] + cCut1t2[1])) + + with subtests.test("traverse from middle clamp (clamp3) should traverse to cuts at start, middle cuts, and other middle clamp"): + next_paths = self.path_provider.next_paths(cClamp3[1] + clamp3[1]) + _verify_paths(next_paths, (clamp3[1] - cut1[2], clamp3[1] - cut2[2], clamp3[1] - cut3[1], clamp3[1] - cut4[1], clamp3[1] - clamp4[1])) + + with subtests.test("traverse from cut3.t1 (external) should traverse to cut4.t1, start cuts, middle clamps, and internally step to cut3.t2"): + next_paths = self.path_provider.next_paths(cCut3t1[1] + cut3[1]) + _verify_paths(next_paths, (cut3[1] - cut4[1], cut3[1] - cut1[2], cut3[1] - cut2[2], cut3[1] - clamp3[1], cut3[1] - clamp4[1], cut3[1] + cut3[2])) + + with subtests.test("traverse from cut3.t1 (internal) should traverse to cut2.t1, clamps at start, middle clamp and step to cCut3t1"): + next_paths = self.path_provider.next_paths(cut3[2] + cut3[1]) + _verify_paths(next_paths, (cut3[1] - cut4[1], cut3[1] - cut1[2], cut3[1] - cut2[2], cut3[1] - clamp3[1], cut3[1] - clamp4[1], cut3[1] + cCut3t1[1])) + + with subtests.test("traverse from cut3.t2 (external) should traverse to cut4.t2, end cuts, end clamps and internally step to cut3.t1"): + next_paths = self.path_provider.next_paths(cCut3t2[1] + cut3[2]) + _verify_paths(next_paths, (cut3[2] - cut4[2], cut3[2] - cut5[1], cut3[2] - cut6[1], cut3[2] - clamp5[1], cut3[2] - clamp6[1], cut3[2] + cut3[1])) + + with subtests.test("traverse from cut3.t2 (internal) should traverse to cut4.t2, end cuts, end clamps and externally to cut3t2"): + next_paths = self.path_provider.next_paths(cut3[1] + cut3[2]) + _verify_paths(next_paths, (cut3[2] - cut4[2], cut3[2] - cut5[1], cut3[2] - cut6[1], cut3[2] - clamp5[1], cut3[2] - clamp6[1], cut3[2] + cCut3t2[1])) + + with subtests.test("traverse from end clamp (clamp5) should traverse to middle cuts, end cuts and other end clamp"): + next_paths = self.path_provider.next_paths(cClamp5[1] + clamp5[1]) + _verify_paths(next_paths, (clamp5[1] - cut3[2], clamp5[1] - cut4[2], clamp5[1] - cut5[1], clamp5[1] - cut6[1], clamp5[1] - clamp6[1])) + + with subtests.test("traverse from cut5.t1 (external) should traverse to cut6.t1, middle cuts, end clamps, and internally step to cut5.t2"): + next_paths = self.path_provider.next_paths(cCut5t1[1] + cut5[1]) + _verify_paths(next_paths, (cut5[1] - cut6[1], cut5[1] - cut3[2], cut5[1] - cut4[2], cut5[1] - clamp5[1], cut5[1] - clamp6[1], cut5[1] + cut5[2])) + + with subtests.test("traverse from cut5.t1 (internal) should traverse to cut6.t1, middle cuts, end clamps, and step to cCut5t1"): + next_paths = self.path_provider.next_paths(cut5[2] + cut5[1]) + _verify_paths(next_paths, (cut5[1] - cut6[1], cut5[1] - cut3[2], cut5[1] - cut4[2], cut5[1] - clamp5[1], cut5[1] - clamp6[1], cut5[1] + cCut5t1[1])) + + with subtests.test("traverse from cut5.t1 (external) should traverse to cut6.t2, c1.t2, and internally step out to cut5.t1"): + next_paths = self.path_provider.next_paths(cCut5t2[2] + cut5[2]) + _verify_paths(next_paths, (cut5[2] - cut6[2], cut5[2] - c1[2], cut5[2] + cut5[1])) + + with subtests.test("traverse from cut5.t2 (internal) should traverse to cut6.t2, c1.t2, end step externally to cCut5t2"): + next_paths = self.path_provider.next_paths(cut5[1] + cut5[2]) + _verify_paths(next_paths, (cut5[2] - cut6[2], cut5[2] - c1[2], cut5[2] + cCut5t2[1])) + + with subtests.test("traverse from c1.t2 should get cuts at end"): + next_paths = self.path_provider.next_paths(b2[1] + c1[2]) + _verify_paths(next_paths, (c1[2] - cut5[2], c1[2] - cut6[2])) + def _busbar_network(self) -> NetworkService: # 1 @@ -182,7 +617,7 @@ def _busbar_network(self) -> NetworkService: return network - def _acls_with_clamps_network(self): + def _acls_with_clamps_network(self) -> NetworkService: # # clamp1 # 1 @@ -202,7 +637,7 @@ def _acls_with_clamps_network(self): return network - def _acls_with_clamps_and_cuts_network(self): + def _acls_with_clamps_and_cuts_network(self) -> NetworkService: # # 2 2 # c3 2 c7 2 @@ -220,14 +655,14 @@ def _acls_with_clamps_and_cuts_network(self): .from_breaker() # b0 .to_acls() # c1 .to_breaker() # b2 - .to_acls() # c3 - .to_acls() # c4 - .to_acls() # c5 - .to_acls() # c6 - .to_acls() # c7 - .to_acls() # c8 - .to_acls() # c9 - .to_acls() # c10 + .from_acls() # c3 + .from_acls() # c4 + .from_acls() # c5 + .from_acls() # c6 + .from_acls() # c7 + .from_acls() # c8 + .from_acls() # c9 + .from_acls() # c10 ).network segment: AcLineSegment = network['c1'] @@ -250,7 +685,81 @@ def _acls_with_clamps_and_cuts_network(self): return network -def _segment_with_clamp(network: NetworkService, segment: AcLineSegment, length_from_terminal1: float) -> Clamp: + def _acls_with_clamps_and_cuts_at_same_position_network(self) -> NetworkService: + # Drawing this is very messy, so it will be described in writing: + # The network has 2 Breakers (b0, b2) with an AcLineSegment (c1) between them ( 1 b0 21--c1--21 b2 1 ) + # There is then 2 Clamps and 2 Cuts at the following position on c1 + # * At the start (0.0) (clamp1, clamp2, cut1, cut2) + # * In the middle (length 1.0) (clamp3, clamp4, cut3, cut4) + # * At the end (length 2.0) (clamp5, clamp6, cut5, cut6) + # On each clamp terminal there is a separate AcLineSegment connected to it. (ids of c-clampX) + # On each cut terminal (both 1 and 2) there is a separate AcLineSegment connected to it. (ids of c-cutXtN) + + def acls_length(acls: AcLineSegment) -> None: + acls.length = 2.0 + + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls(action=acls_length) # c1 + .to_breaker() # b2 + .from_acls(mrid='c-clamp1') + .from_acls(mrid='c-clamp2') + .from_acls(mrid='c-cut1t1') + .from_acls(mrid='c-cut1t2') + .from_acls(mrid='c-cut2t1') + .from_acls(mrid='c-cut2t2') + .from_acls(mrid='c-clamp3') + .from_acls(mrid='c-clamp4') + .from_acls(mrid='c-cut3t1') + .from_acls(mrid='c-cut3t2') + .from_acls(mrid='c-cut4t1') + .from_acls(mrid='c-cut4t2') + .from_acls(mrid='c-clamp5') + .from_acls(mrid='c-clamp6') + .from_acls(mrid='c-cut5t1') + .from_acls(mrid='c-cut5t2') + .from_acls(mrid='c-cut6t1') + .from_acls(mrid='c-cut6t2') + ).network + + segment = network['c1'] + assert segment.length is not None + + clamp1 = _segment_with_clamp(network, segment, 0.0) + clamp2 = _segment_with_clamp(network, segment, None) + cut1 = _segment_with_cut(network, segment, 0.0) + cut2 = _segment_with_cut(network, segment, None) + clamp3 = _segment_with_clamp(network, segment, 1.0) + clamp4 = _segment_with_clamp(network, segment, 1.0) + cut3 = _segment_with_cut(network, segment, 1.0) + cut4 = _segment_with_cut(network, segment, 1.0) + clamp5 = _segment_with_clamp(network, segment, segment.length) + clamp6 = _segment_with_clamp(network, segment, segment.length) + cut5 = _segment_with_cut(network, segment, segment.length) + cut6 = _segment_with_cut(network, segment, segment.length) + + network.connect(clamp1[1], network.get('c-clamp1', ConductingEquipment)[1]) + network.connect(clamp2[1], network.get('c-clamp2', ConductingEquipment)[1]) + network.connect(cut1[1], network.get('c-cut1t1', ConductingEquipment)[1]) + network.connect(cut1[2], network.get('c-cut1t2', ConductingEquipment)[1]) + network.connect(cut2[1], network.get('c-cut2t1', ConductingEquipment)[1]) + network.connect(cut2[2], network.get('c-cut2t2', ConductingEquipment)[1]) + network.connect(clamp3[1], network.get('c-clamp3', ConductingEquipment)[1]) + network.connect(clamp4[1], network.get('c-clamp4', ConductingEquipment)[1]) + network.connect(cut3[1], network.get('c-cut3t1', ConductingEquipment)[1]) + network.connect(cut3[2], network.get('c-cut3t2', ConductingEquipment)[1]) + network.connect(cut4[1], network.get('c-cut4t1', ConductingEquipment)[1]) + network.connect(cut4[2], network.get('c-cut4t2', ConductingEquipment)[1]) + network.connect(clamp5[1], network.get('c-clamp5', ConductingEquipment)[1]) + network.connect(clamp6[1], network.get('c-clamp6', ConductingEquipment)[1]) + network.connect(cut5[1], network.get('c-cut5t1', ConductingEquipment)[1]) + network.connect(cut5[2], network.get('c-cut5t2', ConductingEquipment)[1]) + network.connect(cut6[1], network.get('c-cut6t1', ConductingEquipment)[1]) + network.connect(cut6[2], network.get('c-cut6t2', ConductingEquipment)[1]) + + return network + +def _segment_with_clamp(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Clamp: clamp = Clamp(mrid=f'clamp{segment.num_clamps() + 1}') clamp.add_terminal(Terminal(mrid=f'{clamp.mrid}-t1')) clamp.length_from_terminal_1 = length_from_terminal1 @@ -259,7 +768,7 @@ def _segment_with_clamp(network: NetworkService, segment: AcLineSegment, length_ network.add(clamp) return clamp -def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_from_terminal1: float) -> Cut: +def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Cut: cut = Cut(mrid=f'cut{segment.num_cuts() + 1}', length_from_terminal_1=length_from_terminal1) cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t1')) cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t2')) @@ -269,7 +778,10 @@ def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_fr return cut -def _verify_paths(in_paths: Generator[NetworkTraceStep.Path, None, None], in_expected: Iterable[Path], check_length=True): +def _verify_paths(in_paths: Generator[NetworkTraceStep.Path, None, None], in_expected: Iterable[NetworkTraceStep.Path], check_length=True): paths = sorted(list(in_paths), key=lambda p: (p.from_terminal, p.to_terminal)) expected = sorted(in_expected, key=lambda p: (p.from_terminal, p.to_terminal)) - assert paths == expected + for path in paths: + if path in expected: + continue + assert paths == expected # doesn't represent the actual comparison, but dumps both sides of it. \ No newline at end of file From 3777cde68485143cf20f76210319634f97a0977c Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 21 May 2025 04:14:56 +1000 Subject: [PATCH 07/47] Network Trace now supports cuts and clamps Busbranch trace broke, theres more fixes to pull in yet. Signed-off-by: Max Chesterfield --- changelog.md | 6 +- .../tracing/networktrace/compute_data.py | 2 +- .../tracing/networktrace/network_trace.py | 22 +-- .../networktrace/network_trace_queue_next.py | 131 ++++++++--------- .../networktrace/network_trace_step.py | 12 +- .../network_trace_step_path_provider.py | 4 +- .../networktrace/test_network_trace.py | 1 + .../test_network_trace_queue_next.py | 133 ++++++++++++++++++ .../network/tracing/networktrace/util.py | 41 ++++++ 9 files changed, 261 insertions(+), 91 deletions(-) create mode 100644 test/services/network/tracing/networktrace/test_network_trace_queue_next.py create mode 100644 test/services/network/tracing/networktrace/util.py diff --git a/changelog.md b/changelog.md index c112b7ddf..842001d8d 100644 --- a/changelog.md +++ b/changelog.md @@ -2,12 +2,14 @@ ## [0.48.0] - UNRELEASED ### Breaking Changes * Updated to new Tracing API. All old traces will need to be re-written with the new API. +* `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. ### New Features -* None. +* Added `ClearDirection` that clears feeder directions. ### Enhancements -* None. +* Tracing models with `Cut` and `Clamp` are now supported via the new tracing API. ### Fixes When finding `LvFeeders` in the `Site` we will now exclude `LvFeeders` that start with an open `Switch` diff --git a/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py b/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py index 505a6e304..fd6586644 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py @@ -38,7 +38,7 @@ class ComputeDataWithPaths(Generic[T]): def __init__(self, func): self._func = func or (lambda *args: None) - def compute_next(self, current_step: NetworkTraceStep[T], current_context: StepContext, next_path: NetworkTraceStep.Path, next_paths: list[NetworkTraceStep.Path, Any]) -> T: + def compute_next(self, current_step: NetworkTraceStep[T], current_context: StepContext, next_path: NetworkTraceStep.Path, next_paths: list[NetworkTraceStep.Path]) -> T: """ Called for each new NetworkTraceStep in a NetworkTrace. The value returned from this function will be stored against the next step within NetworkTraceStep. data. 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 139a773bc..6c205fd30 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -3,7 +3,7 @@ # 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 collections.abc import Callable -from typing import TypeVar, Union, Generic +from typing import TypeVar, Union, Generic, Set from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode @@ -15,6 +15,7 @@ from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_condition import NetworkTraceQueueCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_next import NetworkTraceQueueNext from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep +from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider from zepben.evolve.services.network.tracing.networktrace.network_trace_tracker import NetworkTraceTracker from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition @@ -86,10 +87,10 @@ def non_branching(cls, queue: TraversalQueue[NetworkTraceStep[T]], action_type: NetworkTraceActionType, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]] - ): + ) -> 'NetworkTrace[T]': return cls(network_state_operators, - Traversal.BasicQueueType(NetworkTraceQueueNext().basic( - network_state_operators.is_in_service, + Traversal.BasicQueueType(NetworkTraceQueueNext.Basic( + NetworkTraceStepPathProvider(network_state_operators), compute_data_with_action_type(compute_data, action_type) ), queue), None, @@ -103,11 +104,12 @@ def branching(cls, action_type: NetworkTraceActionType, parent: 'NetworkTrace[T]'=None, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]=None, - ): + ) -> 'NetworkTrace[T]': return cls(network_state_operators, - Traversal.BranchingQueueType(NetworkTraceQueueNext().branching( - network_state_operators.is_in_service, compute_data_with_action_type(compute_data, action_type) + Traversal.BranchingQueueType(NetworkTraceQueueNext.Branching( + NetworkTraceStepPathProvider(network_state_operators), + compute_data_with_action_type(compute_data, action_type) ), queue_factory, branch_queue_factory), parent, action_type) @@ -125,7 +127,7 @@ def add_start_item(self, start: Union[Terminal, ConductingEquipment], data: T= N :param phases: Phases to trace; `None` to ignore phases. """ if isinstance(start, Terminal): - start_path = NetworkTraceStep.Path(start, start, self.start_nominal_phase_path(phases)) + start_path = NetworkTraceStep.Path(start, start, None, self.start_nominal_phase_path(phases)) super().add_start_item(NetworkTraceStep(start_path, 0, 0, data)) return self @@ -196,8 +198,8 @@ def create_new_this(self) -> 'NetworkTrace[T]': return NetworkTrace(self.network_state_operators, self._queue_type, self, self._action_type) @staticmethod - def start_nominal_phase_path(phases: PhaseCode) -> list[NominalPhasePath]: - return [NominalPhasePath(it, it) for it in phases.single_phases] if phases and phases.single_phases else [] + 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: return self._tracker.has_visited(terminal, phases) or (self.parent and self.parent.has_visited(terminal, phases)) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index 723f57882..722fe611d 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -2,96 +2,85 @@ # 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 TypeVar, Callable, Sequence, Iterable, Generator +from abc import ABC +from typing import TypeVar, Callable, Generator, TYPE_CHECKING, Generic, List, Union -from zepben.evolve import TerminalConnectivityConnected -from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal -from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection +from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider -from zepben.evolve.services.network.tracing.traversal.step_context import StepContext -from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.traversal.traversal import Traversal T = TypeVar('T') -CheckInService = Callable[[ConductingEquipment], bool] +from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData, ComputeDataWithPaths +from zepben.evolve.services.network.tracing.traversal.step_context import StepContext +QueueItem = Callable[[NetworkTraceStep[T]], bool] +QueueBranch = Callable[[NetworkTraceStep[T]], bool] +GetNextSteps = Callable[[NetworkTraceStep[T], StepContext], Generator[NetworkTraceStep[T], None, None]] +GetNextStepsBranching = Callable[[NetworkTraceStep[T], StepContext], List[NetworkTraceStep[T]]] -class NetworkTraceQueueNext: - def basic(self, is_in_service: CheckInService, compute_data: ComputeData[T]) -> Traversal.QueueNext[NetworkTraceStep[T]]: - return Traversal.QueueNext(lambda item, context, queue_item: list(map(queue_item ,self._next_trace_steps(is_in_service, item, context, compute_data)))) +class NetworkTraceQueueNext(ABC): + path_provider = NetworkTraceStepPathProvider - def branching(self, is_in_service: CheckInService, compute_data: ComputeData[T]) -> Traversal.BranchingQueueNext[NetworkTraceStep[T]]: - return Traversal.BranchingQueueNext(lambda item, context, queue_item, queue_branch: self._queue_next_steps_branching(list(self._next_trace_steps(is_in_service, item, context, compute_data)), queue_item, queue_branch)) + def __init__(self, path_provider: NetworkTraceStepPathProvider): + self.path_provider = path_provider - @staticmethod - def _queue_next_steps_branching(next_steps: list[NetworkTraceStep[T]], - queue_item: Callable[[NetworkTraceStep[T]], bool], - queue_branch: Callable[[NetworkTraceStep[T]], bool]): - if len(next_steps) == 1: - return queue_item(next_steps[0]) - else: - return [queue_branch(step) for step in next_steps] - def _next_trace_steps(self, - is_in_service: CheckInService, + def next_trace_steps(self, current_step: NetworkTraceStep[T], current_context: StepContext, - compute_data: ComputeData[T] - ) -> Sequence[NetworkTraceStep[T]]: + compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]] + ) -> Generator[NetworkTraceStep[T], None, None]: """ Builds a list of next `NetworkTraceStep` to add to the `NetworkTrace` queue """ + next_paths = self.path_provider.next_paths(current_step.path) + if isinstance(compute_data, ComputeData): + compute_next = lambda it: compute_data.compute_next(current_step, current_context, it) + elif isinstance(compute_data, ComputeDataWithPaths): + next_paths = list(next_paths) + compute_next = lambda it: compute_data.compute_next(current_step, current_context, it, next_paths) + else: + raise TypeError(f'ComputeData was not of a recognised class: {compute_data.__class__} not in [ComputeData, ComputeDataWithPaths]') next_num_terminal_steps = current_step.next_num_terminal_steps() - next_num_equipment_steps = current_step.next_num_equipment_steps() - return list(NetworkTraceStep( - path, - next_num_terminal_steps, - next_num_equipment_steps, - compute_data.compute_next(current_step, current_context, path) - ) for path in self._next_step_paths(is_in_service, current_step.path)) + for it in next_paths: + data = compute_next(it) + yield NetworkTraceStep(it, next_num_terminal_steps, it.next_num_equipment_steps(current_step.num_equipment_steps), data) + + @classmethod + def Basic(cls, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + return Basic(path_provider, compute_data) + + @classmethod + def Branching(cls, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + return Branching(path_provider, compute_data) - def _next_step_paths(self, is_in_service: CheckInService, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: - next_terminals = self._next_terminals(is_in_service, path) - if len(path.nominal_phase_paths) > 0: - phase_paths = set(it.to_phase for it in path.nominal_phase_paths) +class Basic(NetworkTraceQueueNext, Traversal.QueueNext[NetworkTraceStep[T]], Generic[T]): + def __init__(self, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + super().__init__(path_provider) - for result in (TerminalConnectivityConnected().terminal_connectivity(path.to_terminal, t, phase_paths) for t in next_terminals): - if result.nominal_phase_paths: - yield NetworkTraceStep.Path(path.to_terminal, result.to_terminal, result.nominal_phase_paths) + self._get_next_steps: GetNextSteps = lambda item, context: self.next_trace_steps(item, context, compute_data) + def __iinit__(self, get_next_steps: GetNextSteps): + self._get_next_steps: GetNextSteps = get_next_steps + + def accept(self, item: NetworkTraceStep[T], context: StepContext, queue_item: QueueItem): + for it in self._get_next_steps(item, context): + queue_item(it) + + +class Branching(NetworkTraceQueueNext, Traversal.BranchingQueueNext[NetworkTraceStep[T]], Generic[T]): + def __init__(self, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + super().__init__(path_provider) + + self._get_next_steps: GetNextStepsBranching = lambda item, context: list(self.next_trace_steps(item, context, compute_data)) + + def accept(self, item: NetworkTraceStep[T], context: StepContext, queue_item: QueueItem, queue_branch: QueueBranch): + next_steps = list(self._get_next_steps(item, context)) + if len(next_steps) == 1: + queue_item(next_steps[0]) else: - for terminal in next_terminals: - yield NetworkTraceStep.Path(path.to_terminal, terminal) - - @staticmethod - def _next_terminals(is_in_service: CheckInService, path: NetworkTraceStep.Path) -> Iterable[Terminal]: - def __next_terminals(): - if path.traced_internally: - # We need to step externally to connected terminals. However: - # Busbars are only modelled with a single terminal. So if we find any we need to step to them before the - # other (non busbar) equipment connected to the same connectivity node. Once the busbar has been - # visited we then step to the other non busbar terminals connected to the same connectivity node. - if path.to_terminal.has_connected_busbars(): - return (t for t in path.to_terminal.connected_terminals() if t.conducting_equipment is BusbarSection) - else: - return path.to_terminal.connected_terminals() - - else: - # If we just visited a busbar, we step to the other terminals that share the same connectivity node. - # Otherwise, we internally step to the other terminals on the equipment - if path.to_equipment is BusbarSection: - # We don't need to step to terminals that are busbars as they would have been queued at the same time this busbar step was. - # We also don't try and go back to the terminals we came from as we already visited it to get to this busbar. - return (t for t in path.to_terminal.connected_terminals() if t != path.from_terminal and t.conducting_equipment is not BusbarSection) - else: - return path.to_terminal.other_terminals() - - def _filter(it: Terminal) -> bool: - if it.conducting_equipment: - return is_in_service(it.conducting_equipment) - return False - - return (t for t in __next_terminals() if _filter(t)) + for step in next_steps: + queue_branch(step) + 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 b3f132c00..dc219da32 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 +from typing import Set, Generic, TypeVar, TYPE_CHECKING, Optional from zepben.evolve.services.network.tracing.connectivity.nominal_phase_path import NominalPhasePath @@ -47,8 +47,8 @@ class Path: """ from_terminal: Terminal to_terminal: Terminal - traversed_ac_line_segment: AcLineSegment = field(default=None) - nominal_phase_paths: Set[NominalPhasePath] = field(default_factory=set) + 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]: if len(self.nominal_phase_paths) == 0: @@ -93,6 +93,10 @@ def traced_externally(self) -> bool: def did_traverse_ac_line_segment(self) -> bool: return self.traversed_ac_line_segment is not None + def next_num_equipment_steps(self, current_num: int) -> int: + return current_num + 1 if self.traced_externally else current_num + + Type = Enum('Type', ('ALL', 'INTERNAL', 'EXTERNAL')) def __init__(self, path: Path, num_terminal_steps: int, num_equipment_steps: int, data: T): @@ -113,5 +117,3 @@ def type(self) -> Path: def next_num_terminal_steps(self): return self.num_terminal_steps + 1 - def next_num_equipment_steps(self): - return self.num_equipment_steps + 1 if self.path.traced_internally else self.num_equipment_steps diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index f1a04ece9..fc580728a 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -13,7 +13,7 @@ from zepben.evolve.services.network.tracing.connectivity.terminal_connectivity_connected import TerminalConnectivityConnected from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep -from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import InServiceStateOperators +from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment PathFactory = Callable[[Terminal, AcLineSegment], Optional[NetworkTraceStep.Path]] @@ -21,7 +21,7 @@ class NetworkTraceStepPathProvider: - def __init__(self, state_operators: InServiceStateOperators): + def __init__(self, state_operators: NetworkStateOperators): self.state_operators = state_operators def next_paths(self, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index 2c2753d99..fac69d763 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -13,6 +13,7 @@ class TestNetworkTrace: + @pytest.mark.skip() @pytest.mark.asyncio async def test_can_run_large_branching_traces(self): try: diff --git a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py new file mode 100644 index 000000000..a85f7c41a --- /dev/null +++ b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py @@ -0,0 +1,133 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 TypeVar, List +from unittest.mock import MagicMock + +import pytest + +from services.network.tracing.networktrace.util import mock_nts_path, mock_nts, mock_ctx +from zepben.evolve import ComputeData, NetworkTraceStep, ngen, NetworkStateOperators +from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_next import NetworkTraceQueueNext +from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider + +T = TypeVar('T') + + +class TestQueuer: + def __init__(self): + self.queued: List[NetworkTraceStep[T]] = [] + + def __call__(self, step: NetworkTraceStep[T]) -> bool: + try: + self.queued.append(step) + return True + except: + return False + + +class TestNetworkTraceQueueNext: + + @pytest.fixture(autouse=True) + def setup_method(self): + self.path_provider = MagicMock(NetworkTraceStepPathProvider) + #self.path_provider = NetworkTraceStepPathProvider(NetworkStateOperators.NORMAL) + self.data_computer = MagicMock(ComputeData) + self.queuer = TestQueuer() + self.branching_queuer = TestQueuer() + yield + + def test_queues_next_basic(self): + queue_next = NetworkTraceQueueNext.Basic(self.path_provider, self.data_computer) + + seed_path = mock_nts_path() + seed_step = mock_nts(seed_path, 3, 1) + seed_step.configure_mock( + num_terminal_steps=3, + num_equipment_steps=1, + path = seed_path + ) + + + seed_context = mock_ctx() + + next_path_1 = mock_nts_path(traced_internally=False) + next_path_2 = mock_nts_path(traced_internally=True) + + self.path_provider.next_paths = lambda seed_path: ngen((next_path_1, next_path_2)) + + def mock_computer(seed_step, seed_context, path): + if path is next_path_1: + return "Foo" + elif path is next_path_2: + return "Bar" + + self.data_computer.compute_next = mock_computer + + queue_next.accept(seed_step, seed_context, self.queuer) + + assert len(self.queuer.queued) == 2 + + _assert_step_equal(self.queuer.queued[0], next_path_1, "Foo", 4, 2) + _assert_step_equal(self.queuer.queued[1], next_path_2, "Bar", 4, 1) + + def test_calls_branching_queuer_when_queing_more_then_1_path_on_branching_queue_next(self): + queue_next = NetworkTraceQueueNext.Branching(self.path_provider, self.data_computer) + + seed_path = mock_nts_path() + seed_step = mock_nts(seed_path, 3, 1) + + seed_context = mock_ctx() + + next_path_1 = mock_nts_path(traced_internally=False) + next_path_2 = mock_nts_path(traced_internally=True) + + self.path_provider.next_paths = lambda seed_path: ngen((next_path_1, next_path_2)) + + def mock_computer(seed_step, seed_context, path): + if path is next_path_1: + return "Foo" + elif path is next_path_2: + return "Bar" + + self.data_computer.compute_next = mock_computer + + queue_next.accept(seed_step, seed_context, self.queuer, self.branching_queuer) + + assert len(self.queuer.queued) == 0 + assert len(self.branching_queuer.queued) == 2 + + _assert_step_equal(self.branching_queuer.queued[0], next_path_1, "Foo", 4, 2) + _assert_step_equal(self.branching_queuer.queued[1], next_path_2, "Bar", 4, 1) + + def test_calls_straight_queuer_when_queuing_a_single_path_on_branching_queue_next(self): + queue_next = NetworkTraceQueueNext.Branching(self.path_provider, self.data_computer) + + seed_path = mock_nts_path() + seed_step = mock_nts(seed_path, 3, 1) + + seed_context = mock_ctx() + + next_path_1 = mock_nts_path(traced_internally=False) + + self.path_provider.next_paths = lambda seed_path: ngen([next_path_1]) + + def mock_computer(seed_step, seed_context, path): + if path is next_path_1: + return "Foo" + + self.data_computer.compute_next = mock_computer + + queue_next.accept(seed_step, seed_context, self.queuer, self.branching_queuer) + + assert len(self.queuer.queued) == 1 + assert len(self.branching_queuer.queued) == 0 + + _assert_step_equal(self.queuer.queued[0], next_path_1, "Foo", 4, 2) + +def _assert_step_equal(step: NetworkTraceStep, path: NetworkTraceStep.Path, data, num_term_step, num_equip_step): + assert step.path is path + assert step.data == data + assert step.num_terminal_steps == num_term_step + assert step.num_equipment_steps == num_equip_step diff --git a/test/services/network/tracing/networktrace/util.py b/test/services/network/tracing/networktrace/util.py new file mode 100644 index 000000000..d284b5583 --- /dev/null +++ b/test/services/network/tracing/networktrace/util.py @@ -0,0 +1,41 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 unittest.mock import MagicMock, Mock + +from zepben.evolve import NetworkTraceStep, ConductingEquipment, StepContext + + +def mock_nts(path: NetworkTraceStep.Path=None, + num_terminal_steps=0, + num_equipment_steps=0, + data=None + ): + nts = Mock(wraps=NetworkTraceStep(path, num_terminal_steps, num_equipment_steps, data)) + nts.configure_mock( + num_terminal_steps=3, + num_equipment_steps=1, + path=path + ) + return nts + + +def mock_nts_path(to_equipment: ConductingEquipment=None, + traced_internally: bool=None): + if traced_internally: + terminal = Mock() + next_path = MagicMock(wraps=NetworkTraceStep.Path(terminal, terminal)) + else: + next_path = MagicMock(wraps=NetworkTraceStep.Path(Mock(), Mock())) + + + return next_path + +def mock_ctx(value: int=None): + ctx = MagicMock(spec=StepContext) + if value is not None: + ctx.get_value = lambda key: value + + return ctx + From 5a6f2d8d04125bb3383d94878458adfb38c2d38d Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 21 May 2025 17:02:51 +1000 Subject: [PATCH 08/47] Reworking some internal classes to be less boilerplate Signed-off-by: Max Chesterfield --- .../network/tracing/feeder/set_direction.py | 23 +++++--- .../networktrace/network_trace_queue_next.py | 10 ++-- .../network_trace_step_path_provider.py | 4 +- .../networktrace/network_trace_tracker.py | 3 +- .../traversal/context_value_computer.py | 4 +- .../network/tracing/traversal/traversal.py | 55 ++++++++++--------- 6 files changed, 57 insertions(+), 42 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py index 894bcf4b1..7658b22ef 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py +++ b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py @@ -6,11 +6,10 @@ from typing import Optional, TYPE_CHECKING from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal +from zepben.evolve.model.cim.iec61970.base.core.equipment_container import Feeder from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection from zepben.evolve.model.cim.iec61970.base.wires.power_transformer import PowerTransformer - -from zepben.evolve import Feeder, Traversal -from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData +from zepben.evolve.model.cim.iec61970.base.wires.cut import Cut from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing @@ -42,11 +41,19 @@ def _compute_data(reprocessed_loop_terminals: list[Terminal], direction_applied = step.data - next_direction = FeederDirection.NONE - if direction_applied == FeederDirection.UPSTREAM: - next_direction = FeederDirection.DOWNSTREAM - elif direction_applied in (FeederDirection.DOWNSTREAM, FeederDirection.CONNECTOR): - next_direction = FeederDirection.UPSTREAM + def next_direction_func(): + if step.data == FeederDirection.NONE: + return FeederDirection.NONE + elif next_path.traced_internally: + return FeederDirection.DOWNSTREAM + elif isinstance(next_path.to_equipment, Cut): + return FeederDirection.UPSTREAM + elif next_path.did_traverse_ac_line_segment: + return FeederDirection.DOWNSTREAM + else: + return FeederDirection.UPSTREAM + + next_direction = next_direction_func() # # NOTE: Stopping / short-circuiting by checking that the next direction is already present in the toTerminal, diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index 722fe611d..ab0d228af 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -3,7 +3,7 @@ # 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 abc import ABC -from typing import TypeVar, Callable, Generator, TYPE_CHECKING, Generic, List, Union +from typing import TypeVar, Callable, Generator, Generic, List, Union from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider @@ -47,12 +47,12 @@ def next_trace_steps(self, data = compute_next(it) yield NetworkTraceStep(it, next_num_terminal_steps, it.next_num_equipment_steps(current_step.num_equipment_steps), data) - @classmethod - def Basic(cls, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + @staticmethod + def Basic(path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): return Basic(path_provider, compute_data) - @classmethod - def Branching(cls, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + @staticmethod + def Branching(path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): return Branching(path_provider, compute_data) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index fc580728a..50d1aeeb7 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -10,11 +10,11 @@ from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection from zepben.evolve.model.cim.iec61970.base.wires.cut import Cut -from zepben.evolve.services.network.tracing.connectivity.terminal_connectivity_connected import TerminalConnectivityConnected from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal +from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment +from zepben.evolve.services.network.tracing.connectivity.terminal_connectivity_connected import TerminalConnectivityConnected from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators -from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment PathFactory = Callable[[Terminal, AcLineSegment], Optional[NetworkTraceStep.Path]] 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 46d0f2e62..f14c41728 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 @@ -4,7 +4,8 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from typing import Set, Any -from zepben.evolve import Terminal, SinglePhaseKind +from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal +from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind class NetworkTraceTracker: diff --git a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py index de2f0e426..afa3f0c72 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py +++ b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py @@ -3,7 +3,7 @@ # 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 abc import ABC +from abc import ABC, abstractmethod from typing import TypeVar, Generic from zepben.evolve.services.network.tracing.traversal.step_context import StepContext @@ -23,6 +23,7 @@ class ContextValueComputer(ABC, Generic[T]): def __init__(self, key: str): self.key = key # A unique key identifying the context value computed by this computer. + @abstractmethod def compute_initial_value(self, item: T): """ Computes the initial context value for the given starting item. @@ -32,6 +33,7 @@ def compute_initial_value(self, item: T): """ pass + @abstractmethod def compute_next_value(self, next_item: T, current_item: T, current_value): """ Computes the next context value based on the current item, next item, and the current context value. diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal.py b/src/zepben/evolve/services/network/tracing/traversal/traversal.py index 32a7a2fb2..8abc9428d 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal.py @@ -166,28 +166,30 @@ def create_new_this(self) -> D: """ raise NotImplementedError - def add_condition(self, condition: Union[QueueCondition, Callable[[NetworkTraceStep[T], StepContext], None]]) -> D: + def add_condition(self, condition: QueueCondition | Callable[[NetworkTraceStep[T], StepContext], None]) -> D: """ Adds a traversal condition to the traversal. `condition` The condition to add. Returns this traversal instance. """ - if callable(condition): - if condition.__code__.co_argcount == 2: - return self.add_stop_condition(condition) - elif condition.__code__.co_argcount == 4: - return self.add_queue_condition(condition) - raise RuntimeError(f'Condition does not match expected: Number of args is not 2(Stop Condition) or 4(QueueCondition)') - - assert issubclass(condition.__class__, (QueueCondition, StopCondition, DirectionCondition)) if isinstance(condition, (QueueCondition, DirectionCondition)): return self.add_queue_condition(condition) elif isinstance(condition, StopCondition): return self.add_stop_condition(condition) + elif callable(condition): # Callable[[NetworkTraceStep[T], StepContext], None] + assert not isinstance(condition, TraversalCondition) + if condition.__code__.co_argcount == 2: + return self.add_stop_condition(condition) + elif condition.__code__.co_argcount == 4: + return self.add_queue_condition(condition) + else: + raise RuntimeError(f'Condition does not match expected: Number of args is not 2(Stop Condition) or 4(QueueCondition)') + else: - raise RuntimeError(f'Condition does not match expected: {condition.__class__.__name__}') + raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: ' + + "[QueueCondition | DirectionCondition | StopCondition | Callable[_,_] | Callable[_,_,_,_]]") def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopConditionWithContextValue[T, U]]) -> D: """ @@ -197,15 +199,16 @@ def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopCo `condition` The stop condition to add. Returns this traversal instance. """ - if callable(condition): - return self.add_stop_condition(StopCondition(condition)) - - elif isinstance(condition, StopCondition): + if isinstance(condition, StopCondition): self.stop_conditions.append(condition) if issubclass(condition.__class__, StopConditionWithContextValue): self.compute_next_context_funs[condition.key] = condition return self - raise RuntimeError(f'Condition does not match expected: {condition.__class__.__name__}') + + elif callable(condition): + return self.add_stop_condition(StopCondition(condition)) + + raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [StopCondition | StopConditionWithContextValue | Callable]') def copy_stop_conditions(self, other: Traversal[T, D]) -> D: """ @@ -232,16 +235,17 @@ def add_queue_condition(self, condition: Union[Callable, QueueCondition[T]]) -> :param condition: The queue condition to add. :returns: The current traversal instance. """ - if callable(condition): - return self.add_queue_condition(QueueCondition(condition)) - - elif isinstance(condition, QueueCondition): + if isinstance(condition, QueueCondition): assert issubclass(condition.__class__, QueueCondition) self.queue_conditions.append(condition) if isinstance(condition, QueueConditionWithContextValue): self.compute_next_context_funs[condition.key] = condition return self - raise RuntimeError(f'Condition does not match expected: {condition.__class__.__name__}') + + elif callable(condition): + return self.add_queue_condition(QueueCondition(condition)) + + raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [QueueCondition | QueueConditionWithContextValue | Callable]') def copy_queue_conditions(self, other: Traversal[T, D]) -> D: @@ -262,16 +266,17 @@ def add_step_action(self, action: Union[Callable, StepAction[T]]) -> D: `action` The action to perform on each item. Returns The current traversal instance. """ - if callable(action): - return self.add_step_action(StepAction(action)) - - elif isinstance(action, StepAction): + if isinstance(action, StepAction): assert issubclass(action.__class__, StepAction) or isinstance(action, StepAction) self.step_actions.append(action) if isinstance(action, StepActionWithContextValue): self.compute_next_context_funs[action.key] = action return self - raise RuntimeError(f'Condition does not match expected: {action.__class__.__name__}') + + elif callable(action): + return self.add_step_action(StepAction(action)) + + raise RuntimeError(f'Condition [{action.__class__.__name__}] does not match expected: [StepAction | StepActionWithContextValue | Callable]') def if_not_stopping(self, action: Callable) -> D: """ From 847539e0d25c974a6c1998156913661268fce90a Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 21 May 2025 18:14:05 +1000 Subject: [PATCH 09/47] this class isnt a test... Signed-off-by: Max Chesterfield --- .../tracing/networktrace/test_network_trace_queue_next.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py index a85f7c41a..78bb43648 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py +++ b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py @@ -15,7 +15,7 @@ T = TypeVar('T') -class TestQueuer: +class Queuer: def __init__(self): self.queued: List[NetworkTraceStep[T]] = [] @@ -34,8 +34,8 @@ def setup_method(self): self.path_provider = MagicMock(NetworkTraceStepPathProvider) #self.path_provider = NetworkTraceStepPathProvider(NetworkStateOperators.NORMAL) self.data_computer = MagicMock(ComputeData) - self.queuer = TestQueuer() - self.branching_queuer = TestQueuer() + self.queuer = Queuer() + self.branching_queuer = Queuer() yield def test_queues_next_basic(self): From a972967b22c3ea22da6ae02eed9a3be6b04bd72f Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 21 May 2025 21:30:47 +1000 Subject: [PATCH 10/47] Reworked NetworkStateOperators so auto complete works We're also not creating any instances of the objects anymore. Signed-off-by: Max Chesterfield --- .../model/cim/iec61970/base/core/equipment.py | 4 +- .../iec61970/base/core/equipment_container.py | 2 +- .../tracing/feeder/assign_to_feeders.py | 6 +- .../tracing/feeder/assign_to_lv_feeders.py | 4 +- .../tracing/networktrace/network_trace.py | 8 +- .../network_trace_step_path_provider.py | 4 +- .../networktrace/operators/__init__.py | 5 +- .../equipment_container_state_operators.py | 115 +++++++++++------- .../feeder_direction_state_operations.py | 4 +- .../operators/in_service_state_operators.py | 8 +- .../operators/network_state_operators.py | 82 +++++++------ .../operators/open_state_operators.py | 4 +- .../operators/phase_state_operators.py | 13 +- .../network/tracing/networktrace/tracing.py | 6 +- src/zepben/evolve/util.py | 5 + .../network/test_network_database_schema.py | 2 +- .../test_network_trace_queue_next.py | 3 +- 17 files changed, 160 insertions(+), 115 deletions(-) diff --git a/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py b/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py index aca5a5f3d..3551d5646 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py @@ -64,7 +64,7 @@ def sites(self) -> Generator[Site, None, None]: """ return ngen(_of_type(self._equipment_containers, Site)) - def feeders(self, network_state_operators: NetworkStateOperators) -> Generator[Feeder, None, None]: + def feeders(self, network_state_operators: Type[NetworkStateOperators]) -> Generator[Feeder, None, None]: """ The `Feeder` this equipment belongs too based on `NetworkStateOperators` """ @@ -80,7 +80,7 @@ def normal_feeders(self) -> Generator[Feeder, None, None]: """ return ngen(_of_type(self._equipment_containers, Feeder)) - def lv_feeders(self, network_state_operators: NetworkStateOperators) -> Generator[LvFeeder, None, None]: + def lv_feeders(self, network_state_operators: Type[NetworkStateOperators]) -> Generator[LvFeeder, None, None]: """ The `LvFeeder` this equipment belongs too based on `NetworkStateOperators` """ diff --git a/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py index f0a6d8b0e..692463e23 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py @@ -446,7 +446,7 @@ class Site(EquipmentContainer): Note this is not a CIM concept - however represents an `EquipmentContainer` in CIM. This is to avoid the use of `EquipmentContainer` as a concrete class. """ - def find_lv_feeders(self, lv_feeder_start_points: Iterable[ConductingEquipment], state_operators: NetworkStateOperators) -> Generator[LvFeeder, None, None]: + def find_lv_feeders(self, lv_feeder_start_points: Iterable[ConductingEquipment], state_operators: Type[NetworkStateOperators]) -> Generator[LvFeeder, None, None]: from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment for ce in state_operators.get_equipment(self): if isinstance(ce, ConductingEquipment): diff --git a/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py b/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py index 63a45c35d..cbbc1f12b 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py +++ b/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py @@ -3,7 +3,7 @@ # 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 collections.abc import Collection -from typing import Iterable, Generator, Union, List, Dict, Any, Set +from typing import Iterable, Generator, Union, List, Dict, Any, Set, Type from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch, Equipment, LvFeeder from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment @@ -32,7 +32,7 @@ class AssignToFeeders: @staticmethod async def run(network: NetworkService, - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL, + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL, start_terminal: Terminal=None): """ Assign equipment to feeders in the specified network, given an optional start terminal. @@ -47,7 +47,7 @@ async def run(network: NetworkService, class BaseFeedersInternal: - def __init__(self, network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + def __init__(self, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): self.network_state_operators = network_state_operators def _feeders_from_terminal(self, terminal: Terminal): diff --git a/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py b/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py index 5c6ac8099..ee0c300a8 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py +++ b/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py @@ -3,7 +3,7 @@ # 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 Collection, List, Generator, TypeVar, Dict, Set +from typing import Collection, List, Generator, TypeVar, Dict, Set, Type from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment @@ -26,7 +26,7 @@ class AssignToLvFeeders: @staticmethod async def run(network: NetworkService, - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL, + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL, start_terminal: Terminal=None): await AssignToLvFeedersInternal(network_state_operators).run(network, start_terminal) 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 6c205fd30..c63c8ba95 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -3,7 +3,7 @@ # 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 collections.abc import Callable -from typing import TypeVar, Union, Generic, Set +from typing import TypeVar, Union, Generic, Set, Type from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode @@ -67,7 +67,7 @@ class NetworkTrace(Traversal[NetworkTraceStep[T], 'NetworkTrace[T]'], Generic[T] """ def __init__(self, - network_state_operators: NetworkStateOperators, + network_state_operators: Type[NetworkStateOperators], queue_type: Union[Traversal.BasicQueueType, Traversal.BranchingQueueType], parent: 'NetworkTrace[T]'=None, action_type: NetworkTraceActionType=None @@ -83,7 +83,7 @@ def __init__(self, @classmethod def non_branching(cls, - network_state_operators: NetworkStateOperators, + network_state_operators: Type[NetworkStateOperators], queue: TraversalQueue[NetworkTraceStep[T]], action_type: NetworkTraceActionType, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]] @@ -98,7 +98,7 @@ def non_branching(cls, @classmethod def branching(cls, - network_state_operators: NetworkStateOperators, + network_state_operators: Type[NetworkStateOperators], queue_factory: Callable[[], TraversalQueue[T]], branch_queue_factory: Callable[[], TraversalQueue['NetworkTrace[T]']], action_type: NetworkTraceActionType, diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index 50d1aeeb7..4610e1a36 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -5,7 +5,7 @@ import itertools import sys -from typing import Generator, Optional, Callable, Iterable, List, Union +from typing import Generator, Optional, Callable, Iterable, List, Union, Type from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection @@ -21,7 +21,7 @@ class NetworkTraceStepPathProvider: - def __init__(self, state_operators: NetworkStateOperators): + def __init__(self, state_operators: Type[NetworkStateOperators]): self.state_operators = state_operators def next_paths(self, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/__init__.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/__init__.py index 4a9128eb9..81a4bae68 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/__init__.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/__init__.py @@ -8,4 +8,7 @@ class StateOperator(ABC): NORMAL = None - CURRENT = None \ No newline at end of file + CURRENT = None + + def __init__(self): + raise TypeError('Any class subclassing (StateOperators) should not be instantiated or have state.') diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py index d4b1dbfe6..6fe407495 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py @@ -21,8 +21,9 @@ class EquipmentContainerStateOperators(StateOperator): Defines operations for managing relationships between [Equipment] and [EquipmentContainer]. """ + @staticmethod @abstractmethod - def get_equipment(self, container: EquipmentContainer) -> Generator[Equipment, None, None]: + def get_equipment(container: EquipmentContainer) -> Generator[Equipment, None, None]: """ Get the collection of equipment associated with the given container. @@ -31,8 +32,9 @@ def get_equipment(self, container: EquipmentContainer) -> Generator[Equipment, N """ pass + @staticmethod @abstractmethod - def get_containers(self, equipment: Equipment) -> Generator[EquipmentContainer, None, None]: + def get_containers(equipment: Equipment) -> Generator[EquipmentContainer, None, None]: """ Retrieves a collection of containers associated with the given equipment. @@ -41,8 +43,9 @@ def get_containers(self, equipment: Equipment) -> Generator[EquipmentContainer, """ pass + @staticmethod @abstractmethod - def get_energizing_feeders(self, lv_feeder: LvFeeder) -> Generator[Feeder, None, None]: + def get_energizing_feeders(lv_feeder: LvFeeder) -> Generator[Feeder, None, None]: """ Retrieves a collection of feeders that energize the given LV feeder. @@ -51,8 +54,9 @@ def get_energizing_feeders(self, lv_feeder: LvFeeder) -> Generator[Feeder, None """ pass + @staticmethod @abstractmethod - def get_energized_lv_feeders(self, feeder: Feeder) -> Generator[LvFeeder, None, None]: + def get_energized_lv_feeders(feeder: Feeder) -> Generator[LvFeeder, None, None]: """ Retrieves a collection of LV feeders energized by the given feeder. @@ -61,8 +65,9 @@ def get_energized_lv_feeders(self, feeder: Feeder) -> Generator[LvFeeder, None, """ pass + @staticmethod @abstractmethod - def add_equipment_to_container(self, equipment: Equipment, container: EquipmentContainer): + def add_equipment_to_container(equipment: Equipment, container: EquipmentContainer): """ Adds the specified equipment to the given container. @@ -71,8 +76,9 @@ def add_equipment_to_container(self, equipment: Equipment, container: EquipmentC """ pass + @staticmethod @abstractmethod - def add_container_to_equipment(self, container: EquipmentContainer, equipment: Equipment): + def add_container_to_equipment(container: EquipmentContainer, equipment: Equipment): """ Adds the specified container to the given equipment. @@ -81,18 +87,20 @@ def add_container_to_equipment(self, container: EquipmentContainer, equipment: E """ pass - def associate_equipment_and_container(self, equipment: Equipment, container: EquipmentContainer): + @classmethod + def associate_equipment_and_container(cls, equipment: Equipment, container: EquipmentContainer): """ Establishes a bidirectional association between the specified equipment and container. `equipment` The equipment to associate with the container. `container` The container to associate with the equipment. """ - self.add_equipment_to_container(equipment, container) - self.add_container_to_equipment(container, equipment) + cls.add_equipment_to_container(equipment, container) + cls.add_container_to_equipment(container, equipment) + @staticmethod @abstractmethod - def remove_equipment_from_container(self, equipment: Equipment, container: EquipmentContainer): + def remove_equipment_from_container(equipment: Equipment, container: EquipmentContainer): """ Removes the specified equipment from the given container. @@ -101,8 +109,9 @@ def remove_equipment_from_container(self, equipment: Equipment, container: Equip """ pass + @staticmethod @abstractmethod - def remove_container_from_equipment(self, container: EquipmentContainer, equipment: Equipment): + def remove_container_from_equipment(container: EquipmentContainer, equipment: Equipment): """ Removes the specified container from the given equipment. @@ -111,18 +120,20 @@ def remove_container_from_equipment(self, container: EquipmentContainer, equipme """ pass - def disassociate_equipment_and_container(self, equipment: Equipment, container: EquipmentContainer): + @classmethod + def disassociate_equipment_and_container(cls, equipment: Equipment, container: EquipmentContainer): """ Remove a bidirectional association between the specified equipment and container. `equipment` The equipment to disassociate with the container. `container` The container to disassociate with the equipment. """ - self.remove_equipment_from_container(equipment, container) - self.remove_container_from_equipment(container, equipment) + cls.remove_equipment_from_container(equipment, container) + cls.remove_container_from_equipment(container, equipment) + @staticmethod @abstractmethod - def add_energizing_feeder_to_lv_feeder(self, feeder: Feeder, lv_feeder: LvFeeder): + def add_energizing_feeder_to_lv_feeder(feeder: Feeder, lv_feeder: LvFeeder): """ Adds the specified energizing feeder to the given lvFeeder. @@ -131,8 +142,9 @@ def add_energizing_feeder_to_lv_feeder(self, feeder: Feeder, lv_feeder: LvFeeder """ pass + @staticmethod @abstractmethod - def add_energizing_lv_feeder_to_feeder(self, lv_feeder: LvFeeder, feeder: Feeder): + def add_energizing_lv_feeder_to_feeder(lv_feeder: LvFeeder, feeder: Feeder): """ Adds the specified energized lvFeeder to the given feeder. @@ -141,49 +153,60 @@ def add_energizing_lv_feeder_to_feeder(self, lv_feeder: LvFeeder, feeder: Feeder """ pass - def associate_energizing_feeder(self, feeder: Feeder, lv_feeder: LvFeeder): + @classmethod + def associate_energizing_feeder(cls, feeder: Feeder, lv_feeder: LvFeeder): """ Establishes a bidirectional association between the specified feeder and LV feeder. `feeder` The feeder energizing the lv feeder. `lvFeeder` The lv feeder energized by the feeder. """ - self.add_energizing_feeder_to_lv_feeder(feeder, lv_feeder) - self.add_energizing_lv_feeder_to_feeder(lv_feeder, feeder) + cls.add_energizing_feeder_to_lv_feeder(feeder, lv_feeder) + cls.add_energizing_lv_feeder_to_feeder(lv_feeder, feeder) class NormalEquipmentContainerStateOperators(EquipmentContainerStateOperators): """ Operates on the normal network state equipment-container relationships """ - def get_equipment(self, container: EquipmentContainer) -> Generator[Equipment, None, None]: + @staticmethod + def get_equipment(container: EquipmentContainer) -> Generator[Equipment, None, None]: return container.equipment - def get_containers(self, equipment: Equipment) -> Generator[EquipmentContainer, None, None]: + @staticmethod + def get_containers(equipment: Equipment) -> Generator[EquipmentContainer, None, None]: return equipment.containers - def get_energizing_feeders(self, lv_feeder: LvFeeder) -> Generator[Feeder, None, None]: + @staticmethod + def get_energizing_feeders(lv_feeder: LvFeeder) -> Generator[Feeder, None, None]: return lv_feeder.normal_energizing_feeders - def get_energized_lv_feeders(self, feeder: Feeder) -> Generator[LvFeeder, None, None]: + @staticmethod + def get_energized_lv_feeders(feeder: Feeder) -> Generator[LvFeeder, None, None]: return feeder.normal_energized_lv_feeders - def add_equipment_to_container(self, equipment: Equipment, container: EquipmentContainer): + @staticmethod + def add_equipment_to_container(equipment: Equipment, container: EquipmentContainer): container.add_equipment(equipment) - def add_container_to_equipment(self, container: EquipmentContainer, equipment: Equipment): + @staticmethod + def add_container_to_equipment(container: EquipmentContainer, equipment: Equipment): equipment.add_container(container) - def remove_equipment_from_container(self, equipment: Equipment, container: EquipmentContainer): + @staticmethod + def remove_equipment_from_container(equipment: Equipment, container: EquipmentContainer): container.remove_equipment(equipment) - def remove_container_from_equipment(self, container: EquipmentContainer, equipment: Equipment): + @staticmethod + def remove_container_from_equipment(container: EquipmentContainer, equipment: Equipment): equipment.remove_container(container) - def add_energizing_feeder_to_lv_feeder(self, feeder: Feeder, lv_feeder: LvFeeder): + @staticmethod + def add_energizing_feeder_to_lv_feeder(feeder: Feeder, lv_feeder: LvFeeder): lv_feeder.add_normal_energizing_feeder(feeder) - def add_energizing_lv_feeder_to_feeder(self, lv_feeder: LvFeeder, feeder: Feeder): + @staticmethod + def add_energizing_lv_feeder_to_feeder(lv_feeder: LvFeeder, feeder: Feeder): feeder.add_normal_energized_lv_feeder(lv_feeder) @@ -191,36 +214,46 @@ class CurrentEquipmentContainerStateOperators(EquipmentContainerStateOperators): """ Operates on the current network state equipment-container relationships """ - def get_equipment(self, container: EquipmentContainer) -> Generator[Equipment, None, None]: + @staticmethod + def get_equipment(container: EquipmentContainer) -> Generator[Equipment, None, None]: return container.current_equipment - def get_containers(self, equipment: Equipment) -> Generator[EquipmentContainer, None, None]: + @staticmethod + def get_containers(equipment: Equipment) -> Generator[EquipmentContainer, None, None]: return equipment.current_containers - def get_energizing_feeders(self, lv_feeder: LvFeeder) -> Generator[Feeder, None, None]: + @staticmethod + def get_energizing_feeders(lv_feeder: LvFeeder) -> Generator[Feeder, None, None]: return lv_feeder.current_energizing_feeders - def get_energized_lv_feeders(self, feeder: Feeder) -> Generator[LvFeeder, None, None]: + @staticmethod + def get_energized_lv_feeders(feeder: Feeder) -> Generator[LvFeeder, None, None]: return feeder.current_energized_lv_feeders - def add_equipment_to_container(self, equipment: Equipment, container: EquipmentContainer): + @staticmethod + def add_equipment_to_container(equipment: Equipment, container: EquipmentContainer): container.add_current_equipment(equipment) - def add_container_to_equipment(self, container: EquipmentContainer, equipment: Equipment): + @staticmethod + def add_container_to_equipment(container: EquipmentContainer, equipment: Equipment): equipment.add_current_container(container) - def remove_equipment_from_container(self, equipment: Equipment, container: EquipmentContainer): + @staticmethod + def remove_equipment_from_container(equipment: Equipment, container: EquipmentContainer): container.remove_current_equipment(equipment) - def remove_container_from_equipment(self, container: EquipmentContainer, equipment: Equipment): + @staticmethod + def remove_container_from_equipment(container: EquipmentContainer, equipment: Equipment): equipment.remove_current_container(container) - def add_energizing_feeder_to_lv_feeder(self, feeder: Feeder, lv_feeder: LvFeeder): + @staticmethod + def add_energizing_feeder_to_lv_feeder(feeder: Feeder, lv_feeder: LvFeeder): lv_feeder.add_current_energizing_feeder(feeder) - def add_energizing_lv_feeder_to_feeder(self, lv_feeder: LvFeeder, feeder: Feeder): + @staticmethod + def add_energizing_lv_feeder_to_feeder(lv_feeder: LvFeeder, feeder: Feeder): feeder.add_current_energized_lv_feeder(lv_feeder) -EquipmentContainerStateOperators.NORMAL = NormalEquipmentContainerStateOperators() -EquipmentContainerStateOperators.CURRENT = CurrentEquipmentContainerStateOperators() +EquipmentContainerStateOperators.NORMAL = NormalEquipmentContainerStateOperators +EquipmentContainerStateOperators.CURRENT = CurrentEquipmentContainerStateOperators diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py index 2e5bd3f9f..0dcbd8e8a 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py @@ -152,5 +152,5 @@ def remove_direction(terminal: Terminal, direction: FeederDirection) -> bool: terminal.current_feeder_direction = new return True -FeederDirectionStateOperations.NORMAL = NormalFeederDirectionStateOperations() -FeederDirectionStateOperations.CURRENT = CurrentFeederDirectionStateOperations() +FeederDirectionStateOperations.NORMAL = NormalFeederDirectionStateOperations +FeederDirectionStateOperations.CURRENT = CurrentFeederDirectionStateOperations diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py index d17cfa59b..be1908447 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py @@ -52,7 +52,7 @@ def is_in_service(equipment: Equipment): return equipment.normally_in_service @staticmethod - def set_in_service(equipment: Equipment, in_service: bool) -> bool: + def set_in_service(equipment: Equipment, in_service: bool) -> None: equipment.normally_in_service = in_service @@ -65,8 +65,8 @@ def is_in_service(equipment: Equipment): return equipment.in_service @staticmethod - def set_in_service(equipment: Equipment, in_service: bool) -> bool: + def set_in_service(equipment: Equipment, in_service: bool) -> None: equipment.in_service = in_service -InServiceStateOperators.NORMAL = NormalInServiceStateOperators() -InServiceStateOperators.CURRENT = CurrentInServiceStateOperators() \ No newline at end of file +InServiceStateOperators.NORMAL = NormalInServiceStateOperators +InServiceStateOperators.CURRENT = CurrentInServiceStateOperators \ No newline at end of file diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py index d8ea15d46..8efb22ddb 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py @@ -2,17 +2,28 @@ # 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 abc import abstractmethod +from typing import Type -from abc import ABC +from zepben.evolve.services.network.tracing.networktrace.operators.equipment_container_state_operators import EquipmentContainerStateOperators, \ + NormalEquipmentContainerStateOperators, CurrentEquipmentContainerStateOperators +from zepben.evolve.services.network.tracing.networktrace.operators.feeder_direction_state_operations import FeederDirectionStateOperations, \ + NormalFeederDirectionStateOperations, CurrentFeederDirectionStateOperations +from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import InServiceStateOperators, NormalInServiceStateOperators, \ + CurrentInServiceStateOperators +from zepben.evolve.services.network.tracing.networktrace.operators.open_state_operators import OpenStateOperators, NormalOpenStateOperators, \ + CurrentOpenStateOperators +from zepben.evolve.services.network.tracing.networktrace.operators.phase_state_operators import PhaseStateOperators, NormalPhaseStateOperators, \ + CurrentPhaseStateOperators -from zepben.evolve.services.network.tracing.networktrace.operators.equipment_container_state_operators import EquipmentContainerStateOperators -from zepben.evolve.services.network.tracing.networktrace.operators.feeder_direction_state_operations import FeederDirectionStateOperations -from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import InServiceStateOperators -from zepben.evolve.services.network.tracing.networktrace.operators.open_state_operators import OpenStateOperators -from zepben.evolve.services.network.tracing.networktrace.operators.phase_state_operators import PhaseStateOperators +from zepben.evolve.util import classproperty -class NetworkStateOperators(ABC): +class NetworkStateOperators(OpenStateOperators, + FeederDirectionStateOperations, + EquipmentContainerStateOperators, + InServiceStateOperators, + PhaseStateOperators): """ Interface providing access to and operations on specific network state properties and functions for items within a network. This interface consolidates several other state operator interfaces, enabling unified management of operations for a network state. @@ -26,56 +37,47 @@ class NetworkStateOperators(ABC): By using this interface, you can apply identical conditions and steps without needing to track which state is active or creating redundant trace implementations for different network states. """ - _operators = [] - def __getattribute__(self, item): - """ - This allows NetworkStateOperators to implement the functions (and accidentally, the attributes) of any class in _operators - if its not present in this object + @classproperty + def NORMAL(cls) -> Type['NormalNetworkStateOperators']: + return NormalNetworkStateOperators - TODO: this is functional, but not optimal and can be made smarter and faster. - """ - try: - return super().__getattribute__(item) - except AttributeError as e: - for operator in self._operators: - if hasattr(operator, item): - return operator.__getattribute__(item) - raise e + @classproperty + def CURRENT(cls) -> Type['CurrentNetworkStateOperators']: + return CurrentNetworkStateOperators + + @staticmethod + @abstractmethod + def condition(): + pass -class NormalNetworkStateOperators(NetworkStateOperators): +class NormalNetworkStateOperators(NetworkStateOperators, + NormalOpenStateOperators, + NormalFeederDirectionStateOperations, + NormalEquipmentContainerStateOperators, + NormalInServiceStateOperators, + NormalPhaseStateOperators): """ Instance that operates on the normal state of network objects. """ - _operators = [ - OpenStateOperators.NORMAL, - FeederDirectionStateOperations.NORMAL, - EquipmentContainerStateOperators.NORMAL, - InServiceStateOperators.NORMAL, - PhaseStateOperators.NORMAL - ] @staticmethod def condition(): return NetworkStateOperators.NORMAL -class CurrentNetworkStateOperators(NetworkStateOperators): + +class CurrentNetworkStateOperators(NetworkStateOperators, + CurrentOpenStateOperators, + CurrentFeederDirectionStateOperations, + CurrentEquipmentContainerStateOperators, + CurrentInServiceStateOperators, + CurrentPhaseStateOperators): """ Instance that operates on the current state of network objects. """ - _operators = [ - OpenStateOperators.CURRENT, - FeederDirectionStateOperations.CURRENT, - EquipmentContainerStateOperators.CURRENT, - InServiceStateOperators.CURRENT, - PhaseStateOperators.CURRENT, - ] @staticmethod def condition(): return NetworkStateOperators.CURRENT - -NetworkStateOperators.NORMAL = NormalNetworkStateOperators() -NetworkStateOperators.CURRENT = CurrentNetworkStateOperators() diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py index 693851500..21ffbd178 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py @@ -96,5 +96,5 @@ def set_open(switch: Switch, is_open: bool, phase: SinglePhaseKind = None) -> No switch.set_open(is_open, phase) -OpenStateOperators.NORMAL = NormalOpenStateOperators() -OpenStateOperators.CURRENT = CurrentOpenStateOperators() +OpenStateOperators.NORMAL = NormalOpenStateOperators +OpenStateOperators.CURRENT = CurrentOpenStateOperators diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py index a9989b15b..3eb3f4883 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py @@ -18,8 +18,9 @@ class PhaseStateOperators(StateOperator): Interface for accessing the phase status of a terminal. """ + @staticmethod @abstractmethod - def phase_status(self, terminal: 'Terminal') -> PhaseStatus: + def phase_status(terminal: 'Terminal') -> PhaseStatus: """ Retrieves the phase status of the specified terminal. @@ -33,7 +34,8 @@ class NormalPhaseStateOperators(PhaseStateOperators): """ Operates on the normal state of the `Phase` """ - def phase_status(self, terminal: 'Terminal') -> PhaseStatus: + @staticmethod + def phase_status(terminal: 'Terminal') -> PhaseStatus: return terminal.normal_phases @@ -41,9 +43,10 @@ class CurrentPhaseStateOperators(PhaseStateOperators): """ Operates on the current state of the `Phase` """ - def phase_status(self, terminal: 'Terminal') -> PhaseStatus: + @staticmethod + def phase_status(terminal: 'Terminal') -> PhaseStatus: return terminal.current_phases -PhaseStateOperators.NORMAL = NormalPhaseStateOperators() -PhaseStateOperators.CURRENT = CurrentPhaseStateOperators() \ No newline at end of file +PhaseStateOperators.NORMAL = NormalPhaseStateOperators +PhaseStateOperators.CURRENT = CurrentPhaseStateOperators \ No newline at end of file diff --git a/src/zepben/evolve/services/network/tracing/networktrace/tracing.py b/src/zepben/evolve/services/network/tracing/networktrace/tracing.py index 37a08009e..94870113d 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/tracing.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/tracing.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 TypeVar, Union, Callable +from typing import TypeVar, Union, Callable, Type from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData, ComputeDataWithPaths from zepben.evolve.services.network.tracing.networktrace.network_trace import NetworkTrace @@ -16,7 +16,7 @@ class Tracing: @staticmethod - def network_trace(network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL, + def network_trace(network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL, action_step_type: NetworkTraceActionType=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT, queue: TraversalQueue[NetworkTraceStep[T]]=TraversalQueue.depth_first(), compute_data: Union[ComputeData[T], Callable]=None @@ -37,7 +37,7 @@ def network_trace(network_state_operators: NetworkStateOperators=NetworkStateOpe return NetworkTrace.non_branching(network_state_operators, queue, action_step_type, compute_data) @staticmethod - def network_trace_branching(network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL, + def network_trace_branching(network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL, action_step_type: NetworkTraceActionType=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT, queue_factory: Callable[[], TraversalQueue[NetworkTraceStep[T]]]=lambda: TraversalQueue.depth_first(), branch_queue_factory: Callable[[], TraversalQueue[NetworkTrace[NetworkTraceStep[T]]]]=lambda: TraversalQueue.breadth_first(), diff --git a/src/zepben/evolve/util.py b/src/zepben/evolve/util.py index a42f3398b..7383e6fbd 100644 --- a/src/zepben/evolve/util.py +++ b/src/zepben/evolve/util.py @@ -167,6 +167,11 @@ def copy(): return str(UUID(bytes=os.urandom(16), version=4)) +class classproperty(property): + def __get__(self, cls, owner: T) -> T: + return classmethod(self.fget).__get__(None, owner)() + + def datetime_to_timestamp(date_time: datetime) -> PBTimestamp: timestamp = PBTimestamp() timestamp.FromDatetime(date_time) diff --git a/test/database/sqlite/network/test_network_database_schema.py b/test/database/sqlite/network/test_network_database_schema.py index 231276469..7b93fc358 100644 --- a/test/database/sqlite/network/test_network_database_schema.py +++ b/test/database/sqlite/network/test_network_database_schema.py @@ -497,7 +497,7 @@ async def test_schema_energy_source(self, energy_source): # Need to apply phases to match after the database load. network_service = SchemaNetworks().network_services_of(EnergySource, energy_source) - await Tracing.set_phases().run(network_service, NetworkStateOperators) + await Tracing.set_phases().run(network_service) await self._validate_schema(network_service) diff --git a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py index 78bb43648..90d1a9fde 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py +++ b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py @@ -8,7 +8,7 @@ import pytest from services.network.tracing.networktrace.util import mock_nts_path, mock_nts, mock_ctx -from zepben.evolve import ComputeData, NetworkTraceStep, ngen, NetworkStateOperators +from zepben.evolve import ComputeData, NetworkTraceStep, ngen from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_next import NetworkTraceQueueNext from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider @@ -32,7 +32,6 @@ class TestNetworkTraceQueueNext: @pytest.fixture(autouse=True) def setup_method(self): self.path_provider = MagicMock(NetworkTraceStepPathProvider) - #self.path_provider = NetworkTraceStepPathProvider(NetworkStateOperators.NORMAL) self.data_computer = MagicMock(ComputeData) self.queuer = Queuer() self.branching_queuer = Queuer() From 44e083dc65ebe4b97bd3be2e050ac4902c14b9a2 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 21 May 2025 22:15:38 +1000 Subject: [PATCH 11/47] I broke some type hinting, and created circular imports. #winning Signed-off-by: Max Chesterfield --- .../model/cim/iec61970/base/core/equipment.py | 8 ++-- .../network/tracing/busbranch_trace.py | 2 +- .../network_trace_queue_condition.py | 1 + .../networktrace/network_trace_queue_next.py | 8 ++-- .../network_trace_step_path_provider.py | 38 ++++++++++------- .../operators/network_state_operators.py | 42 ++++++++++++++----- src/zepben/evolve/util.py | 4 +- .../test_network_trace_queue_next.py | 1 + 8 files changed, 68 insertions(+), 36 deletions(-) diff --git a/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py b/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py index 3551d5646..6e3e436b8 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/evolve/model/cim/iec61970/base/core/equipment.py @@ -9,7 +9,7 @@ from typing import Optional, Generator, List, TYPE_CHECKING, TypeVar, Type if TYPE_CHECKING: - from zepben.evolve import UsagePoint, EquipmentContainer, OperationalRestriction + from zepben.evolve import UsagePoint, EquipmentContainer, OperationalRestriction, NetworkStateOperators TEquipmentContainer = TypeVar("TEquipmentContainer", bound=EquipmentContainer) from zepben.evolve.model.cim.iec61970.base.core.equipment_container import Feeder, Site @@ -17,12 +17,10 @@ from zepben.evolve.model.cim.iec61970.base.core.substation import Substation from zepben.evolve.model.cim.iec61970.infiec61970.feeder.lv_feeder import LvFeeder from zepben.evolve.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators __all__ = ['Equipment'] - class Equipment(PowerSystemResource): """ Abstract class, should only be used through subclasses. @@ -68,7 +66,7 @@ def feeders(self, network_state_operators: Type[NetworkStateOperators]) -> Gener """ The `Feeder` this equipment belongs too based on `NetworkStateOperators` """ - if network_state_operators == NetworkStateOperators.NORMAL: + if network_state_operators.NORMAL: return self.normal_feeders else: return self.current_feeders @@ -84,7 +82,7 @@ def lv_feeders(self, network_state_operators: Type[NetworkStateOperators]) -> Ge """ The `LvFeeder` this equipment belongs too based on `NetworkStateOperators` """ - if network_state_operators == NetworkStateOperators.NORMAL: + if network_state_operators.NORMAL: return self.normal_lv_feeders else: return self.current_lv_feeders diff --git a/src/zepben/evolve/services/network/tracing/busbranch_trace.py b/src/zepben/evolve/services/network/tracing/busbranch_trace.py index 0468ea944..a3b2a7d63 100644 --- a/src/zepben/evolve/services/network/tracing/busbranch_trace.py +++ b/src/zepben/evolve/services/network/tracing/busbranch_trace.py @@ -25,7 +25,7 @@ def __init__(self, queue_next: Traversal.QueueNext): ) super().__init__(queue_type) - def on_reset(self): + def on_reset(self) -> None: self._tracker.clear() def can_visit_item(self, item: BusBranchTraceStep, context: StepContext) -> bool: diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py index ecc838813..0a6df570d 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py @@ -2,6 +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 TypeVar, Generic from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index ab0d228af..e2047a393 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -2,18 +2,18 @@ # 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 abc import ABC from typing import TypeVar, Callable, Generator, Generic, List, Union -from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider - from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.traversal.traversal import Traversal +from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData, ComputeDataWithPaths +from zepben.evolve.services.network.tracing.traversal.step_context import StepContext +from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider T = TypeVar('T') -from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData, ComputeDataWithPaths -from zepben.evolve.services.network.tracing.traversal.step_context import StepContext QueueItem = Callable[[NetworkTraceStep[T]], bool] QueueBranch = Callable[[NetworkTraceStep[T]], bool] GetNextSteps = Callable[[NetworkTraceStep[T], StepContext], Generator[NetworkTraceStep[T], None, None]] diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index 4610e1a36..3c3e7f5b2 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -3,9 +3,11 @@ # 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 __future__ import annotations + import itertools import sys -from typing import Generator, Optional, Callable, Iterable, List, Union, Type +from typing import Generator, Optional, Callable, Iterable, List, Union, Type, TYPE_CHECKING from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection @@ -14,10 +16,13 @@ from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment from zepben.evolve.services.network.tracing.connectivity.terminal_connectivity_connected import TerminalConnectivityConnected from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep -from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators -PathFactory = Callable[[Terminal, AcLineSegment], Optional[NetworkTraceStep.Path]] +if TYPE_CHECKING: + from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators +__all__ = ['NetworkTraceStepPathProvider'] + +PathFactory = Callable[[Terminal, AcLineSegment], Optional[NetworkTraceStep.Path]] class NetworkTraceStepPathProvider: @@ -45,12 +50,14 @@ def _get_next_paths(): return (p for p in _get_next_paths() if p and self.state_operators.is_in_service(p.to_terminal.conducting_equipment)) - def _create_path_factory(self, path: NetworkTraceStep.Path) -> PathFactory: + @staticmethod + def _create_path_factory(path: NetworkTraceStep.Path) -> PathFactory: def path_factory(next_terminal: Terminal, traversed: AcLineSegment) -> NetworkTraceStep.Path: return NetworkTraceStep.Path(path.to_terminal, next_terminal, traversed) return path_factory - def _create_path_with_phases_factory(self, path: NetworkTraceStep.Path) -> PathFactory: + @staticmethod + def _create_path_with_phases_factory(path: NetworkTraceStep.Path) -> PathFactory: phase_paths = set(p.to_phase for p in path.nominal_phase_paths) next_from_terminal = path.to_terminal @@ -62,8 +69,8 @@ def path_factory(next_terminal: Terminal, traversed: AcLineSegment): def _next_paths_from_ac_line_segment(self, segment: AcLineSegment, path: NetworkTraceStep.Path, path_factory: PathFactory ) -> Generator[NetworkTraceStep.Path, None, None]: - #If the current path traversed the segment, we need to step externally from the segment terminal. - #Otherwise, we traverse the segment + # If the current path traversed the segment, we need to step externally from the segment terminal. + # Otherwise, we traverse the segment if path.traced_internally or path.did_traverse_ac_line_segment: return self._next_external_paths(path, path_factory) else: @@ -84,13 +91,16 @@ def _next_paths_from_ac_line_segment(self, segment: AcLineSegment, path: Network cut_at_same_position_from_terminal_number=2, path_factory=path_factory) - def _next_paths_from_busbar(self, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: - return seq_term_map_to_path((t for t in path.to_terminal.connected_terminals() - # We don't go back to the terminal we came from as we already visited it to get to this busbar. - if t != path.from_terminal - # We don't step to terminals that are busbars as they would have been returned at the same time this busbar step was. - and not isinstance(t.conducting_equipment, BusbarSection)) - , path_factory) + @staticmethod + def _next_paths_from_busbar(path: NetworkTraceStep.Path, path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: + return seq_term_map_to_path( + (t for t in path.to_terminal.connected_terminals() + # We don't go back to the terminal we came from as we already visited it to get to this busbar. + if t != path.from_terminal + # We don't step to terminals that are busbars as they would have been returned at the same time this busbar step was. + and not isinstance(t.conducting_equipment, BusbarSection) + ), path_factory + ) def _next_paths_from_clamp(self, clamp: Clamp, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Iterable[NetworkTraceStep.Path]: # If the current path was from traversing an AcLineSegment, we need to step externally to other equipment. diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py index 8efb22ddb..50cc6ef00 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py @@ -2,9 +2,15 @@ # 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 __future__ import annotations + from abc import abstractmethod -from typing import Type +from functools import lru_cache +from typing import Type, Generator, TYPE_CHECKING +from zepben.evolve.util import classproperty +from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider from zepben.evolve.services.network.tracing.networktrace.operators.equipment_container_state_operators import EquipmentContainerStateOperators, \ NormalEquipmentContainerStateOperators, CurrentEquipmentContainerStateOperators from zepben.evolve.services.network.tracing.networktrace.operators.feeder_direction_state_operations import FeederDirectionStateOperations, \ @@ -16,9 +22,11 @@ from zepben.evolve.services.network.tracing.networktrace.operators.phase_state_operators import PhaseStateOperators, NormalPhaseStateOperators, \ CurrentPhaseStateOperators -from zepben.evolve.util import classproperty +if TYPE_CHECKING: + from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep +# noinspection PyPep8Naming class NetworkStateOperators(OpenStateOperators, FeederDirectionStateOperations, EquipmentContainerStateOperators, @@ -46,9 +54,9 @@ def NORMAL(cls) -> Type['NormalNetworkStateOperators']: def CURRENT(cls) -> Type['CurrentNetworkStateOperators']: return CurrentNetworkStateOperators - @staticmethod + @classmethod @abstractmethod - def condition(): + def next_paths(cls, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: pass @@ -62,10 +70,17 @@ class NormalNetworkStateOperators(NetworkStateOperators, Instance that operates on the normal state of network objects. """ - @staticmethod - def condition(): - return NetworkStateOperators.NORMAL + CURRENT = False + NORMAL = True + + @classmethod + @lru_cache + def network_trace_step_path_provider(cls): + return NetworkTraceStepPathProvider(cls) + @classmethod + def next_paths(cls, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: + yield from cls.network_trace_step_path_provider().next_paths(path) class CurrentNetworkStateOperators(NetworkStateOperators, CurrentOpenStateOperators, @@ -77,7 +92,14 @@ class CurrentNetworkStateOperators(NetworkStateOperators, Instance that operates on the current state of network objects. """ - @staticmethod - def condition(): - return NetworkStateOperators.CURRENT + CURRENT = True + NORMAL = False + + @classmethod + @lru_cache + def network_trace_step_path_provider(cls): + return NetworkTraceStepPathProvider(cls) + @classmethod + def next_paths(cls, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: + yield from cls.network_trace_step_path_provider().next_paths(path) diff --git a/src/zepben/evolve/util.py b/src/zepben/evolve/util.py index 7383e6fbd..0d958d62a 100644 --- a/src/zepben/evolve/util.py +++ b/src/zepben/evolve/util.py @@ -1,4 +1,4 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # 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/. @@ -6,7 +6,7 @@ from __future__ import annotations __all__ = ["get_by_mrid", "contains_mrid", "safe_remove", "safe_remove_by_id", "nlen", "ngen", "is_none_or_empty", "require", "pb_or_none", "CopyableUUID", - "datetime_to_timestamp", "none"] + "datetime_to_timestamp", "none", "classproperty"] import os import re diff --git a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py index 90d1a9fde..94613f961 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py +++ b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py @@ -2,6 +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 TypeVar, List from unittest.mock import MagicMock From cf24250c8228043cfacdf99e30de85d214acc839 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Thu, 22 May 2025 05:57:48 +1000 Subject: [PATCH 12/47] Pretty bulky change - Complete rework of TraversalQueue, and its related counterparts - Got the last of the changes in from DEV-2506 in the kotlin sdk Broken stuff: - WeightedPriorityQueue has a hack in it to make it "kinda work" for now, that class needs some love. Signed-off-by: Max Chesterfield --- changelog.md | 3 + .../services/network/network_service.py | 4 +- .../terminal_connectivity_connected.py | 16 +- .../network/tracing/feeder/set_direction.py | 15 +- .../networktrace/conditions/conditions.py | 73 ++++++++ .../conditions/direction_condition.py | 42 ++++- .../equipment_step_limit_condition.py | 2 +- .../tracing/networktrace/network_trace.py | 59 +++++- .../feeder_direction_state_operations.py | 54 ++++-- .../operators/open_state_operators.py | 6 +- .../network/tracing/phases/set_phases.py | 27 ++- .../network/tracing/traversal/queue.py | 177 +++++++----------- .../network/tracing/traversal/traversal.py | 31 +-- .../traversal/weighted_priority_queue.py | 35 +++- .../conditions/test_conditions.py | 61 ++++++ .../conditions/test_direction_condition.py | 16 +- .../networktrace/test_network_trace.py | 61 ++++++ .../test_network_trace_queue_next.py | 16 +- .../test_network_trace_step_path_provider.py | 28 ++- 19 files changed, 505 insertions(+), 221 deletions(-) create mode 100644 src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py create mode 100644 test/services/network/tracing/networktrace/conditions/test_conditions.py diff --git a/changelog.md b/changelog.md index 842001d8d..43bc8c74f 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,9 @@ * Updated to new Tracing API. All old traces will need to be re-written with the new API. * `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. + + ### New Features * Added `ClearDirection` that clears feeder directions. diff --git a/src/zepben/evolve/services/network/network_service.py b/src/zepben/evolve/services/network/network_service.py index 06d25a88b..69086adf9 100644 --- a/src/zepben/evolve/services/network/network_service.py +++ b/src/zepben/evolve/services/network/network_service.py @@ -11,9 +11,7 @@ import logging from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Union, Iterable, Optional, Generator, Set - -from zepben.evolve.util import ngen +from typing import TYPE_CHECKING, Dict, List, Union, Iterable, Optional, Set from zepben.evolve.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.evolve.model.cim.iec61970.infiec61970.feeder.lv_feeder import LvFeeder diff --git a/src/zepben/evolve/services/network/tracing/connectivity/terminal_connectivity_connected.py b/src/zepben/evolve/services/network/tracing/connectivity/terminal_connectivity_connected.py index cb055a059..f421512de 100644 --- a/src/zepben/evolve/services/network/tracing/connectivity/terminal_connectivity_connected.py +++ b/src/zepben/evolve/services/network/tracing/connectivity/terminal_connectivity_connected.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 queue import Queue + from typing import List, Iterable, Optional, Set, Dict, Callable from zepben.evolve.services.network.tracing.connectivity.connectivity_result import ConnectivityResult @@ -10,11 +10,13 @@ from zepben.evolve.services.network.tracing.connectivity.xy_phase_step import XyPhaseStep from zepben.evolve.services.network.tracing.connectivity.phase_paths import viable_inferred_phase_connectivity, straight_phase_connectivity -from zepben.evolve import Terminal, PhaseCode, SinglePhaseKind, Switch, LifoQueue +from zepben.evolve import Terminal, PhaseCode, SinglePhaseKind, Switch from zepben.evolve.services.network.tracing.connectivity.nominal_phase_path import NominalPhasePath __all__ = ["TerminalConnectivityConnected"] +from zepben.evolve.services.network.tracing.traversal.queue import LIFODeque + class TerminalConnectivityConnected: """ @@ -123,7 +125,7 @@ def _add_xy_phase_paths(self, terminal: Terminal, add_path: Callable[[SinglePhas add_path(from_phase, to_phase) def _find_xy_candidate_phases(self, xy_phases: Dict[Terminal, PhaseCode], primary_phases: Dict[Terminal, PhaseCode]) -> XyCandidatePhasePaths: - queue = LifoQueue[XyPhaseStep]() + queue = LIFODeque[XyPhaseStep]() visited = set() candidate_phases = self._create_candidate_phases() @@ -135,7 +137,7 @@ def _find_xy_candidate_phases(self, xy_phases: Dict[Terminal, PhaseCode], primar # noinspection PyArgumentList self._find_more_xy_candidate_phases(XyPhaseStep(terminal, xy_phase_code), visited, queue, candidate_phases) - while not queue.empty(): + while len(queue) > 0: self._find_more_xy_candidate_phases(queue.pop(), visited, queue, candidate_phases) return candidate_phases @@ -144,7 +146,7 @@ def _find_more_xy_candidate_phases( self, step: XyPhaseStep, visited: Set[XyPhaseStep], - queue: Queue[XyPhaseStep], + queue: LIFODeque[XyPhaseStep], candidate_phases: XyCandidatePhasePaths ): if step in visited: @@ -176,7 +178,7 @@ def _check_traced_phases(step: XyPhaseStep, candidate_phases: XyCandidatePhasePa return found_traced @staticmethod - def _queue_next(terminal: Terminal, phase_code: PhaseCode, queue: Queue[XyPhaseStep]): + def _queue_next(terminal: Terminal, phase_code: PhaseCode, queue: LIFODeque[XyPhaseStep]): ce = terminal.conducting_equipment if not ce: return @@ -187,7 +189,7 @@ def _queue_next(terminal: Terminal, phase_code: PhaseCode, queue: Queue[XyPhaseS for connected in other.connectivity_node.terminals: if connected.conducting_equipment != ce: # noinspection PyArgumentList - queue.put(XyPhaseStep(connected, phase_code)) + queue.append(XyPhaseStep(connected, phase_code)) def _find_xy_phases(terminal: Terminal): diff --git a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py index 7658b22ef..cac9477bf 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py +++ b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py @@ -3,7 +3,7 @@ # 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 __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Type from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal from zepben.evolve.model.cim.iec61970.base.core.equipment_container import Feeder @@ -16,6 +16,7 @@ from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.evolve.services.network.tracing.networktrace.network_trace import NetworkTrace from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep +from zepben.evolve.services.network.tracing.traversal.weighted_priority_queue import WeightedPriorityQueue if TYPE_CHECKING: @@ -32,15 +33,13 @@ class SetDirection: @staticmethod def _compute_data(reprocessed_loop_terminals: list[Terminal], - state_operators: NetworkStateOperators, + state_operators: Type[NetworkStateOperators], step: NetworkTraceStep[FeederDirection], next_path: NetworkTraceStep.Path) -> FeederDirection: if next_path.to_equipment is BusbarSection: return FeederDirection.CONNECTOR - direction_applied = step.data - def next_direction_func(): if step.data == FeederDirection.NONE: return FeederDirection.NONE @@ -75,7 +74,7 @@ def next_direction_func(): return next_direction return FeederDirection.NONE - async def _create_traversal(self, state_operators: NetworkStateOperators) -> NetworkTrace[FeederDirection]: + async def _create_traversal(self, state_operators: Type[NetworkStateOperators]) -> NetworkTrace[FeederDirection]: reprocessed_loop_terminals: list[Terminal] = [] def queue_condition(nts: NetworkTraceStep, *args): @@ -92,6 +91,8 @@ def stop_condition(nts: NetworkTraceStep, *args): Tracing.network_trace_branching( network_state_operators=state_operators, action_step_type=NetworkTraceActionType.ALL_STEPS, + queue_factory=lambda: WeightedPriorityQueue.process_queue(lambda it: it.path.to_terminal.phases.num_phases), + branch_queue_factory=lambda: WeightedPriorityQueue.branch_queue(lambda it: it.path.to_terminal.phases.num_phases), compute_data=lambda step, _, next_path: self._compute_data(reprocessed_loop_terminals, state_operators, step, next_path) ) .add_condition(state_operators.stop_at_open()) @@ -112,7 +113,7 @@ def _reached_substation_transformer(terminal: Terminal) -> bool: def _is_normally_open_switch(conducting_equipment: Optional[ConductingEquipment]): return isinstance(conducting_equipment, Switch) and conducting_equipment.is_normally_open() - async def run(self, network: NetworkService, network_state_operators: NetworkStateOperators): + async def run(self, network: NetworkService, network_state_operators: Type[NetworkStateOperators]): """ Apply feeder directions from all feeder head terminals in the network. @@ -126,7 +127,7 @@ async def run(self, network: NetworkService, network_state_operators: NetworkSta if not network_state_operators.is_open(head_terminal, None): await self.run_terminal(terminal, network_state_operators) - async def run_terminal(self, terminal: Terminal, network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + async def run_terminal(self, terminal: Terminal, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): """ Apply [FeederDirection.DOWNSTREAM] from the [terminal]. diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py new file mode 100644 index 000000000..67930cfa9 --- /dev/null +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py @@ -0,0 +1,73 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar, Type + +from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection +from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import EquipmentStepLimitCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import EquipmentTypeStepLimitCondition + +T = TypeVar('T') + +if TYPE_CHECKING: + from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep + from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition + from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition + from zepben.evolve import ConductingEquipment + + NetworkTraceQueueCondition = QueueCondition[NetworkTraceStep[T]] + NetworkTraceStopCondition = StopCondition[NetworkTraceStep[T]] + + +# FIXME: work out how to inject NetworkStateOperators into this from inside NetworkTrace +class Conditions: + @classmethod + def upstream(cls) -> NetworkTraceQueueCondition[T]: + """ + Creates a [NetworkTrace] condition that will cause tracing a feeder upstream (towards the head terminal). + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in upstream tracing. + """ + return cls.with_direction(FeederDirection.UPSTREAM) + + @classmethod + def downstream(cls) -> NetworkTraceQueueCondition[T]: + """ + Creates a [NetworkTrace] condition that will cause tracing a feeder downstream (away from the head terminal). + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in downstream tracing. + """ + return cls.with_direction(FeederDirection.DOWNSTREAM) + + @classmethod + def with_direction(cls, direction: FeederDirection) -> NetworkTraceQueueCondition[T]: + """ + Creates a [NetworkTrace] condition that will cause tracing only terminals with directions that match [direction]. + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in upstream tracing. + """ + return DirectionCondition(direction, cls) # FIXME: cls should be NetworkStateOperators, somehow need + # to load these methods onto there after passing them to NetworkTrace + + @staticmethod + def limit_equipment_steps(limit: int, equipment_type: Type[ConductingEquipment]=None) -> NetworkTraceStopCondition[T]: + """ + Creates a [NetworkTrace] condition that stops tracing a path once a specified number of equipment steps have been reached. + + :param limit: The maximum number of equipment steps allowed before stopping. + :param equipment_type: The class of the equipment type to track against the limit + + :return: A [NetworkTraceStopCondition] that stops tracing the path once the step limit is reached. + """ + if equipment_type is not None: + return EquipmentTypeStepLimitCondition(limit, equipment_type) + return EquipmentStepLimitCondition(limit) + diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py index bd6ee0747..d9c8cc487 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py @@ -2,16 +2,20 @@ # 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 __future__ import annotations -from collections.abc import Callable -from typing import TypeVar, TYPE_CHECKING, Generic +from typing import TypeVar, TYPE_CHECKING, Generic, Type + +from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp +from zepben.evolve.model.cim.iec61970.base.wires.cut import Cut from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep if TYPE_CHECKING: - from zepben.evolve import Terminal, StepContext + from zepben.evolve.services.network.tracing.networktrace.operators.feeder_direction_state_operations import FeederDirectionStateOperations + from zepben.evolve import StepContext from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection T = TypeVar('T') @@ -19,17 +23,37 @@ class DirectionCondition(QueueCondition[NetworkTraceStep[T]], Generic[T]): - def __init__(self, direction: FeederDirection, get_direction: Callable[[Terminal], FeederDirection]): + def __init__(self, direction: FeederDirection, state_operators: Type[FeederDirectionStateOperations]): self.direction = direction - self.get_direction = get_direction + self.state_operators = state_operators + self.get_direction = self.state_operators.get_direction def should_queue(self, next_item: NetworkTraceStep[T], next_context: StepContext[T], current_item: NetworkTraceStep[T], current_context: StepContext[T]) -> bool: - path = next_item.path - if path.traced_internally: + return self._should_queue(next_item.path) + + def _should_queue(self, path: NetworkTraceStep.Path) -> bool: + # Cuts do weird things with directions depending on if they are energised from an external connection, or through a "closed" cut. To prevent + # dealing with this awful mess, it is much simpler to just ask if anything else past it needs queueing. This could be made to short-circuit + # for traversing downstream, but the code is much more complex to only save one extra step. + if isinstance(path.to_equipment, Cut): + return self._should_queue_next_paths(path) + elif path.traced_internally or path.did_traverse_ac_line_segment: return self.direction in self.get_direction(path.to_terminal) else: return self.direction.complementary_external_direction in self.get_direction(path.to_terminal) def should_queue_start_item(self, item: NetworkTraceStep[T]) -> bool: - return self.direction in self.get_direction(item.path.to_terminal) - + if self.direction in self.get_direction(item.path.to_terminal): + return True + # Because cuts and clamps behave a bit different with directions than other equipment terminals, we can also check if any further paths needs to be + # queued, and if they do we queue the start item. + elif isinstance(item.path.to_equipment, (Clamp, Cut)): + return self._should_queue_next_paths(item.path) + return False + + def _should_queue_next_paths(self, path: NetworkTraceStep.Path) -> bool: + for next_path in self.state_operators.next_paths(path): + if next_path.traced_internally and self.state_operators.is_open(path.to_equipment): + if self._should_queue(next_path): + return True + return False \ No newline at end of file diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py index 6ea11395a..519660d04 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py @@ -10,7 +10,7 @@ T = TypeVar('T') -class EquipmentStepLimitCondition(StopCondition, Generic[T]): +class EquipmentStepLimitCondition(StopCondition[NetworkTraceStep[T]], Generic[T]): def __init__(self, limit: int): super().__init__(self.should_stop) self.limit = limit 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 c63c8ba95..a5cfc0b8e 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -2,9 +2,12 @@ # 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 collections.abc import Callable -from typing import TypeVar, Union, Generic, Set, Type +from typing import TypeVar, Union, Generic, Set, Type, Generator +from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp +from zepben.evolve.model.cim.iec61970.base.wires.aclinesegment import AcLineSegment from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal @@ -114,7 +117,7 @@ def branching(cls, parent, action_type) - def add_start_item(self, start: Union[Terminal, ConductingEquipment], data: T= None, phases: PhaseCode=None) -> "NetworkTrace[T]": + def add_start_item(self, start: Union[Terminal, ConductingEquipment], data: T=None, phases: PhaseCode=None) -> "NetworkTrace[T]": """ Depending on the type of `start`, adds either: - A starting [Terminal] to the trace with the associated step data. @@ -126,18 +129,56 @@ def add_start_item(self, start: Union[Terminal, ConductingEquipment], data: T= N :param data: The data associated with the start step. :param phases: Phases to trace; `None` to ignore phases. """ + #if isinstance(start, NetworkTraceStep): + # super().add_start_item(start) + # return self + if isinstance(start, Terminal): - start_path = NetworkTraceStep.Path(start, start, None, self.start_nominal_phase_path(phases)) - super().add_start_item(NetworkTraceStep(start_path, 0, 0, data)) + # We have a special case when starting specifically on a clamp terminal that we mark it as having traversed the segment such that it + # will only trace externally from the clamp terminal. This behaves differently to when the whole Clamp is added as a start item. + traversed_ac_line_segment = None + if isinstance(start.conducting_equipment, Clamp): + traversed_ac_line_segment = start.conducting_equipment.ac_line_segment + self._add_start_item(start, data, phases, traversed_ac_line_segment) return self - if issubclass(start.__class__, ConductingEquipment) or isinstance(start, ConductingEquipment): - for it in start.terminals: - self.add_start_item(it, data, phases) + elif issubclass(start.__class__, ConductingEquipment) or isinstance(start, ConductingEquipment): + # If we start on an AcLineSegment, we queue the segments terminals, and all its Cut and Clamp terminals as if we have traversed the segment, + # so the next steps will be external from all the terminals "belonging" to the segment. + if isinstance(start, AcLineSegment): + def start_terminals() -> Generator[Terminal, None, None]: + for terminal in start.terminals: + yield terminal + for clamp in start.clamps: + for terminal in clamp.terminals: + yield terminal + break + for cut in start.cuts: + for terminal in cut.terminals: + yield terminal + for terminal in start_terminals(): + self._add_start_item(terminal, data, phases, start) + + # We don't have a special case for Clamp here because we say if you start from the whole Clamp rather than its terminal specifically, + # we want to trace externally from it and traverse its segment. + else: + for it in start.terminals: + self._add_start_item(it, data, phases, None) + return self - super().add_start_item(start) - return self + raise Exception('INTERNAL ERROR:: unexpected add_start_item params') + + def _add_start_item(self, + start: Terminal=None, + data: T=None, + phases: PhaseCode=None, + traversed_ac_line_segment: AcLineSegment=None): + + if start is None: + return + start_path = NetworkTraceStep.Path(start, start, traversed_ac_line_segment, self.start_nominal_phase_path(phases)) + super().add_start_item(NetworkTraceStep(start_path, 0, 0, data)) async def run(self, start: Union[ConductingEquipment, Terminal]=None, data: T=None, phases: PhaseCode=None, can_stop_on_start_item: bool=True) -> "NetworkTrace[T]": """ diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py index 0dcbd8e8a..85725a6dc 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py @@ -31,8 +31,9 @@ def get_direction(terminal: Terminal) -> FeederDirection: """ Retrieves the feeder direction for the specified terminal. - `terminal` The terminal for which to retrieve the feeder direction. - Returns The current feeder direction associated with the specified terminal. + :param terminal: The terminal for which to retrieve the feeder direction. + + :return: The current feeder direction associated with the specified terminal. """ pass @@ -42,9 +43,10 @@ def set_direction(terminal: Terminal, direction: FeederDirection) -> bool: """ Sets the feeder direction for the specified terminal. - `terminal` The terminal for which to set the feeder direction. - `direction` The new feeder direction to assign to the terminal. - Returns `true` if the direction was changed; `false` if the direction was already set to the specified value. + :param terminal: The terminal for which to set the feeder direction. + :param direction: The new feeder direction to assign to the terminal. + + :return: `True` if the direction was changed; `false` if the direction was already set to the specified value. """ pass @@ -54,9 +56,10 @@ def add_direction(terminal: Terminal, direction: FeederDirection) -> bool: """ Adds the specified feeder direction to the terminal, preserving existing directions. - `terminal` The terminal for which to add the feeder direction. - `direction` The feeder direction to add. - Returns `true` if the direction was added successfully; `false` if the direction was already present. + :param terminal: The terminal for which to add the feeder direction. + :param direction: The feeder direction to add. + + :return: `True` if the direction was added successfully; `false` if the direction was already present. """ pass @@ -67,23 +70,42 @@ def remove_direction(terminal: Terminal, direction: FeederDirection) -> bool: """ Removes the specified feeder direction from the terminal. - `terminal` The terminal for which to remove the feeder direction. - `direction` The feeder direction to remove. - Returns `true` if the direction was removed; `false` if the direction was not present. + :param terminal: The terminal for which to remove the feeder direction. + :param direction: The feeder direction to remove. + + :return: `true` if the direction was removed; `false` if the direction was not present. """ pass @classmethod def upstream(cls) -> NetworkTraceQueueCondition[T]: - return cls.with_direction(FeederDirection.UPSTREAM, cls.get_direction) + """ + Creates a [NetworkTrace] condition that will cause tracing a feeder upstream (towards the head terminal). + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in upstream tracing. + """ + return cls.with_direction(FeederDirection.UPSTREAM) @classmethod def downstream(cls) -> NetworkTraceQueueCondition[T]: - return cls.with_direction(FeederDirection.DOWNSTREAM, cls.get_direction) + """ + Creates a [NetworkTrace] condition that will cause tracing a feeder downstream (away from the head terminal). + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. - @staticmethod - def with_direction(direction: FeederDirection, get_direction: Callable[[Terminal], FeederDirection]) -> NetworkTraceQueueCondition[T]: - return DirectionCondition(direction, get_direction) + :return: [NetworkTraceQueueCondition] that results in downstream tracing. + """ + return cls.with_direction(FeederDirection.DOWNSTREAM) + + @classmethod + def with_direction(cls, direction: FeederDirection) -> NetworkTraceQueueCondition[T]: + """ + Creates a [NetworkTrace] condition that will cause tracing only terminals with directions that match [direction]. + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in upstream tracing. + """ + return DirectionCondition(direction, cls) class NormalFeederDirectionStateOperations(FeederDirectionStateOperations): @staticmethod diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py index 21ffbd178..12af98c09 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py @@ -4,7 +4,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations -from typing import TypeVar, Optional, TYPE_CHECKING +from typing import TypeVar, Optional, TYPE_CHECKING, Callable from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind @@ -66,8 +66,8 @@ def set_open(switch: Switch, is_open: bool, phase: SinglePhaseKind=None) -> None raise NotImplementedError() @classmethod - def stop_at_open(cls) -> NetworkTraceQueueCondition[T]: - return OpenCondition(cls.is_open) + def stop_at_open(cls, open_test: Optional[Callable[[Switch, Optional[SinglePhaseKind]], bool]]=None, phase: Optional[SinglePhaseKind]=None) -> NetworkTraceQueueCondition[T]: + return OpenCondition(open_test or cls.is_open, phase) class NormalOpenStateOperators(OpenStateOperators): diff --git a/src/zepben/evolve/services/network/tracing/phases/set_phases.py b/src/zepben/evolve/services/network/tracing/phases/set_phases.py index 7bc2c4da1..c872c6c77 100644 --- a/src/zepben/evolve/services/network/tracing/phases/set_phases.py +++ b/src/zepben/evolve/services/network/tracing/phases/set_phases.py @@ -6,7 +6,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Union, Set, Iterable, List +from typing import Union, Set, Iterable, List, Type from zepben.evolve.exceptions import TracingException, PhaseException from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode @@ -22,7 +22,6 @@ from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing -from zepben.evolve.services.network.tracing.traversal.traversal import Traversal from zepben.evolve.services.network.tracing.traversal.weighted_priority_queue import WeightedPriorityQueue __all__ = ["SetPhases"] @@ -43,7 +42,7 @@ def __init__(self, nominal_phase_paths: Iterable[NominalPhasePath], step_flowed_ async def run(self, apply_to: Union[NetworkService, Terminal], phases: Union[PhaseCode, Iterable[SinglePhaseKind]]=None, - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): if isinstance(apply_to, NetworkService): return await self._run(apply_to, network_state_operators) @@ -59,7 +58,7 @@ async def run(self, async def _run(self, network: NetworkService, - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): """ Apply phases from all sources in the network. @@ -74,7 +73,7 @@ async def _run(self, async def _run_with_phases(self, terminal: Terminal, phases: Union[PhaseCode, Iterable[SinglePhaseKind]], - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): """ Apply phases from the `terminal`. @@ -104,7 +103,7 @@ async def _run_spread_phases_and_flow(self, seed_terminal: Terminal, start_terminal: Terminal, phases: List[SinglePhaseKind], - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): nominal_phase_paths = self._get_nominal_phase_paths(network_state_operators, seed_terminal, start_terminal, list(phases)) if self._flow_phases(network_state_operators, seed_terminal, start_terminal, nominal_phase_paths): @@ -116,7 +115,7 @@ async def spread_phases( from_terminal: Terminal, to_terminal: Terminal, phases: List[SinglePhaseKind]=None, - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL ): """ Apply phases from the `from_terminal` to the `to_terminal`. @@ -133,14 +132,14 @@ async def spread_phases( if await self._flow_phases(network_state_operators, from_terminal, to_terminal, paths): await self.run(from_terminal, network_state_operators=network_state_operators) - async def _run_terminal(self, terminal: Terminal, network_state_operators: NetworkStateOperators, trace: NetworkTrace[PhasesToFlow]=None): + async def _run_terminal(self, terminal: Terminal, network_state_operators: Type[NetworkStateOperators], trace: NetworkTrace[PhasesToFlow]=None): if trace is None: trace = await self._create_network_trace(network_state_operators) nominal_phase_paths = list(map(lambda it: NominalPhasePath(SinglePhaseKind.NONE, it), terminal.phases)) await trace.run(terminal, self.PhasesToFlow(nominal_phase_paths), can_stop_on_start_item=False) trace.reset() - async def _create_network_trace(self, state_operators: NetworkStateOperators) -> NetworkTrace[PhasesToFlow]: + async def _create_network_trace(self, state_operators: Type[NetworkStateOperators]) -> NetworkTrace[PhasesToFlow]: async def step_action(nts, ctx): path = nts.path phases_to_flow = nts.data @@ -166,7 +165,7 @@ def _get_weight(it) -> int: .add_step_action(step_action) ) - def _compute_next_phases_to_flow(self, state_operators: NetworkStateOperators) -> ComputeData[PhasesToFlow]: + def _compute_next_phases_to_flow(self, state_operators: Type[NetworkStateOperators]) -> ComputeData[PhasesToFlow]: def inner(step, _, next_path): if not step.data.step_flowed_phases: return self.PhasesToFlow([]) @@ -177,7 +176,7 @@ def inner(step, _, next_path): return ComputeData(inner) @staticmethod - def _apply_phases(state_operators: NetworkStateOperators, + def _apply_phases(state_operators: Type[NetworkStateOperators], terminal: Terminal, phases: List[SinglePhaseKind]): @@ -185,7 +184,7 @@ def _apply_phases(state_operators: NetworkStateOperators, for i, nominal_phase in enumerate(terminal.phases.single_phases): traced_phases[nominal_phase] = phases[i] if phases[i] not in PhaseCode.XY else SinglePhaseKind.NONE - def _get_nominal_phase_paths(self, state_operators: NetworkStateOperators, + def _get_nominal_phase_paths(self, state_operators: Type[NetworkStateOperators], from_terminal: Terminal, to_terminal: Terminal, phases: Sequence[SinglePhaseKind] @@ -199,7 +198,7 @@ def _get_nominal_phase_paths(self, state_operators: NetworkStateOperators, return TerminalConnectivityConnected().terminal_connectivity(from_terminal, to_terminal, phases_to_flow).nominal_phase_paths @staticmethod - async def _flow_phases(state_operators: NetworkStateOperators, + async def _flow_phases(state_operators: Type[NetworkStateOperators], from_terminal: Terminal, to_terminal: Terminal, nominal_phase_paths: Iterable[NominalPhasePath] @@ -250,7 +249,7 @@ def get_ce_details(terminal: Terminal): @staticmethod def _get_phases_to_flow( - state_operators: NetworkStateOperators, + state_operators: Type[NetworkStateOperators], terminal: Terminal, phases: Sequence[SinglePhaseKind], internal_flow: bool diff --git a/src/zepben/evolve/services/network/tracing/traversal/queue.py b/src/zepben/evolve/services/network/tracing/traversal/queue.py index 621de38ed..90bbb1b5f 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/queue.py +++ b/src/zepben/evolve/services/network/tracing/traversal/queue.py @@ -8,166 +8,115 @@ # 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 __future__ import annotations +from abc import ABC, abstractmethod from collections import deque -from typing import TypeVar, Iterable, Generic -from heapq import heappush, heappop +from typing import TypeVar, Iterable, Generic, Deque, TYPE_CHECKING, Union + +__all__ = ["TraversalQueue"] -__all__ = ["FifoQueue", "LifoQueue", "PriorityQueue", "TraversalQueue"] +if TYPE_CHECKING: + from zepben.evolve import NetworkTraceStep T = TypeVar('T') U = TypeVar('U') -# TODO: the methods in these classes overlap in a slightly unclear way, this needs to be tidied up. +class FIFODeque(deque): + def pop(self): + return self.popleft() + + def peek(self) -> T: + return self[-1] + + +class LIFODeque(deque): + def peek(self) -> T: + return self[0] -class TraversalQueue(Generic[T]): + +class TraversalQueue(Generic[T], ABC): """ Basic queue object, implementing some methods to align it with the kotlin sdk syntax, """ - def __init__(self, queue=None): - if queue is None: - self.queue = deque() - else: - self.queue = queue - - def __iter__(self): - return self.queue.__iter__() - + @abstractmethod def __len__(self): - return len(self.queue) + """:return: the length of the queue""" @classmethod - def breadth_first(cls) -> TraversalQueue: + def breadth_first(cls) -> TraversalQueue[T]: """ Creates a new instance backed by a breadth first (FIFO) queue. """ - return cls(FifoQueue()) + return BasicQueue(FIFODeque()) @classmethod - def depth_first(cls) -> TraversalQueue: + def depth_first(cls) -> TraversalQueue[T]: """ Creates a new instance backed by a depth first (LIFO) queue. """ - return cls(LifoQueue()) + return BasicQueue(LIFODeque()) + @abstractmethod def has_next(self) -> bool: - """ :return: True if the queue has more items. """ - return len(self.queue) > 0 + """:return: True if the queue has more items.""" + @abstractmethod def pop(self): - return self.queue.pop() - - def put(self, item: T) -> bool: - self.queue.put(item) - return True - - def extend(self, items: Iterable[T]) -> bool: - return self.queue.extend(items) - - def clear(self): - return self.queue.clear() + """:return: The next item in the queue""" - -class FifoQueue(TraversalQueue[T]): - """Used for Breadth-first Traversal's""" - - def put(self, item: T): - return self.queue.append(item) - - def extend(self, items: Iterable[T]): - return self.queue.extend(items) - - def pop(self) -> T: - """ - Pop an item off the queue. - Raises `IndexError` if the queue is empty. - """ - return self.queue.popleft() - - def empty(self) -> bool: + @abstractmethod + def append(self, item: T) -> bool: """ - Check if queue is empty - Returns True if empty, False otherwise - """ - return len(self.queue) == 0 - - def clear(self): - """Clear the queue.""" - self.queue.clear() - - def copy(self) -> FifoQueue[T]: - return FifoQueue(self.queue.copy()) + Adds an item to the queue + :param item: The item to be added to the queue -class LifoQueue(TraversalQueue[T]): - """Used for Depth-first Traversal's""" - - def put(self, item: T): - self.queue.append(item) + :return: True if the item was added + """ + @abstractmethod def extend(self, items: Iterable[T]): - self.queue.extend(items) - - def pop(self) -> T: - """ - Pop an item off the queue. - Raises `IndexError` if the queue is empty. """ - return self.queue.pop() + Adds the items to the queue - def empty(self) -> bool: - """ - Check if queue is empty - Returns True if empty, False otherwise + :param items: The items to be added to the queue """ - return len(self.queue) == 0 + @abstractmethod + def peek(self) -> T: + """:return: The next item on the queue without removing it""" + + @abstractmethod def clear(self): - """Clear the queue.""" - self.queue.clear() + """Clears the queue""" - def copy(self) -> LifoQueue[T]: - return LifoQueue(self.queue.copy()) +class BasicQueue(TraversalQueue, Generic[T]): + queue: Union[FIFODeque, LIFODeque] -class PriorityQueue(TraversalQueue[T]): - """Used for custom `Traversal`s""" + def __init__(self, queue): + self.queue: Union[FIFODeque, LIFODeque] = queue - def __init__(self, queue=None): - if queue is None: - super().__init__([]) - else: - super().__init__(queue) + def __iter__(self): + return self.queue.__iter__() def __len__(self): return len(self.queue) - def put(self, item: T): - """ - Place an item in the queue based on its priority. - `item` The item to place on the queue. Must implement `__lt__` - Returns True if put was successful, False otherwise. - """ - heappush(self.queue, item) - def extend(self, items: Iterable[T]): - for item in items: - heappush(self.queue, item) + def has_next(self) -> bool: + return len(self.queue) > 0 - def pop(self) -> T: - """ - Get the next item in the queue, removing it from the queue. - Returns The next item in the queue by priority. - Raises `IndexError` if the queue is empty - """ - return heappop(self.queue) + def pop(self): + return self.queue.pop() - def empty(self) -> bool: - return len(self) == 0 + def append(self, item: T) -> bool: + self.queue.append(item) + return True - def clear(self): - """Clear the queue.""" - self.queue.clear() + def extend(self, items: Iterable[T]) -> None: + self.queue.extend(items) - def copy(self) -> PriorityQueue[T]: - return PriorityQueue(self.queue.copy()) + def peek(self) -> T: + return self.queue.peek() + def clear(self): + self.queue.clear() diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal.py b/src/zepben/evolve/services/network/tracing/traversal/traversal.py index 8abc9428d..fbe483927 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal.py @@ -120,10 +120,12 @@ def __init__(self, queue_type, parent: Optional[D] = None): Traversal.BranchingQueueType: lambda current, context: self._queue_next_branching(current, context, self._queue_type.queue_next), } - self.queue: TraversalQueue[T] = queue_type.queue - self.branch_queue: Optional[TraversalQueue[D]] = queue_type.branch_queue + self.queue: TraversalQueue[T] = self._queue_type.queue + self.branch_queue: Optional[TraversalQueue[D]] = self._queue_type.branch_queue self.start_items: deque[T] = deque() + self.running: bool = False + self.has_run: bool = False self.stop_conditions: List[StopCondition[T]] = [] self.queue_conditions: List[QueueCondition[T]] = [] @@ -206,6 +208,7 @@ def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopCo return self elif callable(condition): + assert not isinstance(condition, TraversalCondition) return self.add_stop_condition(StopCondition(condition)) raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [StopCondition | StopConditionWithContextValue | Callable]') @@ -236,13 +239,13 @@ def add_queue_condition(self, condition: Union[Callable, QueueCondition[T]]) -> :returns: The current traversal instance. """ if isinstance(condition, QueueCondition): - assert issubclass(condition.__class__, QueueCondition) self.queue_conditions.append(condition) if isinstance(condition, QueueConditionWithContextValue): self.compute_next_context_funs[condition.key] = condition return self elif callable(condition): + assert not isinstance(condition, TraversalCondition) return self.add_queue_condition(QueueCondition(condition)) raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [QueueCondition | QueueConditionWithContextValue | Callable]') @@ -267,7 +270,6 @@ def add_step_action(self, action: Union[Callable, StepAction[T]]) -> D: Returns The current traversal instance. """ if isinstance(action, StepAction): - assert issubclass(action.__class__, StepAction) or isinstance(action, StepAction) self.step_actions.append(action) if isinstance(action, StepActionWithContextValue): self.compute_next_context_funs[action.key] = action @@ -427,18 +429,18 @@ def _branch_start_items(self): if self.branch_queue is None: raise Exception("INTERNAL ERROR: self.branch_queue should never be null here") - self.branch_queue.put(branch) + self.branch_queue.append(branch) async def _traverse(self, can_stop_on_start_item: bool): while len(self.start_items) > 0: - start_item = self.start_items.popleft() + start_item = self.start_items.pop() if self._parent is None: if self._can_queue_start_item(start_item): self.contexts[start_item] = self._compute_intial_context(start_item) - self.queue.put(start_item) + self.queue.append(start_item) else: - self.queue.put(start_item) + self.queue.append(start_item) can_stop = can_stop_on_start_item while len(self.queue) > 0: @@ -471,16 +473,17 @@ def _create_new_branch(self, start_item: T, context: StepContext) -> D: it.copy_context_value_computer(self) it.contexts[start_item] = context - it.add_start_item(start_item) + Traversal.add_start_item(it, start_item) return it def _item_queuer(self, current_item: T, current_context) -> Callable[[T], bool]: def inner(next_item: T) -> bool: next_context = self._compute_next_context(current_item, current_context, next_item, is_branch_start=False) - if self._can_queue_item(next_item, next_context, current_item, current_context) and self.queue.put(next_item): + if self._can_queue_item(next_item, next_context, current_item, current_context) and self.queue.append(next_item): self.contexts[next_item] = next_context return True - return False + else: + return False return inner @@ -492,9 +495,11 @@ def queue_branch(next_item: T): next_context = self._compute_next_context(current, current_context, next_item, is_branch_start=True) if self._can_queue_item(next_item, next_context, current, current_context): branch = self._create_new_branch(next_item, next_context) - self.branch_queue.put(branch) + self.branch_queue.append(branch) return True - return False + else: + return False + return queue_next.accept(current, current_context, self._item_queuer(current, current_context), queue_branch) async def _traverse_branches(self, can_stop_on_start_item: bool): diff --git a/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py b/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py index 2f255f434..215cb0b61 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py +++ b/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py @@ -3,11 +3,10 @@ # 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 collections import defaultdict -from typing import TypeVar, Callable, Iterable, Any +from typing import TypeVar, Callable, Iterable from zepben.evolve.services.network.tracing.traversal.queue import TraversalQueue - T = TypeVar('T') U = TypeVar('U') @@ -26,10 +25,12 @@ class WeightedPriorityQueue(TraversalQueue[T]): :param queue_provider: A queue provider. This allows you to customise the priority of items with the same weight. :param get_weight: A method to extract the weight of an item being added to the queue. """ - def __init__(self, queue_provider: Callable[[], TraversalQueue[T]], get_weight: Callable[[Any], int]): + + def __init__(self, queue_provider: Callable[[], TraversalQueue[T]], get_weight: Callable[[T], int]): self._queue_provider = queue_provider - self._get_weight = get_weight - super().__init__(queue=SortedDefaultDict(self._queue_provider)) + self._get_weight = lambda a: 1# TODO: this is wrong but at 6am, its as good as its gonna get to remind me where to lookg + + self.queue: SortedDefaultDict[int, TraversalQueue[T]] = SortedDefaultDict(self._queue_provider) def __len__(self) -> int: """need to aggregate the lengths of all queues""" @@ -42,27 +43,27 @@ def __next__(self): yield self.pop() def pop(self): - for weight in self.queue.keys(): + for weight in reversed(self.queue.keys()): if self.queue[weight].has_next(): return self.queue[weight].pop() - def put(self, item: T) -> bool: + def append(self, item: T) -> bool: weight = self._get_weight(item) if weight < 0: raise Exception - self.queue[weight].put(item) + self.queue[weight].append(item) return True def extend(self, items: Iterable[T]) -> bool: raise NotImplementedError() @classmethod - def process_queue(cls, get_weight: Callable[[T], int]) -> TraversalQueue: + def process_queue(cls, get_weight: Callable[[T], int]) -> TraversalQueue[T]: """Special priority queue that queues items with the largest weight as the highest priority.""" return cls(TraversalQueue.depth_first, get_weight) @classmethod - def branch_queue(cls, get_weight: Callable[[T], int]) -> TraversalQueue: + def branch_queue(cls, get_weight: Callable[[T], int]) -> TraversalQueue[T]: """Special priority queue that queues branch items with the largest weight on the starting item as the highest priority""" def condition(traversal): items = traversal.start_items @@ -71,3 +72,17 @@ def condition(traversal): return get_weight(items) or -1 return cls(TraversalQueue.breadth_first, condition) + + def has_next(self) -> bool: + for weight in self.queue.keys(): + _next = self.queue.get(weight) + if _next: + return True + return False + + + def peek(self) -> T: + raise Exception + + def clear(self): + self.queue.clear() diff --git a/test/services/network/tracing/networktrace/conditions/test_conditions.py b/test/services/network/tracing/networktrace/conditions/test_conditions.py new file mode 100644 index 000000000..1e73297a3 --- /dev/null +++ b/test/services/network/tracing/networktrace/conditions/test_conditions.py @@ -0,0 +1,61 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 Optional, Callable + +from zepben.evolve import NetworkStateOperators, FeederDirection, SinglePhaseKind, Switch, PowerTransformer +from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import Conditions +from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import EquipmentStepLimitCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import EquipmentTypeStepLimitCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import OpenCondition + + +class TestCondition: + def test_state_operators_with_direction(self): + state_operators = NetworkStateOperators.NORMAL + condition = state_operators.with_direction(FeederDirection.BOTH) + assert isinstance(condition, DirectionCondition) + assert condition.state_operators is state_operators + assert condition.direction is FeederDirection.BOTH + + def test_state_operators_upstream(self): + state_operators = NetworkStateOperators.NORMAL + condition = state_operators.upstream() + assert isinstance(condition, DirectionCondition) + assert condition.state_operators is state_operators + assert condition.direction is FeederDirection.UPSTREAM + + def test_state_operators_downstream(self): + state_operators = NetworkStateOperators.NORMAL + condition = state_operators.downstream() + assert isinstance(condition, DirectionCondition) + assert condition.state_operators is state_operators + assert condition.direction is FeederDirection.DOWNSTREAM + + def test_stop_at_open(self): + is_open: Callable[[Switch, Optional[SinglePhaseKind]], bool] = Switch.is_open + state_operators = NetworkStateOperators.NORMAL + condition = state_operators.stop_at_open(is_open, SinglePhaseKind.A) + assert isinstance(condition, OpenCondition) + #assert condition._is_open == state_operators.is_open # wont work because we're not actually getting that method directly + assert condition._phase is SinglePhaseKind.A + + def test_open_operators_stop_at_open(self): + state_operators = NetworkStateOperators.NORMAL + condition = state_operators.stop_at_open(phase=SinglePhaseKind.A) + assert isinstance(condition, OpenCondition) + assert condition._is_open == state_operators.is_open + assert condition._phase is SinglePhaseKind.A + + def test_limit_equipment_steps(self): + condition = Conditions.limit_equipment_steps(1) + assert isinstance(condition, EquipmentStepLimitCondition) + assert condition.limit == 1 + + def test_limit_equipment_type_steps(self): + condition = Conditions.limit_equipment_steps(1, PowerTransformer) + assert isinstance(condition, EquipmentTypeStepLimitCondition) + assert condition.limit == 1 + assert condition.equipment_type is PowerTransformer diff --git a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py index 14b1c58b1..8eb2fcd9b 100644 --- a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py +++ b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py @@ -5,7 +5,7 @@ from typing import Tuple from unittest.mock import MagicMock -from zepben.evolve import FeederDirection, NetworkTraceStep, Terminal +from zepben.evolve import FeederDirection, NetworkTraceStep, Terminal, Junction, NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition @@ -92,10 +92,15 @@ def _terminal_should_queue(condition: Tuple[FeederDirection, FeederDirection, bo next_path = MagicMock(spec=NetworkTraceStep.Path)() next_path.traced_internally = traced_internally next_path.to_terminal = Terminal() + next_path.to_equipment = Junction() + next_path.did_traverse_ac_line_segment = False next_item = NetworkTraceStep(next_path, 0, 0, None) - result = DirectionCondition(direction, lambda terminal: to_direction).should_queue(next_item, None, None, None) + state_operators = NetworkStateOperators + state_operators.get_direction = lambda t: to_direction + + result = DirectionCondition(direction, state_operators).should_queue(next_item, None, None, None) assert result == expected def _start_terminal_should_queue(condition: Tuple[FeederDirection, FeederDirection], expected): @@ -103,8 +108,13 @@ def _start_terminal_should_queue(condition: Tuple[FeederDirection, FeederDirecti next_path = MagicMock(spec=NetworkTraceStep.Path)() next_path.to_terminal = Terminal() + next_path.to_equipment = Junction() + next_path.did_traverse_ac_line_segment = False next_item = NetworkTraceStep(next_path, 0, 0, None) - result = DirectionCondition(direction, lambda terminal: to_direction).should_queue_start_item(next_item) + state_operators = NetworkStateOperators + state_operators.get_direction = lambda t: to_direction + + result = DirectionCondition(direction, state_operators).should_queue_start_item(next_item) assert result == expected diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index fac69d763..2b9a39cd2 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -6,13 +6,74 @@ import pytest +from services.network.tracing.networktrace.test_network_trace_step_path_provider import PathTerminal, _verify_paths +from zepben.evolve import AcLineSegment, Clamp, Terminal, NetworkTraceStep, Cut from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.testing.test_network_builder import TestNetworkBuilder +Terminal.__add__ = PathTerminal.__add__ +Terminal.__sub__ = PathTerminal.__sub__ class TestNetworkTrace: + @pytest.mark.asyncio + async def test_add_start_clamp_terminal_as_traversed_segment_path(self): + trace = Tracing.network_trace() + segment = AcLineSegment() + clamp = Clamp().add_terminal(Terminal()) + segment.add_clamp(clamp) + + trace.add_start_item(clamp[1]) + assert trace.start_items[0].path == clamp[1] - clamp[1] + + @pytest.mark.asyncio + def test_adds_start_whole_clamp_as_not_traversed_segment_path(self): + trace = Tracing.network_trace() + segment = AcLineSegment() + clamp = Clamp().add_terminal(Terminal()) + segment.add_clamp(clamp) + + trace.add_start_item(clamp) + _verify_paths((trace.start_items[0].path, ), (clamp[1] + clamp[1], )) + + @pytest.mark.asyncio + def test_adds_start_AcLineSegment_terminals_cut_terminals_and_clamp_terminals_as_traversed_segment(self): + trace = Tracing.network_trace() + segment = AcLineSegment() \ + .add_terminal(Terminal()) \ + .add_terminal(Terminal()) + + clamp1 = Clamp() \ + .add_terminal(Terminal()) + segment.add_clamp(clamp1) + + clamp2 = Clamp() \ + .add_terminal(Terminal()) + segment.add_clamp(clamp2) + + cut1 = Cut() \ + .add_terminal(Terminal()) \ + .add_terminal(Terminal()) + segment.add_cut(cut1) + + cut2 = Cut() \ + .add_terminal(Terminal()) \ + .add_terminal(Terminal()) + segment.add_cut(cut2) + + trace.add_start_item(segment) + + _verify_paths((it.path for it in trace.start_items), ( + segment[1] - segment[1], + segment[2] - segment[2], + clamp1[1] - clamp1[1], + clamp2[1] - clamp2[1], + cut1[1] - cut1[1], + cut1[2] - cut1[2], + cut2[1] - cut2[1], + cut2[2] - cut2[2])) + @pytest.mark.skip() @pytest.mark.asyncio async def test_can_run_large_branching_traces(self): diff --git a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py index 94613f961..39dbd135c 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py +++ b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py @@ -9,7 +9,7 @@ import pytest from services.network.tracing.networktrace.util import mock_nts_path, mock_nts, mock_ctx -from zepben.evolve import ComputeData, NetworkTraceStep, ngen +from zepben.evolve import ComputeData, NetworkTraceStep, ngen, NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_next import NetworkTraceQueueNext from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider @@ -32,14 +32,14 @@ class TestNetworkTraceQueueNext: @pytest.fixture(autouse=True) def setup_method(self): - self.path_provider = MagicMock(NetworkTraceStepPathProvider) + self.state_operators = MagicMock(NetworkStateOperators) self.data_computer = MagicMock(ComputeData) self.queuer = Queuer() self.branching_queuer = Queuer() yield def test_queues_next_basic(self): - queue_next = NetworkTraceQueueNext.Basic(self.path_provider, self.data_computer) + queue_next = NetworkTraceQueueNext.Basic(self.state_operators, self.data_computer) seed_path = mock_nts_path() seed_step = mock_nts(seed_path, 3, 1) @@ -55,7 +55,7 @@ def test_queues_next_basic(self): next_path_1 = mock_nts_path(traced_internally=False) next_path_2 = mock_nts_path(traced_internally=True) - self.path_provider.next_paths = lambda seed_path: ngen((next_path_1, next_path_2)) + self.state_operators.next_paths = lambda seed_path: ngen((next_path_1, next_path_2)) def mock_computer(seed_step, seed_context, path): if path is next_path_1: @@ -73,7 +73,7 @@ def mock_computer(seed_step, seed_context, path): _assert_step_equal(self.queuer.queued[1], next_path_2, "Bar", 4, 1) def test_calls_branching_queuer_when_queing_more_then_1_path_on_branching_queue_next(self): - queue_next = NetworkTraceQueueNext.Branching(self.path_provider, self.data_computer) + queue_next = NetworkTraceQueueNext.Branching(self.state_operators, self.data_computer) seed_path = mock_nts_path() seed_step = mock_nts(seed_path, 3, 1) @@ -83,7 +83,7 @@ def test_calls_branching_queuer_when_queing_more_then_1_path_on_branching_queue_ next_path_1 = mock_nts_path(traced_internally=False) next_path_2 = mock_nts_path(traced_internally=True) - self.path_provider.next_paths = lambda seed_path: ngen((next_path_1, next_path_2)) + self.state_operators.next_paths = lambda seed_path: ngen((next_path_1, next_path_2)) def mock_computer(seed_step, seed_context, path): if path is next_path_1: @@ -102,7 +102,7 @@ def mock_computer(seed_step, seed_context, path): _assert_step_equal(self.branching_queuer.queued[1], next_path_2, "Bar", 4, 1) def test_calls_straight_queuer_when_queuing_a_single_path_on_branching_queue_next(self): - queue_next = NetworkTraceQueueNext.Branching(self.path_provider, self.data_computer) + queue_next = NetworkTraceQueueNext.Branching(self.state_operators, self.data_computer) seed_path = mock_nts_path() seed_step = mock_nts(seed_path, 3, 1) @@ -111,7 +111,7 @@ def test_calls_straight_queuer_when_queuing_a_single_path_on_branching_queue_nex next_path_1 = mock_nts_path(traced_internally=False) - self.path_provider.next_paths = lambda seed_path: ngen([next_path_1]) + self.state_operators.next_paths = lambda seed_path: ngen([next_path_1]) def mock_computer(seed_step, seed_context, path): if path is next_path_1: diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 59114b61e..cd8595a0f 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.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 Generator, Tuple, Iterable, Optional +from typing import Generator, Tuple, Iterable, Optional, Union from pytest_subtests.plugin import subtests @@ -353,7 +353,27 @@ def test_internal_step_to_cut_t1_between_cuts_steps_externally_and_traverses_seg _verify_paths(next_paths, (cut2[1] - clamp2[1], cut2[1] - clamp3[1], cut2[1] - cut1[2], cut2[1] + c8[1])) - def test_traverse_with_cut_with_known_length_from_t1_does_not_return_clamp_with_known_length_from_t1(self, subtests): + def test_starting_on_clamp_terminal_flagged_as_traversed_segment_only_steps_externally(self): + network = self._acls_with_clamps_and_cuts_network() + + c3 = network['c3'] + clamp1 = network['clamp1'] + + next_paths = self.path_provider.next_paths(clamp1[1] - clamp1[1]) + _verify_paths(next_paths, (clamp1[1] + c3[1], )) + + def test_starting_on_clamp_terminal_that_flagged_as_not_traversed_segment_steps_externally_and_traverses(self): + network = self._acls_with_clamps_and_cuts_network() + + c3 = network['c3'] + clamp1 = network['clamp1'] + cut1 = network['cut1'] + c1 = network['c1'] + + next_paths = self.path_provider.next_paths(clamp1[1] + clamp1[1]) + _verify_paths(next_paths, (clamp1[1] + c3[1], clamp1[1] - c1[1], clamp1[1] - cut1[1])) + + def test_traverse_with_cut_with_unknown_length_from_t1_does_not_return_clamp_with_known_length_from_t1(self, subtests): # # (Cut with null length is treated as 0.0 # 1 b0 21*1 cut1 2*-c1-*-21 b2 2 @@ -779,8 +799,8 @@ def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_fr def _verify_paths(in_paths: Generator[NetworkTraceStep.Path, None, None], in_expected: Iterable[NetworkTraceStep.Path], check_length=True): - paths = sorted(list(in_paths), key=lambda p: (p.from_terminal, p.to_terminal)) - expected = sorted(in_expected, key=lambda p: (p.from_terminal, p.to_terminal)) + paths = list(in_paths) + expected = list(in_expected) for path in paths: if path in expected: continue From a410f722976751127f8ee84c09e1874492853ca5 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 23 May 2025 03:09:12 +1000 Subject: [PATCH 13/47] Reworked the internals of network_trace.Conditions Signed-off-by: Max Chesterfield --- src/zepben/evolve/__init__.py | 10 +++++- .../actions/equipment_tree_builder.py | 6 ++-- .../tracing/networktrace/compute_data.py | 4 ++- .../networktrace/conditions/conditions.py | 2 +- .../conditions/direction_condition.py | 3 +- .../equipment_step_limit_condition.py | 15 +++++--- .../equipment_type_step_limit_condition.py | 4 ++- .../networktrace/conditions/open_condition.py | 2 ++ .../network_trace_queue_condition.py | 26 +++++++------- .../network/tracing/phases/remove_phases.py | 16 ++++----- .../traversal/context_value_computer.py | 2 ++ .../network/tracing/traversal/queue.py | 6 ++-- .../tracing/traversal/queue_condition.py | 31 +++++++++------- .../network/tracing/traversal/step_action.py | 2 ++ .../tracing/traversal/stop_condition.py | 35 ++++++++++++------- .../network/tracing/traversal/traversal.py | 13 ++++--- .../tracing/traversal/traversal_condition.py | 15 +++++--- .../conditions/test_open_condition.py | 7 ++-- 18 files changed, 123 insertions(+), 76 deletions(-) diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index d9c9e85a2..67a9e0dc9 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -170,13 +170,21 @@ from zepben.evolve.services.network.tracing.feeder.direction_status import * from zepben.evolve.services.network.tracing.feeder.assign_to_feeders import * from zepben.evolve.services.network.tracing.feeder.assign_to_lv_feeders import * +from zepben.evolve.services.network.tracing.networktrace.actions.equipment_tree_builder import EquipmentTreeBuilder +from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import EquipmentStepLimitCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import EquipmentTypeStepLimitCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import OpenCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import Conditions +from zepben.evolve.services.network.tracing.networktrace.compute_data import * from zepben.evolve.services.network.tracing.phases.phase_status import * from zepben.evolve.services.network.tracing.phases.phase_inferrer import * from zepben.evolve.services.network.tracing.phases.remove_phases import * from zepben.evolve.services.network.tracing.find_swer_equipment import * from zepben.evolve.services.network.tracing.traversal.queue_condition import * from zepben.evolve.services.network.tracing.traversal.context_value_computer import * -from zepben.evolve.services.network.tracing.traversal.step_action import StepAction +from zepben.evolve.services.network.tracing.traversal.step_action import * +from zepben.evolve.services.network.tracing.traversal.stop_condition import * from zepben.evolve.services.network.tracing.feeder.set_direction import * from zepben.evolve.services.common.meta.data_source import * diff --git a/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py b/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py index 14b7f6d65..eb818166a 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py @@ -4,7 +4,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import uuid -from typing import Any +from typing import Any, Generator from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.evolve.services.network.tracing.networktrace.actions.tree_node import TreeNode @@ -38,8 +38,8 @@ def __init__(self): self.key = str(uuid.uuid4()) @property - def roots(self): - return self._roots.values() + def roots(self) -> Generator[TreeNode[ConductingEquipment], None, None]: + return (r for r in self._roots.values()) def compute_initial_value(self, item: NetworkTraceStep[Any]) -> EquipmentTreeNode: node = self._roots.get(item.path.to_equipment) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py b/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py index fd6586644..f345c09d6 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/compute_data.py @@ -2,13 +2,15 @@ # 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 TypeVar, Generic, Any +from typing import TypeVar, Generic from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.traversal.step_context import StepContext T = TypeVar('T') +__all__ = ['ComputeData', 'ComputeDataWithPaths'] + class ComputeData(Generic[T]): """ diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py index 67930cfa9..ac6485ae0 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, TypeVar, Type -from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection +from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import EquipmentStepLimitCondition from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import EquipmentTypeStepLimitCondition diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py index d9c8cc487..6923cb655 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py @@ -2,7 +2,6 @@ # 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 __future__ import annotations from typing import TypeVar, TYPE_CHECKING, Generic, Type @@ -20,6 +19,8 @@ T = TypeVar('T') +__all__ = ['DirectionCondition'] + class DirectionCondition(QueueCondition[NetworkTraceStep[T]], Generic[T]): diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py index 519660d04..e6bfd67eb 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_step_limit_condition.py @@ -2,18 +2,23 @@ # 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 Generic, TypeVar +from __future__ import annotations + +from typing import TypeVar, TYPE_CHECKING -from zepben.evolve import StepContext, NetworkTraceStep from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition T = TypeVar('T') +if TYPE_CHECKING: + from zepben.evolve import StepContext, NetworkTraceStep + +__all__ = ['EquipmentStepLimitCondition'] -class EquipmentStepLimitCondition(StopCondition[NetworkTraceStep[T]], Generic[T]): +class EquipmentStepLimitCondition(StopCondition['NetworkTraceStep[T]']): def __init__(self, limit: int): - super().__init__(self.should_stop) + super().__init__() self.limit = limit def should_stop(self, item: NetworkTraceStep[T], context: StepContext) -> bool: - return item.num_equipment_steps >= self.limit \ No newline at end of file + return item.num_equipment_steps >= self.limit diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py index 081d9dbba..654252ed7 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py @@ -14,10 +14,12 @@ T = TypeVar('T') +__all__ = ['EquipmentTypeStepLimitCondition'] + class EquipmentTypeStepLimitCondition(StopConditionWithContextValue, Generic[T]): def __init__(self, limit: int, equipment_type: Type[ConductingEquipment]): - StopConditionWithContextValue.__init__(self, _func=self.should_stop) + StopConditionWithContextValue.__init__(self, self.should_stop) TypedContextValueComputer.__init__(self, f'sdk:{equipment_type.name}Count') self.limit = limit self.equipment_type = equipment_type diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py index 58566b27a..032bcadc6 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py @@ -19,6 +19,8 @@ T = TypeVar('T') +__all__ = ['OpenCondition'] + class OpenCondition(NetworkTraceQueueCondition[T], Generic[T]): def __init__(self, is_open: Callable[[Switch, SinglePhaseKind], bool], phase: SinglePhaseKind = None): diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py index 0a6df570d..97b3d1d13 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py @@ -13,21 +13,9 @@ class NetworkTraceQueueCondition(QueueCondition[NetworkTraceStep[T]], Generic[T]): - step_type:NetworkTraceStep.Type def __init__(self, step_type: NetworkTraceStep.Type): - self.should_queue_func = { - NetworkTraceStep.Type.ALL: self.should_queue_matched_step, - NetworkTraceStep.Type.INTERNAL: self.should_queue_internal_step, - NetworkTraceStep.Type.EXTERNAL: self.should_queue_external_step - }.get(step_type) - - - def should_queue(self, next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: - """ - interface to call the correct `self.should_queue_****_step` function as defined by `self.should_queue_func` - """ - return self.should_queue_func(next_item, next_context, current_item, current_context) + super().__init__(self._should_queue_func_map[step_type]) def should_queue_matched_step(self, next_item: NetworkTraceStep[T], next_context: StepContext, current_item: NetworkTraceStep[T], current_context: StepContext) -> bool: raise NotImplementedError() @@ -42,6 +30,18 @@ def should_queue_external_step(self, next_item: NetworkTraceStep[T], next_contex return self.should_queue_matched_step(next_item, next_context, current_item, current_context) return True + @property + def _should_queue_func_map(self): + return { + NetworkTraceStep.Type.ALL: self.should_queue_matched_step, + NetworkTraceStep.Type.INTERNAL: self.should_queue_internal_step, + NetworkTraceStep.Type.EXTERNAL: self.should_queue_external_step + } + + @staticmethod + def should_queue(next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: + raise NotImplementedError() + @staticmethod def delegate_to(step_type: NetworkTraceStep.Type, condition: QueueCondition[NetworkTraceStep[T]]) -> 'NetworkTraceQueueCondition[T]': return DelegatedNetworkTraceQueueCondition(step_type, condition) diff --git a/src/zepben/evolve/services/network/tracing/phases/remove_phases.py b/src/zepben/evolve/services/network/tracing/phases/remove_phases.py index b4d4626a3..7c7850d1d 100644 --- a/src/zepben/evolve/services/network/tracing/phases/remove_phases.py +++ b/src/zepben/evolve/services/network/tracing/phases/remove_phases.py @@ -5,20 +5,18 @@ from __future__ import annotations -from typing import Set, Union +from typing import Set, Union, Type from zepben.evolve import NetworkService from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData from zepben.evolve.services.network.tracing.networktrace.network_trace import NetworkTrace from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.services.network.tracing.traversal.step_context import StepContext -from zepben.evolve.services.network.tracing.traversal.traversal import Traversal from zepben.evolve.services.network.tracing.traversal.weighted_priority_queue import WeightedPriorityQueue @@ -37,7 +35,7 @@ class RemovePhases(object): async def run(self, start: Union[NetworkService, Terminal], nominal_phases_to_ebb: Union[PhaseCode, SinglePhaseKind]=None, - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): if nominal_phases_to_ebb is None: if isinstance(start, NetworkService): @@ -49,17 +47,17 @@ async def run(self, return await self._run_with_phases_to_ebb(start, nominal_phases_to_ebb, network_state_operators) @staticmethod - async def _run_with_network(network_service: NetworkService, network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + async def _run_with_network(network_service: NetworkService, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): for t in network_service.objects(Terminal): t.traced_phases.phase_status = 0 - async def _run_with_terminal(self, terminal: Terminal, network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + async def _run_with_terminal(self, terminal: Terminal, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): return await self._run_with_phases_to_ebb(terminal, terminal.phases, network_state_operators) async def _run_with_phases_to_ebb(self, terminal: Terminal, nominal_phases_to_ebb: Union[PhaseCode, Set[SinglePhaseKind]], - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL): + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): if isinstance(nominal_phases_to_ebb, PhaseCode): return await self._run_with_phases_to_ebb(terminal, set(nominal_phases_to_ebb.single_phases), network_state_operators) @@ -67,7 +65,7 @@ async def _run_with_phases_to_ebb(self, trace = await self._create_trace(network_state_operators) return await trace.run(terminal, EbbPhases(nominal_phases_to_ebb), terminal.phases) - async def _create_trace(self, state_operators: NetworkStateOperators) -> NetworkTrace[EbbPhases]: + async def _create_trace(self, state_operators: Type[NetworkStateOperators]) -> NetworkTrace[EbbPhases]: def compute_data(step: NetworkTraceStep[EbbPhases], context: StepContext, next_path: NetworkTraceStep.Path): data = [] @@ -92,7 +90,7 @@ def queue_condition(next_step: NetworkTraceStep, next_ctx: StepContext=None, ste .add_queue_condition(queue_condition) @staticmethod - async def _ebb(state_operators: NetworkStateOperators, terminal: Terminal, phases_to_ebb: Set[SinglePhaseKind]) -> Set[SinglePhaseKind]: + async def _ebb(state_operators: Type[NetworkStateOperators], terminal: Terminal, phases_to_ebb: Set[SinglePhaseKind]) -> Set[SinglePhaseKind]: phases = state_operators.phase_status(terminal) for phase in phases_to_ebb: if phases[phase] != SinglePhaseKind.NONE: diff --git a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py index afa3f0c72..9a697b1ce 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py +++ b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py @@ -11,6 +11,8 @@ T = TypeVar('T') U = TypeVar('U') +__all__ = ['ContextValueComputer', 'TypedContextValueComputer'] + class ContextValueComputer(ABC, Generic[T]): """ diff --git a/src/zepben/evolve/services/network/tracing/traversal/queue.py b/src/zepben/evolve/services/network/tracing/traversal/queue.py index 90bbb1b5f..1fa144dd0 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/queue.py +++ b/src/zepben/evolve/services/network/tracing/traversal/queue.py @@ -10,7 +10,7 @@ from __future__ import annotations -from abc import ABC, abstractmethod +from abc import abstractmethod, ABCMeta from collections import deque from typing import TypeVar, Iterable, Generic, Deque, TYPE_CHECKING, Union @@ -19,6 +19,8 @@ if TYPE_CHECKING: from zepben.evolve import NetworkTraceStep + + T = TypeVar('T') U = TypeVar('U') @@ -36,7 +38,7 @@ def peek(self) -> T: return self[0] -class TraversalQueue(Generic[T], ABC): +class TraversalQueue(Generic[T], metaclass=ABCMeta): """ Basic queue object, implementing some methods to align it with the kotlin sdk syntax, """ diff --git a/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py b/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py index f6695c9f7..5aa1382d8 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py @@ -4,7 +4,8 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations -from typing import TypeVar, Generic +from abc import abstractmethod +from typing import TypeVar, Generic, Callable from zepben.evolve.services.network.tracing.traversal.step_context import StepContext from zepben.evolve.services.network.tracing.traversal.traversal_condition import TraversalCondition @@ -12,15 +13,25 @@ T = TypeVar('T') U = TypeVar('U') +ShouldQueue = Callable[[T, StepContext, T, StepContext], bool] +ShouldQueueStartItem = Callable[[T], bool] -class QueueCondition(TraversalCondition[T], Generic[T]): +__all__ = ['QueueCondition', 'QueueConditionWithContextValue', 'ShouldQueue', 'ShouldQueueStartItem'] + + +@TraversalCondition.register +class QueueCondition(Generic[T]): """ Functional interface representing a condition that determines whether a traversal should queue a next item. `T` The type of items being traversed. """ + def __init__(self, should_queue: ShouldQueue=None): + self.should_queue = should_queue - def should_queue(self, next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: + @staticmethod + @abstractmethod + def should_queue(next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: """ Determines whether the [nextItem] should be queued for traversal. @@ -30,22 +41,17 @@ def should_queue(self, next_item: T, next_context: StepContext, current_item: T, `currentContext` The context associated with the [currentItem]. Returns `true` if the [nextItem] should be queued; `false` otherwise. """ - return self._func(next_item, next_context, current_item, current_context) + raise NotImplementedError - def should_queue_start_item(self, item: T) -> bool: + @staticmethod + def should_queue_start_item(item: T) -> bool: """ Determines whether a traversal startItem should be queued when running a [Traversal]. `item` The item to be potentially queued. Returns `true` if the [item] should be queued; `false` otherwise. Defaults to `true`. """ - try: # this is a filthy hack to avoid this being called on a queue condition function that doesnt match this signature - # TODO: this absolute hack of a method to use this as a functional interface needs to go.. - return self._func(item) - except TypeError as e: - if self._func.__code__.co_argcount == 4: - return True - raise e + return True from zepben.evolve.services.network.tracing.traversal.context_value_computer import TypedContextValueComputer @@ -57,4 +63,3 @@ class QueueConditionWithContextValue(QueueCondition[T], TypedContextValueCompute `T` The type of items being traversed. `U` The type of the context value computed and used in the condition. """ - pass diff --git a/src/zepben/evolve/services/network/tracing/traversal/step_action.py b/src/zepben/evolve/services/network/tracing/traversal/step_action.py index 180268082..feb743eaa 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/step_action.py +++ b/src/zepben/evolve/services/network/tracing/traversal/step_action.py @@ -12,6 +12,8 @@ T = TypeVar('T') U = TypeVar('U') +__all__ = ['StepAction', 'StepActionWithContextValue'] + class StepAction(Generic[T]): """ diff --git a/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py b/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py index 5d9fcbf59..bc11d93fb 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py @@ -3,38 +3,47 @@ # 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 TypeVar, Generic +from abc import abstractmethod +from typing import TypeVar, Generic, Callable from zepben.evolve.services.network.tracing.traversal.context_value_computer import TypedContextValueComputer from zepben.evolve.services.network.tracing.traversal.step_context import StepContext from zepben.evolve.services.network.tracing.traversal.traversal_condition import TraversalCondition - T = TypeVar('T') U = TypeVar('U') +ShouldStop = Callable[[T, StepContext], bool] + +__all__ = ['StopCondition', 'StopConditionWithContextValue', 'ShouldStop'] + -class StopCondition(TraversalCondition[T], Generic[T]): +@TraversalCondition.register +class StopCondition(Generic[T]): """ Functional interface representing a condition that determines whether the traversal should stop at a given item. - `T` The type of items being traversed. + T : The type of items being traversed. """ + def __init__(self, stop_function: ShouldStop=None): + if stop_function is not None: + self.should_stop = stop_function + def should_stop(self, item: T, context: StepContext) -> bool: """ Determines whether the traversal should stop at the specified item. - `item` The current item being processed in the traversal. - `context` The context associated with the current traversal step. - Returns `true` if the traversal should stop at this item; `false` otherwise. + :param item: The current item being processed in the traversal. + :param context: The context associated with the current traversal step. + + :return: `True` if the traversal should stop at this item; `False` otherwise. """ - return self._func(item, context) -class StopConditionWithContextValue(StopCondition[T], TypedContextValueComputer[T, U], Generic[T, U]): + +class StopConditionWithContextValue(StopCondition[T], TypedContextValueComputer[T, U]): """ - Interface representing a stop condition that requires a value stored in the [StepContext] to determine if an item should be queued. + Interface representing a stop condition that requires a value stored in the StepContext to determine if an item should be queued. - `T` The type of items being traversed. - `U` The type of the context value computed and used in the condition. + T : The type of items being traversed. + U : The type of the context value computed and used in the condition. """ - pass diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal.py b/src/zepben/evolve/services/network/tracing/traversal/traversal.py index fbe483927..9aac842e8 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal.py @@ -7,6 +7,7 @@ from abc import abstractmethod from collections import deque +from functools import singledispatch, singledispatchmethod from typing import List, Callable, TypeVar, Generic, Optional, Dict, Union from zepben.evolve import require @@ -168,19 +169,15 @@ def create_new_this(self) -> D: """ raise NotImplementedError - def add_condition(self, condition: QueueCondition | Callable[[NetworkTraceStep[T], StepContext], None]) -> D: + @singledispatchmethod + def add_condition(self, condition: Union[QueueCondition, Callable[[NetworkTraceStep[T], StepContext], None]]) -> D: """ Adds a traversal condition to the traversal. `condition` The condition to add. Returns this traversal instance. """ - if isinstance(condition, (QueueCondition, DirectionCondition)): - return self.add_queue_condition(condition) - elif isinstance(condition, StopCondition): - return self.add_stop_condition(condition) - - elif callable(condition): # Callable[[NetworkTraceStep[T], StepContext], None] + if callable(condition): # Callable[[NetworkTraceStep[T], StepContext], None] assert not isinstance(condition, TraversalCondition) if condition.__code__.co_argcount == 2: return self.add_stop_condition(condition) @@ -193,6 +190,7 @@ def add_condition(self, condition: QueueCondition | Callable[[NetworkTraceStep[T raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: ' + "[QueueCondition | DirectionCondition | StopCondition | Callable[_,_] | Callable[_,_,_,_]]") + @add_condition.register(StopCondition) def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopConditionWithContextValue[T, U]]) -> D: """ Adds a stop condition to the traversal. If any stop condition returns `true`, the traversal @@ -230,6 +228,7 @@ def matches_any_stop_condition(self, item: T, context: StepContext) -> bool: return True return False + @add_condition.register(QueueCondition) def add_queue_condition(self, condition: Union[Callable, QueueCondition[T]]) -> D: """ Adds a queue condition to the traversal. Queue conditions determine whether an item should be queued for traversal. diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py index 063683d23..5bd2715cb 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py @@ -3,11 +3,18 @@ # 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 abc import ABC +import abc from typing import TypeVar, Generic T = TypeVar('T') -class TraversalCondition(ABC, Generic[T]): - def __init__(self, _func): - self._func = _func \ No newline at end of file + +class TraversalCondition(Generic[T], metaclass=abc.ABCMeta): + """ + Protocol, representing a condition used in a traversal. + Implementations of this interface can influence the traversal process by determining + things such as the ability to queue items,stop at specific items, or apply other + conditional logic during traversal + + T : The type of items being processed + """ diff --git a/test/services/network/tracing/networktrace/conditions/test_open_condition.py b/test/services/network/tracing/networktrace/conditions/test_open_condition.py index 426a5d002..ca7a9ebe6 100644 --- a/test/services/network/tracing/networktrace/conditions/test_open_condition.py +++ b/test/services/network/tracing/networktrace/conditions/test_open_condition.py @@ -31,16 +31,19 @@ def should_queue_params(next_step, next_context=None, current_step=None, current ) -> (NetworkTraceStep, StepContext, NetworkTraceStep, StepContext): return next_step, next_context or MagicMock(), current_step or MagicMock(), current_context or MagicMock() +def _is_open(switch: Switch, phase: SinglePhaseKind) -> bool: + pass + class TestOpenCondition: def test_always_queues_external_steps(self): - is_open = Callable[[Switch, SinglePhaseKind], bool] + is_open = _is_open spk = MagicMock(spec=SinglePhaseKind) next_step = mock_nts(step_type=NetworkTraceStep.Type.EXTERNAL) assert OpenCondition(is_open, spk).should_queue(*should_queue_params(next_step)) def test_always_queues_non_switch_equipment(self): - is_open = Callable[[Switch, SinglePhaseKind], bool] + is_open = _is_open spk = MagicMock(spec=SinglePhaseKind) next_path = mock_nts_path(to_equipment=MagicMock(spec=ConductingEquipment)) From 0a3a303f0ab72facf9ed1f6801cd0d82ebd5da09 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 23 May 2025 03:20:57 +1000 Subject: [PATCH 14/47] . Signed-off-by: Max Chesterfield --- src/zepben/evolve/__init__.py | 1 + .../network/tracing/traversal/stop_condition.py | 1 - .../tracing/traversal/traversal_condition.py | 13 +++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index 67a9e0dc9..f4200032e 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -185,6 +185,7 @@ from zepben.evolve.services.network.tracing.traversal.context_value_computer import * from zepben.evolve.services.network.tracing.traversal.step_action import * from zepben.evolve.services.network.tracing.traversal.stop_condition import * +from zepben.evolve.services.network.tracing.traversal.traversal_condition import * from zepben.evolve.services.network.tracing.feeder.set_direction import * from zepben.evolve.services.common.meta.data_source import * diff --git a/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py b/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py index bc11d93fb..21f4425f4 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py @@ -3,7 +3,6 @@ # 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 abc import abstractmethod from typing import TypeVar, Generic, Callable from zepben.evolve.services.network.tracing.traversal.context_value_computer import TypedContextValueComputer diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py index 5bd2715cb..06775edd4 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py @@ -8,6 +8,8 @@ T = TypeVar('T') +__all__ = ['TraversalCondition'] + class TraversalCondition(Generic[T], metaclass=abc.ABCMeta): """ @@ -16,5 +18,16 @@ class TraversalCondition(Generic[T], metaclass=abc.ABCMeta): things such as the ability to queue items,stop at specific items, or apply other conditional logic during traversal + New subclasses of this class should be made via: + + >>>@TraversalCondition.register + >>>class SomeCondition(Generic[T]): + >>> pass + + and not direct subclassing as it will enforce overriding of `__init__` + T : The type of items being processed """ + @abc.abstractmethod + def __init__(self): + """This method is only defined to deny the ability to create this class without subclassing""" From e31b95ea4a3addc6b7e50d5836144e8e19ff8a47 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 23 May 2025 17:05:56 +1000 Subject: [PATCH 15/47] get rid of alot of `if isinstance` in favor of singledispatch Signed-off-by: Max Chesterfield --- src/zepben/evolve/__init__.py | 1 + .../networktrace/conditions/conditions.py | 19 ++- .../conditions/direction_condition.py | 4 +- .../equipment_type_step_limit_condition.py | 3 +- .../networktrace/conditions/open_condition.py | 3 - .../tracing/networktrace/network_trace.py | 110 +++++++++--------- .../network_trace_queue_condition.py | 32 ++--- .../operators/network_state_operators.py | 4 +- .../operators/open_state_operators.py | 5 + .../tracing/traversal/queue_condition.py | 12 +- .../tracing/traversal/stop_condition.py | 3 +- .../network/tracing/traversal/traversal.py | 52 +++++---- .../tracing/traversal/traversal_condition.py | 17 +-- 13 files changed, 125 insertions(+), 140 deletions(-) diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index f4200032e..25184092f 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -177,6 +177,7 @@ from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import OpenCondition from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import Conditions from zepben.evolve.services.network.tracing.networktrace.compute_data import * +from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import * from zepben.evolve.services.network.tracing.phases.phase_status import * from zepben.evolve.services.network.tracing.phases.phase_inferrer import * from zepben.evolve.services.network.tracing.phases.remove_phases import * diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py index ac6485ae0..a2c5c46b7 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar, Type +from typing import TYPE_CHECKING, TypeVar, Type, Callable from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition @@ -18,44 +18,43 @@ from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition - from zepben.evolve import ConductingEquipment + from zepben.evolve import ConductingEquipment, NetworkStateOperators NetworkTraceQueueCondition = QueueCondition[NetworkTraceStep[T]] NetworkTraceStopCondition = StopCondition[NetworkTraceStep[T]] - + DSLLambda = Callable[[NetworkStateOperators], NetworkTraceQueueCondition[T]] # FIXME: work out how to inject NetworkStateOperators into this from inside NetworkTrace class Conditions: @classmethod - def upstream(cls) -> NetworkTraceQueueCondition[T]: + def upstream(cls) -> DSLLambda: """ Creates a [NetworkTrace] condition that will cause tracing a feeder upstream (towards the head terminal). This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. :return: [NetworkTraceQueueCondition] that results in upstream tracing. """ - return cls.with_direction(FeederDirection.UPSTREAM) + return lambda state_operator: state_operator.with_direction(FeederDirection.UPSTREAM) @classmethod - def downstream(cls) -> NetworkTraceQueueCondition[T]: + def downstream(cls) -> DSLLambda: """ Creates a [NetworkTrace] condition that will cause tracing a feeder downstream (away from the head terminal). This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. :return: [NetworkTraceQueueCondition] that results in downstream tracing. """ - return cls.with_direction(FeederDirection.DOWNSTREAM) + return lambda state_operator: state_operator.with_direction(FeederDirection.DOWNSTREAM) @classmethod - def with_direction(cls, direction: FeederDirection) -> NetworkTraceQueueCondition[T]: + def with_direction(cls, direction: FeederDirection) -> DSLLambda: """ Creates a [NetworkTrace] condition that will cause tracing only terminals with directions that match [direction]. This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. :return: [NetworkTraceQueueCondition] that results in upstream tracing. """ - return DirectionCondition(direction, cls) # FIXME: cls should be NetworkStateOperators, somehow need - # to load these methods onto there after passing them to NetworkTrace + return lambda state_operator: DirectionCondition(direction, state_operator) @staticmethod def limit_equipment_steps(limit: int, equipment_type: Type[ConductingEquipment]=None) -> NetworkTraceStopCondition[T]: diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py index 6923cb655..b58b54404 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py @@ -13,7 +13,7 @@ from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep if TYPE_CHECKING: - from zepben.evolve.services.network.tracing.networktrace.operators.feeder_direction_state_operations import FeederDirectionStateOperations + from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve import StepContext from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection @@ -24,7 +24,7 @@ class DirectionCondition(QueueCondition[NetworkTraceStep[T]], Generic[T]): - def __init__(self, direction: FeederDirection, state_operators: Type[FeederDirectionStateOperations]): + def __init__(self, direction: FeederDirection, state_operators: Type[NetworkStateOperators]): self.direction = direction self.state_operators = state_operators self.get_direction = self.state_operators.get_direction diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py index 654252ed7..174becbba 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/equipment_type_step_limit_condition.py @@ -13,11 +13,12 @@ from zepben.evolve import ConductingEquipment, StepContext, NetworkTraceStep T = TypeVar('T') +U = TypeVar('U') __all__ = ['EquipmentTypeStepLimitCondition'] -class EquipmentTypeStepLimitCondition(StopConditionWithContextValue, Generic[T]): +class EquipmentTypeStepLimitCondition(StopConditionWithContextValue[T, U], Generic[T, U]): def __init__(self, limit: int, equipment_type: Type[ConductingEquipment]): StopConditionWithContextValue.__init__(self, self.should_stop) TypedContextValueComputer.__init__(self, f'sdk:{equipment_type.name}Count') diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py index 032bcadc6..bb9bf4ea1 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py @@ -35,6 +35,3 @@ def should_queue_matched_step(self, next_item: NetworkTraceStep[T], next_context return not self._is_open(equip, self._phase) else: return True - - def should_queue_start_item(self, item: T) -> bool: - return True 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 a5cfc0b8e..efc1af58a 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -4,6 +4,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from collections.abc import Callable +from functools import singledispatchmethod from typing import TypeVar, Union, Generic, Set, Type, Generator from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp @@ -117,6 +118,7 @@ def branching(cls, parent, action_type) + @singledispatchmethod def add_start_item(self, start: Union[Terminal, ConductingEquipment], data: T=None, phases: PhaseCode=None) -> "NetworkTrace[T]": """ Depending on the type of `start`, adds either: @@ -129,46 +131,47 @@ def add_start_item(self, start: Union[Terminal, ConductingEquipment], data: T=No :param data: The data associated with the start step. :param phases: Phases to trace; `None` to ignore phases. """ - #if isinstance(start, NetworkTraceStep): - # super().add_start_item(start) - # return self - - if isinstance(start, Terminal): - # We have a special case when starting specifically on a clamp terminal that we mark it as having traversed the segment such that it - # will only trace externally from the clamp terminal. This behaves differently to when the whole Clamp is added as a start item. - traversed_ac_line_segment = None - if isinstance(start.conducting_equipment, Clamp): - traversed_ac_line_segment = start.conducting_equipment.ac_line_segment - self._add_start_item(start, data, phases, traversed_ac_line_segment) - return self - - elif issubclass(start.__class__, ConductingEquipment) or isinstance(start, ConductingEquipment): - # If we start on an AcLineSegment, we queue the segments terminals, and all its Cut and Clamp terminals as if we have traversed the segment, - # so the next steps will be external from all the terminals "belonging" to the segment. - if isinstance(start, AcLineSegment): - def start_terminals() -> Generator[Terminal, None, None]: - for terminal in start.terminals: - yield terminal - for clamp in start.clamps: - for terminal in clamp.terminals: - yield terminal - break - for cut in start.cuts: - for terminal in cut.terminals: - yield terminal - for terminal in start_terminals(): - self._add_start_item(terminal, data, phases, start) - - # We don't have a special case for Clamp here because we say if you start from the whole Clamp rather than its terminal specifically, - # we want to trace externally from it and traverse its segment. - else: - for it in start.terminals: - self._add_start_item(it, data, phases, None) - - return self - raise Exception('INTERNAL ERROR:: unexpected add_start_item params') + @add_start_item.register + def _(self, start: ConductingEquipment, data=None, phases=None): + # We don't have a special case for Clamp here because we say if you start from the whole Clamp rather than its terminal specifically, + # we want to trace externally from it and traverse its segment. + for it in start.terminals: + self._add_start_item(it, data, phases, None) + + return self + + @add_start_item.register + def _(self, start: Terminal, data=None, phases=None): + # We have a special case when starting specifically on a clamp terminal that we mark it as having traversed the segment such that it + # will only trace externally from the clamp terminal. This behaves differently to when the whole Clamp is added as a start item. + traversed_ac_line_segment = None + if isinstance(start.conducting_equipment, Clamp): + traversed_ac_line_segment = start.conducting_equipment.ac_line_segment + self._add_start_item(start, data, phases, traversed_ac_line_segment) + return self + + @add_start_item.register + def _(self, start: AcLineSegment, data=None, phases=None): + # If we start on an AcLineSegment, we queue the segments terminals, and all its Cut and Clamp terminals as if we have traversed the segment, + # so the next steps will be external from all the terminals "belonging" to the segment. + def start_terminals() -> Generator[Terminal, None, None]: + for terminal in start.terminals: + yield terminal + for clamp in start.clamps: + for terminal in clamp.terminals: + yield terminal + break + for cut in start.cuts: + for terminal in cut.terminals: + yield terminal + + + for terminal in start_terminals(): + self._add_start_item(terminal, data, phases, start) + + def _add_start_item(self, start: Terminal=None, data: T=None, @@ -199,14 +202,17 @@ async def run(self, start: Union[ConductingEquipment, Terminal]=None, data: T=No await super().run(can_stop_on_start_item=can_stop_on_start_item) return self + @singledispatchmethod def add_condition(self, condition: QueueCondition[T]) -> "NetworkTrace[T]": + """ Adds a traversal condition to the trace using the trace's [NetworkStateOperators] as the receiver. This overload primarily exists to enable a DSL-like syntax for adding predefined traversal conditions to the trace. For example, to configure the trace to stop at open points using the [Conditions.stopAtOpen] factory, you can use: - >>> NetworkTrace().add_condition(NetworkStateOperators.NORMAL.stop_at_open()) + >>> from zepben.evolve import stop_at_open + >>> NetworkTrace().add_condition(stop_at_open()) :param condition: A lambda function that returns a traversal condition. :returns: This [NetworkTrace] instance @@ -214,14 +220,19 @@ def add_condition(self, condition: QueueCondition[T]) -> "NetworkTrace[T]": super().add_condition(condition) return self - def add_queue_condition(self, condition: Union[Callable, QueueCondition[NetworkTraceStep[T]]], step_type:NetworkTraceStep.Type=None) -> "NetworkTrace[T]": - if callable(condition): - return self.add_queue_condition(QueueCondition(condition)) + @add_condition.register + def _(self, condition: Callable): + if condition.__code__.co_argcount == 1: + return self.add_condition(condition(self.network_state_operators)) + super().add_condition(condition) - if step_type is None: - return super().add_queue_condition(to_network_trace_queue_condition(condition, default_queue_condition_step_type(self._action_type), False)) - else: - return super().add_queue_condition(to_network_trace_queue_condition(condition, step_type, True)) + @singledispatchmethod + def add_queue_condition(self, condition: NetworkTraceQueueCondition[NetworkTraceStep[T]], step_type:NetworkTraceStep.Type=None) -> "NetworkTrace[T]": + return super().add_queue_condition(condition) + + @add_queue_condition.register + def _(self, condition: Callable, step_type: NetworkTraceStep.Type=None): + return self.add_queue_condition(NetworkTraceQueueCondition(default_queue_condition_step_type(step_type or self._action_type), condition)) def can_action_item(self, item: T, context: StepContext) -> bool: return self._action_type(item, context, self.has_visited) @@ -249,13 +260,6 @@ def visit(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: return not (self.parent and self.parent.has_visited(terminal, phases)) and self._tracker.visit(terminal, phases) -def to_network_trace_queue_condition(queue_condition: QueueCondition[NetworkTraceStep[T]], step_type: NetworkTraceStep.Type, override_step_type: bool): - if isinstance(queue_condition, NetworkTraceQueueCondition) and not override_step_type: - return queue_condition - else: - return NetworkTraceQueueCondition.delegate_to(step_type, queue_condition) - - def default_queue_condition_step_type(step_type): if step_type == NetworkTraceActionType.ALL_STEPS: return NetworkTraceStep.Type.ALL diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py index 97b3d1d13..96a9bd56d 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py @@ -6,7 +6,7 @@ from typing import TypeVar, Generic from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep -from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition +from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition, ShouldQueue from zepben.evolve.services.network.tracing.traversal.step_context import StepContext T = TypeVar('T') @@ -14,8 +14,14 @@ class NetworkTraceQueueCondition(QueueCondition[NetworkTraceStep[T]], Generic[T]): - def __init__(self, step_type: NetworkTraceStep.Type): - super().__init__(self._should_queue_func_map[step_type]) + def __init__(self, step_type: NetworkTraceStep.Type, condition: ShouldQueue=None): + super().__init__(self.should_queue) + if condition is not None: + self.should_queue_matched_step = condition + self._should_queue_func = self._should_queue_func_map[step_type] + + def should_queue(self, next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: + return self._should_queue_func(next_item, next_context, current_item, current_context) def should_queue_matched_step(self, next_item: NetworkTraceStep[T], next_context: StepContext, current_item: NetworkTraceStep[T], current_context: StepContext) -> bool: raise NotImplementedError() @@ -37,23 +43,3 @@ def _should_queue_func_map(self): NetworkTraceStep.Type.INTERNAL: self.should_queue_internal_step, NetworkTraceStep.Type.EXTERNAL: self.should_queue_external_step } - - @staticmethod - def should_queue(next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: - raise NotImplementedError() - - @staticmethod - def delegate_to(step_type: NetworkTraceStep.Type, condition: QueueCondition[NetworkTraceStep[T]]) -> 'NetworkTraceQueueCondition[T]': - return DelegatedNetworkTraceQueueCondition(step_type, condition) - - -class DelegatedNetworkTraceQueueCondition(NetworkTraceQueueCondition[T], Generic[T]): - def __init__(self, step_type: NetworkTraceStep.Type, delegate: QueueCondition[NetworkTraceStep[T]]): - super().__init__(step_type) - self.delegate = delegate - - def should_queue_matched_step(self, next_item: NetworkTraceStep[T], next_context: StepContext, current_item: NetworkTraceStep[T], current_context: StepContext) -> bool: - return self.delegate.should_queue(next_item, next_context, current_item, current_context) - - def should_queue_start_item(self, item: NetworkTraceStep[T]) -> bool: - return self.delegate.should_queue_start_item(item) \ No newline at end of file diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py index 50cc6ef00..3d57cd1e8 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py @@ -18,13 +18,14 @@ from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import InServiceStateOperators, NormalInServiceStateOperators, \ CurrentInServiceStateOperators from zepben.evolve.services.network.tracing.networktrace.operators.open_state_operators import OpenStateOperators, NormalOpenStateOperators, \ - CurrentOpenStateOperators + CurrentOpenStateOperators, stop_at_open from zepben.evolve.services.network.tracing.networktrace.operators.phase_state_operators import PhaseStateOperators, NormalPhaseStateOperators, \ CurrentPhaseStateOperators if TYPE_CHECKING: from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep +__all__ = ['NetworkStateOperators', 'stop_at_open'] # noinspection PyPep8Naming class NetworkStateOperators(OpenStateOperators, @@ -103,3 +104,4 @@ def network_trace_step_path_provider(cls): @classmethod def next_paths(cls, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: yield from cls.network_trace_step_path_provider().next_paths(path) + diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py index 12af98c09..40ee43aec 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py @@ -20,6 +20,8 @@ T = TypeVar('T') +__all__ = ['OpenStateOperators', 'stop_at_open'] + class OpenStateOperators(StateOperator): """ @@ -96,5 +98,8 @@ def set_open(switch: Switch, is_open: bool, phase: SinglePhaseKind = None) -> No switch.set_open(is_open, phase) +def stop_at_open(): + return lambda state_operator: state_operator.stop_at_open() + OpenStateOperators.NORMAL = NormalOpenStateOperators OpenStateOperators.CURRENT = CurrentOpenStateOperators diff --git a/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py b/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py index 5aa1382d8..d60b4723d 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/queue_condition.py @@ -19,19 +19,17 @@ __all__ = ['QueueCondition', 'QueueConditionWithContextValue', 'ShouldQueue', 'ShouldQueueStartItem'] -@TraversalCondition.register -class QueueCondition(Generic[T]): +class QueueCondition(Generic[T], TraversalCondition[T]): """ Functional interface representing a condition that determines whether a traversal should queue a next item. `T` The type of items being traversed. """ - def __init__(self, should_queue: ShouldQueue=None): - self.should_queue = should_queue - @staticmethod - @abstractmethod - def should_queue(next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: + def __init__(self, condition): + self.should_queue = condition + + def should_queue(self, next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: """ Determines whether the [nextItem] should be queued for traversal. diff --git a/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py b/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py index 21f4425f4..f63a0f2fb 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/stop_condition.py @@ -17,8 +17,7 @@ __all__ = ['StopCondition', 'StopConditionWithContextValue', 'ShouldStop'] -@TraversalCondition.register -class StopCondition(Generic[T]): +class StopCondition(Generic[T], TraversalCondition[T]): """ Functional interface representing a condition that determines whether the traversal should stop at a given item. diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal.py b/src/zepben/evolve/services/network/tracing/traversal/traversal.py index 9aac842e8..8da3fa1cf 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal.py @@ -7,15 +7,16 @@ from abc import abstractmethod from collections import deque -from functools import singledispatch, singledispatchmethod -from typing import List, Callable, TypeVar, Generic, Optional, Dict, Union +from collections.abc import Callable +from functools import singledispatchmethod +from typing import List, TypeVar, Generic, Optional, Dict, Union, Callable as CallableType from zepben.evolve import require from zepben.evolve.services.network.tracing.traversal.context_value_computer import ContextValueComputer from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition, QueueConditionWithContextValue from zepben.evolve.services.network.tracing.traversal.step_action import StepAction, StepActionWithContextValue from zepben.evolve.services.network.tracing.traversal.step_context import StepContext -from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition, StopConditionWithContextValue +from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition, StopConditionWithContextValue, ShouldStop from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep @@ -178,7 +179,6 @@ def add_condition(self, condition: Union[QueueCondition, Callable[[NetworkTraceS Returns this traversal instance. """ if callable(condition): # Callable[[NetworkTraceStep[T], StepContext], None] - assert not isinstance(condition, TraversalCondition) if condition.__code__.co_argcount == 2: return self.add_stop_condition(condition) elif condition.__code__.co_argcount == 4: @@ -190,6 +190,7 @@ def add_condition(self, condition: Union[QueueCondition, Callable[[NetworkTraceS raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: ' + "[QueueCondition | DirectionCondition | StopCondition | Callable[_,_] | Callable[_,_,_,_]]") + @singledispatchmethod @add_condition.register(StopCondition) def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopConditionWithContextValue[T, U]]) -> D: """ @@ -199,17 +200,19 @@ def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopCo `condition` The stop condition to add. Returns this traversal instance. """ - if isinstance(condition, StopCondition): - self.stop_conditions.append(condition) - if issubclass(condition.__class__, StopConditionWithContextValue): - self.compute_next_context_funs[condition.key] = condition - return self + raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [StopCondition | StopConditionWithContextValue | Callable]') - elif callable(condition): - assert not isinstance(condition, TraversalCondition) - return self.add_stop_condition(StopCondition(condition)) + @add_stop_condition.register + def _(self, condition: Callable): + return self.add_stop_condition(StopCondition(condition)) + + @add_stop_condition.register + def _(self, condition: StopCondition): + self.stop_conditions.append(condition) + if isinstance(condition, StopConditionWithContextValue): + self.compute_next_context_funs[condition.key] = condition + return self - raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [StopCondition | StopConditionWithContextValue | Callable]') def copy_stop_conditions(self, other: Traversal[T, D]) -> D: """ @@ -229,6 +232,7 @@ def matches_any_stop_condition(self, item: T, context: StepContext) -> bool: return False @add_condition.register(QueueCondition) + @singledispatchmethod def add_queue_condition(self, condition: Union[Callable, QueueCondition[T]]) -> D: """ Adds a queue condition to the traversal. Queue conditions determine whether an item should be queued for traversal. @@ -237,18 +241,18 @@ def add_queue_condition(self, condition: Union[Callable, QueueCondition[T]]) -> :param condition: The queue condition to add. :returns: The current traversal instance. """ - if isinstance(condition, QueueCondition): - self.queue_conditions.append(condition) - if isinstance(condition, QueueConditionWithContextValue): - self.compute_next_context_funs[condition.key] = condition - return self - - elif callable(condition): - assert not isinstance(condition, TraversalCondition) - return self.add_queue_condition(QueueCondition(condition)) - raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [QueueCondition | QueueConditionWithContextValue | Callable]') + @add_queue_condition.register + def _(self, condition: Callable): + return self.add_queue_condition(QueueCondition(condition)) + + @add_queue_condition.register + def _(self, condition: QueueCondition): + self.queue_conditions.append(condition) + if isinstance(condition, QueueConditionWithContextValue): + self.compute_next_context_funs[condition.key] = condition + return self def copy_queue_conditions(self, other: Traversal[T, D]) -> D: """ @@ -326,7 +330,7 @@ def add_context_value_computer(self, computer: ContextValueComputer[T]) -> D: `computer` The context value computer to add. Returns The current traversal instance. """ - require(not isinstance(computer, TraversalCondition), lambda: "`computer` must not be a TraversalCondition. Use `addCondition` to add conditions that also compute context values") + #require(not isinstance(computer, TraversalCondition), lambda: "`computer` must not be a TraversalCondition. Use `addCondition` to add conditions that also compute context values") self.compute_next_context_funs[computer.key] = computer return self diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py index 06775edd4..caebc8d59 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py @@ -4,30 +4,19 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import abc -from typing import TypeVar, Generic +from typing import TypeVar, Generic, Protocol, runtime_checkable T = TypeVar('T') __all__ = ['TraversalCondition'] - -class TraversalCondition(Generic[T], metaclass=abc.ABCMeta): +@runtime_checkable +class TraversalCondition(Protocol[T]): """ Protocol, representing a condition used in a traversal. Implementations of this interface can influence the traversal process by determining things such as the ability to queue items,stop at specific items, or apply other conditional logic during traversal - New subclasses of this class should be made via: - - >>>@TraversalCondition.register - >>>class SomeCondition(Generic[T]): - >>> pass - - and not direct subclassing as it will enforce overriding of `__init__` - T : The type of items being processed """ - @abc.abstractmethod - def __init__(self): - """This method is only defined to deny the ability to create this class without subclassing""" From 9589cd34f753a8f44ca160c1a7a4813cb4ca22e1 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Sat, 24 May 2025 12:55:16 +1000 Subject: [PATCH 16/47] Fixed WeightedPriorityQueue, and rework of network trace/traversal Signed-off-by: Max Chesterfield --- .../tracing/networktrace/network_trace.py | 2 +- .../networktrace/network_trace_queue_next.py | 1 - .../traversal/context_value_computer.py | 4 +- .../network/tracing/traversal/step_action.py | 9 ++- .../network/tracing/traversal/traversal.py | 71 +++++++++-------- .../tracing/traversal/traversal_condition.py | 3 +- .../traversal/weighted_priority_queue.py | 17 ++-- .../network/tracing/traversal/test_queue.py | 79 +++++++++++++++++++ .../tracing/traversal/test_traversal.py | 2 +- 9 files changed, 130 insertions(+), 58 deletions(-) create mode 100644 test/services/network/tracing/traversal/test_queue.py 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 efc1af58a..f0d5c72d9 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -222,7 +222,7 @@ def add_condition(self, condition: QueueCondition[T]) -> "NetworkTrace[T]": @add_condition.register def _(self, condition: Callable): - if condition.__code__.co_argcount == 1: + if condition.__code__.co_argcount == 1: # Catches DSL Style lambda conditions from zepben.evolve.Conditions return self.add_condition(condition(self.network_state_operators)) super().add_condition(condition) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index e2047a393..61eb77561 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -83,4 +83,3 @@ def accept(self, item: NetworkTraceStep[T], context: StepContext, queue_item: Qu else: for step in next_steps: queue_branch(step) - diff --git a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py index 9a697b1ce..d6d293e40 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py +++ b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py @@ -3,7 +3,7 @@ # 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 abc import ABC, abstractmethod +from abc import abstractmethod from typing import TypeVar, Generic from zepben.evolve.services.network.tracing.traversal.step_context import StepContext @@ -14,7 +14,7 @@ __all__ = ['ContextValueComputer', 'TypedContextValueComputer'] -class ContextValueComputer(ABC, Generic[T]): +class ContextValueComputer(Generic[T]): """ Interface representing a context value computer used to compute and store values in a [StepContext]. This interface does not specify a generic return type because the [StepContext] stores its values as `Any?`. diff --git a/src/zepben/evolve/services/network/tracing/traversal/step_action.py b/src/zepben/evolve/services/network/tracing/traversal/step_action.py index feb743eaa..6de17e3d4 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/step_action.py +++ b/src/zepben/evolve/services/network/tracing/traversal/step_action.py @@ -3,8 +3,7 @@ # 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 TypeVar, Generic +from typing import TypeVar, Generic, Callable from zepben.evolve.services.network.tracing.traversal.context_value_computer import TypedContextValueComputer from zepben.evolve.services.network.tracing.traversal.step_context import StepContext @@ -12,7 +11,9 @@ T = TypeVar('T') U = TypeVar('U') -__all__ = ['StepAction', 'StepActionWithContextValue'] +__all__ = ['StepAction', 'StepActionWithContextValue', 'StepActionFunc'] + +StepActionFunc = Callable[[T, StepContext], None] class StepAction(Generic[T]): @@ -22,7 +23,7 @@ class StepAction(Generic[T]): `T` The type of items being traversed. """ - def __init__(self, _func): + def __init__(self, _func: StepActionFunc): self._func = _func def apply(self, item: T, context: StepContext): diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal.py b/src/zepben/evolve/services/network/tracing/traversal/traversal.py index 8da3fa1cf..ac1b29f7a 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal.py @@ -9,26 +9,28 @@ from collections import deque from collections.abc import Callable from functools import singledispatchmethod -from typing import List, TypeVar, Generic, Optional, Dict, Union, Callable as CallableType +from typing import List, TypeVar, Generic, Optional, Dict, Union, TYPE_CHECKING from zepben.evolve import require from zepben.evolve.services.network.tracing.traversal.context_value_computer import ContextValueComputer -from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition, QueueConditionWithContextValue -from zepben.evolve.services.network.tracing.traversal.step_action import StepAction, StepActionWithContextValue +from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition, QueueConditionWithContextValue, ShouldQueue +from zepben.evolve.services.network.tracing.traversal.step_action import StepAction, StepActionWithContextValue, StepActionFunc from zepben.evolve.services.network.tracing.traversal.step_context import StepContext from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition, StopConditionWithContextValue, ShouldStop -from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep __all__ = ["Traversal"] -from zepben.evolve.services.network.tracing.traversal.traversal_condition import TraversalCondition from zepben.evolve.services.network.tracing.traversal.queue import TraversalQueue T = TypeVar('T') U = TypeVar('U') D = TypeVar('D', bound='Traversal') +if TYPE_CHECKING: + QueueConditionTypes = Union[ShouldQueue, QueueCondition[T]] + StopConditionTypes = Union[Callable, StopCondition[T]] + ConditionTypes = Union[QueueConditionTypes, StopConditionTypes] class Traversal(Generic[T, D]): @@ -109,18 +111,16 @@ def queue(self) -> TraversalQueue[T]: def branch_queue(self) -> Optional[TraversalQueue[D]]: return self.branch_queue_factory() - - _queue_type: Union[BasicQueueType, BranchingQueueType] = None + _queue_type: Union[BasicQueueType, BranchingQueueType] def __init__(self, queue_type, parent: Optional[D] = None): - if self._queue_type is None: - self._queue_type = queue_type + self._queue_type = queue_type self._parent: D = parent - self._queue_next = { - Traversal.BasicQueueType: lambda current, context: self._queue_next_non_branching(current, context, self._queue_type.queue_next), - Traversal.BranchingQueueType: lambda current, context: self._queue_next_branching(current, context, self._queue_type.queue_next), - } + if type(self._queue_type) == Traversal.BasicQueueType: + self.queue_next = lambda current, context: self._queue_next_non_branching(current, context, self._queue_type.queue_next) + elif type(self._queue_type) == Traversal.BranchingQueueType: + self.queue_next = lambda current, context: self._queue_next_branching(current, context, self._queue_type.queue_next) self.queue: TraversalQueue[T] = self._queue_type.queue self.branch_queue: Optional[TraversalQueue[D]] = self._queue_type.branch_queue @@ -135,9 +135,8 @@ def __init__(self, queue_type, parent: Optional[D] = None): self.compute_next_context_funs: Dict[str, ContextValueComputer[T]] = {} self.contexts: Dict[T, StepContext] = {} - @property - def queue_next(self): - return self._queue_next[self._queue_type.__class__] + def queue_next(self, current_item: T, context: StepContext): + raise NotImplementedError @property def parent(self): @@ -149,7 +148,7 @@ def parent(self, value): self._parent = value raise Exception - def can_action_item(self, item: T, context: 'StepContext') -> bool: + def can_action_item(self, item: T, context: StepContext) -> bool: """ Determines if the traversal can apply step actions and stop conditions on the specified item. @@ -159,7 +158,7 @@ def can_action_item(self, item: T, context: 'StepContext') -> bool: """ return True - def can_visit_item(self, item: T, context: 'StepContext') -> bool: + def can_visit_item(self, item: T, context: StepContext) -> bool: raise NotImplementedError def create_new_this(self) -> D: @@ -171,12 +170,13 @@ def create_new_this(self) -> D: raise NotImplementedError @singledispatchmethod - def add_condition(self, condition: Union[QueueCondition, Callable[[NetworkTraceStep[T], StepContext], None]]) -> D: + def add_condition(self, condition: ConditionTypes) -> D: """ Adds a traversal condition to the traversal. - `condition` The condition to add. - Returns this traversal instance. + :param condition: The condition to add. + + :return: this traversal instance. """ if callable(condition): # Callable[[NetworkTraceStep[T], StepContext], None] if condition.__code__.co_argcount == 2: @@ -192,7 +192,7 @@ def add_condition(self, condition: Union[QueueCondition, Callable[[NetworkTraceS @singledispatchmethod @add_condition.register(StopCondition) - def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopConditionWithContextValue[T, U]]) -> D: + def add_stop_condition(self, condition: StopConditionTypes) -> D: """ Adds a stop condition to the traversal. If any stop condition returns `true`, the traversal will not call the callback to queue more items from the current item. @@ -202,8 +202,8 @@ def add_stop_condition(self, condition: Union[Callable, StopCondition[T], StopCo """ raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [StopCondition | StopConditionWithContextValue | Callable]') - @add_stop_condition.register - def _(self, condition: Callable): + @add_stop_condition.register(Callable) + def _(self, condition: ShouldStop): return self.add_stop_condition(StopCondition(condition)) @add_stop_condition.register @@ -213,7 +213,6 @@ def _(self, condition: StopCondition): self.compute_next_context_funs[condition.key] = condition return self - def copy_stop_conditions(self, other: Traversal[T, D]) -> D: """ Copies all the stop conditions from another traversal to this traversal. @@ -233,7 +232,7 @@ def matches_any_stop_condition(self, item: T, context: StepContext) -> bool: @add_condition.register(QueueCondition) @singledispatchmethod - def add_queue_condition(self, condition: Union[Callable, QueueCondition[T]]) -> D: + def add_queue_condition(self, condition: QueueConditionTypes) -> D: """ Adds a queue condition to the traversal. Queue conditions determine whether an item should be queued for traversal. All registered queue conditions must return true for an item to be queued. @@ -243,8 +242,8 @@ def add_queue_condition(self, condition: Union[Callable, QueueCondition[T]]) -> """ raise RuntimeError(f'Condition [{condition.__class__.__name__}] does not match expected: [QueueCondition | QueueConditionWithContextValue | Callable]') - @add_queue_condition.register - def _(self, condition: Callable): + @add_queue_condition.register(Callable) + def _(self, condition: ShouldQueue): return self.add_queue_condition(QueueCondition(condition)) @add_queue_condition.register @@ -265,7 +264,7 @@ def copy_queue_conditions(self, other: Traversal[T, D]) -> D: self.add_queue_condition(it) return self - def add_step_action(self, action: Union[Callable, StepAction[T]]) -> D: + def add_step_action(self, action: Union[StepActionFunc, StepAction[T]]) -> D: """ Adds an action to be performed on each item in the traversal, including the starting items. @@ -330,7 +329,7 @@ def add_context_value_computer(self, computer: ContextValueComputer[T]) -> D: `computer` The context value computer to add. Returns The current traversal instance. """ - #require(not isinstance(computer, TraversalCondition), lambda: "`computer` must not be a TraversalCondition. Use `addCondition` to add conditions that also compute context values") + #require(not issubclass(computer.__class__, TraversalCondition), lambda: "`computer` must not be a TraversalCondition. Use `addCondition` to add conditions that also compute context values") self.compute_next_context_funs[computer.key] = computer return self @@ -469,11 +468,13 @@ def _get_step_context(self, item: T) -> StepContext: raise KeyError("INTERNAL ERROR: Traversal item should always have a context.") def _create_new_branch(self, start_item: T, context: StepContext) -> D: - it = self.create_new_this() - it.copy_queue_conditions(self) - it.copy_step_actions(self) - it.copy_stop_conditions(self) - it.copy_context_value_computer(self) + it = ( + self.create_new_this() + .copy_queue_conditions(self) + .copy_step_actions(self) + .copy_stop_conditions(self) + .copy_context_value_computer(self) + ) it.contexts[start_item] = context Traversal.add_start_item(it, start_item) diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py index caebc8d59..164690202 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal_condition.py @@ -3,8 +3,7 @@ # 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/. -import abc -from typing import TypeVar, Generic, Protocol, runtime_checkable +from typing import TypeVar, Protocol, runtime_checkable T = TypeVar('T') diff --git a/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py b/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py index 215cb0b61..61a272374 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py +++ b/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py @@ -5,6 +5,7 @@ from collections import defaultdict from typing import TypeVar, Callable, Iterable +from zepben.evolve import Traversal from zepben.evolve.services.network.tracing.traversal.queue import TraversalQueue T = TypeVar('T') @@ -28,7 +29,7 @@ class WeightedPriorityQueue(TraversalQueue[T]): def __init__(self, queue_provider: Callable[[], TraversalQueue[T]], get_weight: Callable[[T], int]): self._queue_provider = queue_provider - self._get_weight = lambda a: 1# TODO: this is wrong but at 6am, its as good as its gonna get to remind me where to lookg + self._get_weight = get_weight self.queue: SortedDefaultDict[int, TraversalQueue[T]] = SortedDefaultDict(self._queue_provider) @@ -36,12 +37,6 @@ def __len__(self) -> int: """need to aggregate the lengths of all queues""" return sum(len(v) for v in self.queue.values()) - def __iter__(self): - return self - - def __next__(self): - yield self.pop() - def pop(self): for weight in reversed(self.queue.keys()): if self.queue[weight].has_next(): @@ -49,8 +44,6 @@ def pop(self): def append(self, item: T) -> bool: weight = self._get_weight(item) - if weight < 0: - raise Exception self.queue[weight].append(item) return True @@ -65,11 +58,11 @@ def process_queue(cls, get_weight: Callable[[T], int]) -> TraversalQueue[T]: @classmethod def branch_queue(cls, get_weight: Callable[[T], int]) -> TraversalQueue[T]: """Special priority queue that queues branch items with the largest weight on the starting item as the highest priority""" - def condition(traversal): + def condition(traversal: Traversal): items = traversal.start_items if len(items) == 0: - return None - return get_weight(items) or -1 + return -1 + return get_weight(items[0]) or -1 return cls(TraversalQueue.breadth_first, condition) diff --git a/test/services/network/tracing/traversal/test_queue.py b/test/services/network/tracing/traversal/test_queue.py new file mode 100644 index 000000000..63b48723f --- /dev/null +++ b/test/services/network/tracing/traversal/test_queue.py @@ -0,0 +1,79 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 zepben.evolve import TraversalQueue, WeightedPriorityQueue +from zepben.evolve.services.network.tracing.traversal.queue import LIFODeque, FIFODeque + + +class TestQueue: + def test_lifo_queue(self): + queue = TraversalQueue.depth_first() + + for i in range(10): + queue.append(i) + assert queue.queue == LIFODeque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert len(queue) == 10 + + assert queue.pop() == 9 + + def test_fifo_queue(self): + queue = TraversalQueue.breadth_first() + + for i in range(10): + queue.append(i) + assert queue.queue == FIFODeque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + assert len(queue) == 10 + + assert queue.pop() == 0 + + def test_lifo_weighted_priority_queue(self): + weight = 0 + + queue = WeightedPriorityQueue( + lambda: TraversalQueue.depth_first(), + lambda t: weight + ) + for i in range(4): + queue.append(i) + + assert queue.pop() == 3 + assert queue.pop() == 2 + + weight = 1 + + for i in range(4): + queue.append(i) + + assert queue.pop() == 3 + assert queue.pop() == 2 + assert queue.pop() == 1 + assert queue.pop() == 0 + assert queue.pop() == 1 + assert queue.pop() == 0 + + def test_fifo_weighted_priority_queue(self): + weight = 0 + + queue = WeightedPriorityQueue( + lambda: TraversalQueue.breadth_first(), + lambda t: weight + ) + for i in range(4): + queue.append(i) + + assert queue.pop() == 0 + assert queue.pop() == 1 + + weight = 1 + + for i in range(4): + queue.append(i) + + assert queue.pop() == 0 + assert queue.pop() == 1 + assert queue.pop() == 2 + assert queue.pop() == 3 + assert queue.pop() == 2 + assert queue.pop() == 3 + diff --git a/test/services/network/tracing/traversal/test_traversal.py b/test/services/network/tracing/traversal/test_traversal.py index e30372c84..b4b9eaa93 100644 --- a/test/services/network/tracing/traversal/test_traversal.py +++ b/test/services/network/tracing/traversal/test_traversal.py @@ -7,7 +7,7 @@ import pytest -from zepben.evolve import StepContext, Traversal, TraversalQueue, NetworkTrace, ContextValueComputer +from zepben.evolve import StepContext, Traversal, TraversalQueue, ContextValueComputer from zepben.evolve.services.network.tracing.traversal.traversal import D T = TypeVar('T') From fb08464fede2d9863ceaa3363547a84bd731cc9f Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Sat, 24 May 2025 14:38:01 +1000 Subject: [PATCH 17/47] More typing, and refactoring code to be more pythonic Signed-off-by: Max Chesterfield --- .../network/tracing/feeder/clear_direction.py | 14 +++++---- .../tracing/networktrace/network_trace.py | 2 ++ .../networktrace/network_trace_action_type.py | 30 ++++++++----------- .../networktrace/network_trace_step.py | 1 - .../network/tracing/networktrace/tracing.py | 1 - .../traversal/context_value_computer.py | 2 +- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py b/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py index 4cb58b93e..992b5a768 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py +++ b/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py @@ -4,11 +4,11 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar, Type from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal -from zepben.evolve import FeederDirection, Traversal +from zepben.evolve import FeederDirection from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.services.network.tracing.traversal.weighted_priority_queue import WeightedPriorityQueue from zepben.evolve.services.network.tracing.networktrace.network_trace import NetworkTrace @@ -18,6 +18,8 @@ if TYPE_CHECKING: from zepben.evolve import StepContext, NetworkTraceStep +T = TypeVar('T') + class ClearDirection: @@ -28,7 +30,7 @@ class ClearDirection: # async def run(self, terminal: Terminal, - network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL ) -> list[Terminal]: """ Clears the feeder direction from a terminal and the connected equipment chain. @@ -47,13 +49,13 @@ async def run(self, return feeder_head_terminals @staticmethod - def _create_trace(state_operators: NetworkStateOperators, + def _create_trace(state_operators: Type[NetworkStateOperators], visited_feeder_head_terminals: list[Terminal] ) -> NetworkTrace[Any]: def queue_condition(step: NetworkTraceStep, context: StepContext, _, __): return state_operators.get_direction(step.path.to_terminal) != FeederDirection.NONE - def step_action(item, context): + def step_action(item: NetworkTraceStep, context: StepContext): state_operators.set_direction(item.path.to_terminal, FeederDirection.NONE) visited_feeder_head_terminals.append(item.path.to_terminal) if item.path.to_terminal.is_feeder_head_terminal() else None @@ -67,4 +69,4 @@ def step_action(item, context): .add_condition(state_operators.stop_at_open()) .add_queue_condition(queue_condition) .add_step_action(step_action) - ) \ No newline at end of file + ) 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 f0d5c72d9..9ae3540ce 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -77,6 +77,8 @@ def __init__(self, action_type: NetworkTraceActionType=None ): + if action_type is None: + raise ValueError('action_type can not be None') self._queue_type = queue_type self.network_state_operators = network_state_operators self._action_type = action_type diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py index 380083522..f47f940b7 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py @@ -1,46 +1,40 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # 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 Callable, Set, Any from enum import Enum +from zepben.evolve import Terminal, SinglePhaseKind from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.traversal.step_context import StepContext +HasTracked = Callable[[Terminal, Set[SinglePhaseKind]], bool] +CanActionItem = Callable[[NetworkTraceStep[Any], StepContext, HasTracked], bool] -class EnumFunc: - def __init__(self, func): - self._func = func - - def __call__(self, *args, **kwargs): - return self._func(*args, **kwargs) - -def _all_steps(item: NetworkTraceStep, context: StepContext, has_tracked) -> bool: +def _all_steps(item: NetworkTraceStep, context: StepContext, has_tracked: HasTracked) -> bool: return True -def _first_step_on_equipment(item: NetworkTraceStep, context: StepContext, has_tracked) -> bool: - for ot in item.path.to_terminal.other_terminals(): - if has_tracked(ot, item.path.to_phases_set()): - return False - return True +def _first_step_on_equipment(item: NetworkTraceStep[Any], context: StepContext, has_tracked: Callable[[Terminal, Set[SinglePhaseKind]], bool]) -> bool: + phases = item.path.to_phases_set() + return not any(has_tracked(it, phases) for it in item.path.to_terminal.other_terminals()) class NetworkTraceActionType(Enum): """ Options to configure when a [NetworkTrace] actions a [NetworkTraceStep]. """ - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs) -> bool: return self.value(*args, **kwargs) - ALL_STEPS = EnumFunc(_all_steps) + ALL_STEPS: CanActionItem = _all_steps """ All steps visited during a [NetworkTrace] will be actioned. """ - FIRST_STEP_ON_EQUIPMENT = EnumFunc(_first_step_on_equipment) + FIRST_STEP_ON_EQUIPMENT: CanActionItem = _first_step_on_equipment """ Only actions steps where the `toEquipment` on the [NetworkTraceStep.path] has not been visited before on the phases within the [NetworkTraceStep.path]. This means that all [NetworkTraceStep.type] of [NetworkTraceStep.Type.INTERNAL] will never be actioned as a first visit will always occur on an 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 dc219da32..c92a837eb 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 @@ -116,4 +116,3 @@ def type(self) -> Path: def next_num_terminal_steps(self): return self.num_terminal_steps + 1 - diff --git a/src/zepben/evolve/services/network/tracing/networktrace/tracing.py b/src/zepben/evolve/services/network/tracing/networktrace/tracing.py index 94870113d..1dd794dd3 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/tracing.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/tracing.py @@ -44,7 +44,6 @@ def network_trace_branching(network_state_operators: Type[NetworkStateOperators] compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]=None ) -> NetworkTrace[T]: - if not isinstance(compute_data, ComputeData): compute_data = ComputeData(compute_data or (lambda *args: None)) diff --git a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py index d6d293e40..514e4cd36 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py +++ b/src/zepben/evolve/services/network/tracing/traversal/context_value_computer.py @@ -48,7 +48,7 @@ def compute_next_value(self, next_item: T, current_item: T, current_value): pass def is_standalone_computer(self): - return all(not isinstance(self, o) for o in (StepAction, StopCondition, QueueCondition)) + return not isinstance(self, (StepAction, StopCondition, QueueCondition)) class TypedContextValueComputer(ContextValueComputer, Generic[T, U]): """ From 81d7f7f0d10664a703676f11c27455d1ae1ed48c Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Sat, 24 May 2025 15:41:46 +1000 Subject: [PATCH 18/47] slight refactoring to clean up API Signed-off-by: Max Chesterfield --- src/zepben/evolve/__init__.py | 2 +- .../networktrace/conditions/conditions.py | 90 +++++++++---------- .../tracing/networktrace/network_trace.py | 11 ++- .../operators/network_state_operators.py | 4 +- .../operators/open_state_operators.py | 8 +- .../conditions/test_conditions.py | 7 +- 6 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index 25184092f..6282dc24c 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -175,7 +175,7 @@ from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import EquipmentStepLimitCondition from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import EquipmentTypeStepLimitCondition from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import OpenCondition -from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import Conditions +from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import * from zepben.evolve.services.network.tracing.networktrace.compute_data import * from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import * from zepben.evolve.services.network.tracing.phases.phase_status import * diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py index a2c5c46b7..02a279b8a 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py @@ -24,49 +24,49 @@ NetworkTraceStopCondition = StopCondition[NetworkTraceStep[T]] DSLLambda = Callable[[NetworkStateOperators], NetworkTraceQueueCondition[T]] -# FIXME: work out how to inject NetworkStateOperators into this from inside NetworkTrace -class Conditions: - @classmethod - def upstream(cls) -> DSLLambda: - """ - Creates a [NetworkTrace] condition that will cause tracing a feeder upstream (towards the head terminal). - This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. - - :return: [NetworkTraceQueueCondition] that results in upstream tracing. - """ - return lambda state_operator: state_operator.with_direction(FeederDirection.UPSTREAM) - - @classmethod - def downstream(cls) -> DSLLambda: - """ - Creates a [NetworkTrace] condition that will cause tracing a feeder downstream (away from the head terminal). - This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. - - :return: [NetworkTraceQueueCondition] that results in downstream tracing. - """ - return lambda state_operator: state_operator.with_direction(FeederDirection.DOWNSTREAM) - - @classmethod - def with_direction(cls, direction: FeederDirection) -> DSLLambda: - """ - Creates a [NetworkTrace] condition that will cause tracing only terminals with directions that match [direction]. - This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. - - :return: [NetworkTraceQueueCondition] that results in upstream tracing. - """ - return lambda state_operator: DirectionCondition(direction, state_operator) - - @staticmethod - def limit_equipment_steps(limit: int, equipment_type: Type[ConductingEquipment]=None) -> NetworkTraceStopCondition[T]: - """ - Creates a [NetworkTrace] condition that stops tracing a path once a specified number of equipment steps have been reached. - - :param limit: The maximum number of equipment steps allowed before stopping. - :param equipment_type: The class of the equipment type to track against the limit - - :return: A [NetworkTraceStopCondition] that stops tracing the path once the step limit is reached. - """ - if equipment_type is not None: - return EquipmentTypeStepLimitCondition(limit, equipment_type) - return EquipmentStepLimitCondition(limit) +__all__ = ['upstream', 'downstream', 'with_direction', 'limit_equipment_steps', 'stop_at_open'] + + +def upstream() -> DSLLambda: + """ + Creates a [NetworkTrace] condition that will cause tracing a feeder upstream (towards the head terminal). + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in upstream tracing. + """ + return lambda state_operator: state_operator.with_direction(FeederDirection.UPSTREAM) + +def downstream() -> DSLLambda: + """ + Creates a [NetworkTrace] condition that will cause tracing a feeder downstream (away from the head terminal). + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in downstream tracing. + """ + return lambda state_operator: state_operator.with_direction(FeederDirection.DOWNSTREAM) + +def with_direction(direction: FeederDirection) -> DSLLambda: + """ + Creates a [NetworkTrace] condition that will cause tracing only terminals with directions that match [direction]. + This uses [FeederDirectionStateOperations.get_direction] receiver instance method within the condition. + + :return: [NetworkTraceQueueCondition] that results in upstream tracing. + """ + return lambda state_operator: DirectionCondition(direction, state_operator) + +def limit_equipment_steps(limit: int, equipment_type: Type[ConductingEquipment]=None) -> NetworkTraceStopCondition[T]: + """ + Creates a [NetworkTrace] condition that stops tracing a path once a specified number of equipment steps have been reached. + + :param limit: The maximum number of equipment steps allowed before stopping. + :param equipment_type: The class of the equipment type to track against the limit + + :return: A [NetworkTraceStopCondition] that stops tracing the path once the step limit is reached. + """ + if equipment_type is not None: + return EquipmentTypeStepLimitCondition(limit, equipment_type) + return EquipmentStepLimitCondition(limit) + +def stop_at_open(): + return lambda state_operator: state_operator.stop_at_open() 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 9ae3540ce..077140fef 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -5,6 +5,7 @@ from collections.abc import Callable from functools import singledispatchmethod +from os import MFD_ALLOW_SEALING from typing import TypeVar, Union, Generic, Set, Type, Generator from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp @@ -256,10 +257,16 @@ 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: - return self._tracker.has_visited(terminal, phases) or (self.parent and self.parent.has_visited(terminal, phases)) + if self._tracker.has_visited(terminal, phases): + return True + if not self.parent: + return False + return self.parent.has_visited(terminal, phases) def visit(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: - return not (self.parent and self.parent.has_visited(terminal, phases)) and self._tracker.visit(terminal, phases) + if self.parent and self.parent.has_visited(terminal, phases): + return False + return self._tracker.visit(terminal, phases) def default_queue_condition_step_type(step_type): diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py index 3d57cd1e8..ce6b79a05 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py @@ -18,14 +18,14 @@ from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import InServiceStateOperators, NormalInServiceStateOperators, \ CurrentInServiceStateOperators from zepben.evolve.services.network.tracing.networktrace.operators.open_state_operators import OpenStateOperators, NormalOpenStateOperators, \ - CurrentOpenStateOperators, stop_at_open + CurrentOpenStateOperators from zepben.evolve.services.network.tracing.networktrace.operators.phase_state_operators import PhaseStateOperators, NormalPhaseStateOperators, \ CurrentPhaseStateOperators if TYPE_CHECKING: from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep -__all__ = ['NetworkStateOperators', 'stop_at_open'] +__all__ = ['NetworkStateOperators'] # noinspection PyPep8Naming class NetworkStateOperators(OpenStateOperators, diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py index 40ee43aec..6ac8e1927 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py @@ -6,10 +6,9 @@ from typing import TypeVar, Optional, TYPE_CHECKING, Callable -from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind - from abc import abstractmethod +from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import OpenCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_condition import NetworkTraceQueueCondition from zepben.evolve.services.network.tracing.networktrace.operators import StateOperator @@ -20,7 +19,7 @@ T = TypeVar('T') -__all__ = ['OpenStateOperators', 'stop_at_open'] +__all__ = ['OpenStateOperators'] class OpenStateOperators(StateOperator): @@ -98,8 +97,5 @@ def set_open(switch: Switch, is_open: bool, phase: SinglePhaseKind = None) -> No switch.set_open(is_open, phase) -def stop_at_open(): - return lambda state_operator: state_operator.stop_at_open() - OpenStateOperators.NORMAL = NormalOpenStateOperators OpenStateOperators.CURRENT = CurrentOpenStateOperators diff --git a/test/services/network/tracing/networktrace/conditions/test_conditions.py b/test/services/network/tracing/networktrace/conditions/test_conditions.py index 1e73297a3..b40aee0ac 100644 --- a/test/services/network/tracing/networktrace/conditions/test_conditions.py +++ b/test/services/network/tracing/networktrace/conditions/test_conditions.py @@ -5,7 +5,7 @@ from typing import Optional, Callable from zepben.evolve import NetworkStateOperators, FeederDirection, SinglePhaseKind, Switch, PowerTransformer -from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import Conditions +from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import limit_equipment_steps from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import EquipmentStepLimitCondition from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import EquipmentTypeStepLimitCondition @@ -39,7 +39,6 @@ def test_stop_at_open(self): state_operators = NetworkStateOperators.NORMAL condition = state_operators.stop_at_open(is_open, SinglePhaseKind.A) assert isinstance(condition, OpenCondition) - #assert condition._is_open == state_operators.is_open # wont work because we're not actually getting that method directly assert condition._phase is SinglePhaseKind.A def test_open_operators_stop_at_open(self): @@ -50,12 +49,12 @@ def test_open_operators_stop_at_open(self): assert condition._phase is SinglePhaseKind.A def test_limit_equipment_steps(self): - condition = Conditions.limit_equipment_steps(1) + condition = limit_equipment_steps(1) assert isinstance(condition, EquipmentStepLimitCondition) assert condition.limit == 1 def test_limit_equipment_type_steps(self): - condition = Conditions.limit_equipment_steps(1, PowerTransformer) + condition = limit_equipment_steps(1, PowerTransformer) assert isinstance(condition, EquipmentTypeStepLimitCondition) assert condition.limit == 1 assert condition.equipment_type is PowerTransformer From 06c7d4e4d987a6dcd32164b0bdf8ee226ca183b7 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Mon, 26 May 2025 14:20:50 +1000 Subject: [PATCH 19/47] Chasing an exponential queueing issue NetworkTrace keeps queueing every terminal from a junction as though the initial trace direction is wrong. Signed-off-by: Max Chesterfield --- .../tracing/networktrace/network_trace.py | 11 ++++---- .../networktrace/network_trace_queue_next.py | 28 +++++++++---------- .../network/tracing/traversal/queue.py | 9 ++---- 3 files changed, 22 insertions(+), 26 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 077140fef..fc4b3c821 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -257,11 +257,12 @@ 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: - if self._tracker.has_visited(terminal, phases): - return True - if not self.parent: - return False - return self.parent.has_visited(terminal, phases) + parent = self.parent + while parent is not None: + if parent._tracker.has_visited(terminal, phases): + return True + parent = parent.parent + return self._tracker.has_visited(terminal, phases) def visit(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: if self.parent and self.parent.has_visited(terminal, phases): diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index 61eb77561..0f68a1787 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -4,13 +4,13 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC -from typing import TypeVar, Callable, Generator, Generic, List, Union +from typing import TypeVar, Callable, Generator, Generic, List, Union, Type +from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.traversal.traversal import Traversal from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData, ComputeDataWithPaths from zepben.evolve.services.network.tracing.traversal.step_context import StepContext -from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider T = TypeVar('T') @@ -21,10 +21,10 @@ class NetworkTraceQueueNext(ABC): - path_provider = NetworkTraceStepPathProvider + state_operators = NetworkStateOperators - def __init__(self, path_provider: NetworkTraceStepPathProvider): - self.path_provider = path_provider + def __init__(self, state_operators: Type[NetworkStateOperators]): + self.state_operators = state_operators def next_trace_steps(self, @@ -33,7 +33,7 @@ def next_trace_steps(self, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]] ) -> Generator[NetworkTraceStep[T], None, None]: """ Builds a list of next `NetworkTraceStep` to add to the `NetworkTrace` queue """ - next_paths = self.path_provider.next_paths(current_step.path) + next_paths = self.state_operators.next_paths(current_step.path) if isinstance(compute_data, ComputeData): compute_next = lambda it: compute_data.compute_next(current_step, current_context, it) elif isinstance(compute_data, ComputeDataWithPaths): @@ -48,17 +48,17 @@ def next_trace_steps(self, yield NetworkTraceStep(it, next_num_terminal_steps, it.next_num_equipment_steps(current_step.num_equipment_steps), data) @staticmethod - def Basic(path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): - return Basic(path_provider, compute_data) + def Basic(state_operators: Type[NetworkStateOperators], compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + return Basic(state_operators, compute_data) @staticmethod - def Branching(path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): - return Branching(path_provider, compute_data) + def Branching(state_operators: Type[NetworkStateOperators], compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + return Branching(state_operators, compute_data) class Basic(NetworkTraceQueueNext, Traversal.QueueNext[NetworkTraceStep[T]], Generic[T]): - def __init__(self, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): - super().__init__(path_provider) + def __init__(self, state_operators: Type[NetworkStateOperators], compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + super().__init__(state_operators) self._get_next_steps: GetNextSteps = lambda item, context: self.next_trace_steps(item, context, compute_data) @@ -71,8 +71,8 @@ def accept(self, item: NetworkTraceStep[T], context: StepContext, queue_item: Qu class Branching(NetworkTraceQueueNext, Traversal.BranchingQueueNext[NetworkTraceStep[T]], Generic[T]): - def __init__(self, path_provider: NetworkTraceStepPathProvider, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): - super().__init__(path_provider) + def __init__(self, state_operators: Type[NetworkStateOperators], compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]]): + super().__init__(state_operators) self._get_next_steps: GetNextStepsBranching = lambda item, context: list(self.next_trace_steps(item, context, compute_data)) diff --git a/src/zepben/evolve/services/network/tracing/traversal/queue.py b/src/zepben/evolve/services/network/tracing/traversal/queue.py index 1fa144dd0..baa915d11 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/queue.py +++ b/src/zepben/evolve/services/network/tracing/traversal/queue.py @@ -14,16 +14,11 @@ from collections import deque from typing import TypeVar, Iterable, Generic, Deque, TYPE_CHECKING, Union -__all__ = ["TraversalQueue"] - -if TYPE_CHECKING: - from zepben.evolve import NetworkTraceStep - - - T = TypeVar('T') U = TypeVar('U') +__all__ = ["TraversalQueue"] + class FIFODeque(deque): def pop(self): From 6c54dbf0ddaf4f952151ff59eb2126e3dcffdce6 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Mon, 26 May 2025 17:21:54 +1000 Subject: [PATCH 20/47] yield != return. Signed-off-by: Max Chesterfield --- .../network/tracing/feeder/set_direction.py | 3 +- .../conditions/direction_condition.py | 2 +- .../networktrace/network_trace_queue_next.py | 3 +- .../network_trace_step_path_provider.py | 91 ++++----- .../network/test_data/looping_network.py | 138 +++++++------- .../tracing/feeder/direction_logger.py | 6 +- .../actions/test_equipment_tree_builder.py | 177 ++++++++++-------- 7 files changed, 222 insertions(+), 198 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py index cac9477bf..cf5e7b635 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py +++ b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py @@ -10,6 +10,7 @@ from zepben.evolve.model.cim.iec61970.base.wires.connectors import BusbarSection from zepben.evolve.model.cim.iec61970.base.wires.power_transformer import PowerTransformer from zepben.evolve.model.cim.iec61970.base.wires.cut import Cut +from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import stop_at_open from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing @@ -95,7 +96,7 @@ def stop_condition(nts: NetworkTraceStep, *args): branch_queue_factory=lambda: WeightedPriorityQueue.branch_queue(lambda it: it.path.to_terminal.phases.num_phases), compute_data=lambda step, _, next_path: self._compute_data(reprocessed_loop_terminals, state_operators, step, next_path) ) - .add_condition(state_operators.stop_at_open()) + .add_condition(stop_at_open()) .add_stop_condition(stop_condition) .add_queue_condition(queue_condition) .add_step_action(step_action) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py index b58b54404..302a7ca8a 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py @@ -54,7 +54,7 @@ def should_queue_start_item(self, item: NetworkTraceStep[T]) -> bool: def _should_queue_next_paths(self, path: NetworkTraceStep.Path) -> bool: for next_path in self.state_operators.next_paths(path): - if next_path.traced_internally and self.state_operators.is_open(path.to_equipment): + if not(next_path.traced_internally and self.state_operators.is_open(path.to_equipment)): if self._should_queue(next_path): return True return False \ No newline at end of file diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index 0f68a1787..36bd10ed6 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -33,11 +33,10 @@ def next_trace_steps(self, compute_data: Union[ComputeData[T], ComputeDataWithPaths[T]] ) -> Generator[NetworkTraceStep[T], None, None]: """ Builds a list of next `NetworkTraceStep` to add to the `NetworkTrace` queue """ - next_paths = self.state_operators.next_paths(current_step.path) + next_paths = list(self.state_operators.next_paths(current_step.path)) if isinstance(compute_data, ComputeData): compute_next = lambda it: compute_data.compute_next(current_step, current_context, it) elif isinstance(compute_data, ComputeDataWithPaths): - next_paths = list(next_paths) compute_next = lambda it: compute_data.compute_next(current_step, current_context, it, next_paths) else: raise TypeError(f'ComputeData was not of a recognised class: {compute_data.__class__} not in [ComputeData, ComputeDataWithPaths]') diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index 3c3e7f5b2..e2446b270 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -200,63 +200,64 @@ def _acls_traverse_from_terminal( # Can do a simple return if we don't need to do any special cuts/clamps processing if not(any((list(acls.cuts), list(acls.clamps)))): yield from seq_term_map_to_path(from_terminal.other_terminals(), path_factory, acls) + else: - # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! - # We also make sure we filter out the cut or the clamp we are starting at, so we don't compare it in our checks - filter_func = lambda it: it != from_terminal.conducting_equipment and self.state_operators.is_in_service(it) - cuts: List[Cut] = list(filter(filter_func, acls.cuts)) - clamps: List[Clamp] = list(filter(filter_func, acls.clamps)) + # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! + # We also make sure we filter out the cut or the clamp we are starting at, so we don't compare it in our checks + filter_func = lambda it: it != from_terminal.conducting_equipment and self.state_operators.is_in_service(it) + cuts: List[Cut] = list(filter(filter_func, acls.cuts)) + clamps: List[Clamp] = list(filter(filter_func, acls.clamps)) - cuts_at_same_position = list(filter(lambda it: it.length_from_T1_or_0 == length_from_T1, cuts)) - stop_at_cuts_at_same_position = bool(can_stop_at_cut_at_same_position and cuts_at_same_position) + cuts_at_same_position = list(filter(lambda it: it.length_from_T1_or_0 == length_from_T1, cuts)) + stop_at_cuts_at_same_position = bool(can_stop_at_cut_at_same_position and cuts_at_same_position) - def next_cut_length_from_terminal_1_func(): - if stop_at_cuts_at_same_position: - return length_from_T1 - elif towards_segment_T2: - return min((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 > length_from_T1), default=None) - else: - return max((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 < length_from_T1), default=None) + def next_cut_length_from_terminal_1_func(): + if stop_at_cuts_at_same_position: + return length_from_T1 + elif towards_segment_T2: + return min((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 > length_from_T1), default=None) + else: + return max((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 < length_from_T1), default=None) - next_cut_length_from_terminal_1 = next_cut_length_from_terminal_1_func() + next_cut_length_from_terminal_1 = next_cut_length_from_terminal_1_func() - next_cuts = [it for it in cuts if it.length_from_T1_or_0 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 is not None else [] + next_cuts = [it for it in cuts if it.length_from_T1_or_0 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 is not None else [] - def next_term_length_from_term_1_func(): - if next_cut_length_from_terminal_1 is not None: - return next_cut_length_from_terminal_1 - elif towards_segment_T2: - return acls_length_or_max(acls) - else: - return 0.0 + def next_term_length_from_term_1_func(): + if next_cut_length_from_terminal_1 is not None: + return next_cut_length_from_terminal_1 + elif towards_segment_T2: + return acls_length_or_max(acls) + else: + return 0.0 - next_terminal_length_from_terminal_1 = next_term_length_from_term_1_func() + next_terminal_length_from_terminal_1 = next_term_length_from_term_1_func() - def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: - if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_T2: - return lambda it: length_from_T1 <= it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 - elif towards_segment_T2: - return lambda it: it.length_from_T1_or_0 > length_from_T1 and it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 - elif (next_terminal_length_from_terminal_1 == 0.0) and len(next_cuts) == 0: - return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_T1_or_0 <= length_from_T1 - else: - return lambda it: it.length_from_T1_or_0 <= length_from_T1 and it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 - _filter = clamps_before_next_terminal_filter() + def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: + if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_T2: + return lambda it: length_from_T1 <= it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 + elif towards_segment_T2: + return lambda it: it.length_from_T1_or_0 > length_from_T1 and it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 + elif (next_terminal_length_from_terminal_1 == 0.0) and len(next_cuts) == 0: + return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_T1_or_0 <= length_from_T1 + else: + return lambda it: it.length_from_T1_or_0 <= length_from_T1 and it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 + _filter = clamps_before_next_terminal_filter() - clamps_before_next_terminal = filter(_filter, clamps) + clamps_before_next_terminal = filter(_filter, clamps) - next_stop_terminals = [] if stop_at_cuts_at_same_position else ( - it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts - ) if next_cuts else [acls.get_terminal(2 if towards_segment_T2 else 1)] + next_stop_terminals = [] if stop_at_cuts_at_same_position else ( + it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts + ) if next_cuts else [acls.get_terminal(2 if towards_segment_T2 else 1)] - next_terminals = ( - (it.get_terminal(cut_at_same_position_from_terminal_number) for it in cuts_at_same_position), - (it.get_terminal(1) for it in clamps_before_next_terminal), - next_stop_terminals - ) + next_terminals = ( + (it.get_terminal(cut_at_same_position_from_terminal_number) for it in cuts_at_same_position), + (it.get_terminal(1) for it in clamps_before_next_terminal), + next_stop_terminals + ) - for generator in next_terminals: - yield from seq_term_map_to_path(generator, path_factory, acls) + for generator in next_terminals: + yield from seq_term_map_to_path(generator, path_factory, acls) def seq_term_map_to_path(terms: Union[Terminal, Iterable[Terminal]], path_factory: PathFactory, traversed_acls: AcLineSegment=None ) -> Generator[NetworkTraceStep.Path, None, None]: diff --git a/test/services/network/test_data/looping_network.py b/test/services/network/test_data/looping_network.py index 7c825c4f0..82dca398d 100644 --- a/test/services/network/test_data/looping_network.py +++ b/test/services/network/test_data/looping_network.py @@ -12,77 +12,79 @@ def create_looping_network(): :return: An example network with loops. """ # - # j0 c1 j2 c13 j14 c15 j16 - # *11------21*21------21*21------21* - # 3 2 - # 1 1 - # c3 | | c17 - # 2 2 - # 1 c20 1 - # j4 *21------21* j21 * b18 (open) - # 3 2 - # 1 1 - # c5 | | c19 - # 2 2 - # 1 c22 j23 c24 2 - # j6 *21------21*21------21* j25 - # 3 - # 1 c29 - # | /--21* j30 - # c7 | / 2 - # | / 1 - # 2 / | c31 - # 1 / 2 - # j8 *21 c9 2 c11 - # 31--------21*31------21* j12 - # 1 2 j10 - # \ | - # \ 1 c28 - # \ 2 - # \--21* j27 - # c26 + # Python Diagram JVM SDK Diagram + # + # j0 c1 j2 c13 j14 c15 j16 // j0 ac0 j1 ac1 j2 ac2 j3 + # *11------21*21------21*21------21* // *11------21*21------21*21------21* + # 3 2 // 3 2 + # 1 1 // 1 1 + # c3 | | c17 // ac3 | | ac4 + # 2 2 // 2 2 + # 1 c20 1 // 1 ac5 1 + # j4 *21------21* j21 * b18 (open) // j4 *21------21* j5 * j6 (open) + # 3 2 // 3 2 + # 1 1 // 1 1 + # c5 | | c19 // ac6 | | ac7 + # 2 2 // 2 2 + # 1 c22 j23 c24 2 // 1 ac8 j8 ac9 2 + # j6 *21------21*21------21* j25 // j7 *21------21*21------21* j9 + # 3 // 3 + # 1 c29 // 1 ac11 + # | /--21* j30 // | /--21* j11 + # c7 | / 2 // ac10 | / 2 + # | / 1 // | / 1 + # 2 / | c31 // 2 / | ac13 + # 1 / 2 // 1 / 2 + # j8 *21 c9 2 c11 // j10 *21 ac12 2 ac14 + # 31--------21*31------21* j12 // 31--------21*31------21* j13 + # 1 2 j10 // 1 2 j12 + # \ | // \ | + # \ 1 c28 // \ 1 ac16 + # \ 2 // \ 2 + # \--21* j27 // \--21* j14 + # c26 // ac15 # return ( TestNetworkBuilder() - .from_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1) # j0 - .to_acls(nominal_phases=PhaseCode.ABCN) # c1 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3) # j2 - .to_acls(nominal_phases=PhaseCode.ABCN) # c3 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3) # j4 - .to_acls(nominal_phases=PhaseCode.ABCN) # c5 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3) # j6 - .to_acls(nominal_phases=PhaseCode.ABCN) # c7 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3) # j8 - .to_acls(nominal_phases=PhaseCode.ABCN) # c9 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3) # j10 - .to_acls(nominal_phases=PhaseCode.ABCN) # c11 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1) # j12 - .branch_from("j2", 2) - .to_acls(nominal_phases=PhaseCode.ABCN) # c13 - .to_junction(nominal_phases=PhaseCode.ABCN) # j14 - .to_acls(nominal_phases=PhaseCode.ABCN) # c15 - .to_junction(nominal_phases=PhaseCode.ABCN) # j16 - .to_acls(nominal_phases=PhaseCode.ABCN) # c17 - .to_breaker(nominal_phases=PhaseCode.ABCN, is_normally_open=True) # b18 - .to_acls(nominal_phases=PhaseCode.ABCN) # c19 + .from_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j0') # j0 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac0') # c1 + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j1') # j2 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac3') # c3 + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j4') # j4 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac6') # c5 + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j7') # j6 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac10') # c7 + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j10') # j8 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac12') # c9 + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j12') # j10 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac14') # c11 + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j13') # j12 + .branch_from("j1", 2) + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac1') # c13 + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j2') # j14 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac2') # c15 + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j3') # j16 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac4') # c17 + .to_breaker(nominal_phases=PhaseCode.ABCN, is_normally_open=True, is_open=True, mrid='j6') # b18 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac7') # c19 .branch_from("j4", 2) - .to_acls(nominal_phases=PhaseCode.ABCN) # c20 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1) # j21 - .branch_from("j6", 2) - .to_acls(nominal_phases=PhaseCode.ABCN) # c22 - .to_junction(nominal_phases=PhaseCode.ABCN) # j23 - .to_acls(nominal_phases=PhaseCode.ABCN) # c24 - .to_junction(nominal_phases=PhaseCode.ABCN) # j25 - .connect("c19", "j25", 2, 2) - .branch_from("j8", 3) - .to_acls(nominal_phases=PhaseCode.ABCN) # c26 - .to_junction(nominal_phases=PhaseCode.ABCN) # j27 - .to_acls(nominal_phases=PhaseCode.ABCN) # c28 - .connect("c28", "j10", 2, 1) - .branch_from("j8", 2) - .to_acls(nominal_phases=PhaseCode.ABCN) # c29 - .to_junction(nominal_phases=PhaseCode.ABCN) # j30 - .to_acls(nominal_phases=PhaseCode.ABCN) # c31 - .connect("c31", "j10", 2, 2) + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac5') # c20 + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j5') # j21 + .branch_from("j7", 2) + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac8') # c22 + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j8') # j23 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac9') # c24 + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j9') # j25 + .connect("ac7", "j9", 2, 2) + .branch_from("j10", 3) + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac15') # c26 + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j14') # j27 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac16') # c28 + .connect("ac16", "j12", 2, 1) + .branch_from("j10", 2) + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac11') # c29 + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j11') # j30 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac13') # c31 + .connect("ac13", "j12", 2, 2) .network ) diff --git a/test/services/network/tracing/feeder/direction_logger.py b/test/services/network/tracing/feeder/direction_logger.py index 2ed3fc6a7..ce0ee6614 100644 --- a/test/services/network/tracing/feeder/direction_logger.py +++ b/test/services/network/tracing/feeder/direction_logger.py @@ -2,11 +2,15 @@ # 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 TypeVar from zepben.evolve import ConductingEquipment, Tracing, Traversal __all__ = ["log_directions"] +T = TypeVar('T') + + from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep @@ -26,6 +30,6 @@ async def log_directions(*conducting_equipment: ConductingEquipment): await trace.run(cond_equip, False) -def _step(step: NetworkTraceStep, _: bool): +def _step(step: NetworkTraceStep[T], _: None): for term in step.path.to_equipment.terminals: print(f"{step.path.to_terminal.conducting_equipment.mrid}-T{term.sequence_number}: {{n:{term.normal_feeder_direction}, c:{term.current_feeder_direction}}}") diff --git a/test/services/network/tracing/networktrace/actions/test_equipment_tree_builder.py b/test/services/network/tracing/networktrace/actions/test_equipment_tree_builder.py index 8a961bc80..250bb1cf9 100644 --- a/test/services/network/tracing/networktrace/actions/test_equipment_tree_builder.py +++ b/test/services/network/tracing/networktrace/actions/test_equipment_tree_builder.py @@ -2,12 +2,16 @@ # 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 collections import deque +import pprint +from collections import deque, defaultdict from typing import Optional, List import pytest +from zepben.evolve import downstream, NetworkTraceActionType + from services.network.test_data.looping_network import create_looping_network +from services.network.tracing.feeder.direction_logger import log_directions from zepben.evolve import ConductingEquipment, Tracing, NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.actions.equipment_tree_builder import EquipmentTreeBuilder from zepben.evolve.services.network.tracing.networktrace.actions.tree_node import TreeNode @@ -16,114 +20,130 @@ @pytest.mark.asyncio async def test_downstream_tree(): n = create_looping_network() + normal = NetworkStateOperators.NORMAL + current = NetworkStateOperators.CURRENT await Tracing.set_phases().run(n) feeder_head = n.get("j0", ConductingEquipment) - await Tracing.set_direction().run_terminal(feeder_head.get_terminal_by_sn(1)) + await Tracing.set_direction().run_terminal(feeder_head, network_state_operators=normal) + await Tracing.set_direction().run_terminal(feeder_head, network_state_operators=current) + await log_directions(n.get('j0', ConductingEquipment)) + + visited_ce = [] - start = n.get("j2", ConductingEquipment) + start = n.get("j1", ConductingEquipment) assert start is not None tree_builder = EquipmentTreeBuilder() - state_operators = NetworkStateOperators.NORMAL - await Tracing.network_trace_branching(network_state_operators=state_operators) \ - .add_condition(state_operators.downstream()) \ + trace = Tracing.network_trace_branching(network_state_operators=normal, action_step_type=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT) \ + .add_condition(downstream()) \ .add_step_action(tree_builder) \ - .run(start) + .add_step_action(lambda item, context: visited_ce.append(item.path.to_equipment.mrid)) + + await trace.run(start) + + visit_counts = {} + for ce in visited_ce: + if visit_counts.get(ce): + visit_counts[ce] += 1 + else: + visit_counts[ce] = 1 + + pprint.pprint(visit_counts) root = list(tree_builder.roots)[0] assert root is not None - _verify_tree_asset(root, n["j2"], None, [n["c3"], n["c13"]]) + _verify_tree_asset(root, n["j1"], None, [n["ac1"], n["ac3"]]) - test_node = next(iter(root.children)) - _verify_tree_asset(test_node, n["c13"], n["j2"], [n["j14"]]) + test_node = root.children[0] + _verify_tree_asset(test_node, n["ac1"], n["j1"], [n["j2"]]) - test_node = next(iter(test_node.children)) - _verify_tree_asset(test_node, n["j14"], n["c13"], [n["c15"]]) + test_node = test_node.children[0] + _verify_tree_asset(test_node, n["j2"], n["ac1"], [n["ac2"]]) - test_node = next(iter(test_node.children)) - _verify_tree_asset(test_node, n["c15"], n["j14"], [n["j16"]]) + test_node = test_node.children[0] + _verify_tree_asset(test_node, n["ac2"], n["j2"], [n["j3"]]) test_node = next(iter(test_node.children)) - _verify_tree_asset(test_node, n["j16"], n["c15"], [n["c17"]]) + _verify_tree_asset(test_node, n["j3"], n["ac2"], [n["ac4"]]) test_node = next(iter(test_node.children)) - _verify_tree_asset(test_node, n["c17"], n["j16"], [n["b18"]]) + _verify_tree_asset(test_node, n["ac4"], n["j3"], [n["j6"]]) test_node = next(iter(test_node.children)) - _verify_tree_asset(test_node, n["b18"], n["c17"], []) + _verify_tree_asset(test_node, n["j6"], n["ac4"], []) test_node = list(root.children)[1] - _verify_tree_asset(test_node, n["c3"], n["j2"], [n["j4"]]) + _verify_tree_asset(test_node, n["ac3"], n["j1"], [n["j4"]]) test_node = next(iter(test_node.children)) - _verify_tree_asset(test_node, n["j4"], n["c3"], [n["c20"], n["c5"]]) + _verify_tree_asset(test_node, n["j4"], n["ac3"], [n["ac5"], n["ac6"]]) assert len(_find_nodes(root, "j0")) == 0 - assert len(_find_nodes(root, "c1")) == 0 + assert len(_find_nodes(root, "ac0")) == 0 + assert len(_find_nodes(root, "j1")) == 1 + assert len(_find_nodes(root, "ac1")) == 1 assert len(_find_nodes(root, "j2")) == 1 - assert len(_find_nodes(root, "c13")) == 1 - assert len(_find_nodes(root, "j14")) == 1 - assert len(_find_nodes(root, "c15")) == 1 - assert len(_find_nodes(root, "j16")) == 1 - assert len(_find_nodes(root, "c3")) == 1 + assert len(_find_nodes(root, "ac2")) == 1 + assert len(_find_nodes(root, "j3")) == 1 + assert len(_find_nodes(root, "ac3")) == 1 assert len(_find_nodes(root, "j4")) == 1 - assert len(_find_nodes(root, "c17")) == 1 - assert len(_find_nodes(root, "j21")) == 1 - assert len(_find_nodes(root, "c20")) == 1 - assert len(_find_nodes(root, "b18")) == 2 - assert len(_find_nodes(root, "c5")) == 1 - assert len(_find_nodes(root, "j6")) == 1 - assert len(_find_nodes(root, "c19")) == 1 - assert len(_find_nodes(root, "j23")) == 1 - assert len(_find_nodes(root, "c22")) == 1 - assert len(_find_nodes(root, "j25")) == 1 - assert len(_find_nodes(root, "c24")) == 1 + assert len(_find_nodes(root, "ac4")) == 1 + assert len(_find_nodes(root, "j5")) == 1 + assert len(_find_nodes(root, "ac5")) == 1 + assert len(_find_nodes(root, "j6")) == 2 + assert len(_find_nodes(root, "ac6")) == 1 + assert len(_find_nodes(root, "j7")) == 1 + assert len(_find_nodes(root, "ac7")) == 1 assert len(_find_nodes(root, "j8")) == 1 - assert len(_find_nodes(root, "c7")) == 1 - assert len(_find_nodes(root, "j30")) == 3 # j11 java sdk - assert len(_find_nodes(root, "c29")) == 3 # acLineSegment11 java sdk - assert len(_find_nodes(root, "j10")) == 3 - assert len(_find_nodes(root, "c9")) == 4 + assert len(_find_nodes(root, "ac8")) == 1 + assert len(_find_nodes(root, "j9")) == 1 + assert len(_find_nodes(root, "ac9")) == 1 + assert len(_find_nodes(root, "j10")) == 1 + assert len(_find_nodes(root, "ac10")) == 1 + assert len(_find_nodes(root, "j11")) == 3 # j11 java sdk + assert len(_find_nodes(root, "ac11")) == 3 # acLineSegment11 java sdk assert len(_find_nodes(root, "j12")) == 3 - assert len(_find_nodes(root, "c31")) == 3 # acLineSegment13 java jdk - assert len(_find_nodes(root, "j27")) == 4 - assert len(_find_nodes(root, "c11")) == 3 - assert len(_find_nodes(root, "c26")) == 4 - assert len(_find_nodes(root, "c28")) == 4 + assert len(_find_nodes(root, "ac12")) == 4 + assert len(_find_nodes(root, "j13")) == 3 + assert len(_find_nodes(root, "ac13")) == 3 # acLineSegment13 java jdk + assert len(_find_nodes(root, "j14")) == 4 + assert len(_find_nodes(root, "ac14")) == 3 + assert len(_find_nodes(root, "ac15")) == 4 + assert len(_find_nodes(root, "ac16")) == 4 assert _find_node_depths(root, "j0") == [] - assert _find_node_depths(root, "c1") == [] - assert _find_node_depths(root, "j2") == [0] - assert _find_node_depths(root, "c13") == [1] - assert _find_node_depths(root, "j14") == [2] - assert _find_node_depths(root, "c15") == [3] - assert _find_node_depths(root, "j16") == [4] - assert _find_node_depths(root, "c3") == [1] + assert _find_node_depths(root, "ac0") == [] + assert _find_node_depths(root, "j1") == [0] + assert _find_node_depths(root, "ac1") == [1] + assert _find_node_depths(root, "j2") == [2] + assert _find_node_depths(root, "ac2") == [3] + assert _find_node_depths(root, "j3") == [4] + assert _find_node_depths(root, "ac3") == [1] assert _find_node_depths(root, "j4") == [2] - assert _find_node_depths(root, "c17") == [5] - assert _find_node_depths(root, "j21") == [4] - assert _find_node_depths(root, "c20") == [3] - assert _find_node_depths(root, "b18") == [6, 10] - assert _find_node_depths(root, "c5") == [3] - assert _find_node_depths(root, "j6") == [4] - assert _find_node_depths(root, "c19") == [9] - assert _find_node_depths(root, "j23") == [6] - assert _find_node_depths(root, "c22") == [5] - assert _find_node_depths(root, "j25") == [8] - assert _find_node_depths(root, "c24") == [7] + assert _find_node_depths(root, "ac4") == [5] + assert _find_node_depths(root, "j5") == [4] + assert _find_node_depths(root, "ac5") == [3] + assert _find_node_depths(root, "j6") == [6, 10] + assert _find_node_depths(root, "ac6") == [3] + assert _find_node_depths(root, "j7") == [4] + assert _find_node_depths(root, "ac7") == [9] assert _find_node_depths(root, "j8") == [6] - assert _find_node_depths(root, "c7") == [5] - assert _find_node_depths(root, "j30") == [8, 10, 12] - assert _find_node_depths(root, "c29") == [7, 11, 13] - assert _find_node_depths(root, "j10") == [8, 10, 10] - assert _find_node_depths(root, "c9") == [7, 10, 11, 14] - assert _find_node_depths(root, "j12") == [10, 12, 12] - assert _find_node_depths(root, "c31") == [9, 9, 11] - assert _find_node_depths(root, "j27") == [8, 9, 12, 13] - assert _find_node_depths(root, "c11") == [9, 11, 11] - assert _find_node_depths(root, "c26") == [7, 10, 12, 13] - assert _find_node_depths(root, "c28") == [8, 9, 11, 14] + assert _find_node_depths(root, "ac8") == [5] + assert _find_node_depths(root, "j9") == [8] + assert _find_node_depths(root, "ac9") == [7] + assert _find_node_depths(root, "j10") == [6] + assert _find_node_depths(root, "ac10") == [5] + assert _find_node_depths(root, "j11") == [8, 10, 12] + assert _find_node_depths(root, "ac11") == [7, 11, 13] + assert _find_node_depths(root, "j12") == [8, 10, 10] + assert _find_node_depths(root, "ac12") == [7, 10, 11, 14] + assert _find_node_depths(root, "j13") == [10, 12, 12] + assert _find_node_depths(root, "ac13") == [9, 9, 11] + assert _find_node_depths(root, "j14") == [8, 9, 12, 13] + assert _find_node_depths(root, "ac14") == [9, 11, 11] + assert _find_node_depths(root, "ac15") == [7, 10, 12, 13] + assert _find_node_depths(root, "ac16") == [8, 9, 11, 14] def _verify_tree_asset( @@ -141,11 +161,8 @@ def _verify_tree_asset( else: assert tree_node.parent is None - children_nodes = list(tree_node.children) - assert len(children_nodes) == len(expected_children) - - for child in children_nodes: - assert child.identified_object in expected_children + children_nodes = list(c.identified_object for c in tree_node.children) + assert children_nodes == expected_children def _find_nodes(root: TreeNode[ConductingEquipment], asset_id: str) -> List[TreeNode[ConductingEquipment]]: From 8b4f434dd9104484649994c39634fdc194522662 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Mon, 26 May 2025 17:36:52 +1000 Subject: [PATCH 21/47] codebase inline with ewb-sdk-jvm PR#220 Signed-off-by: Max Chesterfield --- .../services/network/tracing/networktrace/network_trace.py | 4 ++-- .../network/tracing/networktrace/network_trace_queue_next.py | 4 ++-- .../network/tracing/networktrace/test_network_trace.py | 3 +-- .../tracing/networktrace/test_network_trace_queue_next.py | 1 - 4 files changed, 5 insertions(+), 7 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 fc4b3c821..545813718 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace.py @@ -97,7 +97,7 @@ def non_branching(cls, ) -> 'NetworkTrace[T]': return cls(network_state_operators, Traversal.BasicQueueType(NetworkTraceQueueNext.Basic( - NetworkTraceStepPathProvider(network_state_operators), + network_state_operators, compute_data_with_action_type(compute_data, action_type) ), queue), None, @@ -115,7 +115,7 @@ def branching(cls, return cls(network_state_operators, Traversal.BranchingQueueType(NetworkTraceQueueNext.Branching( - NetworkTraceStepPathProvider(network_state_operators), + network_state_operators, compute_data_with_action_type(compute_data, action_type) ), queue_factory, branch_queue_factory), parent, diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index 36bd10ed6..c4bcc42eb 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -35,9 +35,9 @@ def next_trace_steps(self, """ Builds a list of next `NetworkTraceStep` to add to the `NetworkTrace` queue """ next_paths = list(self.state_operators.next_paths(current_step.path)) if isinstance(compute_data, ComputeData): - compute_next = lambda it: compute_data.compute_next(current_step, current_context, it) + compute_next = lambda _it: compute_data.compute_next(current_step, current_context, _it) elif isinstance(compute_data, ComputeDataWithPaths): - compute_next = lambda it: compute_data.compute_next(current_step, current_context, it, next_paths) + compute_next = lambda _it: compute_data.compute_next(current_step, current_context, _it, next_paths) else: raise TypeError(f'ComputeData was not of a recognised class: {compute_data.__class__} not in [ComputeData, ComputeDataWithPaths]') diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index 2b9a39cd2..075892553 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -74,7 +74,6 @@ def test_adds_start_AcLineSegment_terminals_cut_terminals_and_clamp_terminals_as cut2[1] - cut2[1], cut2[2] - cut2[2])) - @pytest.mark.skip() @pytest.mark.asyncio async def test_can_run_large_branching_traces(self): try: @@ -86,7 +85,7 @@ async def test_can_run_large_branching_traces(self): builder.from_junction(num_terminals=1) \ .to_acls() - for i in range(1000): + for i in range(500): builder.to_junction(mrid=f'junc-{i}', num_terminals=3) \ .to_acls(mrid=f'acls-{i}-top') \ .from_acls(mrid=f'acls-{i}-bottom') \ diff --git a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py index 39dbd135c..7f9c29180 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_queue_next.py +++ b/test/services/network/tracing/networktrace/test_network_trace_queue_next.py @@ -11,7 +11,6 @@ from services.network.tracing.networktrace.util import mock_nts_path, mock_nts, mock_ctx from zepben.evolve import ComputeData, NetworkTraceStep, ngen, NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_next import NetworkTraceQueueNext -from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider T = TypeVar('T') From 519c0b950cb52c50424e8108eef39404a807579a Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 01:44:06 +1000 Subject: [PATCH 22/47] DEV-2568 PR#220, tracing fixes, and more unit tests - 2 unexpected test failures Signed-off-by: Max Chesterfield --- .../network/tracing/feeder/set_direction.py | 4 + .../networktrace/conditions/__init__.py | 4 + .../networktrace/conditions/conditions.py | 6 +- .../conditions/direction_condition.py | 5 +- .../network_trace_queue_condition.py | 37 ++- .../network_trace_stop_condition.py | 64 +++++ .../networktrace/conditions/open_condition.py | 2 +- .../tracing/networktrace/network_trace.py | 79 ++++-- .../networktrace/network_trace_step.py | 2 +- .../feeder_direction_state_operations.py | 4 +- .../operators/open_state_operators.py | 2 +- .../network/tracing/traversal/traversal.py | 52 ++-- .../test_data/cuts_and_clamps_network.py | 80 ++++++ .../tracing/feeder/test_set_direction.py | 243 +++++++++++++++++- .../conditions/test_direction_condition.py | 95 ++++++- .../test_network_trace_step_path_provider.py | 99 ++----- .../tracing/traversal/test_traversal.py | 100 ++++--- 17 files changed, 685 insertions(+), 193 deletions(-) create mode 100644 src/zepben/evolve/services/network/tracing/networktrace/conditions/__init__.py rename src/zepben/evolve/services/network/tracing/networktrace/{ => conditions}/network_trace_queue_condition.py (53%) create mode 100644 src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py create mode 100644 test/services/network/test_data/cuts_and_clamps_network.py diff --git a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py index cf5e7b635..6746c7d41 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/set_direction.py +++ b/src/zepben/evolve/services/network/tracing/feeder/set_direction.py @@ -3,6 +3,8 @@ # 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 __future__ import annotations + +from functools import singledispatchmethod from typing import Optional, TYPE_CHECKING, Type from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal @@ -114,6 +116,7 @@ def _reached_substation_transformer(terminal: Terminal) -> bool: def _is_normally_open_switch(conducting_equipment: Optional[ConductingEquipment]): return isinstance(conducting_equipment, Switch) and conducting_equipment.is_normally_open() + @singledispatchmethod async def run(self, network: NetworkService, network_state_operators: Type[NetworkStateOperators]): """ Apply feeder directions from all feeder head terminals in the network. @@ -128,6 +131,7 @@ async def run(self, network: NetworkService, network_state_operators: Type[Netwo if not network_state_operators.is_open(head_terminal, None): await self.run_terminal(terminal, network_state_operators) + @run.register async def run_terminal(self, terminal: Terminal, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): """ Apply [FeederDirection.DOWNSTREAM] from the [terminal]. diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/__init__.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/__init__.py new file mode 100644 index 000000000..e7d95cd55 --- /dev/null +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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/. diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py index 02a279b8a..432c4c2c2 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/conditions.py @@ -20,9 +20,7 @@ from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition from zepben.evolve import ConductingEquipment, NetworkStateOperators - NetworkTraceQueueCondition = QueueCondition[NetworkTraceStep[T]] - NetworkTraceStopCondition = StopCondition[NetworkTraceStep[T]] - DSLLambda = Callable[[NetworkStateOperators], NetworkTraceQueueCondition[T]] + DSLLambda = Callable[[NetworkStateOperators], QueueCondition[NetworkTraceStep[T]]] __all__ = ['upstream', 'downstream', 'with_direction', 'limit_equipment_steps', 'stop_at_open'] @@ -54,7 +52,7 @@ def with_direction(direction: FeederDirection) -> DSLLambda: """ return lambda state_operator: DirectionCondition(direction, state_operator) -def limit_equipment_steps(limit: int, equipment_type: Type[ConductingEquipment]=None) -> NetworkTraceStopCondition[T]: +def limit_equipment_steps(limit: int, equipment_type: Type[ConductingEquipment]=None) -> StopCondition[NetworkTraceStep[T]]: """ Creates a [NetworkTrace] condition that stops tracing a path once a specified number of equipment steps have been reached. diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py index 302a7ca8a..6f7e63c7e 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py @@ -6,16 +6,18 @@ from typing import TypeVar, TYPE_CHECKING, Generic, Type +from zepben.evolve import require + from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp from zepben.evolve.model.cim.iec61970.base.wires.cut import Cut +from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep if TYPE_CHECKING: from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve import StepContext - from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection T = TypeVar('T') @@ -25,6 +27,7 @@ class DirectionCondition(QueueCondition[NetworkTraceStep[T]], Generic[T]): def __init__(self, direction: FeederDirection, state_operators: Type[NetworkStateOperators]): + require(direction != FeederDirection.CONNECTOR, lambda: 'A direction of CONNECTOR is not currently supported') self.direction = direction self.state_operators = state_operators self.get_direction = self.state_operators.get_direction diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_queue_condition.py similarity index 53% rename from src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py rename to src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_queue_condition.py index 96a9bd56d..ecfdf4c24 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_queue_condition.py @@ -11,19 +11,37 @@ T = TypeVar('T') +__all__ = ['NetworkTraceQueueCondition'] + class NetworkTraceQueueCondition(QueueCondition[NetworkTraceStep[T]], Generic[T]): + """ + A special queue condition implementation that allows only checking `should_queue` when a [NetworkTraceStep] matches a given + [NetworkTraceStep.Type]. When [step_type] is: + *[NetworkTraceStep.Type.ALL]: [should_queue] will be called for every step. + *[NetworkTraceStep.Type.INTERNAL]: [shouldQueue] will be called only when [NetworkTraceStep.type] is [NetworkTraceStep.Type.INTERNAL]. + *[NetworkTraceStep.Type.EXTERNAL]: [shouldQueue] will be called only when [NetworkTraceStep.type] is [NetworkTraceStep.Type.EXTERNAL]. + + If the step does not match the given step type, `true` will always be returned. + """ def __init__(self, step_type: NetworkTraceStep.Type, condition: ShouldQueue=None): + """ + :param step_type: The step type to match to check `should_queue`. + :param condition: function with the signature of `ShouldQueue` to be called when step_type matches the current items step + """ super().__init__(self.should_queue) if condition is not None: self.should_queue_matched_step = condition - self._should_queue_func = self._should_queue_func_map[step_type] + self.step_type = step_type def should_queue(self, next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: - return self._should_queue_func(next_item, next_context, current_item, current_context) + return self._should_queue_func(self.step_type)(next_item, next_context, current_item, current_context) def should_queue_matched_step(self, next_item: NetworkTraceStep[T], next_context: StepContext, current_item: NetworkTraceStep[T], current_context: StepContext) -> bool: + """ + The logic you would normally put in `should_queue`. However, this will only be called when a step matches the `step_type` + """ raise NotImplementedError() def should_queue_internal_step(self, next_item: NetworkTraceStep[T], next_context: StepContext, current_item: NetworkTraceStep[T], current_context: StepContext) -> bool: @@ -36,10 +54,11 @@ def should_queue_external_step(self, next_item: NetworkTraceStep[T], next_contex return self.should_queue_matched_step(next_item, next_context, current_item, current_context) return True - @property - def _should_queue_func_map(self): - return { - NetworkTraceStep.Type.ALL: self.should_queue_matched_step, - NetworkTraceStep.Type.INTERNAL: self.should_queue_internal_step, - NetworkTraceStep.Type.EXTERNAL: self.should_queue_external_step - } + def _should_queue_func(self, step_type: NetworkTraceStep.Type) -> ShouldQueue: + if step_type == NetworkTraceStep.Type.ALL: + return self.should_queue_matched_step + elif step_type == NetworkTraceStep.Type.INTERNAL: + return self.should_queue_internal_step + elif step_type == NetworkTraceStep.Type.EXTERNAL: + return self.should_queue_external_step + raise ValueError(f'INTERNAL ERROR: step type [{step_type}] didn\'t match expected') diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py new file mode 100644 index 000000000..5ca9cbc82 --- /dev/null +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py @@ -0,0 +1,64 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 TypeVar, Generic + +from zepben.evolve.services.network.tracing.traversal.step_context import StepContext +from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep +from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition, ShouldStop + +T = TypeVar('T') + +__all__ = ['NetworkTraceStopCondition'] + + +class NetworkTraceStopCondition(StopCondition[T], Generic[T]): + """ + A special stop condition implementation that allows only checking `should_stop` when a [NetworkTraceStep] matches a given + [NetworkTraceStep.Type]. When [step_type] is: + *[NetworkTraceStep.Type.ALL]: [should_stop] will be checked for every step. + *[NetworkTraceStep.Type.INTERNAL]: [should_stop] will be checked only when [NetworkTraceStep.type] is [NetworkTraceStep.Type.INTERNAL]. + *[NetworkTraceStep.Type.EXTERNAL]: [should_stop] will be checked only when [NetworkTraceStep.type] is [NetworkTraceStep.Type.EXTERNAL]. + + If the step does not match the given step type, `false` will always be returned. + """ + + def __init__(self, step_type: NetworkTraceStep.Type, condition: ShouldStop): + """ + :param step_type: The step type to match to check `should_stop`. + :param condition: function with the signature of `ShouldStop` to be called when step_type matches the current items step + """ + super().__init__(self.should_stop) + if condition is not None: + self.should_stop_matched_step = condition + self.step_type = step_type + + def should_stop(self, item: NetworkTraceStep[T], context: StepContext) -> bool: + return self._should_stop_func(self.step_type)(item, context) + + def should_stop_matched_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: + """ + The logic you would normally put in `should_stop`. However, this will only be called when a step matches the `step_type` + """ + raise NotImplemented + + def should_stop_internal_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: + if item.type == NetworkTraceStep.Type.INTERNAL: + return self.should_stop_matched_step(item, context) + return False + + def should_stop_external_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: + if item.type == NetworkTraceStep.Type.EXTERNAL: + return self.should_stop_matched_step(item, context) + return False + + def _should_stop_func(self, step_type: NetworkTraceStep.Type) -> ShouldStop: + if step_type == NetworkTraceStep.Type.ALL: + return self.should_stop_matched_step + elif step_type == NetworkTraceStep.Type.INTERNAL: + return self.should_stop_internal_step + elif step_type == NetworkTraceStep.Type.EXTERNAL: + return self.should_stop_external_step + raise ValueError(f'INTERNAL ERROR: step type [{step_type}] didn\'t match expected') diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py index bb9bf4ea1..e6a989180 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py @@ -9,7 +9,7 @@ from typing_extensions import TypeVar -from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_condition import NetworkTraceQueueCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.network_trace_queue_condition import NetworkTraceQueueCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep if TYPE_CHECKING: 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 545813718..20dd1ac1a 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,6 @@ from collections.abc import Callable from functools import singledispatchmethod -from os import MFD_ALLOW_SEALING from typing import TypeVar, Union, Generic, Set, Type, Generator from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp @@ -17,15 +16,15 @@ from zepben.evolve.services.network.tracing.networktrace.compute_data import ComputeData, ComputeDataWithPaths from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType -from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_condition import NetworkTraceQueueCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.network_trace_stop_condition import NetworkTraceStopCondition, ShouldStop +from zepben.evolve.services.network.tracing.networktrace.conditions.network_trace_queue_condition import NetworkTraceQueueCondition from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_next import NetworkTraceQueueNext from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep -from zepben.evolve.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider from zepben.evolve.services.network.tracing.networktrace.network_trace_tracker import NetworkTraceTracker from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.traversal.queue_condition import QueueCondition from zepben.evolve.services.network.tracing.traversal.step_context import StepContext -from zepben.evolve.services.network.tracing.traversal.traversal import Traversal +from zepben.evolve.services.network.tracing.traversal.traversal import Traversal, StopConditionTypes from zepben.evolve.services.network.tracing.traversal.queue import TraversalQueue from zepben.evolve.services.network.tracing.connectivity.nominal_phase_path import NominalPhasePath @@ -191,10 +190,10 @@ async def run(self, start: Union[ConductingEquipment, Terminal]=None, data: T=No Runs the network trace starting from `start` Depending on the type of `start`, this will either start from: - - A starting [Terminal] to the trace with the associated step data. - - All terminals of the given [ConductingEquipment] as starting points in the trace, with the associated data. + - A starting `Terminal` to the trace with the associated step data. + - All terminals of the given `ConductingEquipment` as starting points in the trace, with the associated data. - :param start: The starting [Terminal] or [ConductingEquipment] for the trace. + :param start: The starting `Terminal` or `ConductingEquipment` for the trace. :param data: The data associated with the start step. :param phases: Phases to trace; `None` to ignore phases. :param can_stop_on_start_item: indicates whether the trace should check stop conditions on start items. @@ -208,34 +207,74 @@ async def run(self, start: Union[ConductingEquipment, Terminal]=None, data: T=No @singledispatchmethod def add_condition(self, condition: QueueCondition[T]) -> "NetworkTrace[T]": + """ + Adds a traversal condition to the trace. + + Valid types for `condition` are: + - A predefined traversal condition (eg: Conditions.stop_at_open()) + - A function implementing ShouldQueue or ShouldStop signature. + - A class subclassing StopCondition or QueueCondition + + :param condition: The condition to be added + :returns: This `NetworkTrace` instance + """ + return super().add_condition(condition) + + @add_condition.register + def _(self, condition: Callable): """ Adds a traversal condition to the trace using the trace's [NetworkStateOperators] as the receiver. This overload primarily exists to enable a DSL-like syntax for adding predefined traversal conditions to the trace. - For example, to configure the trace to stop at open points using the [Conditions.stopAtOpen] factory, you can use: + For example, to configure the trace to stop at open points using the [Conditions.stop_at_open] factory, you can use: >>> from zepben.evolve import stop_at_open >>> NetworkTrace().add_condition(stop_at_open()) - - :param condition: A lambda function that returns a traversal condition. - :returns: This [NetworkTrace] instance """ - super().add_condition(condition) - return self - @add_condition.register - def _(self, condition: Callable): if condition.__code__.co_argcount == 1: # Catches DSL Style lambda conditions from zepben.evolve.Conditions return self.add_condition(condition(self.network_state_operators)) - super().add_condition(condition) + return super().add_condition(condition) @singledispatchmethod - def add_queue_condition(self, condition: NetworkTraceQueueCondition[NetworkTraceStep[T]], step_type:NetworkTraceStep.Type=None) -> "NetworkTrace[T]": + def add_queue_condition(self, condition: NetworkTraceQueueCondition[NetworkTraceStep[T]], step_type: NetworkTraceStep.Type=None) -> "NetworkTrace[T]": + """ + Adds a `QueueCondition` to the traversal. However, before registering it with the traversal, it will make sure that the queue condition + is only checked on step types relevant to the `NetworkTraceActionType` assigned to this instance. That is when: + + - `action_type` is `NetworkTraceActionType.ALL_STEPS` the condition will be checked on all steps. + - `action_type` is `NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT` the condition will be checked on external steps. + + However, if the `condition` is an instance of `NetworkTraceQueueCondition` the `NetworkTraceQueueCondition.step_type` will be honoured. + + :param condition: The queue condition to add. + :returns: This `NetworkTrace` instance + """ return super().add_queue_condition(condition) @add_queue_condition.register def _(self, condition: Callable, step_type: NetworkTraceStep.Type=None): - return self.add_queue_condition(NetworkTraceQueueCondition(default_queue_condition_step_type(step_type or self._action_type), condition)) + return self.add_queue_condition(NetworkTraceQueueCondition(default_condition_step_type(self._action_type) or step_type, condition)) + + @singledispatchmethod + def add_stop_condition(self, condition: StopConditionTypes, step_type: NetworkTraceStep.Type=None) -> "NetworkTrace[T]": + """ + Adds a `StopCondition` to the traversal. However, before registering it with the traversal, it will make sure that the queue condition + is only checked on step types relevant to the `NetworkTraceActionType` assigned to this instance. That is when: + + - `action_type` is `NetworkTraceActionType.ALL_STEPS` the condition will be checked on all steps. + - `action_type` is `NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT` the condition will be checked on external steps. + + However, if the `condition` is an instance of `NetworkTraceStopCondition` the `NetworkTraceStopCondition.step_type` will be honoured. + + :param condition: The stop condition to add. + :returns: This `NetworkTrace` instance + """ + return super().add_stop_condition(condition) + + @add_stop_condition.register(Callable) + def _(self, condition: ShouldStop, step_type=None): + return self.add_stop_condition(NetworkTraceStopCondition(default_condition_step_type(self._action_type) or step_type, condition)) def can_action_item(self, item: T, context: StepContext) -> bool: return self._action_type(item, context, self.has_visited) @@ -270,7 +309,9 @@ def visit(self, terminal: Terminal, phases: set[SinglePhaseKind]) -> bool: return self._tracker.visit(terminal, phases) -def default_queue_condition_step_type(step_type): +def default_condition_step_type(step_type): + if step_type is None: + return False if step_type == NetworkTraceActionType.ALL_STEPS: return NetworkTraceStep.Type.ALL elif step_type == NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT: 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 c92a837eb..d0ab8fccf 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 @@ -105,7 +105,7 @@ def __init__(self, path: Path, num_terminal_steps: int, num_equipment_steps: int self.num_equipment_steps = num_equipment_steps self.data = data - def type(self) -> Path: + def type(self) -> Type: """ Returns the [Type] of the step. This will be [Type.INTERNAL] if [Path.tracedInternally] is true, [Type.EXTERNAL] when [Path.tracedExternally] is true and will never be [Type.ALL] which is used in other NetworkTrace functionality to determine if all steps should be used for that particular function. diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py index 85725a6dc..24f529516 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/feeder_direction_state_operations.py @@ -5,13 +5,13 @@ from __future__ import annotations from abc import abstractmethod -from typing import TYPE_CHECKING, Callable, TypeVar +from typing import TYPE_CHECKING, TypeVar from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection if TYPE_CHECKING: from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal - from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_condition import NetworkTraceQueueCondition + from zepben.evolve.services.network.tracing.networktrace.conditions.network_trace_queue_condition import NetworkTraceQueueCondition __all__ = ['FeederDirectionStateOperations', 'NormalFeederDirectionStateOperations', 'CurrentFeederDirectionStateOperations'] diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py index 6ac8e1927..54af5d755 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py @@ -10,7 +10,7 @@ from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import OpenCondition -from zepben.evolve.services.network.tracing.networktrace.network_trace_queue_condition import NetworkTraceQueueCondition +from zepben.evolve.services.network.tracing.networktrace.conditions.network_trace_queue_condition import NetworkTraceQueueCondition from zepben.evolve.services.network.tracing.networktrace.operators import StateOperator if TYPE_CHECKING: diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal.py b/src/zepben/evolve/services/network/tracing/traversal/traversal.py index ac1b29f7a..e680b83f8 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal.py @@ -9,7 +9,7 @@ from collections import deque from collections.abc import Callable from functools import singledispatchmethod -from typing import List, TypeVar, Generic, Optional, Dict, Union, TYPE_CHECKING +from typing import List, TypeVar, Generic, Optional, Dict, Union from zepben.evolve import require from zepben.evolve.services.network.tracing.traversal.context_value_computer import ContextValueComputer @@ -17,7 +17,6 @@ from zepben.evolve.services.network.tracing.traversal.step_action import StepAction, StepActionWithContextValue, StepActionFunc from zepben.evolve.services.network.tracing.traversal.step_context import StepContext from zepben.evolve.services.network.tracing.traversal.stop_condition import StopCondition, StopConditionWithContextValue, ShouldStop -from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep __all__ = ["Traversal"] @@ -27,10 +26,9 @@ U = TypeVar('U') D = TypeVar('D', bound='Traversal') -if TYPE_CHECKING: - QueueConditionTypes = Union[ShouldQueue, QueueCondition[T]] - StopConditionTypes = Union[Callable, StopCondition[T]] - ConditionTypes = Union[QueueConditionTypes, StopConditionTypes] +QueueConditionTypes = Union[ShouldQueue, QueueCondition[T]] +StopConditionTypes = Union[ShouldStop, StopCondition[T]] +ConditionTypes = Union[QueueConditionTypes, StopConditionTypes] class Traversal(Generic[T, D]): @@ -55,7 +53,7 @@ class QueueType(Generic[T, D]): """ Defines the types of queues used in the traversal. """ - queue_next:Traversal.QueueNext[T] + queue_next: Traversal.QueueNext[T] @property def queue(self) -> TraversalQueue[T]: @@ -379,7 +377,7 @@ async def run(self, start_item: T=None, can_stop_on_start_item: bool=True) -> D: `canStopOnStartItem` Indicates if the traversal should check stop conditions on the starting item. Returns The current traversal instance. """ - if start_item: + if start_item is not None: self.start_items.append(start_item) require(not self.running, lambda: "Traversal is already running") @@ -392,10 +390,13 @@ async def run(self, start_item: T=None, can_stop_on_start_item: bool=True) -> D: if self._parent is None and isinstance(self._queue_type, Traversal.BranchingQueueType) and len(self.start_items) > 1: self._branch_start_items() + # Because we don't traverse anything at the top level parent, we need to pass can_stop_at_start item + # to the child branch only in this case because they are actually start items. + await self._traverse_branches(can_stop_on_start_item) else: await self._traverse(can_stop_on_start_item) - - await self._traverse_branches(can_stop_on_start_item) + # Child branches should never stop at start items because a branch start item is not a whole trace start item. + await self._traverse_branches(True) self.running = False return self @@ -435,8 +436,12 @@ def _branch_start_items(self): async def _traverse(self, can_stop_on_start_item: bool): while len(self.start_items) > 0: - start_item = self.start_items.pop() + start_item = self.start_items.popleft() + # If the traversal is not a branch we need to compute an initial context and check if it + # should even be queued to trace. If the traversal is a branch, the branch creators should + # have only created the branch if the item was eligible to be queued and added the item + # context as part of the branch creation. if self._parent is None: if self._can_queue_start_item(start_item): self.contexts[start_item] = self._compute_intial_context(start_item) @@ -444,21 +449,22 @@ async def _traverse(self, can_stop_on_start_item: bool): else: self.queue.append(start_item) - can_stop = can_stop_on_start_item - while len(self.queue) > 0: - current = self.queue.pop() - context = self._get_step_context(current) - if self.can_visit_item(current, context): - context.is_actionable_item = self.can_action_item(current, context) + can_stop = can_stop_on_start_item + while self.queue.has_next(): + current = self.queue.pop() + context = self._get_step_context(current) + if self.can_visit_item(current, context): + context.is_stopping = can_stop and self.matches_any_stop_condition(current, context) + + context.is_actionable_item = self.can_action_item(current, context) - if context.is_actionable_item: - context.is_stopping = can_stop and self.matches_any_stop_condition(current, context) - await self.apply_step_actions(current, context) + if context.is_actionable_item: + await self.apply_step_actions(current, context) - if not context.is_stopping: - self.queue_next(current, context) + if not context.is_stopping: + self.queue_next(current, context) - can_stop = True + can_stop = True def _get_step_context(self, item: T) -> StepContext: try: diff --git a/test/services/network/test_data/cuts_and_clamps_network.py b/test/services/network/test_data/cuts_and_clamps_network.py new file mode 100644 index 000000000..250d80a25 --- /dev/null +++ b/test/services/network/test_data/cuts_and_clamps_network.py @@ -0,0 +1,80 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# 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 Optional + +from zepben.evolve import NetworkService, TestNetworkBuilder, AcLineSegment, Clamp, Terminal, ConductingEquipment, Cut + + +class CutsAndClampsNetwork: + @staticmethod + def multi_cut_and_clamp_network() -> TestNetworkBuilder: + # + # 2 2 + # c3 2 c7 2 + # 1 c5 1 c9 + # 1 clamp1 1 1 clamp3 1 + # | | | | + # 1 b0 21--*--*1 cut1 2*--*--c1--*--*1 cut2 2*--*--21 b2 2 + # | | | | + # 1 1 clamp2 1 1 clamp4 + # c4 1 c8 1 + # 2 c6 2 c10 + # 2 2 + # + builder = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_breaker() # b2 + .from_acls() # c3 + .from_acls() # c4 + .from_acls() # c5 + .from_acls() # c6 + .from_acls() # c7 + .from_acls() # c8 + .from_acls() # c9 + .from_acls() # c10 + ) + + network = builder.network + + segment: AcLineSegment = network['c1'] + + clamp1 = _segment_with_clamp(network, segment, 1.0) + cut1 = _segment_with_cut(network, segment, 2.0) + clamp2 = _segment_with_clamp(network, segment, 3.0) + clamp3 = _segment_with_clamp(network, segment, 4.0) + cut2 = _segment_with_cut(network, segment, 5.0) + clamp4 = _segment_with_clamp(network, segment, 6.0) + + network.connect(clamp1[1], network.get('c3', ConductingEquipment)[1]) + network.connect(cut1[1], network.get('c4', ConductingEquipment)[1]) + network.connect(cut1[2], network.get('c5', ConductingEquipment)[1]) + network.connect(clamp2[1], network.get('c6', ConductingEquipment)[1]) + network.connect(clamp3[1], network.get('c7', ConductingEquipment)[1]) + network.connect(cut2[1], network.get('c8', ConductingEquipment)[1]) + network.connect(cut2[2], network.get('c9', ConductingEquipment)[1]) + network.connect(clamp4[1], network.get('c10', ConductingEquipment)[1]) + + return builder + + +def _segment_with_clamp(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Clamp: + clamp = Clamp(mrid=f'clamp{segment.num_clamps() + 1}') + clamp.add_terminal(Terminal(mrid=f'{clamp.mrid}-t1')) + clamp.length_from_terminal_1 = length_from_terminal1 + + segment.add_clamp(clamp) + network.add(clamp) + return clamp + + +def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Cut: + cut = Cut(mrid=f'cut{segment.num_cuts() + 1}', length_from_terminal_1=length_from_terminal1) + cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t1')) + cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t2')) + + segment.add_cut(cut) + network.add(cut) + return cut diff --git a/test/services/network/tracing/feeder/test_set_direction.py b/test/services/network/tracing/feeder/test_set_direction.py index f68739ce9..58a9771ea 100644 --- a/test/services/network/tracing/feeder/test_set_direction.py +++ b/test/services/network/tracing/feeder/test_set_direction.py @@ -1,14 +1,17 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # 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 Type, Union + import pytest +from services.network.test_data.cuts_and_clamps_network import CutsAndClampsNetwork from services.network.test_data.phase_swap_loop_network import create_phase_swap_loop_network from services.network.tracing.feeder.direction_logger import log_directions from zepben.evolve import FeederDirection, TestNetworkBuilder, SetDirection, PhaseCode, NetworkService, Feeder, Terminal, ConductingEquipment, Substation, \ - NetworkStateOperators, Traversal, StepContext -from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep + NetworkStateOperators, Cut UPSTREAM = FeederDirection.UPSTREAM DOWNSTREAM = FeederDirection.DOWNSTREAM @@ -22,8 +25,7 @@ class TestSetDirection: async def test_set_direction(self): n = create_phase_swap_loop_network() - await self._do_set_direction_trace(n, NetworkStateOperators.NORMAL) - await self._do_set_direction_trace(n, NetworkStateOperators.CURRENT) + await self._do_set_direction_trace(n) self._check_expected_direction(self._get_t(n, "ac_line_segment0", 1), UPSTREAM) self._check_expected_direction(self._get_t(n, "ac_line_segment0", 2), DOWNSTREAM) @@ -205,7 +207,7 @@ async def test_set_direction_in_closed_loop(self): .add_feeder("s0") \ .network # Do not call build as we do not want to trace the directions yet. - await self._do_set_direction_trace(n, NetworkStateOperators.NORMAL) + await self._do_set_direction_trace(n) self._check_expected_direction(self._get_t(n, "s0", 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, "c1", 1), UPSTREAM) @@ -454,11 +456,232 @@ async def test_set_direction_doesnt_flow_through_feeder_heads(self): self._check_expected_direction(self._get_t(n, "b2", 1), BOTH) self._check_expected_direction(self._get_t(n, "b2", 2), NONE) + @pytest.mark.asyncio + async def test_set_direction_on_acls_with_cuts_and_clamps_from_acls_end_terminal(self): + n = CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('b0', 2) \ + .network + + n.get('cut1', Cut).set_normally_open(False) + n.get('cut2', Cut).set_normally_open(True) + + await self._do_set_direction_trace(self._get_t(n, 'b0', 2)) + + self._check_expected_direction(self._get_t(n, 'b0', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + + @pytest.mark.asyncio + async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_clamp(self): + n = CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('c6', 1) \ + .network + + n.get('cut1', Cut).set_normally_open(False) + n.get('cut2', Cut).set_normally_open(True) + + await self._do_set_direction_trace(self._get_t(n, 'c6', 1)) + + self._check_expected_direction(self._get_t(n, 'c6', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c6', 2), NONE) + self._check_expected_direction(self._get_t(n, 'clamp2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 2), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'b0', 2), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'b0', 1), DOWNSTREAM) + + @pytest.mark.asyncio + async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_cut(self): + n = CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('c5', 1) \ + .network + + n.get('cut1', Cut).set_normally_open(False) + n.get('cut2', Cut).set_normally_open(True) + + await self._do_set_direction_trace(self._get_t(n, 'c5', 1)) + + self._check_expected_direction(self._get_t(n, 'c5', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c5', 2), NONE) + self._check_expected_direction(self._get_t(n, 'cut1', 2), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'b0', 2), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'b0', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c6', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c6', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + + @pytest.mark.asyncio + async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_both_acls_ends(self): + n = CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('b0', 2) \ + .add_feeder('b2', 1) \ + .network + + n.get('cut1', Cut).set_normally_open(False) + n.get('cut2', Cut).set_normally_open(False) + + await self._do_set_direction_trace(self._get_t(n, 'b0', 2)) + await self._do_set_direction_trace(self._get_t(n, 'b2', 1)) + + self._check_expected_direction(self._get_t(n, 'b0', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'b0', 1), NONE) + self._check_expected_direction(self._get_t(n, 'c1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'cut1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c6', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c6', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'cut2', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'c9', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c9', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp4', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c10', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c10', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'b2', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'b2', 2), NONE) + + @pytest.mark.asyncio + async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_acls_end_and_clamp(self): + n = CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('b0', 2) \ + .add_feeder('c6', 1) \ + .network + + n.get('cut1', Cut).set_normally_open(False) + n.get('cut2', Cut).set_normally_open(True) + + await self._do_set_direction_trace(self._get_t(n, 'b0', 2)) + await self._do_set_direction_trace(self._get_t(n, 'c6', 1)) + + self._check_expected_direction(self._get_t(n, 'b0', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'b0', 1), NONE) + self._check_expected_direction(self._get_t(n, 'c1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'cut1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp2', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c6', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c6', 2), NONE) + self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + + @pytest.mark.asyncio + async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_acls_clamp_and_cut(self): + n = CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('c3', 1) \ + .add_feeder('c5', 1) \ + .network + + n.get('cut1', Cut).set_normally_open(False) + n.get('cut2', Cut).set_normally_open(True) + + await self._do_set_direction_trace(self._get_t(n, 'c3', 1)) + await self._do_set_direction_trace(self._get_t(n, 'c5', 1)) + + self._check_expected_direction(self._get_t(n, 'b0', 2), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'b0', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c3', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c3', 2), NONE) + self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut1', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'cut1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c5', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c5', 2), NONE) + self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c6', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c6', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + + @staticmethod - async def _do_set_direction_trace(n: NetworkService, nso: NetworkStateOperators): - await SetDirection().run(n, network_state_operators=nso) - for it in n.objects(Feeder): - await log_directions(it.normal_head_terminal.conducting_equipment) + async def _do_set_direction_trace(n: Union[NetworkService, Terminal]): + async def _all_nso(start): + for nso in (NetworkStateOperators.NORMAL, NetworkStateOperators.CURRENT): + await SetDirection().run(start, network_state_operators=nso) + + await log_directions(start) + if isinstance(n, NetworkService): + for it in n.objects(Feeder): + if it.normal_head_terminal: + await _all_nso(it.normal_head_terminal) + else: + await _all_nso(n) @staticmethod def _get_t(network: NetworkService, mrid: str, sequence_number: int) -> Terminal: diff --git a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py index 8eb2fcd9b..4a89f6ce9 100644 --- a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py +++ b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py @@ -5,12 +5,15 @@ from typing import Tuple from unittest.mock import MagicMock +import pytest + +from services.network.test_data.cuts_and_clamps_network import CutsAndClampsNetwork from zepben.evolve import FeederDirection, NetworkTraceStep, Terminal, Junction, NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition class TestDirectionCondition: - def test_should_queue(self): + def test_should_queue_for_non_cut_or_clamp_path(self): traced_internally = True _terminal_should_queue((FeederDirection.NONE, FeederDirection.NONE, traced_internally), True) _terminal_should_queue((FeederDirection.NONE, FeederDirection.UPSTREAM, traced_internally), False) @@ -61,7 +64,7 @@ def test_should_queue(self): _terminal_should_queue((FeederDirection.BOTH, FeederDirection.NONE, traced_internally), False) _terminal_should_queue((FeederDirection.BOTH, FeederDirection.CONNECTOR, traced_internally), True) - def test_should_queue_start_item(self): + def test_should_queue_start_item_for_non_cut_or_clamp(self): _start_terminal_should_queue((FeederDirection.NONE, FeederDirection.NONE), True) _start_terminal_should_queue((FeederDirection.NONE, FeederDirection.UPSTREAM), False) _start_terminal_should_queue((FeederDirection.NONE, FeederDirection.DOWNSTREAM), False) @@ -86,6 +89,75 @@ def test_should_queue_start_item(self): _start_terminal_should_queue((FeederDirection.BOTH, FeederDirection.BOTH), True) _start_terminal_should_queue((FeederDirection.BOTH, FeederDirection.CONNECTOR), True) + @pytest.mark.asyncio + async def test_cuts_queue_when_direction_set_from_segment_end(self): + network = await CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('b0', 2) \ + .build() + + c1 = network['c1'] + cut1 = network['cut1'] + c4 = network['c4'] + c5 = network['c5'] + + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[1], cut1[2])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[1], cut1[1])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[2], cut1[2])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(c1[1], cut1[1])), True) + + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[1], cut1[1])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[2], cut1[2])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[2], cut1[1])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(c4[1], cut1[1])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(c5[1], cut1[2])), True) + + @pytest.mark.asyncio + async def test_cuts_queue_when_direction_set_from_clamp(self): + network = await CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('c3', 1) \ + .build() + + c1 = network['c1'] + clamp1 = network['clamp1'] + cut1 = network['cut1'] + c4 = network['c4'] + c5 = network['c5'] + + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[1], cut1[1])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[2], cut1[2])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[1], cut1[2])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(clamp1[1], cut1[1], c1)), True) + + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(cut1[1], cut1[1])), True) + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(cut1[2], cut1[2])), False) + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(cut1[2], cut1[1])), True) + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(c5[1], cut1[2])), True) + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(c4[1], cut1[1])), True) + + @pytest.mark.asyncio + async def test_cuts_queue_when_direction_set_from_cut(self): + network = await CutsAndClampsNetwork.multi_cut_and_clamp_network() \ + .add_feeder('c4', 1) \ + .build() + + cut1 = network['cut1'] + c4 = network['c4'] + c5 = network['c5'] + + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[1], cut1[1])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[2], cut1[2])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(cut1[1], cut1[2])), True) + _should_queue((FeederDirection.DOWNSTREAM, NetworkTraceStep.Path(c4[1], cut1[1])), True) + + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(cut1[1], cut1[1])), True) + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(cut1[2], cut1[2])), False) + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(cut1[2], cut1[1])), True) + _should_queue((FeederDirection.UPSTREAM, NetworkTraceStep.Path(c5[1], cut1[2])), True) + + def test_does_not_support_connector_conditions(self): + with pytest.raises(ValueError): + DirectionCondition(FeederDirection.CONNECTOR, NetworkStateOperators.NORMAL) + def _terminal_should_queue(condition: Tuple[FeederDirection, FeederDirection, bool], expected): direction, to_direction, traced_internally = condition @@ -106,15 +178,30 @@ def _terminal_should_queue(condition: Tuple[FeederDirection, FeederDirection, bo def _start_terminal_should_queue(condition: Tuple[FeederDirection, FeederDirection], expected): direction, to_direction = condition - next_path = MagicMock(spec=NetworkTraceStep.Path)() + next_path = MagicMock(spec=NetworkTraceStep.Path) + next_path.configure_mock( + to_terminal=Terminal(), + to_equipment=Junction() + ) next_path.to_terminal = Terminal() next_path.to_equipment = Junction() next_path.did_traverse_ac_line_segment = False - next_item = NetworkTraceStep(next_path, 0, 0, None) + next_item = NetworkTraceStep(next_path(), 0, 0, None) state_operators = NetworkStateOperators state_operators.get_direction = lambda t: to_direction result = DirectionCondition(direction, state_operators).should_queue_start_item(next_item) assert result == expected + +def _should_queue(condition: Tuple[FeederDirection, NetworkTraceStep.Path], expected: bool): + direction, path = condition + next_step = MagicMock(spec=NetworkTraceStep) + next_step.configure_mock( + path=path + ) + should_queue = DirectionCondition(direction, NetworkStateOperators.NORMAL).should_queue(next_step(), None, None, None) + print(f'direction: {direction}') + print(f'path: internal: {path.traced_internally}\n from: {path.from_terminal}\n to: {path.to_terminal}\n') + assert should_queue == expected \ No newline at end of file diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index cd8595a0f..fa1fb4fd6 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -2,10 +2,11 @@ # 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 Generator, Tuple, Iterable, Optional, Union +from typing import Generator, Iterable from pytest_subtests.plugin import subtests +from services.network.test_data.cuts_and_clamps_network import _segment_with_clamp, _segment_with_cut, CutsAndClampsNetwork from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.evolve.services.network.network_service import NetworkService @@ -183,7 +184,7 @@ def test_traversing_segment_with_clamps_from_t2_includes_all_clamp_steps(self): _verify_paths(next_paths, (segment[2] - clamp2[1], segment[2] - clamp1[1], segment[2] - segment[1])) def test_non_traverse_step_to_segment_t1_traverses_towards_t2_stopping_at_cut(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network b0 = network['b0'] segment = network['c1'] @@ -196,7 +197,7 @@ def test_non_traverse_step_to_segment_t1_traverses_towards_t2_stopping_at_cut(se _verify_paths(next_paths, (segment[1] - clamp1[1], segment[1] - cut1[1])) def test_non_traverse_step_to_segment_t2_traverses_towards_t1_stopping_at_cut(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network b2 = network['b2'] segment = network['c1'] @@ -209,7 +210,7 @@ def test_non_traverse_step_to_segment_t2_traverses_towards_t1_stopping_at_cut(se _verify_paths(next_paths, (segment[2] - clamp4[1], segment[2] - cut2[2])) def test_traverse_step_to_cut_t1_steps_externally_and_across_cut(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] cut1 = network['cut1'] @@ -221,7 +222,7 @@ def test_traverse_step_to_cut_t1_steps_externally_and_across_cut(self): _verify_paths(next_paths, (cut1[1] + cut1[2], cut1[1] + c4[1])) def test_traverse_step_to_cut_t2_steps_externally_and_across_cut(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] cut2 = network['cut2'] @@ -233,7 +234,7 @@ def test_traverse_step_to_cut_t2_steps_externally_and_across_cut(self): _verify_paths(next_paths, (cut2[2] + cut2[1], cut2[2] + c9[1])) def test_non_traverse_step_to_cut_t1_traverses_segment_towards_t1_and_internally_through_cut_to_t2(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] clamp1 = network['clamp1'] @@ -246,7 +247,7 @@ def test_non_traverse_step_to_cut_t1_traverses_segment_towards_t1_and_internally _verify_paths(next_paths, (cut1[1] - clamp1[1], cut1[1] - segment[1], cut1[1] + cut1[2])) def test_non_traverse_step_to_cut_t2_traverses_segment_towards_t2_and_internally_through_cut_to_t1(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] clamp4 = network['clamp4'] @@ -259,7 +260,7 @@ def test_non_traverse_step_to_cut_t2_traverses_segment_towards_t2_and_internally _verify_paths(next_paths, (cut2[2] - clamp4[1], cut2[2] - segment[2], cut2[2] + cut2[1])) def test_non_traverse_step_to_clamp_traverses_segment_in_both_directions(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] clamp1 = network['clamp1'] @@ -272,7 +273,7 @@ def test_non_traverse_step_to_clamp_traverses_segment_in_both_directions(self): _verify_paths(next_paths, (clamp1[1] - segment[1], clamp1[1] - cut1[1])) def test_traverse_step_to_clamp_traces_externally_and_does_not_traverse_back_along_segment(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] clamp1 = network['clamp1'] @@ -284,7 +285,7 @@ def test_traverse_step_to_clamp_traces_externally_and_does_not_traverse_back_alo _verify_paths(next_paths, (clamp1[1] + c3[1], )) def test_non_traverse_step_to_clamp_between_cuts_traverses_segment_both_ways_stopping_at_cuts(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c6 = network['c6'] clamp2 = network['clamp2'] @@ -298,7 +299,7 @@ def test_non_traverse_step_to_clamp_between_cuts_traverses_segment_both_ways_sto _verify_paths(next_paths, (clamp2[1] - cut1[2], clamp2[1] - clamp3[1], clamp2[1] - cut2[1])) def test_non_traverse_external_step_to_cut_t2_between_cuts_traverses_segment_towards_t2_stopping_at_next_cut_and_steps_internally_to_cut_t1(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c5 = network['c5'] clamp2 = network['clamp2'] @@ -312,7 +313,7 @@ def test_non_traverse_external_step_to_cut_t2_between_cuts_traverses_segment_tow _verify_paths(next_paths, (cut1[2] - clamp2[1], cut1[2] - clamp3[1], cut1[2] - cut2[1], cut1[2] + cut1[1])) def test_non_traverse_external_step_to_cut_t1_between_cuts_traverses_segment_towards_t1_stopping_at_next_cut_and_steps_internally_to_cut_t2(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c8 = network['c8'] clamp2 = network['clamp2'] @@ -326,7 +327,7 @@ def test_non_traverse_external_step_to_cut_t1_between_cuts_traverses_segment_tow _verify_paths(next_paths, (cut2[1] - clamp3[1], cut2[1] - clamp2[1], cut2[1] - cut1[2], cut2[1] + cut2[2])) def test_internal_step_to_cut_t2_between_cuts_steps_externally_and_traverses_segment_towards_t2_stopping_at_the_next_cut(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c5 = network['c5'] clamp2 = network['clamp2'] @@ -340,7 +341,7 @@ def test_internal_step_to_cut_t2_between_cuts_steps_externally_and_traverses_seg _verify_paths(next_paths, (cut1[2] - clamp2[1], cut1[2] - clamp3[1], cut1[2] - cut2[1], cut1[2] + c5[1])) def test_internal_step_to_cut_t1_between_cuts_steps_externally_and_traverses_segment_towards_t1_stopping_at_the_next_cut(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c8 = network['c8'] clamp2 = network['clamp2'] @@ -354,7 +355,7 @@ def test_internal_step_to_cut_t1_between_cuts_steps_externally_and_traverses_seg _verify_paths(next_paths, (cut2[1] - clamp2[1], cut2[1] - clamp3[1], cut2[1] - cut1[2], cut2[1] + c8[1])) def test_starting_on_clamp_terminal_flagged_as_traversed_segment_only_steps_externally(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c3 = network['c3'] clamp1 = network['clamp1'] @@ -363,7 +364,7 @@ def test_starting_on_clamp_terminal_flagged_as_traversed_segment_only_steps_exte _verify_paths(next_paths, (clamp1[1] + c3[1], )) def test_starting_on_clamp_terminal_that_flagged_as_not_traversed_segment_steps_externally_and_traverses(self): - network = self._acls_with_clamps_and_cuts_network() + network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c3 = network['c3'] clamp1 = network['clamp1'] @@ -657,54 +658,6 @@ def _acls_with_clamps_network(self) -> NetworkService: return network - def _acls_with_clamps_and_cuts_network(self) -> NetworkService: - # - # 2 2 - # c3 2 c7 2 - # 1 c5 1 c9 - # 1 clamp1 1 1 clamp3 1 - # | | | | - # 1 b0 21--*--*1 cut1 2*--*--c1--*--*1 cut2 2*--*--21 b2 2 - # | | | | - # 1 1 clamp2 1 1 clamp4 - # c4 1 c8 1 - # 2 c6 2 c10 - # 2 2 - # - network = (TestNetworkBuilder() - .from_breaker() # b0 - .to_acls() # c1 - .to_breaker() # b2 - .from_acls() # c3 - .from_acls() # c4 - .from_acls() # c5 - .from_acls() # c6 - .from_acls() # c7 - .from_acls() # c8 - .from_acls() # c9 - .from_acls() # c10 - ).network - - segment: AcLineSegment = network['c1'] - - clamp1 = _segment_with_clamp(network, segment, 1.0) - cut1 = _segment_with_cut(network, segment, 2.0) - clamp2 = _segment_with_clamp(network, segment, 3.0) - clamp3 = _segment_with_clamp(network, segment, 4.0) - cut2 = _segment_with_cut(network, segment, 5.0) - clamp4 = _segment_with_clamp(network, segment, 6.0) - - network.connect(clamp1[1], network.get('c3', ConductingEquipment)[1]) - network.connect(cut1[1], network.get('c4', ConductingEquipment)[1]) - network.connect(cut1[2], network.get('c5', ConductingEquipment)[1]) - network.connect(clamp2[1], network.get('c6', ConductingEquipment)[1]) - network.connect(clamp3[1], network.get('c7', ConductingEquipment)[1]) - network.connect(cut2[1], network.get('c8', ConductingEquipment)[1]) - network.connect(cut2[2], network.get('c9', ConductingEquipment)[1]) - network.connect(clamp4[1], network.get('c10', ConductingEquipment)[1]) - - return network - def _acls_with_clamps_and_cuts_at_same_position_network(self) -> NetworkService: # Drawing this is very messy, so it will be described in writing: # The network has 2 Breakers (b0, b2) with an AcLineSegment (c1) between them ( 1 b0 21--c1--21 b2 1 ) @@ -779,24 +732,6 @@ def acls_length(acls: AcLineSegment) -> None: return network -def _segment_with_clamp(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Clamp: - clamp = Clamp(mrid=f'clamp{segment.num_clamps() + 1}') - clamp.add_terminal(Terminal(mrid=f'{clamp.mrid}-t1')) - clamp.length_from_terminal_1 = length_from_terminal1 - - segment.add_clamp(clamp) - network.add(clamp) - return clamp - -def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Cut: - cut = Cut(mrid=f'cut{segment.num_cuts() + 1}', length_from_terminal_1=length_from_terminal1) - cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t1')) - cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t2')) - - segment.add_cut(cut) - network.add(cut) - return cut - def _verify_paths(in_paths: Generator[NetworkTraceStep.Path, None, None], in_expected: Iterable[NetworkTraceStep.Path], check_length=True): paths = list(in_paths) diff --git a/test/services/network/tracing/traversal/test_traversal.py b/test/services/network/tracing/traversal/test_traversal.py index b4b9eaa93..83a381324 100644 --- a/test/services/network/tracing/traversal/test_traversal.py +++ b/test/services/network/tracing/traversal/test_traversal.py @@ -3,18 +3,18 @@ # 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 collections import deque -from typing import Callable, TypeVar, Tuple, Any +from typing import Callable, TypeVar, Tuple, Any, Optional import pytest from zepben.evolve import StepContext, Traversal, TraversalQueue, ContextValueComputer -from zepben.evolve.services.network.tracing.traversal.traversal import D T = TypeVar('T') +D = TypeVar('D') -class TraversalTest(Traversal[T, 'TestTraversal[T]']): - def __init__(self, queue_type, parent, +class TraversalTest(Traversal[T, D]): + def __init__(self, queue_type, parent: Optional["TraversalTest[T]"], can_visit_item: Callable[[T, StepContext], bool], can_action_item: Callable[[T, StepContext], bool], on_reset: Callable[[], Any]): @@ -32,13 +32,15 @@ def can_action_item(self, item: T, context: StepContext) -> bool: def on_reset(self): return self._on_reset_impl() - def create_new_this(self) -> D: + def create_new_this(self) -> "TraversalTest[int]": return TraversalTest(self._queue_type, self, self._can_visit_item_impl, self._can_action_item_impl, self._on_reset_impl) def _create_traversal(can_visit_item: Callable[[int, StepContext], bool]=lambda x, y: True, can_action_item: Callable[[int, StepContext], bool]=lambda x, y: True, - on_reset: Callable[[], Any]=lambda: None) -> TraversalTest[int]: + on_reset: Callable[[], Any]=lambda: None, + queue: TraversalQueue[int]=TraversalQueue.depth_first() + ) -> TraversalTest[int, D]: def queue_next(item, _, queue_item): if item < 0: @@ -46,23 +48,24 @@ def queue_next(item, _, queue_item): else: queue_item(item + 1) - queue_type = Traversal.BasicQueueType[int, TraversalTest[int]]( + queue_type = Traversal.BasicQueueType[int, TraversalTest[int, D]]( queue_next=Traversal.QueueNext(queue_next), - queue=TraversalQueue.depth_first() + queue=queue ) return TraversalTest(queue_type, None, can_visit_item, can_action_item, on_reset) -def _create_branching_traversal() -> TraversalTest[int]: +def _create_branching_traversal() -> TraversalTest[int, D]: def queue_next(item, _, queue_item, queue_branch): - if item == 100: - queue_branch(-100) - elif item % 10 == 0: - queue_branch(item + 1) - else: + if item == 0: + queue_branch(-10) + queue_branch(10) + elif item < 0: queue_item(item + 1) + else: + queue_item(item - 1) - queue_type = Traversal.BranchingQueueType[int, TraversalTest[int]]( + queue_type = Traversal.BranchingQueueType[int, TraversalTest[int, D]]( queue_next=Traversal.BranchingQueueNext(queue_next), queue_factory=lambda: TraversalQueue.depth_first(), branch_queue_factory=lambda: TraversalQueue.depth_first() @@ -276,7 +279,6 @@ async def test_only_actions_items_that_can_be_actioned(self): steps = [] await (_create_traversal(can_action_item=lambda item, _: item % 2 == 1) - .add_stop_condition(lambda item, _: item == 2) .add_stop_condition(lambda item, _: item == 3) .add_step_action(lambda item, _: steps.append(item)) .run(1)) @@ -307,31 +309,57 @@ async def test_supports_branching_traversals(self): def step_action(item, ctx): steps[item] = ctx - await(_create_branching_traversal() - .add_queue_condition(lambda item, ctx, x, y: ctx.branch_depth <= 2) - .add_step_action(step_action) - .run(1)) + trace =(_create_branching_traversal() + .add_queue_condition(lambda item, ctx, x, y: (ctx.branch_depth <= 1) and (item != 0)) + .add_step_action(step_action) + ) + await trace.run(0, can_stop_on_start_item=False) + + + assert not steps[0].is_branch_start_item + assert steps[0].is_start_item + assert steps[0].branch_depth == 0 + + assert steps[10].is_branch_start_item + assert steps[10].branch_depth == 1 assert not steps[1].is_branch_start_item - assert steps[1].is_start_item - assert steps[1].branch_depth == 0 + assert not steps[1].is_start_item + assert steps[1].branch_depth == 1 + + assert steps[-10].is_branch_start_item + assert steps[-10].branch_depth == 1 - assert not steps[10].is_branch_start_item - assert steps[10].branch_depth == 0 + assert not steps[-1].is_branch_start_item + assert not steps[-1].is_start_item + assert steps[-1].branch_depth == 1 - assert steps[11].is_branch_start_item - assert not steps[11].is_start_item - assert steps[11].branch_depth == 1 + @pytest.mark.asyncio + async def test_can_stop_on_start_item_is_not_assessed_on_branch_start_items(self): + stop_condition_triggered = [] - assert not steps[20].is_branch_start_item - assert steps[20].branch_depth == 1 + def stop_condition(item: int, context): + if abs(item) == 10: + stop_condition_triggered.append(True) + return stop_condition_triggered - assert steps[21].is_branch_start_item - assert not steps[21].is_start_item - assert steps[21].branch_depth == 2 + await (_create_branching_traversal() + .add_stop_condition(stop_condition) + .add_queue_condition(lambda x, ctx, y, z: ctx.branch_depth < 2) + .add_start_item(1) + .add_start_item(-1) + ).run(can_stop_on_start_item=False) - assert not steps[30].is_branch_start_item - assert steps[30].branch_depth == 2 + assert all(stop_condition_triggered) - with pytest.raises(KeyError): - assert not steps[31] \ No newline at end of file + @pytest.mark.asyncio + async def test_start_items_are_queued_before_traversal_starts_so_queue_type_is_honoured_for_start_items(self): + steps = [] + await (_create_traversal(queue=TraversalQueue.breadth_first()) + .add_stop_condition(lambda item, x: item >= 2 or item <= -2) + .add_step_action(lambda item, x: steps.append(item)) + .add_start_item(-1) + .add_start_item(1) + ).run() + + assert steps == [-1, 1, -2, 2] From c80c9be0bb7bd6cf5b265d1bf93cbf51613bfa5d Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 10:39:48 +1000 Subject: [PATCH 23/47] because ofcourse i accidentally wrote tests that modify global state Signed-off-by: Max Chesterfield --- .../networktrace/operators/network_state_operators.py | 2 +- .../conditions/test_direction_condition.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py index ce6b79a05..80aebb851 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/network_state_operators.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep -__all__ = ['NetworkStateOperators'] +__all__ = ['NetworkStateOperators', 'NormalNetworkStateOperators', 'CurrentNetworkStateOperators'] # noinspection PyPep8Naming class NetworkStateOperators(OpenStateOperators, diff --git a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py index 4a89f6ce9..7d80be27c 100644 --- a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py +++ b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py @@ -7,12 +7,13 @@ import pytest +from zepben.evolve import NetworkStateOperators, FeederDirection, NetworkTraceStep, Terminal, Junction from services.network.test_data.cuts_and_clamps_network import CutsAndClampsNetwork -from zepben.evolve import FeederDirection, NetworkTraceStep, Terminal, Junction, NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition class TestDirectionCondition: + def test_should_queue_for_non_cut_or_clamp_path(self): traced_internally = True _terminal_should_queue((FeederDirection.NONE, FeederDirection.NONE, traced_internally), True) @@ -169,7 +170,7 @@ def _terminal_should_queue(condition: Tuple[FeederDirection, FeederDirection, bo next_item = NetworkTraceStep(next_path, 0, 0, None) - state_operators = NetworkStateOperators + state_operators = MagicMock(NetworkStateOperators.NORMAL) state_operators.get_direction = lambda t: to_direction result = DirectionCondition(direction, state_operators).should_queue(next_item, None, None, None) @@ -187,9 +188,9 @@ def _start_terminal_should_queue(condition: Tuple[FeederDirection, FeederDirecti next_path.to_equipment = Junction() next_path.did_traverse_ac_line_segment = False - next_item = NetworkTraceStep(next_path(), 0, 0, None) + next_item = NetworkTraceStep(next_path, 0, 0, None) - state_operators = NetworkStateOperators + state_operators = MagicMock(NetworkStateOperators.NORMAL) state_operators.get_direction = lambda t: to_direction result = DirectionCondition(direction, state_operators).should_queue_start_item(next_item) @@ -201,7 +202,7 @@ def _should_queue(condition: Tuple[FeederDirection, NetworkTraceStep.Path], expe next_step.configure_mock( path=path ) - should_queue = DirectionCondition(direction, NetworkStateOperators.NORMAL).should_queue(next_step(), None, None, None) + should_queue = DirectionCondition(direction, NetworkStateOperators.NORMAL).should_queue(next_step, None, None, None) print(f'direction: {direction}') print(f'path: internal: {path.traced_internally}\n from: {path.from_terminal}\n to: {path.to_terminal}\n') assert should_queue == expected \ No newline at end of file From 66d0449bb0940af419e65826e6b2437de07d2422 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 11:23:54 +1000 Subject: [PATCH 24/47] pointless code prettifying Signed-off-by: Max Chesterfield --- .../networktrace/network_trace_step_path_provider.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index e2446b270..4eec1d197 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -198,15 +198,17 @@ def _acls_traverse_from_terminal( :param towards_segment_T2: Use `true` if the segment should be traversed towards terminal 2, otherwise `False` to traverse towards terminal 1 """ # Can do a simple return if we don't need to do any special cuts/clamps processing - if not(any((list(acls.cuts), list(acls.clamps)))): + cuts, clamps = list(acls.cuts), list(acls.clamps) + + if not any((cuts, clamps)): yield from seq_term_map_to_path(from_terminal.other_terminals(), path_factory, acls) else: # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! # We also make sure we filter out the cut or the clamp we are starting at, so we don't compare it in our checks filter_func = lambda it: it != from_terminal.conducting_equipment and self.state_operators.is_in_service(it) - cuts: List[Cut] = list(filter(filter_func, acls.cuts)) - clamps: List[Clamp] = list(filter(filter_func, acls.clamps)) + cuts: List[Cut] = list(filter(filter_func, cuts)) + clamps: List[Clamp] = list(filter(filter_func, clamps)) cuts_at_same_position = list(filter(lambda it: it.length_from_T1_or_0 == length_from_T1, cuts)) stop_at_cuts_at_same_position = bool(can_stop_at_cut_at_same_position and cuts_at_same_position) From 4947e4bf15c381b36abc5ce19bebc79961b94701 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 11:24:31 +1000 Subject: [PATCH 25/47] comparing a bound method to an enum will always return False silently Signed-off-by: Max Chesterfield --- .../network_trace_stop_condition.py | 4 +- .../networktrace/test_network_trace.py | 66 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py index 5ca9cbc82..a92c83175 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py @@ -45,12 +45,12 @@ def should_stop_matched_step(self, item: NetworkTraceStep[T], context: StepConte raise NotImplemented def should_stop_internal_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: - if item.type == NetworkTraceStep.Type.INTERNAL: + if item.type() == NetworkTraceStep.Type.INTERNAL: return self.should_stop_matched_step(item, context) return False def should_stop_external_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: - if item.type == NetworkTraceStep.Type.EXTERNAL: + if item.type() == NetworkTraceStep.Type.EXTERNAL: return self.should_stop_matched_step(item, context) return False diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index 075892553..0ed32507e 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -3,11 +3,14 @@ # 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/. import sys +DEFAULT_RECURSION_LIMIT = sys.getrecursionlimit() + +from typing import List import pytest from services.network.tracing.networktrace.test_network_trace_step_path_provider import PathTerminal, _verify_paths -from zepben.evolve import AcLineSegment, Clamp, Terminal, NetworkTraceStep, Cut +from zepben.evolve import AcLineSegment, Clamp, Terminal, NetworkTraceStep, Cut, ConductingEquipment, TraversalQueue, Junction, ngen from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.testing.test_network_builder import TestNetworkBuilder @@ -35,7 +38,7 @@ def test_adds_start_whole_clamp_as_not_traversed_segment_path(self): segment.add_clamp(clamp) trace.add_start_item(clamp) - _verify_paths((trace.start_items[0].path, ), (clamp[1] + clamp[1], )) + _verify_paths(ngen([trace.start_items[0].path]), (clamp[1] + clamp[1], )) @pytest.mark.asyncio def test_adds_start_AcLineSegment_terminals_cut_terminals_and_clamp_terminals_as_traversed_segment(self): @@ -74,6 +77,65 @@ def test_adds_start_AcLineSegment_terminals_cut_terminals_and_clamp_terminals_as cut2[1] - cut2[1], cut2[2] - cut2[2])) + @pytest.mark.asyncio + async def test_doesnt_bypass_stop_conditions_with_multiple_branches_in_equipment_traces_loop(self): + # + # /--21--c1--21 + # c0 j2 21--c3--2 + # \--12--c4--13 + # + ns = (TestNetworkBuilder() + .from_acls() # c0 + .to_acls() # c1 + .to_junction(num_terminals=3) # j2 + .branch_from('j2', 2) + .to_acls() # c3 + .branch_from('j2', 3) + .to_acls() # c4 + .connect('c4', 'c0', 2, 1) + ).network + + stepped_on: List[str] = [] + await Tracing.network_trace() \ + .add_stop_condition(lambda step, _: step.path.to_equipment.mrid == 'j2') \ + .add_step_action(lambda step, _: stepped_on.append(step.path.to_equipment.mrid)) \ + .run(ns.get('c0', ConductingEquipment)) + + assert stepped_on == ['c0', 'c1', 'j2', 'c4'] + + @pytest.mark.asyncio + async def test_breadth_first_queue_supports_multiple_start_items(self): + # + # 1--c1--21--c2--2 + # 2 1 + # j0 j3 + # 1 2 + # 2--c5--12--c4--1 + # + ns = (TestNetworkBuilder() + .from_junction() # j0 + .to_acls() # c1 + .to_acls() # c2 + .to_junction() # j3 + .to_acls() # c4 + .to_acls() # c5 + .connect('c5', 'j0', 2, 1) + ).network + + steps: List[NetworkTraceStep] = [] + await Tracing.network_trace(queue=TraversalQueue.breadth_first()) \ + .add_step_action(lambda step, _: steps.append(step)) \ + .run(ns.get('j0', Junction)) + + assert list(map(lambda it: (it.num_equipment_steps, it.path.to_equipment.mrid), steps)) \ + == [(0, 'j0'), + (1, 'c5'), + (1, 'c1'), + (2, 'c4'), + (2, 'c2'), + (3, 'j3')] + + @pytest.mark.asyncio async def test_can_run_large_branching_traces(self): try: From d4d9d17809470d7319be2d040225898c6a4c6600 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 18:02:50 +1000 Subject: [PATCH 26/47] [DEV-2573] changes ported from Kotlin SDK (#221) Signed-off-by: Max Chesterfield --- changelog.md | 15 +- .../network_trace_stop_condition.py | 3 +- .../evolve/testing/test_network_builder.py | 92 ++++++- .../test_data/cuts_and_clamps_network.py | 52 +--- .../tracing/feeder/test_set_direction.py | 148 +++++----- .../conditions/test_direction_condition.py | 8 +- .../networktrace/test_network_trace.py | 78 ++++-- .../test_network_trace_step_path_provider.py | 256 +++++++++++------- 8 files changed, 408 insertions(+), 244 deletions(-) diff --git a/changelog.md b/changelog.md index 43bc8c74f..f82299bf1 100644 --- a/changelog.md +++ b/changelog.md @@ -13,9 +13,22 @@ ### Enhancements * Tracing models with `Cut` and `Clamp` are now supported via the new tracing API. +* Added support to `TestNetworkBuilder` for: + * `with_clamp` - Adds a clamp to the previously added `AcLineSegment` + * `with_cut` - Adds a cut to the previously added `AcLineSegment` + * `connect_to` - Connects the previously added item, rather than having to specify it again in `connect`. + + ### Fixes - When finding `LvFeeders` in the `Site` we will now exclude `LvFeeders` that start with an open `Switch` +* When finding `LvFeeders` in the `Site` we will now exclude `LvFeeders` that start with an open `Switch` +* The follow fixes were added to Traversal and NetworkTrace: + * `canStopAtStartItem` now works for branching traversals. + * Traversal start items are added to the queue before traversal starts, so that the start items honour the queue type order. + * Stop conditions on the `NetworkTrace` now are checked based on a step type, like `QueueCondition` does, rather than by checking `canActionItem`. + * `Cut` and `Clamp` are now correctly supported in `SetDirection` and `DirectionCondition`. + * `NetworkTrace` now handles starting on `Cut` , `Clamp`, and `AcLineSegment` and their terminals in a explicit / sensible way. + * `NetworkTracePathProvider` now correctly handles next paths when starting on a `Clamp` terminal. ### Notes * None. diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py index a92c83175..ef6bd98e3 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py @@ -50,7 +50,8 @@ def should_stop_internal_step(self, item: NetworkTraceStep[T], context: StepCont return False def should_stop_external_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: - if item.type() == NetworkTraceStep.Type.EXTERNAL: + # We also need to check start items as they are always marked as internal, but we still want to be able to stop on them. + if (item.type() == NetworkTraceStep.Type.EXTERNAL) or context.is_start_item: return self.should_stop_matched_step(item, context) return False diff --git a/src/zepben/evolve/testing/test_network_builder.py b/src/zepben/evolve/testing/test_network_builder.py index 67a2311fe..9376e720d 100644 --- a/src/zepben/evolve/testing/test_network_builder.py +++ b/src/zepben/evolve/testing/test_network_builder.py @@ -13,7 +13,7 @@ from zepben.evolve import ConductingEquipment, NetworkService, PhaseCode, EnergySource, AcLineSegment, Breaker, Junction, Terminal, Feeder, LvFeeder, \ PowerTransformerEnd, PowerTransformer, EnergyConsumer, \ - PowerElectronicsConnection, BusbarSection + PowerElectronicsConnection, BusbarSection, Clamp, Cut def null_action(_): @@ -345,9 +345,9 @@ def to_energy_consumer( def from_busbar_section( self, - nominal_phases: PhaseCode=PhaseCode.ABC, - mrid: str=None, - action: Callable[[BusbarSection], None]=null_action + nominal_phases: PhaseCode = PhaseCode.ABC, + mrid: str = None, + action: Callable[[BusbarSection], None] = null_action ) -> 'TestNetworkBuilder': """ Start a new network island from a `BusbarSection`, updating the network pointer to the new `BusbarSection`. @@ -443,6 +443,63 @@ def to_other( self._current = it return self + def with_clamp( + self, + mrid: Optional[str] = None, + length_from_terminal_1: float = None + ) -> 'TestNetworkBuilder': + """ + Create a clamp on the current network pointer (must be an `AcLineSegment`) without moving the current network pointer. + + :param mrid: Optional mRID for the new `Clamp` + :param length_from_terminal_1: The length from terminal 1 of the `AcLineSegment` being clamped + :return: This `TestNetworkBuilder` to allow for fluent use + """ + acls = self._current + if not isinstance(acls, AcLineSegment): + raise ValueError("`with_clamp` can only be called when the last added item was an AcLineSegment") + + clamp = Clamp(mrid=mrid or f'{acls.mrid}-clamp{acls.num_clamps() + 1}', length_from_terminal_1=length_from_terminal_1) + clamp.add_terminal(Terminal(mrid=f'{clamp.mrid}-t1')) + + acls.add_clamp(clamp) + self.network.add(clamp) + return self + + def with_cut( + self, + mrid: Optional[str] = None, + length_from_terminal_1: Optional[float] = None, + is_normally_open: bool = True, + is_open: bool = None + ) -> 'TestNetworkBuilder': + """ + Create a cut on the current network pointer (must be an `AcLineSegment`) without moving the current network pointer. + + :param mrid: Optional mRID for the new `Cut` + :param length_from_terminal_1: The length from terminal 1 of the `AcLineSegment` being cut + :param is_normally_open: The normal state of the cut, defaults to True + :param is_open: The current state of the cut. Defaults to `is_normally_open` + :return: This `TestNetworkBuilder` to allow for fluent use + """ + acls = self._current + if not isinstance(acls, AcLineSegment): + raise ValueError("`with_cut` can only be called when the last added item was an AcLineSegment") + + cut = Cut(mrid=mrid or f'{acls.mrid}-cut{acls.num_cuts() + 1}', length_from_terminal_1=length_from_terminal_1) + cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t1')) + cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t2')) + + cut.set_normally_open(is_normally_open) + if is_open is None: + cut.set_open(is_normally_open) + else: + cut.set_open(is_open) + + acls.add_cut(cut) + self.network.add(cut) + return self + def branch_from(self, from_: str, terminal: Optional[int] = None) -> 'TestNetworkBuilder': """ Move the current network pointer to the specified `from` allowing branching of the network. This has the effect of changing the current network pointer. @@ -456,6 +513,33 @@ def branch_from(self, from_: str, terminal: Optional[int] = None) -> 'TestNetwor self._current_terminal = terminal return self + def connect_to( + self, + to: str, + to_terminal: int = None, + from_terminal: int = None, + connectivity_node_mrid: Optional[str] = None + ) -> 'TestNetworkBuilder': + """ + Connect to current network pointer to the specified `to` without moving the current network pointer. + + :param to: The mRID of the second `ConductingEquipment` to be connected. + :param to_terminal: The sequence number or terminal on `to` which will be connected. + :param from_terminal: Optional sequence number of the terminal on current network pointer which will be connected. + :param connectivity_node_mrid: Optional id of the connectivity node used to connect the terminals. Will only be used if both terminals are not already + connected. + :return: This `TestNetworkBuilder` to allow for fluent use. + """ + + self._connect( + self._current, + self.network.get(to, ConductingEquipment), + connectivity_node_mrid, + from_terminal, + to_terminal + ) + return self + def connect( self, from_: str, diff --git a/test/services/network/test_data/cuts_and_clamps_network.py b/test/services/network/test_data/cuts_and_clamps_network.py index 250d80a25..14b34ccab 100644 --- a/test/services/network/test_data/cuts_and_clamps_network.py +++ b/test/services/network/test_data/cuts_and_clamps_network.py @@ -26,55 +26,31 @@ def multi_cut_and_clamp_network() -> TestNetworkBuilder: builder = (TestNetworkBuilder() .from_breaker() # b0 .to_acls() # c1 + .with_clamp(length_from_terminal_1=1.0) # c1-clamp1 + .with_cut(length_from_terminal_1=2.0, is_normally_open=False) # c1-cut1 + .with_clamp(length_from_terminal_1=3.0) # c1-clamp2 + .with_clamp(length_from_terminal_1=4.0) # c1-clamp3 + .with_cut(length_from_terminal_1=5.0, is_normally_open=False) # c1-cut2 + .with_clamp(length_from_terminal_1=6.0) # c1-clamp4 .to_breaker() # b2 .from_acls() # c3 + .connect_to('c1-clamp1', from_terminal=1) .from_acls() # c4 + .connect_to('c1-cut1', from_terminal=1) .from_acls() # c5 + .connect_to('c1-cut1', to_terminal=2, from_terminal=1) .from_acls() # c6 + .connect_to('c1-clamp2', from_terminal=1) .from_acls() # c7 + .connect_to('c1-clamp3', from_terminal=1) .from_acls() # c8 + .connect_to('c1-cut2', from_terminal=1) .from_acls() # c9 + .connect_to('c1-cut2', to_terminal=2, from_terminal=1) .from_acls() # c10 + .connect_to('c1-clamp4', from_terminal=1) ) - network = builder.network - - segment: AcLineSegment = network['c1'] - - clamp1 = _segment_with_clamp(network, segment, 1.0) - cut1 = _segment_with_cut(network, segment, 2.0) - clamp2 = _segment_with_clamp(network, segment, 3.0) - clamp3 = _segment_with_clamp(network, segment, 4.0) - cut2 = _segment_with_cut(network, segment, 5.0) - clamp4 = _segment_with_clamp(network, segment, 6.0) - - network.connect(clamp1[1], network.get('c3', ConductingEquipment)[1]) - network.connect(cut1[1], network.get('c4', ConductingEquipment)[1]) - network.connect(cut1[2], network.get('c5', ConductingEquipment)[1]) - network.connect(clamp2[1], network.get('c6', ConductingEquipment)[1]) - network.connect(clamp3[1], network.get('c7', ConductingEquipment)[1]) - network.connect(cut2[1], network.get('c8', ConductingEquipment)[1]) - network.connect(cut2[2], network.get('c9', ConductingEquipment)[1]) - network.connect(clamp4[1], network.get('c10', ConductingEquipment)[1]) - return builder -def _segment_with_clamp(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Clamp: - clamp = Clamp(mrid=f'clamp{segment.num_clamps() + 1}') - clamp.add_terminal(Terminal(mrid=f'{clamp.mrid}-t1')) - clamp.length_from_terminal_1 = length_from_terminal1 - - segment.add_clamp(clamp) - network.add(clamp) - return clamp - - -def _segment_with_cut(network: NetworkService, segment: AcLineSegment, length_from_terminal1: Optional[float]) -> Cut: - cut = Cut(mrid=f'cut{segment.num_cuts() + 1}', length_from_terminal_1=length_from_terminal1) - cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t1')) - cut.add_terminal(Terminal(mrid=f'{cut.mrid}-t2')) - - segment.add_cut(cut) - network.add(cut) - return cut diff --git a/test/services/network/tracing/feeder/test_set_direction.py b/test/services/network/tracing/feeder/test_set_direction.py index 58a9771ea..593ee3eee 100644 --- a/test/services/network/tracing/feeder/test_set_direction.py +++ b/test/services/network/tracing/feeder/test_set_direction.py @@ -320,25 +320,25 @@ async def test_dual_path_loop_bottom(self): # | | # \-c10-21 j11 21-c12-/ # - n = TestNetworkBuilder() \ - .from_junction(num_terminals=1) \ - .to_acls() \ - .to_junction(num_terminals=3) \ - .to_acls() \ - .to_junction() \ - .to_acls() \ - .to_junction(num_terminals=3) \ - .to_acls() \ - .to_junction(num_terminals=1) \ - .from_acls() \ - .from_acls() \ - .to_junction() \ - .to_acls() \ - .connect("c9", "j6", 1, 2) \ - .connect("c9", "j2", 2, 2) \ - .connect("c10", "j2", 1, 2) \ - .connect("c12", "j6", 2, 2) \ - .network + n = (TestNetworkBuilder() + .from_junction(num_terminals=1) #j0 + .to_acls() #c1 + .to_junction(num_terminals=3) #j2 + .to_acls() #c3 + .to_junction() #j4 + .to_acls() #c5 + .to_junction(num_terminals=3) #j6 + .to_acls() #c7 + .to_junction(num_terminals=1) #j8 + .from_acls() #c9 + .from_acls() #c10 + .to_junction() #c11 + .to_acls() #c12 + .connect_to('j6', 2) + .connect("c9", "j6", 1, 2) + .connect("c9", "j2", 2, 2) + .connect("c10", "j2", 1, 2) + ).network await SetDirection().run_terminal(self._get_t(n, "j0", 1)) await log_directions(n["j0"]) @@ -462,28 +462,28 @@ async def test_set_direction_on_acls_with_cuts_and_clamps_from_acls_end_terminal .add_feeder('b0', 2) \ .network - n.get('cut1', Cut).set_normally_open(False) - n.get('cut2', Cut).set_normally_open(True) + n.get('c1-cut1', Cut).set_normally_open(False) + n.get('c1-cut2', Cut).set_normally_open(True) await self._do_set_direction_trace(self._get_t(n, 'b0', 2)) self._check_expected_direction(self._get_t(n, 'b0', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c1', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 2), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp3', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 2), NONE) @pytest.mark.asyncio async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_clamp(self): @@ -491,28 +491,28 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_clamp(self): .add_feeder('c6', 1) \ .network - n.get('cut1', Cut).set_normally_open(False) - n.get('cut2', Cut).set_normally_open(True) + n.get('c1-cut1', Cut).set_normally_open(False) + n.get('c1-cut2', Cut).set_normally_open(True) await self._do_set_direction_trace(self._get_t(n, 'c6', 1)) self._check_expected_direction(self._get_t(n, 'c6', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c6', 2), NONE) - self._check_expected_direction(self._get_t(n, 'clamp2', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp3', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 2), NONE) self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 2), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 2), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c1', 1), DOWNSTREAM) @@ -525,33 +525,33 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_cut(self): .add_feeder('c5', 1) \ .network - n.get('cut1', Cut).set_normally_open(False) - n.get('cut2', Cut).set_normally_open(True) + n.get('c1-cut1', Cut).set_normally_open(False) + n.get('c1-cut2', Cut).set_normally_open(True) await self._do_set_direction_trace(self._get_t(n, 'c5', 1)) self._check_expected_direction(self._get_t(n, 'c5', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c5', 2), NONE) - self._check_expected_direction(self._get_t(n, 'cut1', 2), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 2), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'b0', 2), UPSTREAM) self._check_expected_direction(self._get_t(n, 'b0', 1), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp2', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c6', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c6', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp3', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 2), NONE) @pytest.mark.asyncio async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_both_acls_ends(self): @@ -560,8 +560,8 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_both_acls_en .add_feeder('b2', 1) \ .network - n.get('cut1', Cut).set_normally_open(False) - n.get('cut2', Cut).set_normally_open(False) + n.get('c1-cut1', Cut).set_normally_open(False) + n.get('c1-cut2', Cut).set_normally_open(False) await self._do_set_direction_trace(self._get_t(n, 'b0', 2)) await self._do_set_direction_trace(self._get_t(n, 'b2', 1)) @@ -569,28 +569,28 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_both_acls_en self._check_expected_direction(self._get_t(n, 'b0', 2), BOTH) self._check_expected_direction(self._get_t(n, 'b0', 1), NONE) self._check_expected_direction(self._get_t(n, 'c1', 1), BOTH) - self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 2), BOTH) - self._check_expected_direction(self._get_t(n, 'cut1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp2', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c6', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c6', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp3', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 1), BOTH) - self._check_expected_direction(self._get_t(n, 'cut2', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 2), BOTH) self._check_expected_direction(self._get_t(n, 'c9', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c9', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp4', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp4', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c10', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c10', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'b2', 1), BOTH) @@ -603,8 +603,8 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_acls_end_and .add_feeder('c6', 1) \ .network - n.get('cut1', Cut).set_normally_open(False) - n.get('cut2', Cut).set_normally_open(True) + n.get('c1-cut1', Cut).set_normally_open(False) + n.get('c1-cut2', Cut).set_normally_open(True) await self._do_set_direction_trace(self._get_t(n, 'b0', 2)) await self._do_set_direction_trace(self._get_t(n, 'c6', 1)) @@ -612,25 +612,25 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_acls_end_and self._check_expected_direction(self._get_t(n, 'b0', 2), BOTH) self._check_expected_direction(self._get_t(n, 'b0', 1), NONE) self._check_expected_direction(self._get_t(n, 'c1', 1), BOTH) - self._check_expected_direction(self._get_t(n, 'clamp1', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp1', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c3', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 2), BOTH) - self._check_expected_direction(self._get_t(n, 'cut1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c5', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c5', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp2', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-clamp2', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c6', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c6', 2), NONE) - self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp3', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 2), NONE) @pytest.mark.asyncio async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_acls_clamp_and_cut(self): @@ -639,8 +639,8 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_acls_clamp_a .add_feeder('c5', 1) \ .network - n.get('cut1', Cut).set_normally_open(False) - n.get('cut2', Cut).set_normally_open(True) + n.get('c1-cut1', Cut).set_normally_open(False) + n.get('c1-cut2', Cut).set_normally_open(True) await self._do_set_direction_trace(self._get_t(n, 'c3', 1)) await self._do_set_direction_trace(self._get_t(n, 'c5', 1)) @@ -648,25 +648,25 @@ async def test_sets_direction_on_acls_with_cuts_and_clamps_fed_from_acls_clamp_a self._check_expected_direction(self._get_t(n, 'b0', 2), UPSTREAM) self._check_expected_direction(self._get_t(n, 'b0', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c1', 1), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-clamp1', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c3', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c3', 2), NONE) self._check_expected_direction(self._get_t(n, 'c4', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c4', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut1', 2), BOTH) - self._check_expected_direction(self._get_t(n, 'cut1', 1), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 2), BOTH) + self._check_expected_direction(self._get_t(n, 'c1-cut1', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c5', 1), BOTH) self._check_expected_direction(self._get_t(n, 'c5', 2), NONE) - self._check_expected_direction(self._get_t(n, 'clamp2', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp2', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c6', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c6', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'clamp3', 1), DOWNSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-clamp3', 1), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c7', 2), DOWNSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 1), UPSTREAM) self._check_expected_direction(self._get_t(n, 'c8', 2), DOWNSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 1), UPSTREAM) - self._check_expected_direction(self._get_t(n, 'cut2', 2), NONE) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 1), UPSTREAM) + self._check_expected_direction(self._get_t(n, 'c1-cut2', 2), NONE) @staticmethod diff --git a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py index 7d80be27c..c084ba1cf 100644 --- a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py +++ b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py @@ -97,7 +97,7 @@ async def test_cuts_queue_when_direction_set_from_segment_end(self): .build() c1 = network['c1'] - cut1 = network['cut1'] + cut1 = network['c1-cut1'] c4 = network['c4'] c5 = network['c5'] @@ -119,8 +119,8 @@ async def test_cuts_queue_when_direction_set_from_clamp(self): .build() c1 = network['c1'] - clamp1 = network['clamp1'] - cut1 = network['cut1'] + clamp1 = network['c1-clamp1'] + cut1 = network['c1-cut1'] c4 = network['c4'] c5 = network['c5'] @@ -141,7 +141,7 @@ async def test_cuts_queue_when_direction_set_from_cut(self): .add_feeder('c4', 1) \ .build() - cut1 = network['cut1'] + cut1 = network['c1-cut1'] c4 = network['c4'] c5 = network['c5'] diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index 0ed32507e..c035b6bdf 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -2,10 +2,11 @@ # 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/. +import os import sys DEFAULT_RECURSION_LIMIT = sys.getrecursionlimit() -from typing import List +from typing import List, Set import pytest @@ -135,26 +136,69 @@ async def test_breadth_first_queue_supports_multiple_start_items(self): (2, 'c2'), (3, 'j3')] + @pytest.mark.asyncio + async def test_can_stop_on_start_item_when_running_from_conducting_equipment(self): + # + # 1 b0 21--c1--2 + # + ns = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + ).network + + steps: List[NetworkTraceStep] = [] + await Tracing.network_trace() \ + .add_step_action(lambda step, _: steps.append(step)) \ + .add_stop_condition(lambda step, _: True) \ + .run(ns.get('b0', ConductingEquipment)) + + assert list(map(lambda it: (it.num_equipment_steps, it.path.to_equipment.mrid), steps)) \ + == [(0, 'b0')] @pytest.mark.asyncio - async def test_can_run_large_branching_traces(self): - try: - sys.setrecursionlimit(100000) # need to bump this for this test, we're going 1000+ recursive calls deep + async def test_can_Stop_on_start_item_when_running_from_conducting_equipment_branching(self): + # + # 1 b0 21--c1--2 + # 1 + # \--c2--2 + # + ns = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .branch_from('b0') + .to_acls() # c2 + ).network + + steps: Set[NetworkTraceStep] = set() + await Tracing.network_trace_branching() \ + .add_step_action(lambda step, _: steps.add(step)) \ + .add_stop_condition(lambda step, _: True) \ + .run(ns.get('b0', ConductingEquipment)) + + assert set(map(lambda it: (it.num_equipment_steps, it.path.to_equipment.mrid), steps)) \ + == {(0, 'b0')} + + if 'TOX_ENV_NAME' not in os.environ: + + @pytest.mark.asyncio + async def test_can_run_large_branching_traces(self): + try: + sys.setrecursionlimit(100000) # need to bump this for this test, we're going 1000+ recursive calls deep - builder = TestNetworkBuilder() - network = builder.network + builder = TestNetworkBuilder() + network = builder.network - builder.from_junction(num_terminals=1) \ - .to_acls() + builder.from_junction(num_terminals=1) \ + .to_acls() - for i in range(500): - builder.to_junction(mrid=f'junc-{i}', num_terminals=3) \ - .to_acls(mrid=f'acls-{i}-top') \ - .from_acls(mrid=f'acls-{i}-bottom') \ - .connect(f'junc-{i}', f'acls-{i}-bottom', 2, 1) + for i in range(500): + builder.to_junction(mrid=f'junc-{i}', num_terminals=3) \ + .to_acls(mrid=f'acls-{i}-top') \ + .from_acls(mrid=f'acls-{i}-bottom') \ + .connect(f'junc-{i}', f'acls-{i}-bottom', 2, 1) - await Tracing.network_trace_branching().run(network['j0'].get_terminal_by_sn(1)) + await Tracing.network_trace_branching().run(network['j0'].get_terminal_by_sn(1)) - except Exception as e: - sys.setrecursionlimit(1000) # back to default - raise e + except Exception as e: + sys.setrecursionlimit(1000) # back to default + raise e diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index fa1fb4fd6..baea1bff0 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -6,7 +6,7 @@ from pytest_subtests.plugin import subtests -from services.network.test_data.cuts_and_clamps_network import _segment_with_clamp, _segment_with_cut, CutsAndClampsNetwork +from services.network.test_data.cuts_and_clamps_network import CutsAndClampsNetwork from zepben.evolve.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.evolve.services.network.network_service import NetworkService @@ -162,8 +162,8 @@ def test_traversing_segment_with_clamps_from_t1_includes_all_clamp_steps(self): breaker = network['b0'] segment: AcLineSegment = network['c1'] - clamp1 = network['clamp1'] - clamp2 = network['clamp2'] + clamp1 = network['c1-clamp1'] + clamp2 = network['c1-clamp2'] current_path = breaker[2] + segment[1] next_paths = self.path_provider.next_paths(current_path) @@ -175,8 +175,8 @@ def test_traversing_segment_with_clamps_from_t2_includes_all_clamp_steps(self): breaker = network['b2'] segment: AcLineSegment = network['c1'] - clamp1 = network['clamp1'] - clamp2 = network['clamp2'] + clamp1 = network['c1-clamp1'] + clamp2 = network['c1-clamp2'] current_path = breaker[1] + segment[2] next_paths = self.path_provider.next_paths(current_path) @@ -188,8 +188,8 @@ def test_non_traverse_step_to_segment_t1_traverses_towards_t2_stopping_at_cut(se b0 = network['b0'] segment = network['c1'] - clamp1 = network['clamp1'] - cut1 = network['cut1'] + clamp1 = network['c1-clamp1'] + cut1 = network['c1-cut1'] current_path = b0[2] + segment[1] next_paths = self.path_provider.next_paths(current_path) @@ -201,8 +201,8 @@ def test_non_traverse_step_to_segment_t2_traverses_towards_t1_stopping_at_cut(se b2 = network['b2'] segment = network['c1'] - clamp4 = network['clamp4'] - cut2 = network['cut2'] + clamp4 = network['c1-clamp4'] + cut2 = network['c1-cut2'] current_path = b2[1] + segment[2] next_paths = self.path_provider.next_paths(current_path) @@ -213,7 +213,7 @@ def test_traverse_step_to_cut_t1_steps_externally_and_across_cut(self): network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] - cut1 = network['cut1'] + cut1 = network['c1-cut1'] c4 = network['c4'] current_path = segment[1] - cut1[1] @@ -225,7 +225,7 @@ def test_traverse_step_to_cut_t2_steps_externally_and_across_cut(self): network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] - cut2 = network['cut2'] + cut2 = network['c1-cut2'] c9 = network['c9'] current_path = segment[2] - cut2[2] @@ -237,8 +237,8 @@ def test_non_traverse_step_to_cut_t1_traverses_segment_towards_t1_and_internally network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] - clamp1 = network['clamp1'] - cut1 = network['cut1'] + clamp1 = network['c1-clamp1'] + cut1 = network['c1-cut1'] c4 = network['c4'] current_path = c4[1] + cut1[1] @@ -250,8 +250,8 @@ def test_non_traverse_step_to_cut_t2_traverses_segment_towards_t2_and_internally network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] - clamp4 = network['clamp4'] - cut2 = network['cut2'] + clamp4 = network['c1-clamp4'] + cut2 = network['c1-cut2'] c9 = network['c9'] current_path = c9[1] + cut2[2] @@ -263,8 +263,8 @@ def test_non_traverse_step_to_clamp_traverses_segment_in_both_directions(self): network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] - clamp1 = network['clamp1'] - cut1 = network['cut1'] + clamp1 = network['c1-clamp1'] + cut1 = network['c1-cut1'] c3 = network['c3'] current_path = c3[1] + clamp1[1] @@ -276,7 +276,7 @@ def test_traverse_step_to_clamp_traces_externally_and_does_not_traverse_back_alo network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network segment = network['c1'] - clamp1 = network['clamp1'] + clamp1 = network['c1-clamp1'] c3 = network['c3'] current_path = segment[1] - clamp1[1] @@ -288,10 +288,10 @@ def test_non_traverse_step_to_clamp_between_cuts_traverses_segment_both_ways_sto network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c6 = network['c6'] - clamp2 = network['clamp2'] - clamp3 = network['clamp3'] - cut1 = network['cut1'] - cut2 = network['cut2'] + clamp2 = network['c1-clamp2'] + clamp3 = network['c1-clamp3'] + cut1 = network['c1-cut1'] + cut2 = network['c1-cut2'] current_path = c6[1] + clamp2[1] next_paths = self.path_provider.next_paths(current_path) @@ -302,10 +302,10 @@ def test_non_traverse_external_step_to_cut_t2_between_cuts_traverses_segment_tow network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c5 = network['c5'] - clamp2 = network['clamp2'] - clamp3 = network['clamp3'] - cut1 = network['cut1'] - cut2 = network['cut2'] + clamp2 = network['c1-clamp2'] + clamp3 = network['c1-clamp3'] + cut1 = network['c1-cut1'] + cut2 = network['c1-cut2'] current_path = c5[1] + cut1[2] next_paths = self.path_provider.next_paths(current_path) @@ -316,10 +316,10 @@ def test_non_traverse_external_step_to_cut_t1_between_cuts_traverses_segment_tow network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c8 = network['c8'] - clamp2 = network['clamp2'] - clamp3 = network['clamp3'] - cut1 = network['cut1'] - cut2 = network['cut2'] + clamp2 = network['c1-clamp2'] + clamp3 = network['c1-clamp3'] + cut1 = network['c1-cut1'] + cut2 = network['c1-cut2'] current_path = c8[1] + cut2[1] next_paths = self.path_provider.next_paths(current_path) @@ -330,10 +330,10 @@ def test_internal_step_to_cut_t2_between_cuts_steps_externally_and_traverses_seg network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c5 = network['c5'] - clamp2 = network['clamp2'] - clamp3 = network['clamp3'] - cut1 = network['cut1'] - cut2 = network['cut2'] + clamp2 = network['c1-clamp2'] + clamp3 = network['c1-clamp3'] + cut1 = network['c1-cut1'] + cut2 = network['c1-cut2'] current_path = cut1[1] + cut1[2] next_paths = self.path_provider.next_paths(current_path) @@ -344,10 +344,10 @@ def test_internal_step_to_cut_t1_between_cuts_steps_externally_and_traverses_seg network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c8 = network['c8'] - clamp2 = network['clamp2'] - clamp3 = network['clamp3'] - cut1 = network['cut1'] - cut2 = network['cut2'] + clamp2 = network['c1-clamp2'] + clamp3 = network['c1-clamp3'] + cut1 = network['c1-cut1'] + cut2 = network['c1-cut2'] current_path = cut2[2] + cut2[1] next_paths = self.path_provider.next_paths(current_path) @@ -358,7 +358,7 @@ def test_starting_on_clamp_terminal_flagged_as_traversed_segment_only_steps_exte network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c3 = network['c3'] - clamp1 = network['clamp1'] + clamp1 = network['c1-clamp1'] next_paths = self.path_provider.next_paths(clamp1[1] - clamp1[1]) _verify_paths(next_paths, (clamp1[1] + c3[1], )) @@ -367,8 +367,8 @@ def test_starting_on_clamp_terminal_that_flagged_as_not_traversed_segment_steps_ network = CutsAndClampsNetwork.multi_cut_and_clamp_network().network c3 = network['c3'] - clamp1 = network['clamp1'] - cut1 = network['cut1'] + clamp1 = network['c1-clamp1'] + cut1 = network['c1-cut1'] c1 = network['c1'] next_paths = self.path_provider.next_paths(clamp1[1] + clamp1[1]) @@ -384,15 +384,16 @@ def test_traverse_with_cut_with_unknown_length_from_t1_does_not_return_clamp_wit network = (TestNetworkBuilder() .from_breaker() # b0 .to_acls() # c1 + .with_clamp(length_from_terminal_1=1.0) # c1-clamp1 + .with_cut() # c1-cut1 .to_breaker() # b2 ).network c1 = network['c1'] b0 = network['b0'] b2 = network['b2'] - - clamp = _segment_with_clamp(network, c1, 1.0) - cut = _segment_with_cut(network, c1, None) + clamp = network['c1-clamp1'] + cut = network['c1-cut1'] with subtests.test('Traverse from T1 towards T2'): current_path = b0[2] + c1[1] @@ -412,15 +413,16 @@ def test_multiple_cuts_at_same_positions_step_to_all_cuts_at_that_position(self, network = (TestNetworkBuilder() .from_breaker() # b0 .to_acls() # c1 + .with_cut(length_from_terminal_1=1.0) # c1-cut1 + .with_cut(length_from_terminal_1=1.0) # c1-cut2 .to_breaker() # b2 ).network c1 = network['c1'] b0 = network['b0'] b2 = network['b2'] - - cut1 = _segment_with_cut(network, c1, 1.0) - cut2 = _segment_with_cut(network, c1, 1.0) + cut1 = network['c1-cut1'] + cut2 = network['c1-cut2'] with subtests.test('Traverse from T1 towards T2 should have both cuts t1'): current_path = b0[2] + c1[1] @@ -451,15 +453,56 @@ def test_cut_and_clamp_without_length_only_returns_clamp_on_T1_side_of_cut(self, network = (TestNetworkBuilder() .from_breaker() # b0 .to_acls() # c1 + .with_clamp() # c1-clamp1 + .with_cut() # c1-cut1 .to_breaker() # b2 ).network c1 = network['c1'] b0 = network['b0'] b2 = network['b2'] + clamp = network['c1-clamp1'] + cut = network['c1-cut1'] + + with subtests.test('Traverse from T1 towards T2'): + current_path = b0[2] + c1[1] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[1] - cut[1], c1[1] - clamp[1])) + + with subtests.test('Traverse from T2 towards T1'): + current_path = b2[1] + c1[2] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (c1[2] - cut[2], )) - clamp = _segment_with_clamp(network, c1, None) - cut = _segment_with_cut(network, c1, None) + with subtests.test('Internally stepped on cut T1 to T2, traverse towards c1.t2'): + current_path = cut[1] + cut[2] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (cut[2] - c1[2], )) + + with subtests.test('Internally stepped on cut T2 to T2, traverse towards c1.t1'): + current_path =cut[2] + cut[1] + next_paths = self.path_provider.next_paths(current_path) + _verify_paths(next_paths, (cut[1] - c1[1], cut[1] - clamp[1])) + + def test_cut_and_clamp_at_same_length_only_returns_clamp_on_T1_side_of_cut(self, subtests): + # + # 1 b0 21*1 cut1 2*-c1-*-21 b2 2 + # 1 + # clamp1 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .with_clamp(length_from_terminal_1=1.0) # c1-clamp1 + .with_cut(length_from_terminal_1=1.0) # c1-cut1 + .to_breaker() # b2 + ).network + + c1 = network['c1'] + b0 = network['b0'] + b2 = network['b2'] + clamp = network['c1-clamp1'] + cut = network['c1-cut1'] with subtests.test('Traverse from T1 towards T2'): current_path = b0[2] + c1[1] @@ -492,14 +535,16 @@ def test_multiple_clamps_at_same_position_does_not_return_the_other_clamps_more_ network = (TestNetworkBuilder() .from_breaker() # b0 .to_acls() # c1 + .with_clamp() # c1-clamp1 + .with_clamp() # c1-clamp2 + .with_cut() # c1-cut1 .to_breaker() # b2 ).network c1 = network['c1'] - - clamp1 = _segment_with_clamp(network, c1, None) - clamp2 = _segment_with_clamp(network, c1, None) - cut = _segment_with_cut(network, c1, None) + clamp1 = network['c1-clamp1'] + clamp2 = network['c1-clamp2'] + cut = network['c1-cut1'] next_paths = self.path_provider.next_paths(clamp1[1] + clamp1[1]) _verify_paths(next_paths, (clamp1[1] - c1[1], clamp1[1] - clamp2[1], clamp1[1] - cut[1])) @@ -510,18 +555,18 @@ def test_unrealistic_cuts_and_clamps_network_doesnt_break_the_pathing_algorith(s b0 = network['b0'] b2 = network['b2'] c1 = network['c1'] - clamp1 = network['clamp1'] - clamp2 = network['clamp2'] - clamp3 = network['clamp3'] - clamp4 = network['clamp4'] - clamp5 = network['clamp5'] - clamp6 = network['clamp6'] - cut1 = network['cut1'] - cut2 = network['cut2'] - cut3 = network['cut3'] - cut4 = network['cut4'] - cut5 = network['cut5'] - cut6 = network['cut6'] + clamp1 = network['c1-clamp1'] + clamp2 = network['c1-clamp2'] + clamp3 = network['c1-clamp3'] + clamp4 = network['c1-clamp4'] + clamp5 = network['c1-clamp5'] + clamp6 = network['c1-clamp6'] + cut1 = network['c1-cut1'] + cut2 = network['c1-cut2'] + cut3 = network['c1-cut3'] + cut4 = network['c1-cut4'] + cut5 = network['c1-cut5'] + cut6 = network['c1-cut6'] cClamp1 = network['c-clamp1'] cCut1t1 = network['c-cut1t1'] cCut1t2 = network['c-cut1t2'] @@ -649,13 +694,12 @@ def _acls_with_clamps_network(self) -> NetworkService: network = (TestNetworkBuilder() .from_breaker() # b0 .to_acls() # c1 + .with_clamp(length_from_terminal_1=1.0) # c1-clamp1 + .with_clamp(length_from_terminal_1=2.0) # c1-clamp2 .to_breaker() # b2 ).network segment: AcLineSegment = network['c1'] - _segment_with_clamp(network, segment, 1.0) - _segment_with_clamp(network, segment, 2.0) - return network def _acls_with_clamps_and_cuts_at_same_position_network(self) -> NetworkService: @@ -667,69 +711,71 @@ def _acls_with_clamps_and_cuts_at_same_position_network(self) -> NetworkService: # * At the end (length 2.0) (clamp5, clamp6, cut5, cut6) # On each clamp terminal there is a separate AcLineSegment connected to it. (ids of c-clampX) # On each cut terminal (both 1 and 2) there is a separate AcLineSegment connected to it. (ids of c-cutXtN) - + + segment_length = 2.0 + def acls_length(acls: AcLineSegment) -> None: - acls.length = 2.0 + acls.length = segment_length network = (TestNetworkBuilder() .from_breaker() # b0 .to_acls(action=acls_length) # c1 + # At start (combination of 0 and unknown). + .with_clamp(length_from_terminal_1=0.0) # c1-clamp1 + .with_clamp(length_from_terminal_1=None) # c1-clamp2 + .with_cut(length_from_terminal_1=0.0) # c1-cut1 + .with_cut(length_from_terminal_1=None) # c1-cut2 + + # At mid-point. + .with_clamp(length_from_terminal_1=segment_length / 2) # c1-clamp3 + .with_clamp(length_from_terminal_1=segment_length / 2) # c1-clamp4 + .with_cut(length_from_terminal_1=segment_length / 2) # c1-cut3 + .with_cut(length_from_terminal_1=segment_length / 2) # c1-cut4 + + # At end. + .with_clamp(length_from_terminal_1=segment_length) # c1-clamp5 + .with_clamp(length_from_terminal_1=segment_length) # c1-clamp6 + .with_cut(length_from_terminal_1=segment_length) # c1-cut5 + .with_cut(length_from_terminal_1=segment_length) # c1-cut6 .to_breaker() # b2 .from_acls(mrid='c-clamp1') + .connect_to('c1-clamp1', 1, from_terminal=1) .from_acls(mrid='c-clamp2') + .connect_to('c1-clamp2', 1, from_terminal=1) .from_acls(mrid='c-cut1t1') + .connect_to('c1-cut1', 1, from_terminal=1) .from_acls(mrid='c-cut1t2') + .connect_to('c1-cut1', 2, from_terminal=1) .from_acls(mrid='c-cut2t1') + .connect_to('c1-cut2', 1, from_terminal=1) .from_acls(mrid='c-cut2t2') + .connect_to('c1-cut2', 2, from_terminal=1) .from_acls(mrid='c-clamp3') + .connect_to('c1-clamp3', 1, from_terminal=1) .from_acls(mrid='c-clamp4') + .connect_to('c1-clamp4', 1, from_terminal=1) .from_acls(mrid='c-cut3t1') + .connect_to('c1-cut3', 1, from_terminal=1) .from_acls(mrid='c-cut3t2') + .connect_to('c1-cut3', 2, from_terminal=1) .from_acls(mrid='c-cut4t1') + .connect_to('c1-cut4', 1, from_terminal=1) .from_acls(mrid='c-cut4t2') + .connect_to('c1-cut4', 2, from_terminal=1) .from_acls(mrid='c-clamp5') + .connect_to('c1-clamp5', 1, from_terminal=1) .from_acls(mrid='c-clamp6') + .connect_to('c1-clamp6', 1, from_terminal=1) .from_acls(mrid='c-cut5t1') + .connect_to('c1-cut5', 1, from_terminal=1) .from_acls(mrid='c-cut5t2') + .connect_to('c1-cut5', 2, from_terminal=1) .from_acls(mrid='c-cut6t1') + .connect_to('c1-cut6', 1, from_terminal=1) .from_acls(mrid='c-cut6t2') + .connect_to('c1-cut6', 2, from_terminal=1) ).network - segment = network['c1'] - assert segment.length is not None - - clamp1 = _segment_with_clamp(network, segment, 0.0) - clamp2 = _segment_with_clamp(network, segment, None) - cut1 = _segment_with_cut(network, segment, 0.0) - cut2 = _segment_with_cut(network, segment, None) - clamp3 = _segment_with_clamp(network, segment, 1.0) - clamp4 = _segment_with_clamp(network, segment, 1.0) - cut3 = _segment_with_cut(network, segment, 1.0) - cut4 = _segment_with_cut(network, segment, 1.0) - clamp5 = _segment_with_clamp(network, segment, segment.length) - clamp6 = _segment_with_clamp(network, segment, segment.length) - cut5 = _segment_with_cut(network, segment, segment.length) - cut6 = _segment_with_cut(network, segment, segment.length) - - network.connect(clamp1[1], network.get('c-clamp1', ConductingEquipment)[1]) - network.connect(clamp2[1], network.get('c-clamp2', ConductingEquipment)[1]) - network.connect(cut1[1], network.get('c-cut1t1', ConductingEquipment)[1]) - network.connect(cut1[2], network.get('c-cut1t2', ConductingEquipment)[1]) - network.connect(cut2[1], network.get('c-cut2t1', ConductingEquipment)[1]) - network.connect(cut2[2], network.get('c-cut2t2', ConductingEquipment)[1]) - network.connect(clamp3[1], network.get('c-clamp3', ConductingEquipment)[1]) - network.connect(clamp4[1], network.get('c-clamp4', ConductingEquipment)[1]) - network.connect(cut3[1], network.get('c-cut3t1', ConductingEquipment)[1]) - network.connect(cut3[2], network.get('c-cut3t2', ConductingEquipment)[1]) - network.connect(cut4[1], network.get('c-cut4t1', ConductingEquipment)[1]) - network.connect(cut4[2], network.get('c-cut4t2', ConductingEquipment)[1]) - network.connect(clamp5[1], network.get('c-clamp5', ConductingEquipment)[1]) - network.connect(clamp6[1], network.get('c-clamp6', ConductingEquipment)[1]) - network.connect(cut5[1], network.get('c-cut5t1', ConductingEquipment)[1]) - network.connect(cut5[2], network.get('c-cut5t2', ConductingEquipment)[1]) - network.connect(cut6[1], network.get('c-cut6t1', ConductingEquipment)[1]) - network.connect(cut6[2], network.get('c-cut6t2', ConductingEquipment)[1]) - return network From 84453a976a3b99f3010ba776a8e0fdc0b9441531 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 21:06:28 +1000 Subject: [PATCH 27/47] Busbranch tests fixed, this needs verification that its not dumb Signed-off-by: Max Chesterfield --- .../data/negligible_impedance_equipment_basic_network.py | 4 ++-- test/busbranch/test_bus_branch.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/busbranch/data/negligible_impedance_equipment_basic_network.py b/test/busbranch/data/negligible_impedance_equipment_basic_network.py index a1810c6de..27e8d7d1c 100644 --- a/test/busbranch/data/negligible_impedance_equipment_basic_network.py +++ b/test/busbranch/data/negligible_impedance_equipment_basic_network.py @@ -33,7 +33,7 @@ def negligible_impedance_equipment_basic_network(nie_constructor) -> NetworkServ # NegligibleImpedanceEquipment1 nie1 = nie_constructor("nie1") network.add(nie1) - nie1_ts = create_terminals(network, nie1, 1) + nie1_ts = create_terminals(network, nie1, 2) network.connect_terminals(a0_t, nie1_ts[0]) @@ -42,7 +42,7 @@ def negligible_impedance_equipment_basic_network(nie_constructor) -> NetworkServ network.add(a1) a1_ts = create_terminals(network, a1, 2) - network.connect_terminals(nie1_ts[0], a1_ts[0]) + network.connect_terminals(nie1_ts[1], a1_ts[0]) # AcLineSegment2 a2 = AcLineSegment(mrid="a2", length=2.0, per_length_impedance=plsi) diff --git a/test/busbranch/test_bus_branch.py b/test/busbranch/test_bus_branch.py index f5a1a31fa..08b69c4e3 100644 --- a/test/busbranch/test_bus_branch.py +++ b/test/busbranch/test_bus_branch.py @@ -239,9 +239,10 @@ def has_neg_imp(ce) -> bool: await _validate_term_grouping(has_neg_imp, nb_network, "a6_a7", set(), set(), {get_term(a6, 2), *a7.terminals}) -@pytest.mark.skip() # FIXME: @pytest.mark.asyncio -@given(nie_constructor=sampled_from([Junction, Disconnector, BusbarSection])) +@given(nie_constructor=sampled_from([Junction, Disconnector])) # BusbarSection used to be included in this test, but having 1 terminal breaks the logic + # and it got really messy making it uniform, suggest reworking this test using +# # TestNetworkBuilder, and some less verbose logic. async def test_group_negligible_impedance_terminals_groups_negligible_impedance_equipment(nie_constructor): nb_network = negligible_impedance_equipment_basic_network(lambda mrid: nie_constructor(mrid=mrid)) From 2510f218d3b070aeb5b3c78432b2037d69a5cbc5 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 21:44:49 +1000 Subject: [PATCH 28/47] =?UTF-8?q?phase=20inferring=20is=20solved=20?= =?UTF-8?q?=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Max Chesterfield --- .../network/tracing/phases/phase_inferrer.py | 18 +++++++++--------- .../network/tracing/phases/set_phases.py | 12 ++++++------ .../tracing/phases/test_phase_inferrer.py | 5 ----- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py b/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py index e9c4bb61a..f19aef1a1 100644 --- a/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py +++ b/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py @@ -4,7 +4,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import logging from dataclasses import dataclass -from typing import Dict, Callable, List, Set, Awaitable +from typing import Dict, Callable, List, Set, Awaitable, Type from zepben.evolve import Terminal, SinglePhaseKind, ConductingEquipment, NetworkService, \ FeederDirection, X_PRIORITY, Y_PRIORITY, is_before, is_after @@ -36,7 +36,7 @@ def description(self) -> str: else: return f"phase for '{self.conducting_equipment.name}' [{self.conducting_equipment.mrid}] which should be correct. The phase was inferred" - async def run(self, network: NetworkService, network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL) -> list[InferredPhase]: + async def run(self, network: NetworkService, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL) -> list[InferredPhase]: """ Infer the missing phases on the specified `network`. @@ -51,7 +51,7 @@ async def run(self, network: NetworkService, network_state_operators: NetworkSta class PhaseInferrerInternal: - def __init__(self, state_operators: NetworkStateOperators): + def __init__(self, state_operators: Type[NetworkStateOperators]): self.state_operators = state_operators async def infer_missing_phases(self, network: NetworkService, tracking: Dict[ConductingEquipment, bool]): @@ -59,9 +59,9 @@ async def infer_missing_phases(self, network: NetworkService, tracking: Dict[Con terms_missing_phases = [it for it in network.objects(Terminal) if self._is_connected_to_others(it) and self._has_none_phase(it)] terms_missing_xy_phases = [it for it in terms_missing_phases if self._has_xy_phases(it)] - if not (await self._process(terms_missing_phases, lambda t: self._set_missing_to_nominal(t, tracking)) or - await self._process(terms_missing_xy_phases, lambda t: self._infer_xy_phases(t, 1, tracking)) or - await self._process(terms_missing_xy_phases, lambda t: self._infer_xy_phases(t, 4, tracking)) + if not (await self._process(terms_missing_phases, lambda t: self._set_missing_to_nominal(t, tracking)) + or await self._process(terms_missing_xy_phases, lambda t: self._infer_xy_phases(t, 1, tracking)) + or await self._process(terms_missing_xy_phases, lambda t: self._infer_xy_phases(t, 4, tracking)) ): break @@ -90,8 +90,8 @@ def _find_terminal_at_start_of_missing_phases( def _missing_from_down_to_up(self, terminals: List[Terminal]) -> List[Terminal]: return [ terminal for terminal in terminals - if (self._missing_from_down_filter(terminal) and - (FeederDirection.UPSTREAM in self.state_operators.get_direction(terminal))) + if (FeederDirection.UPSTREAM in self.state_operators.get_direction(terminal)) + and self._missing_from_down_filter(terminal) ] def _missing_from_down_to_any(self, terminals: List[Terminal]) -> List[Terminal]: @@ -188,7 +188,7 @@ async def _infer_xy_phases(self, terminal: Terminal, max_missing_phases: int, tr async def _continue_phases(self, terminal: Terminal): set_phases_trace = Tracing.set_phases() for other in terminal.other_terminals(): - await set_phases_trace.spread_phases(terminal, other, terminal.phases.single_phases, network_state_operators=self.state_operators) + await set_phases_trace.run_spread_phases_and_flow(terminal, other, terminal.phases.single_phases, network_state_operators=self.state_operators) @staticmethod def _first_unused(phases: List[SinglePhaseKind], used_phases: Set[SinglePhaseKind], validate: Callable[[SinglePhaseKind], bool]) -> SinglePhaseKind: diff --git a/src/zepben/evolve/services/network/tracing/phases/set_phases.py b/src/zepben/evolve/services/network/tracing/phases/set_phases.py index c872c6c77..4876b68a8 100644 --- a/src/zepben/evolve/services/network/tracing/phases/set_phases.py +++ b/src/zepben/evolve/services/network/tracing/phases/set_phases.py @@ -99,14 +99,14 @@ def validate_phases(_phases): await self._run_terminal(terminal, network_state_operators) - async def _run_spread_phases_and_flow(self, - seed_terminal: Terminal, - start_terminal: Terminal, - phases: List[SinglePhaseKind], - network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): + async def run_spread_phases_and_flow(self, + seed_terminal: Terminal, + start_terminal: Terminal, + phases: List[SinglePhaseKind], + network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): nominal_phase_paths = self._get_nominal_phase_paths(network_state_operators, seed_terminal, start_terminal, list(phases)) - if self._flow_phases(network_state_operators, seed_terminal, start_terminal, nominal_phase_paths): + if await self._flow_phases(network_state_operators, seed_terminal, start_terminal, nominal_phase_paths): await self.run(start_terminal, network_state_operators=network_state_operators) diff --git a/test/services/network/tracing/phases/test_phase_inferrer.py b/test/services/network/tracing/phases/test_phase_inferrer.py index 195b24ca5..772e31ec2 100644 --- a/test/services/network/tracing/phases/test_phase_inferrer.py +++ b/test/services/network/tracing/phases/test_phase_inferrer.py @@ -26,7 +26,6 @@ class TestPhaseInferrer: Test the `PhaseInferrer` """ - @pytest.mark.skip() ## FIXME: @pytest.mark.asyncio async def test_ab_to_bc_to_xy_to_abc(self, caplog): """ @@ -58,7 +57,6 @@ async def test_ab_to_bc_to_xy_to_abc(self, caplog): self._validate_returned_phases(network, changes, ['c1', 'c3']) self._validate_log(caplog, correct=["c1", "c3", 'c1', 'c3']) - @pytest.mark.skip() # FIXME: @pytest.mark.asyncio async def test_abn_to_bcn_to_xyn_to_abcn(self, caplog): """ @@ -90,7 +88,6 @@ async def test_abn_to_bcn_to_xyn_to_abcn(self, caplog): self._validate_returned_phases(network, changes, ['c1', 'c3']) self._validate_log(caplog, correct=["c1", "c3", 'c1', 'c3']) - @pytest.mark.skip() # FIXME: @pytest.mark.asyncio async def test_bc_to_ac_to_xy_to_abc(self, caplog): """ @@ -342,7 +339,6 @@ async def test_dual_feed_an_to_abcn(self, caplog): self._validate_returned_phases(network, changes, ['c1']) self._validate_log(caplog, correct=["c1", 'c1']) - @pytest.mark.skip() # FIXME: @pytest.mark.asyncio async def test_abcn_to_n_to_ab_to_xy(self, caplog): """ @@ -408,7 +404,6 @@ async def test_with_open_switch(self, caplog): self._validate_returned_phases(network, changes, []) self._validate_log(caplog) - @pytest.mark.skip() # #FIXME: @pytest.mark.asyncio async def test_validate_directions_with_dropped_direction_loop(self, caplog): """ From 3c17140b8fcced4601c8cd7d7545f34ea8647ff6 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 21:51:48 +1000 Subject: [PATCH 29/47] Added action blocks for `TestNetworkBuilder.with_clamp` and `TestNetworkBuilder.with_cut` Signed-off-by: Max Chesterfield --- src/zepben/evolve/testing/test_network_builder.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/zepben/evolve/testing/test_network_builder.py b/src/zepben/evolve/testing/test_network_builder.py index 9376e720d..710c1bc10 100644 --- a/src/zepben/evolve/testing/test_network_builder.py +++ b/src/zepben/evolve/testing/test_network_builder.py @@ -446,13 +446,16 @@ def to_other( def with_clamp( self, mrid: Optional[str] = None, - length_from_terminal_1: float = None + length_from_terminal_1: float = None, + action: Callable[[Clamp], None] = null_action ) -> 'TestNetworkBuilder': """ Create a clamp on the current network pointer (must be an `AcLineSegment`) without moving the current network pointer. :param mrid: Optional mRID for the new `Clamp` :param length_from_terminal_1: The length from terminal 1 of the `AcLineSegment` being clamped + :param action: An action that accepts the new `Clamp` to allow for additional initialisation. + :return: This `TestNetworkBuilder` to allow for fluent use """ acls = self._current @@ -463,6 +466,7 @@ def with_clamp( clamp.add_terminal(Terminal(mrid=f'{clamp.mrid}-t1')) acls.add_clamp(clamp) + action(clamp) self.network.add(clamp) return self @@ -471,7 +475,8 @@ def with_cut( mrid: Optional[str] = None, length_from_terminal_1: Optional[float] = None, is_normally_open: bool = True, - is_open: bool = None + is_open: bool = None, + action: Callable[[Cut], None] = null_action ) -> 'TestNetworkBuilder': """ Create a cut on the current network pointer (must be an `AcLineSegment`) without moving the current network pointer. @@ -480,6 +485,8 @@ def with_cut( :param length_from_terminal_1: The length from terminal 1 of the `AcLineSegment` being cut :param is_normally_open: The normal state of the cut, defaults to True :param is_open: The current state of the cut. Defaults to `is_normally_open` + :param action: An action that accepts the new `Cut` to allow for additional initialisation. + :return: This `TestNetworkBuilder` to allow for fluent use """ acls = self._current @@ -497,6 +504,7 @@ def with_cut( cut.set_open(is_open) acls.add_cut(cut) + action(cut) self.network.add(cut) return self From bfd7adde27c11102ab5b2ad2d6106784c130e5fe Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 22:04:54 +1000 Subject: [PATCH 30/47] [DEV-2592] - added tests from kotlin sdk python SDK didnt have the same bug, tests added to make sure it never does Signed-off-by: Max Chesterfield --- .../test_network_trace_step_path_provider.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index baea1bff0..cc4c7307d 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -645,6 +645,43 @@ def test_unrealistic_cuts_and_clamps_network_doesnt_break_the_pathing_algorith(s next_paths = self.path_provider.next_paths(b2[1] + c1[2]) _verify_paths(next_paths, (c1[2] - cut5[2], c1[2] - cut6[2])) + def test_traverses_from_single_clamp_on_a_segment(self): + n = (TestNetworkBuilder() + .from_acls() # c0 + .with_clamp() # c0-clamp1 + .branch_from('c0-clamp1', 1) + .to_source() # s1 + ).network + + source = n['s1'] + clamp1 = n['c0-clamp1'] + c0 = n['c0'] + + _verify_paths(self.path_provider.next_paths(source[1] + clamp1[1]), + ((clamp1[1] - c0[1]), (clamp1[1] - c0[2]))) + + def test_traverses_from_both_sides_of_a_single_cut(self, subtests): + n = (TestNetworkBuilder() + .from_acls() # c0 + .with_cut() # c0-cut1 + .branch_from('c0-cut1', 1) + .to_source() # s1 + .branch_from('c0-cut1', 2) + .to_source() # s2 + ).network + + source1 = n['s1'] + source2 = n['s2'] + cut1 = n['c0-cut1'] + c0 = n['c0'] + + with subtests.test("goes from t1 side of cut and finds t1 side of segment"): + _verify_paths(self.path_provider.next_paths(source1[1] + cut1[1]), + ((cut1[1] - c0[1]), (cut1[1] + cut1[2]))) + + with subtests.test("goes from t2 side of cut and finds t2 side of segment"): + _verify_paths(self.path_provider.next_paths(source2[1] + cut1[2]), + ((cut1[2] - c0[2]), cut1[2] + cut1[1])) def _busbar_network(self) -> NetworkService: # 1 From abff444f2d9ad83ba7361bb24a992dbdb8d62226 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 22:35:34 +1000 Subject: [PATCH 31/47] [DEV-2766] `NetworkTrace`/`Traversal` now correctly respects `can_stop_on_start` when providing multiple start items. Signed-off-by: Max Chesterfield --- changelog.md | 6 +- src/zepben/evolve/__init__.py | 1 - .../network/tracing/traversal/traversal.py | 7 +- src/zepben/evolve/testing/test_traversal.py | 66 ------------------- .../networktrace/test_network_trace.py | 29 +++++++- .../tracing/traversal/test_traversal.py | 13 ++++ 6 files changed, 46 insertions(+), 76 deletions(-) delete mode 100644 src/zepben/evolve/testing/test_traversal.py diff --git a/changelog.md b/changelog.md index f82299bf1..787d56cba 100644 --- a/changelog.md +++ b/changelog.md @@ -23,12 +23,14 @@ ### Fixes * When finding `LvFeeders` in the `Site` we will now exclude `LvFeeders` that start with an open `Switch` * The follow fixes were added to Traversal and NetworkTrace: - * `canStopAtStartItem` now works for branching traversals. + * `can_stop_on_start_item` now works for branching traversals. * Traversal start items are added to the queue before traversal starts, so that the start items honour the queue type order. - * Stop conditions on the `NetworkTrace` now are checked based on a step type, like `QueueCondition` does, rather than by checking `canActionItem`. + * Stop conditions on the `NetworkTrace` now are checked based on a step type, like `QueueCondition` does, rather than by checking `can_action_item`. * `Cut` and `Clamp` are now correctly supported in `SetDirection` and `DirectionCondition`. * `NetworkTrace` now handles starting on `Cut` , `Clamp`, and `AcLineSegment` and their terminals in a explicit / sensible way. * `NetworkTracePathProvider` now correctly handles next paths when starting on a `Clamp` terminal. +* `NetworkTrace`/`Traversal` now correctly respects `can_stop_on_start_item` when providing multiple start items. + ### Notes * None. diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index 6282dc24c..0271d4c3e 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -436,6 +436,5 @@ from zepben.evolve.services.network.tracing.phases.set_phases import * from zepben.evolve.testing.test_network_builder import * -from zepben.evolve.testing.test_traversal import * # @formatter:on diff --git a/src/zepben/evolve/services/network/tracing/traversal/traversal.py b/src/zepben/evolve/services/network/tracing/traversal/traversal.py index e680b83f8..f65e8defe 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/traversal.py +++ b/src/zepben/evolve/services/network/tracing/traversal/traversal.py @@ -280,7 +280,7 @@ def add_step_action(self, action: Union[StepActionFunc, StepAction[T]]) -> D: raise RuntimeError(f'Condition [{action.__class__.__name__}] does not match expected: [StepAction | StepActionWithContextValue | Callable]') - def if_not_stopping(self, action: Callable) -> D: + def if_not_stopping(self, action: Callable[[T, StepContext], None]) -> D: """ Adds an action to be performed on each item that does not match any stop condition. @@ -291,7 +291,7 @@ def if_not_stopping(self, action: Callable) -> D: return self - def if_stopping(self, action: Callable) -> D: + def if_stopping(self, action: Callable[[T, StepContext], None]) -> D: """ Adds an action to be performed on each item that matches a stop condition. @@ -449,10 +449,10 @@ async def _traverse(self, can_stop_on_start_item: bool): else: self.queue.append(start_item) - can_stop = can_stop_on_start_item while self.queue.has_next(): current = self.queue.pop() context = self._get_step_context(current) + can_stop = can_stop_on_start_item or (not context.is_start_item) if self.can_visit_item(current, context): context.is_stopping = can_stop and self.matches_any_stop_condition(current, context) @@ -464,7 +464,6 @@ async def _traverse(self, can_stop_on_start_item: bool): if not context.is_stopping: self.queue_next(current, context) - can_stop = True def _get_step_context(self, item: T) -> StepContext: try: diff --git a/src/zepben/evolve/testing/test_traversal.py b/src/zepben/evolve/testing/test_traversal.py deleted file mode 100644 index 39ed56aa6..000000000 --- a/src/zepben/evolve/testing/test_traversal.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd -# 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 Callable, TypeVar, Tuple, Awaitable -from unittest.mock import Mock - -T = TypeVar('T') - -__all__ = ["verify_stop_conditions", "step_on_when_run", "step_on_when_run_with_is_stopping"] - - -async def verify_stop_conditions(traversal: Mock, *stop_condition_validation: Callable[[Callable[[T], Awaitable[None]]], Awaitable[None]]): - """ - Verify that stop conditions are registered, and they behave correctly. - - :param traversal: The mocked `BasicTraversal` to verify. - :param stop_condition_validation: A collection of verification blocks that are executed on each stop condition. The number of entries in this collection - must match the number of expected stop conditions, and match the registration order of the stop conditions. - """ - # To get access to private stop conditions (and to check they are actually registered) we need to capture the conditions that are registered. - assert traversal.add_stop_condition.call_count == len(stop_condition_validation) - - for index, stop_condition_call in enumerate(traversal.add_stop_condition.call_args_list): - await stop_condition_validation[index](stop_condition_call.args[0]) - - -def step_on_when_run(traversal: Mock, *step_on: T): - """ - Call the step action with the specified arguments when the trace is run. Only supports single step actions. - - :param traversal: The mocked `BasicTraversal` that will be run. - :param step_on: A collection of items to step on when `traversal` is run. - """ - - # The step actions must be called while the trace is running, so this needs to be done in the mock of the run command. - async def mock_run(_): - traversal.add_step_action.assert_called_once() - - # To get access to private step actions (and to check they are actually registered) we need to capture the actions that are registered. - step_action = traversal.add_step_action.call_args.args[0] - for item in step_on: - await step_action(item, False) - - traversal.run.side_effect = mock_run - - -def step_on_when_run_with_is_stopping(traversal: Mock, *step_on: Tuple[T, bool]): - """ - Call the step action with the specified arguments when the trace is run. Only supports single step actions. - - :param traversal: The mocked `BasicTraversal` that will be run. - :param step_on: A collection of items and is_stopping flags to step on when `traversal` is run. - """ - - # The step actions must be called while the trace is running, so this needs to be done in the mock of the run command. - async def mock_run(_): - traversal.add_step_action.assert_called_once() - - # To get access to private step actions (and to check they are actually registered) we need to capture the actions that are registered. - step_action = traversal.add_step_action.call_args.args[0] - for (item, is_stopping) in step_on: - await step_action(item, is_stopping) - - traversal.run.side_effect = mock_run diff --git a/test/services/network/tracing/networktrace/test_network_trace.py b/test/services/network/tracing/networktrace/test_network_trace.py index c035b6bdf..3967d64ae 100644 --- a/test/services/network/tracing/networktrace/test_network_trace.py +++ b/test/services/network/tracing/networktrace/test_network_trace.py @@ -11,7 +11,7 @@ import pytest from services.network.tracing.networktrace.test_network_trace_step_path_provider import PathTerminal, _verify_paths -from zepben.evolve import AcLineSegment, Clamp, Terminal, NetworkTraceStep, Cut, ConductingEquipment, TraversalQueue, Junction, ngen +from zepben.evolve import AcLineSegment, Clamp, Terminal, NetworkTraceStep, Cut, ConductingEquipment, TraversalQueue, Junction, ngen, NetworkTraceActionType from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.testing.test_network_builder import TestNetworkBuilder @@ -178,8 +178,7 @@ async def test_can_Stop_on_start_item_when_running_from_conducting_equipment_bra assert set(map(lambda it: (it.num_equipment_steps, it.path.to_equipment.mrid), steps)) \ == {(0, 'b0')} - if 'TOX_ENV_NAME' not in os.environ: - + if 'TOX_ENV_NAME' not in os.environ: # Skips the test during tox runs as variable hardware will affect speed @pytest.mark.asyncio async def test_can_run_large_branching_traces(self): try: @@ -202,3 +201,27 @@ async def test_can_run_large_branching_traces(self): except Exception as e: sys.setrecursionlimit(1000) # back to default raise e + + @pytest.mark.asyncio + async def test_multiple_start_items_can_stop_on_start_doesnt_prevent_stop_checks_when_visiting_via_loop(self): + ns = (TestNetworkBuilder() + .from_acls() # c0 + .to_acls() # c1 + .to_acls() # c2 + .connect_to('c0') + ).network + + stop_checks: List[str] = [] + steps: List[str] = [] + + def stop_condition(item, _): + stop_checks.append(item.path.to_terminal.mrid) + return item.path.to_equipment.mrid == 'c1' + + trace = Tracing.network_trace(action_step_type=NetworkTraceActionType.ALL_STEPS) \ + .add_stop_condition(stop_condition) \ + .if_not_stopping(lambda item, _: steps.append(item.path.to_terminal.mrid)) + await trace.run(ns.get('c1', ConductingEquipment), can_stop_on_start_item=False) + + assert stop_checks == ['c2-t1', 'c2-t2', 'c0-t1', 'c0-t2', 'c1-t1'] + assert steps == ['c1-t2', 'c2-t1', 'c2-t2', 'c0-t1', 'c0-t2'] diff --git a/test/services/network/tracing/traversal/test_traversal.py b/test/services/network/tracing/traversal/test_traversal.py index 83a381324..d2adcf1fa 100644 --- a/test/services/network/tracing/traversal/test_traversal.py +++ b/test/services/network/tracing/traversal/test_traversal.py @@ -363,3 +363,16 @@ async def test_start_items_are_queued_before_traversal_starts_so_queue_type_is_h ).run() assert steps == [-1, 1, -2, 2] + + @pytest.mark.asyncio + async def test_multiple_start_items_respect_can_stop_on_start(self): + steps = [] + traversal = (_create_traversal(queue=TraversalQueue.breadth_first()) + .add_stop_condition(lambda item, x: True) + .add_step_action(lambda item, x: steps.append(item)) + .add_start_item(1) + .add_start_item(11) + ) + await traversal.run(can_stop_on_start_item=False) + + assert steps == [1, 11, 2, 12] From 242c10bb20743558fdaecd77ec80ab4bd1822fe1 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 22:58:36 +1000 Subject: [PATCH 32/47] rebase off main incase new build system takes a while Signed-off-by: Max Chesterfield --- .gitignore | 2 ++ setup.py | 1 + test/cim/iec61970/base/core/test_site.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b2de12ef6..5ff2d8ada 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,5 @@ dmypy.json /todo.md test/resources/test-network-database.txt + +*.iml diff --git a/setup.py b/setup.py index a90aad7dd..782b02fa3 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ "pytest-cov==2.10.1", "pytest-asyncio==0.19.0", "pytest-timeout==1.4.2", + 'pytest-subtests==0.14.1', "hypothesis==6.56.3", "grpcio-testing==1.61.3", "pylint==2.14.5", diff --git a/test/cim/iec61970/base/core/test_site.py b/test/cim/iec61970/base/core/test_site.py index daf68563e..f83a5be8c 100644 --- a/test/cim/iec61970/base/core/test_site.py +++ b/test/cim/iec61970/base/core/test_site.py @@ -5,7 +5,7 @@ import pytest from hypothesis import given -from build.lib.zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators +from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from cim.iec61970.base.core.test_equipment_container import equipment_container_kwargs, verify_equipment_container_constructor_default, \ verify_equipment_container_constructor_kwargs, verify_equipment_container_constructor_args, equipment_container_args from zepben.evolve import Site, TestNetworkBuilder, Equipment, AssignToLvFeeders, LvFeeder From 5ebd93264b52857b96caf92be3fcd892c3ef1220 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 27 May 2025 23:11:39 +1000 Subject: [PATCH 33/47] remove version lock for pytest-subtests, it can sort itself out Signed-off-by: Max Chesterfield --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 782b02fa3..43d141980 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "pytest-cov==2.10.1", "pytest-asyncio==0.19.0", "pytest-timeout==1.4.2", - 'pytest-subtests==0.14.1', + 'pytest-subtests', "hypothesis==6.56.3", "grpcio-testing==1.61.3", "pylint==2.14.5", From 5c543ae6c68b65d9f448acc65f6144f1ab03a950 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 28 May 2025 03:56:36 +1000 Subject: [PATCH 34/47] DEV-2382 Fixed some stuff that was broken in assign to [Lv]Feeders as ported over missing tests Signed-off-by: Max Chesterfield --- .../iec61970/base/core/equipment_container.py | 2 +- .../tracing/feeder/assign_to_feeders.py | 31 ++- .../tracing/feeder/assign_to_lv_feeders.py | 39 ++- .../evolve/testing/test_network_builder.py | 23 +- .../network/tracing/test_assign_to_feeders.py | 186 +++++++++++++- .../tracing/test_assign_to_lv_feeders.py | 231 +++++++++++++++++- 6 files changed, 472 insertions(+), 40 deletions(-) diff --git a/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py index 692463e23..12e5d366b 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable +from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable, Type if TYPE_CHECKING: from zepben.evolve import Equipment, Terminal, Substation, LvFeeder, NetworkStateOperators diff --git a/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py b/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py index cbbc1f12b..c8fae1a85 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py +++ b/src/zepben/evolve/services/network/tracing/feeder/assign_to_feeders.py @@ -3,9 +3,9 @@ # 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 collections.abc import Collection -from typing import Iterable, Generator, Union, List, Dict, Any, Set, Type +from typing import Iterable, Union, List, Dict, Any, Set, Type, Generator -from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch, Equipment, LvFeeder +from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch, Equipment, LvFeeder, PowerElectronicsConnection from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.evolve.model.cim.iec61970.base.core.equipment_container import Feeder, EquipmentContainer from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal @@ -18,6 +18,7 @@ from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing +from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import stop_at_open from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.traversal.step_context import StepContext @@ -50,7 +51,7 @@ class BaseFeedersInternal: def __init__(self, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL): self.network_state_operators = network_state_operators - def _feeders_from_terminal(self, terminal: Terminal): + def _feeders_from_terminal(self, terminal: Terminal) -> Generator[Feeder, None, None]: return terminal.conducting_equipment.feeders(self.network_state_operators) def _associate_equipment_with_containers(self, equipment_containers: Iterable[EquipmentContainer], equipment: Iterable[Equipment]): @@ -65,7 +66,10 @@ def _associate_relay_systems_with_containers(self, equipment_containers: Iterabl for relayFunction in to_equipment.relay_functions for scheme in relayFunction.schemes if scheme.system is not None] - ) + ) + + def _associate_power_electronic_units(self, equipment_containers: Iterable[EquipmentContainer], to_equipment: PowerElectronicsConnection): + self._associate_equipment_with_containers(equipment_containers, to_equipment.units) def _feeder_energizes(self, feeders: Iterable[Union[LvFeeder, Feeder]], lv_feeders: Iterable[LvFeeder]): for feeder in feeders: @@ -73,14 +77,13 @@ def _feeder_energizes(self, feeders: Iterable[Union[LvFeeder, Feeder]], lv_feede self.network_state_operators.associate_energizing_feeder(feeder, lv_feeder) def _feeder_try_energize_lv_feeders(self, feeders: Iterable[Feeder], lv_feeder_start_points: Set[ConductingEquipment], to_equipment: PowerTransformer): - sites = [] - for eq in to_equipment: - sites.extend(eq.sites) + sites = list(to_equipment.sites) + lv_feeders = [] if len(sites) > 0: - lv_feeders = [s.find_lv_feeders(lv_feeder_start_points, self.network_state_operators) for s in sites] + for s in sites: + lv_feeders.extend(lv_f for lv_f in s.find_lv_feeders(lv_feeder_start_points, self.network_state_operators)) else: - lv_feeders = [] for eq in to_equipment: lv_feeders.extend(eq.lv_feeders(self.network_state_operators)) @@ -110,7 +113,7 @@ async def run(self, feeder_start_points, lv_feeder_start_points, terminal_to_aux_equipment, - self._feeders_from_terminal(start_terminal)) + list(self._feeders_from_terminal(start_terminal))) async def run_with_feeders(self, terminal: Terminal, @@ -148,7 +151,7 @@ async def step_action(nts: NetworkTraceStep, context: StepContext): return ( Tracing.network_trace(self.network_state_operators, NetworkTraceActionType.ALL_STEPS) - .add_condition(self.network_state_operators.stop_at_open()) + .add_condition(stop_at_open()) .add_stop_condition(lambda step, ctx: step.path.to_equipment in feeder_start_points) .add_queue_condition(lambda step, ctx, _, __: not _reached_substation_transformer(step.path.to_equipment)) .add_queue_condition(lambda step, ctx, _, __: not _reached_lv(step.path.to_equipment)) @@ -165,13 +168,15 @@ async def _process(self, if step_path.traced_internally and not step_context.is_start_item: return - for equip_group in (terminal_to_aux_equipment.get(step_path.to_terminal, {}), [step_path.to_equipment]): - self._associate_equipment_with_containers(feeders_to_assign, equip_group) + self._associate_equipment_with_containers(feeders_to_assign, terminal_to_aux_equipment.get(step_path.to_terminal, {})) + self._associate_equipment_with_containers(feeders_to_assign, [step_path.to_equipment]) if isinstance(step_path.to_equipment, PowerTransformer): self._feeder_try_energize_lv_feeders(feeders_to_assign, lv_feeder_start_points, step_path.to_equipment) elif isinstance(step_path.to_equipment, ProtectedSwitch): self._associate_relay_systems_with_containers(feeders_to_assign, step_path.to_equipment) + elif isinstance(step_path.to_equipment, PowerElectronicsConnection): + self._associate_power_electronic_units(feeders_to_assign, step_path.to_equipment) diff --git a/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py b/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py index ee0c300a8..21622bbad 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py +++ b/src/zepben/evolve/services/network/tracing/feeder/assign_to_lv_feeders.py @@ -2,10 +2,10 @@ # 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 functools import singledispatchmethod from typing import Collection, List, Generator, TypeVar, Dict, Set, Type -from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch +from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch, PowerElectronicsConnection from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal from zepben.evolve.model.cim.iec61970.infiec61970.feeder.lv_feeder import LvFeeder @@ -15,6 +15,7 @@ from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType from zepben.evolve.services.network.tracing.networktrace.network_trace_step import NetworkTraceStep from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators +from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import stop_at_open from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.services.network.tracing.traversal.step_context import StepContext @@ -24,12 +25,22 @@ class AssignToLvFeeders: + @singledispatchmethod @staticmethod async def run(network: NetworkService, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL, start_terminal: Terminal=None): await AssignToLvFeedersInternal(network_state_operators).run(network, start_terminal) + @run.register + @staticmethod + async def _(terminal: Terminal, + lv_feeder_start_points: Set[ConductingEquipment], + terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]], + lv_feeders_to_assign: List[LvFeeder], + network_state_operators: Type[NetworkStateOperators] = NetworkStateOperators.NORMAL + ): + await AssignToLvFeedersInternal(network_state_operators).run_with_feeders(terminal, lv_feeder_start_points, terminal_to_aux_equipment, lv_feeders_to_assign) class AssignToLvFeedersInternal(BaseFeedersInternal): """ @@ -112,7 +123,7 @@ async def step_action(nts: NetworkTraceStep, context): action_step_type=NetworkTraceActionType.ALL_STEPS, compute_data=(lambda _, __, next_path: next_path.to_equipment in lv_feeder_start_points) ) - .add_condition(self.network_state_operators.stop_at_open()) + .add_condition(stop_at_open()) .add_stop_condition(lambda step, ctx: step.data) .add_queue_condition(queue_condition) .add_step_action(step_action) @@ -129,23 +140,27 @@ async def _process(self, if step_path.traced_internally and not step_context.is_start_item: return + # It might be tempting to check `stepContext.isStopping`, but that would also pick up open points between LV feeders which is not good. if found_lv_feeder: found_lv_feeders = list(self._find_lv_feeders(step_path.to_equipment, lv_feeder_start_points)) - for energizing_feeder in (self.network_state_operators.get_energizing_feeders(it) for it in found_lv_feeders): - for feeder_group in (lv_feeders_to_assign, found_lv_feeders): - self._feeder_energizes(feeder_group, energizing_feeder) + for it in found_lv_feeders: + # Energize the LV feeders that we are processing by the energizing feeders of what we found + self._feeder_energizes(self.network_state_operators.get_energizing_feeders(it), lv_feeders_to_assign) + + for it in lv_feeders_to_assign: + # Energize the LV feeders we found by the energizing feeders we are processing + self._feeder_energizes(self.network_state_operators.get_energizing_feeders(it), found_lv_feeders) - try: - aux_equip_for_this_terminal = terminal_to_aux_equipment[step_path.to_terminal] - except KeyError: - aux_equip_for_this_terminal = [] + aux_equip_for_this_terminal = terminal_to_aux_equipment.get(step_path.to_terminal, {}) - for equip_group in (aux_equip_for_this_terminal, [step_path.to_equipment]): - self._associate_equipment_with_containers(lv_feeders_to_assign, equip_group) + self._associate_equipment_with_containers(lv_feeders_to_assign, [step_path.to_equipment]) + self._associate_equipment_with_containers(lv_feeders_to_assign, aux_equip_for_this_terminal) if isinstance(step_path.to_equipment, ProtectedSwitch): self._associate_relay_systems_with_containers(lv_feeders_to_assign, step_path.to_equipment) + elif isinstance(step_path.to_equipment, PowerElectronicsConnection): + self._associate_power_electronic_units(lv_feeders_to_assign, step_path.to_equipment) def _find_lv_feeders(self, ce: ConductingEquipment, lv_feeder_start_points: Set[ConductingEquipment]) -> Generator[LvFeeder, None, None]: sites = list(ce.sites) diff --git a/src/zepben/evolve/testing/test_network_builder.py b/src/zepben/evolve/testing/test_network_builder.py index 710c1bc10..db501a0dd 100644 --- a/src/zepben/evolve/testing/test_network_builder.py +++ b/src/zepben/evolve/testing/test_network_builder.py @@ -2,6 +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 zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing try: @@ -12,8 +13,7 @@ from typing import Optional, Callable, List, Union, Type from zepben.evolve import ConductingEquipment, NetworkService, PhaseCode, EnergySource, AcLineSegment, Breaker, Junction, Terminal, Feeder, LvFeeder, \ - PowerTransformerEnd, PowerTransformer, EnergyConsumer, \ - PowerElectronicsConnection, BusbarSection, Clamp, Cut + PowerTransformerEnd, PowerTransformer, EnergyConsumer, PowerElectronicsConnection, BusbarSection, Clamp, Cut, Site def null_action(_): @@ -603,6 +603,25 @@ def add_lv_feeder(self, head_mrid: str, sequence_number: Optional[int] = None, m self._create_lv_feeder(mrid, self.network.get(head_mrid, ConductingEquipment), sequence_number) return self + def add_site(self, equipment_mrids: List[str], mrid: Optional[str] = None) -> 'TestNetworkBuilder': + """ + Create a new Site containing the specified equipment. + + :param equipment_mrids: The mRID's of the equipment to add to the site + :param mrid: Optional mRID for the new `Site`. + :return: This [TestNetworkBuilder] to allow for fluent use. + """ + + site = Site(mrid=self._next_id(mrid, 'site')) + + for _id in equipment_mrids: + ce = self.network[_id] + site.add_equipment(ce) + ce.add_container(site) + self.network.add(site) + + return self + async def build(self, apply_directions_from_sources: bool = True, assign_feeders: bool = True) -> NetworkService: """ Get the `NetworkService` after apply traced phasing and feeder directions. diff --git a/test/services/network/tracing/test_assign_to_feeders.py b/test/services/network/tracing/test_assign_to_feeders.py index 03b2442ae..32a4a6c0f 100644 --- a/test/services/network/tracing/test_assign_to_feeders.py +++ b/test/services/network/tracing/test_assign_to_feeders.py @@ -5,7 +5,9 @@ from typing import Iterable import pytest -from zepben.evolve import Equipment, TestNetworkBuilder, Feeder, BaseVoltage, Tracing, NetworkStateOperators +from zepben.evolve import Equipment, TestNetworkBuilder, Feeder, BaseVoltage, Tracing, NetworkStateOperators, CurrentTransformer, FaultIndicator, \ + ProtectedSwitch, CurrentRelay, ProtectionRelayScheme, ProtectionRelaySystem, PhotoVoltaicUnit, PowerElectronicsConnection, Junction, ConductingEquipment, \ + PowerTransformerEnd def validate_equipment(equipment: Iterable[Equipment], *expected_mrids: str): @@ -20,6 +22,16 @@ class TestAssignToFeeders: bv_hv = BaseVoltage(nominal_voltage=11000) bv_lv = BaseVoltage(nominal_voltage=400) + @staticmethod + def base_voltage(ce: ConductingEquipment, voltage: BaseVoltage): + ce.base_voltage = voltage + + def _make_hv(self, ce: ConductingEquipment): + return self.base_voltage(ce, self.bv_hv) + + def _make_lv(self, ce: ConductingEquipment): + return self.base_voltage(ce, self.bv_lv) + @pytest.mark.asyncio @pytest.mark.parametrize('feeder_start_point_between_conductors_network', [(False,)], indirect=True) async def test_applies_to_equipment_on_head_terminal_side(self, feeder_start_point_between_conductors_network): @@ -56,9 +68,9 @@ async def test_assigns_equipment_to_feeders_with_loops(self, caplog, loop_under_ async def test_stops_at_lv_equipment(self): # noinspection PyArgumentList network_service = (TestNetworkBuilder() - .from_breaker(action=lambda ce: setattr(ce, "base_voltage", self.bv_hv)) - .to_acls(action=lambda ce: setattr(ce, "base_voltage", self.bv_hv)) - .to_acls(action=lambda ce: setattr(ce, "base_voltage", self.bv_lv)) + .from_breaker(action=self._make_hv) + .to_acls(action=self._make_hv) + .to_acls(action=self._make_lv) .add_feeder("b0") .network) @@ -74,10 +86,10 @@ async def test_stops_at_lv_equipment(self): async def test_includes_transformers(self): # noinspection PyArgumentList network_service = (TestNetworkBuilder() - .from_breaker(action=lambda ce: setattr(ce, "base_voltage", self.bv_hv)) - .to_acls(action=lambda ce: setattr(ce, "base_voltage", self.bv_hv)) - .to_power_transformer(end_actions=[lambda ce: setattr(ce, "base_voltage", self.bv_hv), lambda ce: setattr(ce, "base_voltage", self.bv_lv)]) - .to_acls(action=lambda ce: setattr(ce, "base_voltage", self.bv_lv)) + .from_breaker(action=self._make_hv) + .to_acls(action=self._make_hv) + .to_power_transformer(end_actions=[self._make_hv, self._make_lv]) + .to_acls(action=self._make_lv) .add_feeder("b0") .network) @@ -88,3 +100,161 @@ async def test_includes_transformers(self): await Tracing.assign_equipment_to_feeders().run(network_service, NetworkStateOperators.NORMAL) validate_equipment(feeder.equipment, "b0", "c1", "tx2") + + @pytest.mark.asyncio + async def test_assigns_auxilary_equipment_to_feeder(self): + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .add_feeder('b0') + ).network + + a1 = CurrentTransformer(mrid='a1') + a1.terminal = network.get('c1-t1') + network.add(a1) + + a2 = FaultIndicator(mrid='a2') + a2.terminal = network.get('c1-t1') + network.add(a2) + + feeder = network['fdr2'] + + await Tracing.assign_equipment_to_feeders().run(network, NetworkStateOperators.NORMAL) + validate_equipment(feeder.equipment, 'b0', 'c1', 'a1', 'a2') + + @pytest.mark.asyncio + async def test_assigns_protection_equipment_to_feeder(self): + network = (TestNetworkBuilder() + .from_breaker() # b0 + .add_feeder('b0') + ).network + + ps = network.get('b0', ProtectedSwitch) + cr = CurrentRelay(mrid='cr1') + ps.add_relay_function(cr) + cr.add_protected_switch(ps) + + prs = ProtectionRelayScheme(mrid='psr2') + cr.add_scheme(prs) + prs.add_function(cr) + + prsys = ProtectionRelaySystem(mrid='prsys3') + prs.system = prsys + prsys.add_scheme(prs) + + network.add(cr) + network.add(prs) + network.add(prsys) + + feeder = network['fdr1'] + + await Tracing.assign_equipment_to_feeders().run(network, NetworkStateOperators.NORMAL) + + validate_equipment(feeder.equipment, 'b0', 'prsys3') + + @pytest.mark.asyncio + async def test_assigns_power_electronic_units_to_feeder(self): + peu1 = PhotoVoltaicUnit(mrid='peu1') + + def pec_action(this: PowerElectronicsConnection): + this.add_unit(peu1) + peu1.power_electronics_connection = this + + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_power_electronics_connection(action=pec_action) # pec1 + .add_feeder('b0') + ).network + + network.add(peu1) + + feeder = network['fdr2'] + + await Tracing.assign_equipment_to_feeders().run(network, NetworkStateOperators.NORMAL) + + validate_equipment(feeder.equipment, 'b0', 'pec1', 'peu1') + + @pytest.mark.asyncio + async def test_can_be_run_from_a_single_terminal(self): + # + # 1 b0 21--c1--2 j2 31--c3--21--c4--2 + # 2 + # 1 + # | + # c5 + # | + # 21--c6--2 + # + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .to_junction(num_terminals=3) # j2 + .to_acls() # c3 + .to_acls() # c4 + .from_acls() # c5 + .to_acls() # c6 + .connect('j2', 'c5', 2, 1) + .add_feeder('b0') # fdr7 + ).network + + feeder = network['fdr7'] + junction = network['j2'] + + feeder.add_equipment(junction) + junction.add_container(feeder) + + await Tracing.assign_equipment_to_feeders().run(network, NetworkStateOperators.NORMAL) + + # b0 is included from the network builder. + # j2 was added to allow us to test the terminal based assignment. + # c3 and c4 should have been added via the trace. + # c1, c5 and c6 shouldn't have been added if the assignment only went out t3 of j2. + validate_equipment(feeder.equipment, 'b0', 'j2', 'c3', 'c4') + + @pytest.mark.asyncio + async def test_energizes_all_lv_feeders_for_a_dist_tx_site_that_is_energized(self): + # + # 1--c4--21 b5 2 + # 1 b0 21--c121 tx2 21--c3--2 + # 1--c6--21 b7 2 + # + network = (TestNetworkBuilder() + .from_breaker(action=self._make_hv) # b0 + .to_acls(action=self._make_hv) # c1 + .to_power_transformer(end_actions=[lambda t: setattr(t, 'rated_u', self.bv_hv.nominal_voltage), lambda t: setattr(t, 'rated_u', self.bv_lv.nominal_voltage)]) # tx2 + .to_acls(action=self._make_lv) # c3 + .to_acls(action=self._make_lv) # c4 + .to_breaker(action=self._make_lv) # b5 + .from_acls(action=self._make_lv) # c6 + .to_breaker(action=self._make_lv) # b7 + .connect('c3', 'c6', 2, 1) + .add_feeder('b0') # fdr8 + .add_lv_feeder('tx2') # lvf9 + .add_lv_feeder('b5') # lvf10 + .add_lv_feeder('b7') # lvf11 + .add_site(['tx2', 'c3', 'c4', 'b5', 'c6', 'b7']) # site12 + ).network + + feeder = network['fdr8'] + + await Tracing.assign_equipment_to_feeders().run(network, NetworkStateOperators.NORMAL) + + # We ensure the HV trace stopped at the transformer, but the additional LV feeders from b5 and b7 are still + # marked as energized through the dist substation site. + validate_equipment(feeder.equipment, 'b0', 'c1', 'tx2') + assert [it.mrid for it in feeder.normal_energized_lv_feeders] == ['lvf9', 'lvf10', 'lvf11'] + + @pytest.mark.asyncio + async def test_does_not_trace_out_from_terminal_belonging_to_open_switch(self): + network = (TestNetworkBuilder() + .from_breaker(is_normally_open=True) # b0 + .to_acls() # c1 + .add_feeder('b0') + ).network + + await Tracing.assign_equipment_to_feeders().run(network, NetworkStateOperators.NORMAL, network['b0'][2]) + + feeder = network['fdr2'] + validate_equipment(feeder.equipment, 'b0') + + diff --git a/test/services/network/tracing/test_assign_to_lv_feeders.py b/test/services/network/tracing/test_assign_to_lv_feeders.py index 96edf2f76..4740cabc0 100644 --- a/test/services/network/tracing/test_assign_to_lv_feeders.py +++ b/test/services/network/tracing/test_assign_to_lv_feeders.py @@ -5,18 +5,33 @@ from typing import Iterable import pytest -from zepben.evolve import Equipment, TestNetworkBuilder, Feeder, BaseVoltage, LvFeeder, NetworkStateOperators +from zepben.evolve import Equipment, TestNetworkBuilder, Feeder, BaseVoltage, LvFeeder, NetworkStateOperators, CurrentTransformer, FaultIndicator, \ + ProtectedSwitch, CurrentRelay, ProtectionRelayScheme, ProtectionRelaySystem, PhotoVoltaicUnit, PowerElectronicsConnection, ConductingEquipment, Breaker from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing def validate_equipment(equipment: Iterable[Equipment], *expected_mrids: str): - equip_mrids = [e.mrid for e in equipment] + equip_mrids = tuple(e.mrid for e in equipment) + assert equip_mrids == expected_mrids for mrid in expected_mrids: assert mrid in equip_mrids class TestAssignToLvFeeders: + bv_hv = BaseVoltage(nominal_voltage=11000) + bv_lv = BaseVoltage(nominal_voltage=400) + + @staticmethod + def base_voltage(ce: ConductingEquipment, voltage: BaseVoltage): + ce.base_voltage = voltage + + def _make_hv(self, ce: ConductingEquipment): + return self.base_voltage(ce, self.bv_hv) + + def _make_lv(self, ce: ConductingEquipment): + return self.base_voltage(ce, self.bv_lv) + @pytest.mark.asyncio @pytest.mark.parametrize('feeder_start_point_between_conductors_network', [(True,)], indirect=True) async def test_applies_to_equipment_on_head_terminal_side(self, feeder_start_point_between_conductors_network): @@ -39,7 +54,7 @@ async def test_assigns_equipment_to_feeders_with_loops(self, caplog, loop_under_ await Tracing.assign_equipment_to_lv_feeders().run(loop_under_feeder_head_network) lv_feeder = loop_under_feeder_head_network.get("f", LvFeeder) - validate_equipment(lv_feeder.equipment, "s0", "c1", "c2", "c3", "c4") + validate_equipment(lv_feeder.equipment, "s0", "c1", "c3", "c4", "c2") @pytest.mark.asyncio async def test_stops_at_hv_equipment(self): @@ -133,7 +148,7 @@ async def test_single_feeder_powers_multiple_lv_feeders(self): assert set(lv_feeder2.normal_energizing_feeders) == {feeder} @pytest.mark.asyncio - async def test_single_feeder_powers_multiple_lv_feeders(self): + async def test_multiple_feeders_power_single_lv_feeder(self): network_service = (TestNetworkBuilder() .from_breaker() .add_feeder("b0") @@ -151,3 +166,211 @@ async def test_single_feeder_powers_multiple_lv_feeders(self): assert set(feeder1.normal_energized_lv_feeders) == {lv_feeder} assert set(feeder2.normal_energized_lv_feeders) == {lv_feeder} assert set(lv_feeder.normal_energizing_feeders) == {feeder1, feeder2} + + @pytest.mark.asyncio + async def test_assigns_auxiliary_equipment_to_lv_feeder(self): + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_acls() # c1 + .add_lv_feeder('b0') + ).network + + a1 = CurrentTransformer(mrid='a1') + a1.terminal = network.get('c1-t1') + network.add(a1) + + a2 = FaultIndicator(mrid='a2') + a2.terminal = network.get('c1-t1') + network.add(a2) + + lv_feeder = network['lvf2'] + + await Tracing.assign_equipment_to_lv_feeders().run(network, NetworkStateOperators.NORMAL) + validate_equipment(lv_feeder.equipment, 'b0', 'c1', 'a1', 'a2') + + @pytest.mark.asyncio + async def test_assigns_protection_equipment_to_feeder(self): + network = (TestNetworkBuilder() + .from_breaker() # b0 + .add_lv_feeder('b0') + ).network + + ps = network.get('b0', ProtectedSwitch) + cr = CurrentRelay(mrid='cr1') + ps.add_relay_function(cr) + cr.add_protected_switch(ps) + + prs = ProtectionRelayScheme(mrid='psr2') + cr.add_scheme(prs) + prs.add_function(cr) + + prsys = ProtectionRelaySystem(mrid='prsys3') + prs.system = prsys + prsys.add_scheme(prs) + + network.add(cr) + network.add(prs) + network.add(prsys) + + lv_feeder = network['lvf1'] + + await Tracing.assign_equipment_to_lv_feeders().run(network, NetworkStateOperators.NORMAL) + + validate_equipment(lv_feeder.equipment, 'b0', 'prsys3') + + @pytest.mark.asyncio + async def test_assigns_power_electronic_units_to_feeder(self): + peu1 = PhotoVoltaicUnit(mrid='peu1') + + def pec_action(this: PowerElectronicsConnection): + this.add_unit(peu1) + peu1.power_electronics_connection = this + + network = (TestNetworkBuilder() + .from_breaker() # b0 + .to_power_electronics_connection(action=pec_action) # pec1 + .add_lv_feeder('b0') + ).network + + network.add(peu1) + + lv_feeder = network['lvf2'] + + await Tracing.assign_equipment_to_lv_feeders().run(network, NetworkStateOperators.NORMAL) + + validate_equipment(lv_feeder.equipment, 'b0', 'pec1', 'peu1') + + @pytest.mark.asyncio + async def lv_feeders_detect_back_feeds_for_energizing_feeders(self): + # 1 b0 21 tx1 21--c2--21--c3--21 tx4 21 b5 2 + # + # NOTE: Transformer is deliberately set to use the hv voltage as their base voltage to ensure they are still processed. + # + network = (TestNetworkBuilder() + .from_breaker(action=self._make_hv) # b0 + .to_power_transformer(action=self._make_hv) # tx1 + .to_acls(action=self._make_lv) # c2 + .to_acls(action=self._make_lv) # c3 + .to_power_transformer(action=self._make_hv) # tx4 + .to_breaker(action=self._make_hv) + .add_feeder("b0") + .add_lv_feeder("tx1") + .add_lv_feeder("tx4", 1) + .add_feeder("b5", 1) + ).network + + feeder6: Feeder = network["fdr6"] + feeder9: Feeder = network["fdr9"] + lv_feeder7: LvFeeder = network["lvf7"] + lv_feeder8: LvFeeder = network["lvf8"] + + await Tracing.assign_equipment_to_feeders().run(network, NetworkStateOperators.NORMAL) + await Tracing.assign_equipment_to_lv_feeders().run(network, NetworkStateOperators.NORMAL) + + assert feeder6.normal_energized_lv_feeders == [lv_feeder7, lv_feeder8] + assert feeder9.normal_energized_lv_feeders == [lv_feeder7, lv_feeder8] + assert lv_feeder7.normal_energizing_feeders == [feeder6, feeder9] + assert lv_feeder8.normal_energizing_feeders == [feeder6, feeder9] + + @pytest.mark.asyncio + async def test_lv_feeders_detect_back_feeds_for_dist_substation_sites(self): + # + # 1--c2--21 b3 2 + # 1 tx0 21--c1--2 + # 1--c4--21 b5 21--c6--21 b7 2 + # + network = (TestNetworkBuilder() + .from_power_transformer(end_actions=[lambda t: setattr(t, 'rated_u', self.bv_hv.nominal_voltage), lambda t: setattr(t, 'rated_u', self.bv_lv.nominal_voltage)]) # tx0 + .to_acls(action=self._make_lv) # c1 + .to_acls(action=self._make_lv) # c2 + .to_breaker(action=self._make_lv) # b3 + .from_acls(action=self._make_lv) # c4 + .to_breaker(action=self._make_lv) # b5 + .to_acls(action=self._make_lv) # c6 + .to_breaker(action=self._make_lv) # b7 + .connect('c1', 'c4', 2, 1) + .add_lv_feeder('tx0') # lvf8 + .add_lv_feeder('b3') # lvf9 + .add_lv_feeder('b5') # lvf10 + .add_lv_feeder('b7', 1) # lvf11 + .add_site(['tx0', 'c1', 'c2', 'b3', 'c4', 'b5']) # site12 + ).network + + operators = NetworkStateOperators.NORMAL + b7: Breaker = network['b7'] + + feeder = Feeder() + lv_feeder8 = network['lvf8'] + operators.associate_energizing_feeder(feeder, lv_feeder8) + lv_feeder9 = network['lvf9'] + operators.associate_energizing_feeder(feeder, lv_feeder9) + lv_feeder10 = network['lvf10'] + operators.associate_energizing_feeder(feeder, lv_feeder10) + + # We create an LV feeder to assign from b7 with its associated energizing feeder, which we will test is assigned to all LV feeders + # in the dist substation site, not just the one on b5. + back_feed = Feeder() + lv_feeder = LvFeeder() + operators.associate_energizing_feeder(back_feed, lv_feeder) + + await Tracing.assign_equipment_to_lv_feeders().run( + b7.get_terminal_by_sn(1), + network.lv_feeder_start_points, + {}, + [lv_feeder], + operators + ) + + # Make sure the LV feeder trace stopped at the first LV feeder head. + assert [it.mrid for it in lv_feeder.equipment] == ['b7', 'c6', 'b5'] + + # Make sure both feeders are now considered to be energizing all LV feeders. + assert list(feeder.normal_energized_lv_feeders) == [lv_feeder8, lv_feeder9, lv_feeder10, lv_feeder] + assert list(back_feed.normal_energized_lv_feeders) == [lv_feeder, lv_feeder8, lv_feeder9, lv_feeder10] + + # Make sure all LV feeders are now considered to be energized by both feeders. + assert list(lv_feeder.normal_energizing_feeders) == [back_feed, feeder] + assert list(lv_feeder8.normal_energizing_feeders) == [feeder, back_feed] + assert list(lv_feeder9.normal_energizing_feeders) == [feeder, back_feed] + assert list(lv_feeder10.normal_energizing_feeders) == [feeder, back_feed] + + @pytest.mark.asyncio + async def test_assigns_normal_and_current_energising_feeders_based_on_state(self): + network = (TestNetworkBuilder() + .from_breaker() # b0 + .add_lv_feeder('b0') # lvf1 + ).network + + normal_feeder = Feeder() + current_feeder = Feeder() + breaker = network['b0'] + lv_feeder = network['lvf1'] + + breaker.add_container(normal_feeder) + breaker.add_current_container(current_feeder) + + await Tracing.assign_equipment_to_lv_feeders().run(network, NetworkStateOperators.NORMAL) + await Tracing.assign_equipment_to_lv_feeders().run(network, NetworkStateOperators.CURRENT) + + assert list(normal_feeder.normal_energized_lv_feeders) == [lv_feeder] + assert list(lv_feeder.normal_energizing_feeders) == [normal_feeder] + + assert list(current_feeder.current_energized_lv_feeders) == [lv_feeder] + assert list(lv_feeder.current_energizing_feeders) == [current_feeder] + + @pytest.mark.asyncio + async def test_does_not_trace_out_from_terminal_belonging_to_open_switch(self): + # + # 1 b0 21--c1--2 + # + network = (TestNetworkBuilder() + .from_breaker(is_normally_open=True) # b0 + .to_acls() # c1 + .add_lv_feeder('b0') # lvf2 + ).network + + await Tracing.assign_equipment_to_lv_feeders().run(network, NetworkStateOperators.NORMAL, network['b0'][2]) + + feeder = network['lvf2'] + validate_equipment(feeder.equipment, 'b0') + \ No newline at end of file From 9dbfb653afb6945800bda95b57f60a8d95b7ca7d Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 28 May 2025 04:26:04 +1000 Subject: [PATCH 35/47] should update the changelog. Signed-off-by: Max Chesterfield --- changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.md b/changelog.md index 787d56cba..12bec367c 100644 --- a/changelog.md +++ b/changelog.md @@ -30,6 +30,9 @@ * `NetworkTrace` now handles starting on `Cut` , `Clamp`, and `AcLineSegment` and their terminals in a explicit / sensible way. * `NetworkTracePathProvider` now correctly handles next paths when starting on a `Clamp` terminal. * `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`. + ### Notes From e7781b365f892d80fa379b8f86c8a95d15f979bf Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 28 May 2025 18:47:21 +1000 Subject: [PATCH 36/47] log inferred phases properly. Signed-off-by: Max Chesterfield --- .../sqlite/network/network_database_reader.py | 40 ++++++---- .../network/tracing/phases/phase_inferrer.py | 10 +-- .../tracing/phases/test_phase_inferrer.py | 77 +++++++++++-------- 3 files changed, 74 insertions(+), 53 deletions(-) diff --git a/src/zepben/evolve/database/sqlite/network/network_database_reader.py b/src/zepben/evolve/database/sqlite/network/network_database_reader.py index 726e01002..4f7d01446 100644 --- a/src/zepben/evolve/database/sqlite/network/network_database_reader.py +++ b/src/zepben/evolve/database/sqlite/network/network_database_reader.py @@ -78,26 +78,29 @@ async def _post_load(self) -> bool: status = await super()._post_load() self._logger.info("Applying feeder direction to network...") - await self.set_feeder_direction.run(self.service, NetworkStateOperators.NORMAL) - await self.set_feeder_direction.run(self.service, NetworkStateOperators.CURRENT) + await self.set_feeder_direction.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) + await self.set_feeder_direction.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) self._logger.info("Feeder direction applied to network.") self._logger.info("Applying phases to network...") - await self.set_phases.run(self.service, NetworkStateOperators.NORMAL) - await self.set_phases.run(self.service, NetworkStateOperators.CURRENT) + await self.set_phases.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) + await self.set_phases.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) if self.infer_phases: - await self.phase_inferrer.run(self.service, NetworkStateOperators.NORMAL) - await self.phase_inferrer.run(self.service, NetworkStateOperators.CURRENT) + self._log_inferred_phases( + await self.phase_inferrer.run(self.service, network_state_operators=NetworkStateOperators.NORMAL), + await self.phase_inferrer.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) + ) + self._logger.info("Phasing applied to network.") self._logger.info("Assigning equipment to feeders...") - await self.assign_to_feeders.run(self.service, NetworkStateOperators.NORMAL) - await self.assign_to_feeders.run(self.service, NetworkStateOperators.CURRENT) + await self.assign_to_feeders.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) + await self.assign_to_feeders.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) self._logger.info("Equipment assigned to feeders.") self._logger.info("Assigning equipment to LV feeders...") - await self.assign_to_lv_feeders.run(self.service, NetworkStateOperators.NORMAL) - await self.assign_to_lv_feeders.run(self.service, NetworkStateOperators.CURRENT) + await self.assign_to_lv_feeders.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) + await self.assign_to_lv_feeders.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) self._logger.info("Equipment assigned to LV feeders.") self._logger.info("Validating that each equipment is assigned to a container...") @@ -110,16 +113,21 @@ async def _post_load(self) -> bool: return status - def _log_inferred_phases(self, normal_inferred_phases: List, current_inferred_phases: List): # FIXME: set list contents classes, this'll likely explode until then - # FIXME: im pretty sure this should be building a dict of lists, not just a simple KV store. if so, this logic is way too simple + def _log_inferred_phases(self, + normal_inferred_phases: List[PhaseInferrer.InferredPhase], + current_inferred_phases: List[PhaseInferrer.InferredPhase]): + inferred_phases = {item.conducting_equipment: item for item in normal_inferred_phases} for it in current_inferred_phases: - ce = it.conducting_equipment - inferred_phases[ce] = (inferred_phases[ce] if inferred_phases[ce].suspect else it) + if it.conducting_equipment in inferred_phases: + left = inferred_phases[it.conducting_equipment] + inferred_phases[it.conducting_equipment] = left if left.suspect else it + else: + inferred_phases[it.conducting_equipment] = it - for phase in inferred_phases: - self._logger.warning(f"*** Action Required *** {phase.description()}") + for phase in inferred_phases.values(): + self._logger.warning(f"*** Action Required *** {phase.description}") def _validate_equipment_containers(self): missing_containers = [it for it in self.service.objects(Equipment) if not it.containers] diff --git a/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py b/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py index f19aef1a1..955daaf07 100644 --- a/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py +++ b/src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py @@ -13,8 +13,6 @@ __all__ = ["PhaseInferrer"] -logger = logging.getLogger(__name__) - class PhaseInferrer: """ @@ -26,15 +24,15 @@ class InferredPhase: def __init__(self, conducting_equipment: ConductingEquipment, suspect: bool): self.conducting_equipment = conducting_equipment self.suspect = suspect - logger.warning(f'*** Action Required *** Inferred missing {self.description} due to a disconnected nominal phase because of an ' - f'upstream error in the source data. Phasing information for the upstream equipment should be fixed in the source system.') @property def description(self) -> str: if self.suspect: - return f"phases for '{self.conducting_equipment.name}' [{self.conducting_equipment.mrid}] which may not be correct. The phases were inferred" + _inner_desc = f"phases for '{self.conducting_equipment.name}' [{self.conducting_equipment.mrid}] which may not be correct. The phases were inferred" else: - return f"phase for '{self.conducting_equipment.name}' [{self.conducting_equipment.mrid}] which should be correct. The phase was inferred" + _inner_desc = f"phase for '{self.conducting_equipment.name}' [{self.conducting_equipment.mrid}] which should be correct. The phase was inferred" + return (f'Inferred missing {_inner_desc} due to a disconnected nominal phase because of an ' + f'upstream error in the source data. Phasing information for the upstream equipment should be fixed in the source system.') async def run(self, network: NetworkService, network_state_operators: Type[NetworkStateOperators]=NetworkStateOperators.NORMAL) -> list[InferredPhase]: """ diff --git a/test/services/network/tracing/phases/test_phase_inferrer.py b/test/services/network/tracing/phases/test_phase_inferrer.py index 772e31ec2..65141073d 100644 --- a/test/services/network/tracing/phases/test_phase_inferrer.py +++ b/test/services/network/tracing/phases/test_phase_inferrer.py @@ -2,12 +2,14 @@ # 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/. +import logging from typing import List, Optional from unittest.mock import patch import pytest from services.network.tracing.phases.util import validate_phases_from_term_or_equip +from zepben.evolve.database.sqlite.network.network_database_reader import NetworkDatabaseReader from zepben.evolve import TestNetworkBuilder, PhaseCode, SinglePhaseKind, PhaseInferrer, Terminal, NetworkService, NetworkStateOperators A = SinglePhaseKind.A @@ -16,11 +18,6 @@ N = SinglePhaseKind.N NONE = SinglePhaseKind.NONE -async def run_phase_inferrer(network: NetworkService, do_current=True) -> tuple[List[PhaseInferrer.InferredPhase], List[PhaseInferrer.InferredPhase]]: - normal = await PhaseInferrer().run(network, network_state_operators=NetworkStateOperators.NORMAL) - current = await PhaseInferrer().run(network, network_state_operators=NetworkStateOperators.CURRENT) if do_current else [] - return normal, current - class TestPhaseInferrer: """ Test the `PhaseInferrer` @@ -48,14 +45,14 @@ async def test_ab_to_bc_to_xy_to_abc(self, caplog): validate_phases_from_term_or_equip(network, "c2", [B, NONE]) validate_phases_from_term_or_equip(network, "c3", [NONE, B, NONE]) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.BC) validate_phases_from_term_or_equip(network, "c2", PhaseCode.BC) validate_phases_from_term_or_equip(network, "c3", PhaseCode.ABC) self._validate_returned_phases(network, changes, ['c1', 'c3']) - self._validate_log(caplog, correct=["c1", "c3", 'c1', 'c3']) + self._validate_log(caplog, correct=["c1", "c3"]) @pytest.mark.asyncio async def test_abn_to_bcn_to_xyn_to_abcn(self, caplog): @@ -79,14 +76,14 @@ async def test_abn_to_bcn_to_xyn_to_abcn(self, caplog): validate_phases_from_term_or_equip(network, "c2", [B, NONE, N]) validate_phases_from_term_or_equip(network, "c3", [NONE, B, NONE, N]) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.BCN) validate_phases_from_term_or_equip(network, "c2", PhaseCode.BCN) validate_phases_from_term_or_equip(network, "c3", PhaseCode.ABCN) self._validate_returned_phases(network, changes, ['c1', 'c3']) - self._validate_log(caplog, correct=["c1", "c3", 'c1', 'c3']) + self._validate_log(caplog, correct=["c1", "c3"]) @pytest.mark.asyncio async def test_bc_to_ac_to_xy_to_abc(self, caplog): @@ -110,14 +107,14 @@ async def test_bc_to_ac_to_xy_to_abc(self, caplog): validate_phases_from_term_or_equip(network, "c2", [NONE, C]) validate_phases_from_term_or_equip(network, "c3", [NONE, NONE, C]) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.AC) validate_phases_from_term_or_equip(network, "c2", PhaseCode.AC) validate_phases_from_term_or_equip(network, "c3", PhaseCode.ABC) self._validate_returned_phases(network, changes, ['c1', 'c3']) - self._validate_log(caplog, correct=["c1", "c3", 'c1', 'c3']) + self._validate_log(caplog, correct=["c1", "c3"]) @pytest.mark.asyncio async def test_abc_to_xyn_to_xy_to_bc(self, caplog): @@ -141,13 +138,13 @@ async def test_abc_to_xyn_to_xy_to_bc(self, caplog): validate_phases_from_term_or_equip(network, "c2", PhaseCode.BC) validate_phases_from_term_or_equip(network, "c3", PhaseCode.BC) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.BCN) validate_phases_from_term_or_equip(network, "c2", PhaseCode.BC) validate_phases_from_term_or_equip(network, "c3", PhaseCode.BC) - self._validate_log(caplog, correct=["c1", 'c1']) + self._validate_log(caplog, correct=["c1"]) self._validate_returned_phases(network, changes, ['c1']) @pytest.mark.asyncio @@ -172,14 +169,14 @@ async def test_abc_to_xy_to_xyn_to_bc(self, caplog): validate_phases_from_term_or_equip(network, "c2", [B, C, NONE]) validate_phases_from_term_or_equip(network, "c3", PhaseCode.BC) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.BC) validate_phases_from_term_or_equip(network, "c2", PhaseCode.BCN) validate_phases_from_term_or_equip(network, "c3", PhaseCode.BC) self._validate_returned_phases(network, changes, ['c2']) - self._validate_log(caplog, correct=["c2", 'c2']) + self._validate_log(caplog, correct=["c2"]) @pytest.mark.asyncio async def test_abc_to_n_to_abcn(self, caplog): @@ -203,14 +200,14 @@ async def test_abc_to_n_to_abcn(self, caplog): validate_phases_from_term_or_equip(network, "c2", PhaseCode.NONE) validate_phases_from_term_or_equip(network, "c3", PhaseCode.NONE) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.ABC) validate_phases_from_term_or_equip(network, "c2", PhaseCode.N) validate_phases_from_term_or_equip(network, "c3", PhaseCode.ABCN) self._validate_returned_phases(network, changes, ['c2', 'c3']) - self._validate_log(caplog, correct=["c2", "c3", 'c2', 'c3']) + self._validate_log(caplog, correct=["c2", "c3"]) @pytest.mark.asyncio async def test_abc_to_b_to_xyn(self, caplog): @@ -236,14 +233,14 @@ async def test_abc_to_b_to_xyn(self, caplog): validate_phases_from_term_or_equip(network, "c2", PhaseCode.B) validate_phases_from_term_or_equip(network, "c3", [B, NONE, NONE]) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.ABC) validate_phases_from_term_or_equip(network, "c2", PhaseCode.B) validate_phases_from_term_or_equip(network, "c3", PhaseCode.BCN) self._validate_returned_phases(network, changes, ['c3']) - self._validate_log(caplog, suspect=["c3", 'c3']) + self._validate_log(caplog, suspect=["c3"]) @pytest.mark.asyncio async def test_abc_to_c_to_xyn(self, caplog): @@ -269,14 +266,14 @@ async def test_abc_to_c_to_xyn(self, caplog): validate_phases_from_term_or_equip(network, "c2", PhaseCode.C) validate_phases_from_term_or_equip(network, "c3", [C, NONE, NONE]) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.ABC) validate_phases_from_term_or_equip(network, "c2", PhaseCode.C) validate_phases_from_term_or_equip(network, "c3", [C, NONE, N]) self._validate_returned_phases(network, changes, ['c3']) - self._validate_log(caplog, suspect=["c3", 'c3']) + self._validate_log(caplog, suspect=["c3"]) @pytest.mark.asyncio async def test_abc_to_a_to_xn(self, caplog): @@ -300,14 +297,14 @@ async def test_abc_to_a_to_xn(self, caplog): validate_phases_from_term_or_equip(network, "c2", PhaseCode.A) validate_phases_from_term_or_equip(network, "c3", [A, NONE]) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.ABC) validate_phases_from_term_or_equip(network, "c2", PhaseCode.A) validate_phases_from_term_or_equip(network, "c3", PhaseCode.AN) self._validate_returned_phases(network, changes, ['c3']) - self._validate_log(caplog, correct=["c3", 'c3']) + self._validate_log(caplog, correct=["c3"]) @pytest.mark.asyncio async def test_dual_feed_an_to_abcn(self, caplog): @@ -330,14 +327,14 @@ async def test_dual_feed_an_to_abcn(self, caplog): validate_phases_from_term_or_equip(network, "c1", [A, NONE, NONE, N]) validate_phases_from_term_or_equip(network, "s2", PhaseCode.AN) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "s0", PhaseCode.AN) validate_phases_from_term_or_equip(network, "c1", PhaseCode.ABCN) validate_phases_from_term_or_equip(network, "s2", PhaseCode.AN) self._validate_returned_phases(network, changes, ['c1']) - self._validate_log(caplog, correct=["c1", 'c1']) + self._validate_log(caplog, correct=["c1"]) @pytest.mark.asyncio async def test_abcn_to_n_to_ab_to_xy(self, caplog): @@ -363,7 +360,7 @@ async def test_abcn_to_n_to_ab_to_xy(self, caplog): validate_phases_from_term_or_equip(network, "c3", PhaseCode.NONE) validate_phases_from_term_or_equip(network, "c4", PhaseCode.NONE) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.ABCN) validate_phases_from_term_or_equip(network, "c2", PhaseCode.N) @@ -371,7 +368,7 @@ async def test_abcn_to_n_to_ab_to_xy(self, caplog): validate_phases_from_term_or_equip(network, "c4", PhaseCode.AB) self._validate_returned_phases(network, changes, ['c3']) - self._validate_log(caplog, correct=["c3", 'c3']) + self._validate_log(caplog, correct=["c3"]) @pytest.mark.asyncio async def test_with_open_switch(self, caplog): @@ -395,7 +392,7 @@ async def test_with_open_switch(self, caplog): validate_phases_from_term_or_equip(network, "b2", PhaseCode.ABC, PhaseCode.NONE) validate_phases_from_term_or_equip(network, "c3", PhaseCode.NONE) - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c1", PhaseCode.ABC) validate_phases_from_term_or_equip(network, "b2", PhaseCode.ABC, PhaseCode.NONE) @@ -435,7 +432,7 @@ async def test_validate_directions_with_dropped_direction_loop(self, caplog): terminals = [network.get("c6-t2", Terminal)] + [t for t in network.objects(Terminal) if t.mrid != "c6-t2"] with patch.object(NetworkService, 'objects', wraps=lambda _: terminals): - changes = await run_phase_inferrer(network) + changes = await self.run_phase_inferrer(network) validate_phases_from_term_or_equip(network, "c2", PhaseCode.AC, PhaseCode.AC) validate_phases_from_term_or_equip(network, "c3", PhaseCode.ABC, PhaseCode.ABC) @@ -447,7 +444,22 @@ async def test_validate_directions_with_dropped_direction_loop(self, caplog): validate_phases_from_term_or_equip(network, "c9", PhaseCode.ABC, PhaseCode.ABC) self._validate_returned_phases(network, changes, ['c6']) - self._validate_log(caplog, correct=["c6", 'c6']) + self._validate_log(caplog, correct=["c6"]) + + + class LoggerOnly: + _logger = logging.getLogger(__name__) + + async def run_phase_inferrer(self, network: NetworkService, do_current=True) -> tuple[List[PhaseInferrer.InferredPhase], List[PhaseInferrer.InferredPhase]]: + normal = await PhaseInferrer().run(network, network_state_operators=NetworkStateOperators.NORMAL) + current = await PhaseInferrer().run(network, network_state_operators=NetworkStateOperators.CURRENT) if do_current else [] + + # This has to be called manually as we don't actually use the NetworkDatabaseReader + # and copy pasting the logging code in here didn't make any sense. + # noinspection PyTypeChecker + NetworkDatabaseReader._log_inferred_phases(self.LoggerOnly, normal, current) + + return normal, current @staticmethod def _validate_returned_phases(network: NetworkService, @@ -463,8 +475,11 @@ def check_phases(phases): if current_phases: check_phases(current_phases) - def _validate_log(self, caplog, correct: Optional[List[str]] = None, suspect: Optional[List[str]] = None): + """ + This test is removed from the kotlin SDK, kept it in here as it caught some bugs that otherwise would have + slipped through, remove whenever it seems logical. + """ correct = correct or [] suspect = suspect or [] From 18e06d5ffefd83b76efa5ffea8788b9d12f41491 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 12:53:59 +1000 Subject: [PATCH 37/47] small changes adressing some PR comments, more left to address. Signed-off-by: Max Chesterfield --- .../services/network/tracing/feeder/clear_direction.py | 2 -- .../networktrace/conditions/direction_condition.py | 2 +- .../conditions/network_trace_queue_condition.py | 6 +++--- .../conditions/network_trace_stop_condition.py | 8 ++++---- .../tracing/networktrace/network_trace_action_type.py | 2 +- .../networktrace/network_trace_step_path_provider.py | 9 ++++----- .../test_network_trace_step_path_provider.py | 2 +- test/services/network/tracing/test_assign_to_feeders.py | 2 -- 8 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py b/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py index 992b5a768..2d74e1ff4 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py +++ b/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py @@ -18,8 +18,6 @@ if TYPE_CHECKING: from zepben.evolve import StepContext, NetworkTraceStep -T = TypeVar('T') - class ClearDirection: diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py index 6f7e63c7e..86021650c 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/direction_condition.py @@ -60,4 +60,4 @@ def _should_queue_next_paths(self, path: NetworkTraceStep.Path) -> bool: if not(next_path.traced_internally and self.state_operators.is_open(path.to_equipment)): if self._should_queue(next_path): return True - return False \ No newline at end of file + return False diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_queue_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_queue_condition.py index ecfdf4c24..88a619380 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_queue_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_queue_condition.py @@ -33,10 +33,10 @@ def __init__(self, step_type: NetworkTraceStep.Type, condition: ShouldQueue=None super().__init__(self.should_queue) if condition is not None: self.should_queue_matched_step = condition - self.step_type = step_type + self.should_queue = self._should_queue_func(step_type) def should_queue(self, next_item: T, next_context: StepContext, current_item: T, current_context: StepContext) -> bool: - return self._should_queue_func(self.step_type)(next_item, next_context, current_item, current_context) + raise NotImplementedError() def should_queue_matched_step(self, next_item: NetworkTraceStep[T], next_context: StepContext, current_item: NetworkTraceStep[T], current_context: StepContext) -> bool: """ @@ -61,4 +61,4 @@ def _should_queue_func(self, step_type: NetworkTraceStep.Type) -> ShouldQueue: return self.should_queue_internal_step elif step_type == NetworkTraceStep.Type.EXTERNAL: return self.should_queue_external_step - raise ValueError(f'INTERNAL ERROR: step type [{step_type}] didn\'t match expected') + raise ValueError(f"INTERNAL ERROR: step type [{step_type}] didn't match expected") diff --git a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py index ef6bd98e3..e9c5c3e90 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/conditions/network_trace_stop_condition.py @@ -33,16 +33,16 @@ def __init__(self, step_type: NetworkTraceStep.Type, condition: ShouldStop): super().__init__(self.should_stop) if condition is not None: self.should_stop_matched_step = condition - self.step_type = step_type + self.should_stop = self._should_stop_func(step_type) def should_stop(self, item: NetworkTraceStep[T], context: StepContext) -> bool: - return self._should_stop_func(self.step_type)(item, context) + raise NotImplementedError() def should_stop_matched_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: """ The logic you would normally put in `should_stop`. However, this will only be called when a step matches the `step_type` """ - raise NotImplemented + raise NotImplementedError() def should_stop_internal_step(self, item: NetworkTraceStep[T], context: StepContext) -> bool: if item.type() == NetworkTraceStep.Type.INTERNAL: @@ -62,4 +62,4 @@ def _should_stop_func(self, step_type: NetworkTraceStep.Type) -> ShouldStop: return self.should_stop_internal_step elif step_type == NetworkTraceStep.Type.EXTERNAL: return self.should_stop_external_step - raise ValueError(f'INTERNAL ERROR: step type [{step_type}] didn\'t match expected') + raise ValueError(f"INTERNAL ERROR: step type [{step_type}] didn't match expected") diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py index f47f940b7..e793a74e7 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_action_type.py @@ -17,7 +17,7 @@ def _all_steps(item: NetworkTraceStep, context: StepContext, has_tracked: HasTra return True -def _first_step_on_equipment(item: NetworkTraceStep[Any], context: StepContext, has_tracked: Callable[[Terminal, Set[SinglePhaseKind]], bool]) -> bool: +def _first_step_on_equipment(item: NetworkTraceStep[Any], context: StepContext, has_tracked: HasTracked) -> bool: phases = item.path.to_phases_set() return not any(has_tracked(it, phases) for it in item.path.to_terminal.other_terminals()) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index 4eec1d197..ef4516e79 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -42,11 +42,10 @@ def _get_next_paths(): return self._next_paths_from_clamp(to_equipment, path, path_factory) elif isinstance(to_equipment, Cut): return self._next_paths_from_cut(to_equipment, path, path_factory) + elif path.traced_internally: + return self._next_external_paths(path, path_factory) else: - if path.traced_internally: - return self._next_external_paths(path, path_factory) - else: - return seq_term_map_to_path(path.to_terminal.other_terminals(), path_factory) + return seq_term_map_to_path(path.to_terminal.other_terminals(), path_factory) return (p for p in _get_next_paths() if p and self.state_operators.is_in_service(p.to_terminal.conducting_equipment)) @@ -270,4 +269,4 @@ def seq_term_map_to_path(terms: Union[Terminal, Iterable[Terminal]], path_factor else: yield path_factory(terms, traversed_acls) -acls_length_or_max = lambda acls: acls.length or sys.float_info.max \ No newline at end of file +acls_length_or_max = lambda acls: acls.length or sys.float_info.max diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index cc4c7307d..cd0d0242a 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -822,4 +822,4 @@ def _verify_paths(in_paths: Generator[NetworkTraceStep.Path, None, None], in_exp for path in paths: if path in expected: continue - assert paths == expected # doesn't represent the actual comparison, but dumps both sides of it. \ No newline at end of file + assert paths == expected # doesn't represent the actual comparison, but dumps both sides of it. diff --git a/test/services/network/tracing/test_assign_to_feeders.py b/test/services/network/tracing/test_assign_to_feeders.py index 32a4a6c0f..2730c4c34 100644 --- a/test/services/network/tracing/test_assign_to_feeders.py +++ b/test/services/network/tracing/test_assign_to_feeders.py @@ -256,5 +256,3 @@ async def test_does_not_trace_out_from_terminal_belonging_to_open_switch(self): feeder = network['fdr2'] validate_equipment(feeder.equipment, 'b0') - - From bef32b95eb59ec91738ea7a3d5a33130ab499ec5 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 13:59:52 +1000 Subject: [PATCH 38/47] dont keep overwritten class methods after the test scope Signed-off-by: Max Chesterfield --- .../test_network_trace_step_path_provider.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index cd0d0242a..5886053e6 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -4,6 +4,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from typing import Generator, Iterable +import pytest from pytest_subtests.plugin import subtests from services.network.test_data.cuts_and_clamps_network import CutsAndClampsNetwork @@ -34,12 +35,23 @@ def traversed_ce(ce): raise TypeError('Did not traverse') return NetworkTraceStep.Path(self, other, traversed_ce(self.conducting_equipment)) -Terminal.__add__ = PathTerminal.__add__ -Terminal.__sub__ = PathTerminal.__sub__ + +@pytest.fixture(scope="function", autouse=True) +def setup_class(): + """override `Terminal.__add__` to make test writing more convenient""" + Terminal.__add__ = PathTerminal.__add__ + Terminal.__sub__ = PathTerminal.__sub__ + yield + + # delete the methods when were done, so we don't modify global state between tests + delattr(Terminal, '__add__') + delattr(Terminal, '__sub__') + SPK = SinglePhaseKind class TestNetworkTraceStepPathProvider: + path_provider = NetworkTraceStepPathProvider(NetworkStateOperators.NORMAL) def test_current_external_path_steps_internally(self): From b30bd9ca2cbf4b91f14f1fd0506b6955bb0572ff Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 14:13:59 +1000 Subject: [PATCH 39/47] added definitely missed changelog notes Signed-off-by: Max Chesterfield --- changelog.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 12bec367c..ef51d354a 100644 --- a/changelog.md +++ b/changelog.md @@ -5,7 +5,9 @@ * `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`. +* `Cut` supports adding a maximum of 2 terminals. ### New Features @@ -17,11 +19,15 @@ * `with_clamp` - Adds a clamp to the previously added `AcLineSegment` * `with_cut` - Adds a cut to the previously added `AcLineSegment` * `connect_to` - Connects the previously added item, rather than having to specify it again in `connect`. + * 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` +* 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` +* `AssignToFeeder` and `AssignToLvFeeder` will no longer trace from start terminals that belong to open switches * The follow fixes were added to Traversal and NetworkTrace: * `can_stop_on_start_item` now works for branching traversals. * Traversal start items are added to the queue before traversal starts, so that the start items honour the queue type order. From db71519b8f06cbf723d90e152039c4369c33c390 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 14:28:51 +1000 Subject: [PATCH 40/47] added __all__ where missing to affected imports, also added missing imports Signed-off-by: Max Chesterfield --- src/zepben/evolve/__init__.py | 23 ++++++++++++++----- .../actions/equipment_tree_builder.py | 2 ++ .../tracing/networktrace/actions/tree_node.py | 2 ++ .../equipment_container_state_operators.py | 2 ++ .../operators/in_service_state_operators.py | 2 ++ .../operators/open_state_operators.py | 2 +- .../operators/phase_state_operators.py | 2 ++ 7 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index 0271d4c3e..4acbb8b53 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -167,17 +167,28 @@ from zepben.evolve.services.network.tracing.connectivity.transformer_phase_paths import * from zepben.evolve.services.network.tracing.connectivity.xy_candidate_phase_paths import * from zepben.evolve.services.network.tracing.connectivity.xy_phase_step import * + from zepben.evolve.services.network.tracing.feeder.direction_status import * from zepben.evolve.services.network.tracing.feeder.assign_to_feeders import * from zepben.evolve.services.network.tracing.feeder.assign_to_lv_feeders import * -from zepben.evolve.services.network.tracing.networktrace.actions.equipment_tree_builder import EquipmentTreeBuilder -from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import DirectionCondition -from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import EquipmentStepLimitCondition -from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import EquipmentTypeStepLimitCondition -from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import OpenCondition + +from zepben.evolve.services.network.tracing.networktrace.actions.equipment_tree_builder import * +from zepben.evolve.services.network.tracing.networktrace.actions.tree_node import * from zepben.evolve.services.network.tracing.networktrace.conditions.conditions import * -from zepben.evolve.services.network.tracing.networktrace.compute_data import * +from zepben.evolve.services.network.tracing.networktrace.conditions.direction_condition import * +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_step_limit_condition import * +from zepben.evolve.services.network.tracing.networktrace.conditions.equipment_type_step_limit_condition import * +from zepben.evolve.services.network.tracing.networktrace.conditions.network_trace_stop_condition import * +from zepben.evolve.services.network.tracing.networktrace.conditions.network_trace_queue_condition import * +from zepben.evolve.services.network.tracing.networktrace.conditions.open_condition import * +from zepben.evolve.services.network.tracing.networktrace.operators.equipment_container_state_operators import * +from zepben.evolve.services.network.tracing.networktrace.operators.feeder_direction_state_operations import * +from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import * from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import * +from zepben.evolve.services.network.tracing.networktrace.operators.open_state_operators import * +from zepben.evolve.services.network.tracing.networktrace.operators.phase_state_operators import * +from zepben.evolve.services.network.tracing.networktrace.compute_data import * + from zepben.evolve.services.network.tracing.phases.phase_status import * from zepben.evolve.services.network.tracing.phases.phase_inferrer import * from zepben.evolve.services.network.tracing.phases.remove_phases import * diff --git a/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py b/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py index eb818166a..ea65d4026 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/actions/equipment_tree_builder.py @@ -14,6 +14,8 @@ EquipmentTreeNode = TreeNode[ConductingEquipment] +__all__ = ['EquipmentTreeBuilder'] + class EquipmentTreeBuilder(StepActionWithContextValue): """ diff --git a/src/zepben/evolve/services/network/tracing/networktrace/actions/tree_node.py b/src/zepben/evolve/services/network/tracing/networktrace/actions/tree_node.py index 140474c0f..47720b079 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/actions/tree_node.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/actions/tree_node.py @@ -9,6 +9,8 @@ T = TypeVar('T') +__all__ = ['TreeNode'] + class TreeNode(Generic[T]): """ diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py index 6fe407495..6431fa1f4 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/equipment_container_state_operators.py @@ -15,6 +15,8 @@ if TYPE_CHECKING: from zepben.evolve.model.cim.iec61970.base.core.equipment import Equipment +__all__ = ['EquipmentContainerStateOperators', 'NormalEquipmentContainerStateOperators', 'CurrentEquipmentContainerStateOperators'] + class EquipmentContainerStateOperators(StateOperator): """ diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py index be1908447..3b3681f77 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py @@ -14,6 +14,8 @@ if TYPE_CHECKING: from zepben.evolve.model.cim.iec61970.base.core.equipment import Equipment +__all__ = ['InServiceStateOperators', 'NormalInServiceStateOperators', 'CurrentInServiceStateOperators'] + class InServiceStateOperators(StateOperator): """ diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py index 54af5d755..8be1b4198 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py @@ -19,7 +19,7 @@ T = TypeVar('T') -__all__ = ['OpenStateOperators'] +__all__ = ['OpenStateOperators', 'NormalOpenStateOperators', 'CurrentOpenStateOperators'] class OpenStateOperators(StateOperator): diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py index 3eb3f4883..be464d112 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py @@ -12,6 +12,8 @@ if TYPE_CHECKING: from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal +__all__ = ['PhaseStateOperators', 'NormalPhaseStateOperators', 'CurrentPhaseStateOperators'] + class PhaseStateOperators(StateOperator): """ From e2d09d47b3fd061a6d24b3436df2961973f51c4a Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 14:39:10 +1000 Subject: [PATCH 41/47] Another one Signed-off-by: Max Chesterfield --- src/zepben/evolve/__init__.py | 20 ++++++++++++------- .../network/tracing/feeder/clear_direction.py | 6 ++++-- .../network/tracing/traversal/step_context.py | 3 +++ .../traversal/weighted_priority_queue.py | 5 ++++- .../evolve/services/network/tracing/util.py | 6 +++++- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index 4acbb8b53..8e5507d11 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -149,10 +149,7 @@ from zepben.evolve.model.phases import * from zepben.evolve.model.resistance_reactance import * -from zepben.evolve.services.network.tracing.traversal.traversal import * -from zepben.evolve.services.network.tracing.traversal.queue import * -from zepben.evolve.services.network.tracing.feeder.feeder_direction import * from zepben.evolve.services.network.tracing.util import * from zepben.evolve.services.network.translator.network_proto2cim import * @@ -168,9 +165,12 @@ from zepben.evolve.services.network.tracing.connectivity.xy_candidate_phase_paths import * from zepben.evolve.services.network.tracing.connectivity.xy_phase_step import * -from zepben.evolve.services.network.tracing.feeder.direction_status import * from zepben.evolve.services.network.tracing.feeder.assign_to_feeders import * from zepben.evolve.services.network.tracing.feeder.assign_to_lv_feeders import * +from zepben.evolve.services.network.tracing.feeder.clear_direction import * +from zepben.evolve.services.network.tracing.feeder.direction_status import * +from zepben.evolve.services.network.tracing.feeder.feeder_direction import * +from zepben.evolve.services.network.tracing.feeder.set_direction import * from zepben.evolve.services.network.tracing.networktrace.actions.equipment_tree_builder import * from zepben.evolve.services.network.tracing.networktrace.actions.tree_node import * @@ -192,13 +192,19 @@ from zepben.evolve.services.network.tracing.phases.phase_status import * from zepben.evolve.services.network.tracing.phases.phase_inferrer import * from zepben.evolve.services.network.tracing.phases.remove_phases import * -from zepben.evolve.services.network.tracing.find_swer_equipment import * -from zepben.evolve.services.network.tracing.traversal.queue_condition import * +from zepben.evolve.services.network.tracing.phases.set_phases import * + from zepben.evolve.services.network.tracing.traversal.context_value_computer import * +from zepben.evolve.services.network.tracing.traversal.queue import * +from zepben.evolve.services.network.tracing.traversal.queue_condition import * from zepben.evolve.services.network.tracing.traversal.step_action import * +from zepben.evolve.services.network.tracing.traversal.step_context import * from zepben.evolve.services.network.tracing.traversal.stop_condition import * +from zepben.evolve.services.network.tracing.traversal.traversal import * from zepben.evolve.services.network.tracing.traversal.traversal_condition import * -from zepben.evolve.services.network.tracing.feeder.set_direction import * +from zepben.evolve.services.network.tracing.traversal.weighted_priority_queue import * + +from zepben.evolve.services.network.tracing.find_swer_equipment import * from zepben.evolve.services.common.meta.data_source import * from zepben.evolve.services.common.meta.metadata_collection import * diff --git a/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py b/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py index 2d74e1ff4..4bcde67b8 100644 --- a/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py +++ b/src/zepben/evolve/services/network/tracing/feeder/clear_direction.py @@ -4,11 +4,11 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeVar, Type +from typing import TYPE_CHECKING, Any, Type from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal -from zepben.evolve import FeederDirection +from zepben.evolve.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing from zepben.evolve.services.network.tracing.traversal.weighted_priority_queue import WeightedPriorityQueue from zepben.evolve.services.network.tracing.networktrace.network_trace import NetworkTrace @@ -18,6 +18,8 @@ if TYPE_CHECKING: from zepben.evolve import StepContext, NetworkTraceStep +__all__ = ['ClearDirection'] + class ClearDirection: diff --git a/src/zepben/evolve/services/network/tracing/traversal/step_context.py b/src/zepben/evolve/services/network/tracing/traversal/step_context.py index f21259bef..65e72b3d5 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/step_context.py +++ b/src/zepben/evolve/services/network/tracing/traversal/step_context.py @@ -6,6 +6,9 @@ T = TypeVar('T') +__all__ = ['StepContext'] + + class StepContext(Generic[T]): """ Represents the context of a traversal step, holding information about the traversal state and the ability to store arbitrary values with the context. diff --git a/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py b/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py index 61a272374..814708ffb 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py +++ b/src/zepben/evolve/services/network/tracing/traversal/weighted_priority_queue.py @@ -5,12 +5,15 @@ from collections import defaultdict from typing import TypeVar, Callable, Iterable -from zepben.evolve import Traversal +from zepben.evolve.services.network.tracing.traversal.traversal import Traversal from zepben.evolve.services.network.tracing.traversal.queue import TraversalQueue T = TypeVar('T') U = TypeVar('U') +__all__ = ['WeightedPriorityQueue'] + + class SortedDefaultDict(defaultdict): def keys(self): return sorted(super().keys()) diff --git a/src/zepben/evolve/services/network/tracing/util.py b/src/zepben/evolve/services/network/tracing/util.py index d18fd7dae..6817a923f 100644 --- a/src/zepben/evolve/services/network/tracing/util.py +++ b/src/zepben/evolve/services/network/tracing/util.py @@ -8,7 +8,11 @@ import logging from typing import Optional -from zepben.evolve import Switch, ConductingEquipment, SinglePhaseKind, Traversal + +from zepben.evolve.model.cim.iec61970.base.wires.switch import Switch +from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment +from zepben.evolve.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind +from zepben.evolve.services.network.tracing.traversal.traversal import Traversal __all__ = ["normally_open", "currently_open", "ignore_open", "phase_log"] phase_logger = logging.getLogger("phase_logger") From 0636f5cba491f3379144ddfffc65e5804a81f8ab Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 17:10:20 +1000 Subject: [PATCH 42/47] Type hints, formatting, and tidyups requested Signed-off-by: Max Chesterfield --- .../networktrace/network_trace_queue_next.py | 2 +- .../networktrace/network_trace_step.py | 2 +- .../operators/in_service_state_operators.py | 2 +- .../operators/phase_state_operators.py | 2 +- .../network/tracing/traversal/queue.py | 6 +- test/cim/cim_creators.py | 2 +- test/cim/iec61970/base/core/test_terminal.py | 2 +- .../network/test_data/looping_network.py | 124 +++++++++--------- .../conditions/test_direction_condition.py | 2 +- 9 files changed, 70 insertions(+), 74 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py index c4bcc42eb..3a60800d7 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_queue_next.py @@ -76,7 +76,7 @@ def __init__(self, state_operators: Type[NetworkStateOperators], compute_data: U self._get_next_steps: GetNextStepsBranching = lambda item, context: list(self.next_trace_steps(item, context, compute_data)) def accept(self, item: NetworkTraceStep[T], context: StepContext, queue_item: QueueItem, queue_branch: QueueBranch): - next_steps = list(self._get_next_steps(item, context)) + next_steps = self._get_next_steps(item, context) if len(next_steps) == 1: queue_item(next_steps[0]) else: 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 d0ab8fccf..5b469db9b 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 +from typing import Set, Generic, TypeVar, TYPE_CHECKING, Optional, List from zepben.evolve.services.network.tracing.connectivity.nominal_phase_path import NominalPhasePath diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py index 3b3681f77..aca882132 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/in_service_state_operators.py @@ -71,4 +71,4 @@ def set_in_service(equipment: Equipment, in_service: bool) -> None: equipment.in_service = in_service InServiceStateOperators.NORMAL = NormalInServiceStateOperators -InServiceStateOperators.CURRENT = CurrentInServiceStateOperators \ No newline at end of file +InServiceStateOperators.CURRENT = CurrentInServiceStateOperators diff --git a/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py b/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py index be464d112..be511c2b6 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/operators/phase_state_operators.py @@ -51,4 +51,4 @@ def phase_status(terminal: 'Terminal') -> PhaseStatus: PhaseStateOperators.NORMAL = NormalPhaseStateOperators -PhaseStateOperators.CURRENT = CurrentPhaseStateOperators \ No newline at end of file +PhaseStateOperators.CURRENT = CurrentPhaseStateOperators diff --git a/src/zepben/evolve/services/network/tracing/traversal/queue.py b/src/zepben/evolve/services/network/tracing/traversal/queue.py index baa915d11..d7bc0cb05 100644 --- a/src/zepben/evolve/services/network/tracing/traversal/queue.py +++ b/src/zepben/evolve/services/network/tracing/traversal/queue.py @@ -87,10 +87,9 @@ def clear(self): class BasicQueue(TraversalQueue, Generic[T]): - queue: Union[FIFODeque, LIFODeque] - def __init__(self, queue): - self.queue: Union[FIFODeque, LIFODeque] = queue + def __init__(self, queue: Union[FIFODeque, LIFODeque]): + self.queue = queue def __iter__(self): return self.queue.__iter__() @@ -98,7 +97,6 @@ def __iter__(self): def __len__(self): return len(self.queue) - def has_next(self) -> bool: return len(self.queue) > 0 diff --git a/test/cim/cim_creators.py b/test/cim/cim_creators.py index 87dabf96a..88f4df07d 100644 --- a/test/cim/cim_creators.py +++ b/test/cim/cim_creators.py @@ -1027,7 +1027,7 @@ def create_power_electronics_wind_unit(include_runtime: bool = True): def create_ac_line_segment(include_runtime: bool = True): args = create_conductor(include_runtime) - args["terminals"] = lists(builds(Terminal, **create_identified_object(include_runtime)), min_size=1, max_size=1) + args["terminals"] = lists(builds(Terminal, **create_identified_object(include_runtime)), min_size=1, max_size=2) return builds( AcLineSegment, **args, diff --git a/test/cim/iec61970/base/core/test_terminal.py b/test/cim/iec61970/base/core/test_terminal.py index fdc3cd6ff..fdc30c801 100644 --- a/test/cim/iec61970/base/core/test_terminal.py +++ b/test/cim/iec61970/base/core/test_terminal.py @@ -48,7 +48,7 @@ def test_terminal_constructor_kwargs(conducting_equipment, phases, sequence_numb sequence_number=sequence_number, normal_feeder_direction=normal_feeder_direction, current_feeder_direction=current_feeder_direction, - traced_phases= traced_phases, + traced_phases=traced_phases, connectivity_node=connectivity_node, **kwargs) diff --git a/test/services/network/test_data/looping_network.py b/test/services/network/test_data/looping_network.py index 82dca398d..db5522bf6 100644 --- a/test/services/network/test_data/looping_network.py +++ b/test/services/network/test_data/looping_network.py @@ -12,79 +12,77 @@ def create_looping_network(): :return: An example network with loops. """ # - # Python Diagram JVM SDK Diagram - # - # j0 c1 j2 c13 j14 c15 j16 // j0 ac0 j1 ac1 j2 ac2 j3 - # *11------21*21------21*21------21* // *11------21*21------21*21------21* - # 3 2 // 3 2 - # 1 1 // 1 1 - # c3 | | c17 // ac3 | | ac4 - # 2 2 // 2 2 - # 1 c20 1 // 1 ac5 1 - # j4 *21------21* j21 * b18 (open) // j4 *21------21* j5 * j6 (open) - # 3 2 // 3 2 - # 1 1 // 1 1 - # c5 | | c19 // ac6 | | ac7 - # 2 2 // 2 2 - # 1 c22 j23 c24 2 // 1 ac8 j8 ac9 2 - # j6 *21------21*21------21* j25 // j7 *21------21*21------21* j9 - # 3 // 3 - # 1 c29 // 1 ac11 - # | /--21* j30 // | /--21* j11 - # c7 | / 2 // ac10 | / 2 - # | / 1 // | / 1 - # 2 / | c31 // 2 / | ac13 - # 1 / 2 // 1 / 2 - # j8 *21 c9 2 c11 // j10 *21 ac12 2 ac14 - # 31--------21*31------21* j12 // 31--------21*31------21* j13 - # 1 2 j10 // 1 2 j12 - # \ | // \ | - # \ 1 c28 // \ 1 ac16 - # \ 2 // \ 2 - # \--21* j27 // \--21* j14 - # c26 // ac15 + # j0 ac0 j1 ac1 j2 ac2 j3 + # *11------21*21------21*21------21* + # 3 2 + # 1 1 + # ac3 | | ac4 + # 2 2 + # 1 ac5 1 + # j4 *21------21* j5 * j6 (open) + # 3 2 + # 1 1 + # ac6 | | ac7 + # 2 2 + # 1 ac8 j8 ac9 2 + # j7 *21------21*21------21* j9 + # 3 + # 1 ac11 + # | /--21* j11 + # ac10 | / 2 + # | / 1 + # 2 / | ac13 + # 1 / 2 + # j10 *21 ac12 2 ac14 + # 31--------21*31------21* j13 + # 1 2 j12 + # \ | + # \ 1 ac16 + # \ 2 + # \--21* j14 + # ac15 # return ( TestNetworkBuilder() - .from_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j0') # j0 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac0') # c1 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j1') # j2 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac3') # c3 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j4') # j4 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac6') # c5 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j7') # j6 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac10') # c7 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j10') # j8 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac12') # c9 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j12') # j10 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac14') # c11 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j13') # j12 + .from_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j0') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac0') + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j1') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac3') + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j4') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac6') + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j7') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac10') + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j10') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac12') + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=3, mrid='j12') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac14') + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j13') .branch_from("j1", 2) - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac1') # c13 - .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j2') # j14 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac2') # c15 - .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j3') # j16 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac4') # c17 - .to_breaker(nominal_phases=PhaseCode.ABCN, is_normally_open=True, is_open=True, mrid='j6') # b18 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac7') # c19 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac1') + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j2') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac2') + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j3') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac4') + .to_breaker(nominal_phases=PhaseCode.ABCN, is_normally_open=True, is_open=True, mrid='j6') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac7') .branch_from("j4", 2) - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac5') # c20 - .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j5') # j21 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac5') + .to_junction(nominal_phases=PhaseCode.ABCN, num_terminals=1, mrid='j5') .branch_from("j7", 2) - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac8') # c22 - .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j8') # j23 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac9') # c24 - .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j9') # j25 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac8') + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j8') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac9') + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j9') .connect("ac7", "j9", 2, 2) .branch_from("j10", 3) - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac15') # c26 - .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j14') # j27 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac16') # c28 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac15') + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j14') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac16') .connect("ac16", "j12", 2, 1) .branch_from("j10", 2) - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac11') # c29 - .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j11') # j30 - .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac13') # c31 + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac11') + .to_junction(nominal_phases=PhaseCode.ABCN, mrid='j11') + .to_acls(nominal_phases=PhaseCode.ABCN, mrid='ac13') .connect("ac13", "j12", 2, 2) .network ) diff --git a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py index c084ba1cf..96266d3a3 100644 --- a/test/services/network/tracing/networktrace/conditions/test_direction_condition.py +++ b/test/services/network/tracing/networktrace/conditions/test_direction_condition.py @@ -205,4 +205,4 @@ def _should_queue(condition: Tuple[FeederDirection, NetworkTraceStep.Path], expe should_queue = DirectionCondition(direction, NetworkStateOperators.NORMAL).should_queue(next_step, None, None, None) print(f'direction: {direction}') print(f'path: internal: {path.traced_internally}\n from: {path.from_terminal}\n to: {path.to_terminal}\n') - assert should_queue == expected \ No newline at end of file + assert should_queue == expected From d85b342bf6f58995f11fb4685d3edbc2251a05f6 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 17:11:13 +1000 Subject: [PATCH 43/47] rework of NetworkTraceStepPathProvider while i was there fixing a logic issue Signed-off-by: Max Chesterfield --- .../network_trace_step_path_provider.py | 341 +++++++++++------- 1 file changed, 209 insertions(+), 132 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index ef4516e79..e588abe70 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -26,11 +26,19 @@ class NetworkTraceStepPathProvider: - def __init__(self, state_operators: Type[NetworkStateOperators]): + def __init__( + self, + state_operators: Type[NetworkStateOperators] + ): self.state_operators = state_operators - def next_paths(self, path: NetworkTraceStep.Path) -> Generator[NetworkTraceStep.Path, None, None]: - path_factory = self._create_path_with_phases_factory(path) if path.nominal_phase_paths else self._create_path_factory(path) + def next_paths( + self, + path: NetworkTraceStep.Path + ) -> Generator[NetworkTraceStep.Path, None, None]: + + path_factory = (self._create_path_with_phases_factory(path) if path.nominal_phase_paths + else self._create_path_factory(path)) def _get_next_paths(): to_equipment = path.to_equipment @@ -42,6 +50,7 @@ def _get_next_paths(): return self._next_paths_from_clamp(to_equipment, path, path_factory) elif isinstance(to_equipment, Cut): return self._next_paths_from_cut(to_equipment, path, path_factory) + elif path.traced_internally: return self._next_external_paths(path, path_factory) else: @@ -50,13 +59,19 @@ def _get_next_paths(): return (p for p in _get_next_paths() if p and self.state_operators.is_in_service(p.to_terminal.conducting_equipment)) @staticmethod - def _create_path_factory(path: NetworkTraceStep.Path) -> PathFactory: + def _create_path_factory( + path: NetworkTraceStep.Path + ) -> PathFactory: + def path_factory(next_terminal: Terminal, traversed: AcLineSegment) -> NetworkTraceStep.Path: return NetworkTraceStep.Path(path.to_terminal, next_terminal, traversed) return path_factory @staticmethod - def _create_path_with_phases_factory(path: NetworkTraceStep.Path) -> PathFactory: + def _create_path_with_phases_factory( + path: NetworkTraceStep.Path + ) -> PathFactory: + phase_paths = set(p.to_phase for p in path.nominal_phase_paths) next_from_terminal = path.to_terminal @@ -64,35 +79,49 @@ def path_factory(next_terminal: Terminal, traversed: AcLineSegment): next_paths = TerminalConnectivityConnected().terminal_connectivity(next_from_terminal, next_terminal, phase_paths) if next_paths.nominal_phase_paths: return NetworkTraceStep.Path(next_from_terminal, next_terminal, traversed, set(next_paths.nominal_phase_paths)) + else: + return None + return path_factory - def _next_paths_from_ac_line_segment(self, segment: AcLineSegment, path: NetworkTraceStep.Path, path_factory: PathFactory - ) -> Generator[NetworkTraceStep.Path, None, None]: + def _next_paths_from_ac_line_segment( + self, + segment: AcLineSegment, + path: NetworkTraceStep.Path, + path_factory: PathFactory + ) -> Generator[NetworkTraceStep.Path, None, None]: + # If the current path traversed the segment, we need to step externally from the segment terminal. # Otherwise, we traverse the segment if path.traced_internally or path.did_traverse_ac_line_segment: - return self._next_external_paths(path, path_factory) + yield from self._next_external_paths(path, path_factory) else: if path.to_terminal.sequence_number == 1: - return self._acls_traverse_from_terminal(segment, - path.to_terminal, - length_from_T1=0.0, - towards_segment_T2=True, - can_stop_at_cut_at_same_position=True, - cut_at_same_position_from_terminal_number=1, - path_factory=path_factory) + yield from self._acls_traverse_from_terminal( + segment, + path.to_terminal, + length_from_t1=0.0, + towards_segment_t2=True, + can_stop_at_cut_at_same_position=True, + cut_at_same_position_from_terminal_number=1, + path_factory=path_factory) else: - return self._acls_traverse_from_terminal(segment, - path.to_terminal, - length_from_T1=acls_length_or_max(segment), - towards_segment_T2=False, - can_stop_at_cut_at_same_position=True, - cut_at_same_position_from_terminal_number=2, - path_factory=path_factory) + yield from self._acls_traverse_from_terminal( + segment, + path.to_terminal, + length_from_t1=acls_length_or_max(segment), + towards_segment_t2=False, + can_stop_at_cut_at_same_position=True, + cut_at_same_position_from_terminal_number=2, + path_factory=path_factory) @staticmethod - def _next_paths_from_busbar(path: NetworkTraceStep.Path, path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: - return seq_term_map_to_path( + def _next_paths_from_busbar( + path: NetworkTraceStep.Path, + path_factory: PathFactory + ) -> Generator[NetworkTraceStep.Path, None, None]: + + yield from seq_term_map_to_path( (t for t in path.to_terminal.connected_terminals() # We don't go back to the terminal we came from as we already visited it to get to this busbar. if t != path.from_terminal @@ -101,77 +130,120 @@ def _next_paths_from_busbar(path: NetworkTraceStep.Path, path_factory: PathFacto ), path_factory ) - def _next_paths_from_clamp(self, clamp: Clamp, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Iterable[NetworkTraceStep.Path]: + def _next_paths_from_clamp( + self, + clamp: Clamp, + path: NetworkTraceStep.Path, + path_factory: PathFactory + ) -> Generator[NetworkTraceStep.Path, None, None]: + # If the current path was from traversing an AcLineSegment, we need to step externally to other equipment. # Otherwise, we need to traverse the segment both ways. if path.did_traverse_ac_line_segment: - return self._next_external_paths(path, path_factory) - else: - # Because we consider clamps at the same position as a cut on the terminal 1 side, we do not stop at cuts at the same position when - # traversing towards t1, but we do when traversing towards t2. - if not clamp.ac_line_segment: - return set() - - next_paths_towards_T1 = self._acls_traverse_from_terminal(clamp.ac_line_segment, - path.to_terminal, - length_from_T1=clamp.length_from_T1_or_0, - towards_segment_T2=False, - can_stop_at_cut_at_same_position=False, - cut_at_same_position_from_terminal_number=1, - path_factory=path_factory) - - next_paths_towards_T2 = self._acls_traverse_from_terminal(clamp.ac_line_segment, - path.to_terminal, - length_from_T1=clamp.length_from_T1_or_0, - towards_segment_T2=True, - can_stop_at_cut_at_same_position=True, - cut_at_same_position_from_terminal_number=1, - path_factory=path_factory) - - return itertools.chain(next_paths_towards_T1, next_paths_towards_T2) - - def _next_paths_from_cut(self, cut: Cut, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Iterable[NetworkTraceStep.Path]: + yield from self._next_external_paths(path, path_factory) + return + + elif path.traced_internally: + yield from self._next_external_paths(path, path_factory) + yield from self._traverse_ac_line_segment_from_clamp(clamp, path, path_factory) + + def _traverse_ac_line_segment_from_clamp( + self, + clamp: Clamp, + path: NetworkTraceStep.Path, + path_factory: PathFactory + ) -> Generator[NetworkTraceStep.Path, None, None]: + + # Because we consider clamps at the same position as a cut on the terminal 1 side, we do not stop at cuts at the same position when + # traversing towards t1, but we do when traversing towards t2. + if not clamp.ac_line_segment: + return + + _yielded_paths = set() + + def _mark(_path): + _yielded_paths.add(_path.to_terminal) + return _path + + yield from ( + _mark(path) for path in self._acls_traverse_from_terminal( + clamp.ac_line_segment, + path.to_terminal, + length_from_t1=clamp.length_from_T1_or_0, + towards_segment_t2=False, + can_stop_at_cut_at_same_position=False, + cut_at_same_position_from_terminal_number=1, + path_factory=path_factory + ) + ) + + yield from ( + _mark(path) for path in self._acls_traverse_from_terminal( + clamp.ac_line_segment, + path.to_terminal, + length_from_t1=clamp.length_from_T1_or_0, + towards_segment_t2=True, + can_stop_at_cut_at_same_position=True, + cut_at_same_position_from_terminal_number=1, + path_factory=path_factory + ) if path.to_terminal not in _yielded_paths + ) + + def _next_paths_from_cut( + self, + cut: Cut, + path: NetworkTraceStep.Path, + path_factory: PathFactory + ) -> Iterable[NetworkTraceStep.Path]: + # If the current path was from traversing an AcLineSegment, we need to step externally to other equipment. + if path.did_traverse_ac_line_segment: + yield from self._next_external_paths(path, path_factory) # Else we need to traverse the segment. - next_terminals = ( - self._next_external_paths(path, path_factory) if path.did_traverse_ac_line_segment else - (self._acls_traverse_from_terminal(cut.ac_line_segment, - path.to_terminal, - length_from_T1=cut.length_from_T1_or_0, - towards_segment_T2=path.to_terminal.sequence_number != 1, - can_stop_at_cut_at_same_position=False, - cut_at_same_position_from_terminal_number=path.to_terminal.sequence_number, - path_factory=path_factory - ) if cut.ac_line_segment else []) - ) + elif cut.ac_line_segment: + yield from self._acls_traverse_from_terminal( + cut.ac_line_segment, + path.to_terminal, + length_from_t1=cut.length_from_T1_or_0, + towards_segment_t2=path.to_terminal.sequence_number != 1, + can_stop_at_cut_at_same_position=False, + cut_at_same_position_from_terminal_number=path.to_terminal.sequence_number, + path_factory=path_factory + ) + # If the current path traced internally, we need to also return the external terminals - # Else we need to step internally to the Cut's other terminal. if path.traced_internally: # traversedAcLineSegment and tracedInternally should never both be true, so we should never get external terminals twice - return itertools.chain(next_terminals, self._next_external_paths(path, path_factory)) + yield from self._next_external_paths(path, path_factory) + # Else we need to step internally to the Cut's other terminal. else: other_terminal = cut.get_terminal_by_sn(2 if path.to_terminal.sequence_number == 1 else 1) - return itertools.chain(next_terminals, seq_term_map_to_path(other_terminal, path_factory)) + yield from seq_term_map_to_path(other_terminal, path_factory) - def _next_external_paths(self, path: NetworkTraceStep.Path, path_factory: PathFactory) -> Generator[NetworkTraceStep.Path, None, None]: + def _next_external_paths( + self, + path: NetworkTraceStep.Path, + path_factory: PathFactory + ) -> Generator[NetworkTraceStep.Path, None, None]: + #Busbars are only modelled with a single terminal. So if we find any we need to step to them before the #other (non busbar) equipment connected to the same connectivity node. Once the busbar has been #visited we then step to the other non busbar terminals connected to the same connectivity node. #If there are no busbars we can just step to all other connected terminals. if isinstance(path.to_equipment, BusbarSection): - return self._next_paths_from_busbar(path, path_factory) + yield from self._next_paths_from_busbar(path, path_factory) elif path.to_terminal.has_connected_busbars(): - return seq_term_map_to_path((t for t in path.to_terminal.connected_terminals() if isinstance(t.conducting_equipment, BusbarSection)), path_factory) + yield from seq_term_map_to_path((t for t in path.to_terminal.connected_terminals() if isinstance(t.conducting_equipment, BusbarSection)), path_factory) else: - return seq_term_map_to_path(path.to_terminal.connected_terminals(), path_factory) + yield from seq_term_map_to_path(path.to_terminal.connected_terminals(), path_factory) def _acls_traverse_from_terminal( self, acls: AcLineSegment, from_terminal: Terminal, - length_from_T1: float, - towards_segment_T2: bool, + length_from_t1: float, + towards_segment_t2: bool, can_stop_at_cut_at_same_position: bool, cut_at_same_position_from_terminal_number: int, path_factory: PathFactory @@ -193,75 +265,80 @@ def _acls_traverse_from_terminal( not exactly the same as the cut. This would yield more accurate and deterministic behaviour. :param from_terminal: The terminal on the segment to traverse from. This could either be a segment terminal, or a terminal from any cut or clamp on the segment. - :param length_from_T1: The length from terminal 1 the fromTerminal is. - :param towards_segment_T2: Use `true` if the segment should be traversed towards terminal 2, otherwise `False` to traverse towards terminal 1 + :param length_from_t1: The length from terminal 1 the fromTerminal is. + :param towards_segment_t2: Use `true` if the segment should be traversed towards terminal 2, otherwise `False` to traverse towards terminal 1 """ - # Can do a simple return if we don't need to do any special cuts/clamps processing + cuts, clamps = list(acls.cuts), list(acls.clamps) + # Can do a simple return if we don't need to do any special cuts/clamps processing if not any((cuts, clamps)): yield from seq_term_map_to_path(from_terminal.other_terminals(), path_factory, acls) - else: + return + + # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! + # We also make sure we filter out the cut or the clamp we are starting at, so we don't compare it in our checks + filter_func = lambda it: it != from_terminal.conducting_equipment and self.state_operators.is_in_service(it) + cuts: List[Cut] = list(filter(filter_func, cuts)) + clamps: List[Clamp] = list(filter(filter_func, clamps)) + + cuts_at_same_position = [it for it in cuts if it.length_from_T1_or_0 == length_from_t1] + stop_at_cuts_at_same_position = bool(can_stop_at_cut_at_same_position and cuts_at_same_position) + + def next_cut_length_from_terminal_1_func(): + if stop_at_cuts_at_same_position: + return length_from_t1 + elif towards_segment_t2: + return min((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 > length_from_t1), default=None) + else: + return max((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 < length_from_t1), default=None) - # We need to ignore cuts and clamps that are not "in service" because that means they do not exist! - # We also make sure we filter out the cut or the clamp we are starting at, so we don't compare it in our checks - filter_func = lambda it: it != from_terminal.conducting_equipment and self.state_operators.is_in_service(it) - cuts: List[Cut] = list(filter(filter_func, cuts)) - clamps: List[Clamp] = list(filter(filter_func, clamps)) - - cuts_at_same_position = list(filter(lambda it: it.length_from_T1_or_0 == length_from_T1, cuts)) - stop_at_cuts_at_same_position = bool(can_stop_at_cut_at_same_position and cuts_at_same_position) - - def next_cut_length_from_terminal_1_func(): - if stop_at_cuts_at_same_position: - return length_from_T1 - elif towards_segment_T2: - return min((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 > length_from_T1), default=None) - else: - return max((it.length_from_T1_or_0 for it in cuts if it.length_from_T1_or_0 < length_from_T1), default=None) - - next_cut_length_from_terminal_1 = next_cut_length_from_terminal_1_func() - - next_cuts = [it for it in cuts if it.length_from_T1_or_0 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 is not None else [] - - def next_term_length_from_term_1_func(): - if next_cut_length_from_terminal_1 is not None: - return next_cut_length_from_terminal_1 - elif towards_segment_T2: - return acls_length_or_max(acls) - else: - return 0.0 - - next_terminal_length_from_terminal_1 = next_term_length_from_term_1_func() - - def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: - if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_T2: - return lambda it: length_from_T1 <= it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 - elif towards_segment_T2: - return lambda it: it.length_from_T1_or_0 > length_from_T1 and it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 - elif (next_terminal_length_from_terminal_1 == 0.0) and len(next_cuts) == 0: - return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_T1_or_0 <= length_from_T1 - else: - return lambda it: it.length_from_T1_or_0 <= length_from_T1 and it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 - _filter = clamps_before_next_terminal_filter() - - clamps_before_next_terminal = filter(_filter, clamps) - - next_stop_terminals = [] if stop_at_cuts_at_same_position else ( - it.get_terminal(1 if towards_segment_T2 else 2) for it in next_cuts - ) if next_cuts else [acls.get_terminal(2 if towards_segment_T2 else 1)] - - next_terminals = ( - (it.get_terminal(cut_at_same_position_from_terminal_number) for it in cuts_at_same_position), - (it.get_terminal(1) for it in clamps_before_next_terminal), - next_stop_terminals - ) + next_cut_length_from_terminal_1 = next_cut_length_from_terminal_1_func() + + next_cuts = [it for it in cuts if it.length_from_T1_or_0 == next_cut_length_from_terminal_1] if next_cut_length_from_terminal_1 is not None else [] + + def next_term_length_from_term_1_func(): + if next_cut_length_from_terminal_1 is not None: + return next_cut_length_from_terminal_1 + elif towards_segment_t2: + return acls_length_or_max(acls) + else: + return 0.0 + + next_terminal_length_from_terminal_1 = next_term_length_from_term_1_func() + + def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: + if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_t2: + return lambda it: length_from_t1 <= it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 + elif towards_segment_t2: + return lambda it: it.length_from_T1_or_0 > length_from_t1 and it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 + elif (next_terminal_length_from_terminal_1 == 0.0) and len(next_cuts) == 0: + return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_T1_or_0 <= length_from_t1 + else: + return lambda it: it.length_from_T1_or_0 <= length_from_t1 and it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 + _filter = clamps_before_next_terminal_filter() + + clamps_before_next_terminal = (c for c in clamps if _filter(c)) + + next_stop_terminals = [] if stop_at_cuts_at_same_position else ( + it.get_terminal(1 if towards_segment_t2 else 2) for it in next_cuts + ) if next_cuts else [acls.get_terminal(2 if towards_segment_t2 else 1)] + + next_terminals = ( + (it.get_terminal(cut_at_same_position_from_terminal_number) for it in cuts_at_same_position), + (it.get_terminal(1) for it in clamps_before_next_terminal), + next_stop_terminals + ) - for generator in next_terminals: - yield from seq_term_map_to_path(generator, path_factory, acls) + for generator in next_terminals: + yield from seq_term_map_to_path(generator, path_factory, acls) -def seq_term_map_to_path(terms: Union[Terminal, Iterable[Terminal]], path_factory: PathFactory, traversed_acls: AcLineSegment=None - ) -> Generator[NetworkTraceStep.Path, None, None]: +def seq_term_map_to_path( + terms: Union[Terminal, Iterable[Terminal]], + path_factory: PathFactory, + traversed_acls: AcLineSegment=None +) -> Generator[NetworkTraceStep.Path, None, None]: + if isinstance(terms, Iterable): for terminal in terms: if terminal is not None: From 08c09c14981ff21515563b7b451e5adf4679e2d2 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 17:13:05 +1000 Subject: [PATCH 44/47] . Signed-off-by: Max Chesterfield --- .../networktrace/network_trace_step_path_provider.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py index e588abe70..28f67620e 100644 --- a/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py +++ b/src/zepben/evolve/services/network/tracing/networktrace/network_trace_step_path_provider.py @@ -5,7 +5,6 @@ from __future__ import annotations -import itertools import sys from typing import Generator, Optional, Callable, Iterable, List, Union, Type, TYPE_CHECKING @@ -311,11 +310,12 @@ def clamps_before_next_terminal_filter() -> Callable[[Clamp], bool]: if isinstance(from_terminal.conducting_equipment, AcLineSegment) and towards_segment_t2: return lambda it: length_from_t1 <= it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 elif towards_segment_t2: - return lambda it: it.length_from_T1_or_0 > length_from_t1 and it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 + return lambda it: length_from_t1 < it.length_from_T1_or_0 <= next_terminal_length_from_terminal_1 elif (next_terminal_length_from_terminal_1 == 0.0) and len(next_cuts) == 0: return lambda it: next_terminal_length_from_terminal_1 <= it.length_from_T1_or_0 <= length_from_t1 else: - return lambda it: it.length_from_T1_or_0 <= length_from_t1 and it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 + return lambda it: length_from_t1 >= it.length_from_T1_or_0 > next_terminal_length_from_terminal_1 + _filter = clamps_before_next_terminal_filter() clamps_before_next_terminal = (c for c in clamps if _filter(c)) @@ -338,7 +338,7 @@ def seq_term_map_to_path( path_factory: PathFactory, traversed_acls: AcLineSegment=None ) -> Generator[NetworkTraceStep.Path, None, None]: - + if isinstance(terms, Iterable): for terminal in terms: if terminal is not None: From a23ac3e44249186d717d9d58023e15daa3782e73 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 30 May 2025 17:50:03 +1000 Subject: [PATCH 45/47] add default_mrid_prefix to TestNetworkBuilder Signed-off-by: Max Chesterfield --- changelog.md | 1 + .../evolve/testing/test_network_builder.py | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index ef51d354a..a6cfb13f0 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ * `connect_to` - Connects the previously added item, rather than having to specify it again in `connect`. * 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. diff --git a/src/zepben/evolve/testing/test_network_builder.py b/src/zepben/evolve/testing/test_network_builder.py index db501a0dd..26655a1a3 100644 --- a/src/zepben/evolve/testing/test_network_builder.py +++ b/src/zepben/evolve/testing/test_network_builder.py @@ -394,7 +394,8 @@ def from_other( nominal_phases: PhaseCode = PhaseCode.ABC, num_terminals: Optional[int] = None, mrid: Optional[str] = None, - action: Callable[[ConductingEquipment], None] = null_action + action: Callable[[ConductingEquipment], None] = null_action, + default_mrid_prefix: Optional[str] = None ) -> 'TestNetworkBuilder': """ Start a new network island from a `ConductingEquipment` created by `creator`, updating the network pointer to the new `ConductingEquipment`. @@ -405,10 +406,13 @@ def from_other( :param num_terminals: The number of terminals to create on the new `ConductingEquipment`. Defaults to 2. :param mrid: Optional mRID for the new `ConductingEquipment`. :param action: An action that accepts the new `ConductingEquipment` to allow for additional initialisation. + :param default_mrid_prefix: mRID prefix to use for the new `ConductingEquipment` :return: This `TestNetworkBuilder` to allow for fluent use. """ - it = self._create_other(mrid, creator, nominal_phases, num_terminals) + if mrid and default_mrid_prefix: + raise ValueError('cant specify both mrid and default_mrid_prefix as your intention is unclear') + it = self._create_other(mrid, creator, nominal_phases, num_terminals, default_mrid_prefix=default_mrid_prefix) action(it) self._current = it return self @@ -420,7 +424,8 @@ def to_other( num_terminals: Optional[int] = None, mrid: Optional[str] = None, connectivity_node_mrid: Optional[str] = None, - action: Callable[[ConductingEquipment], None] = null_action + action: Callable[[ConductingEquipment], None] = null_action, + default_mrid_prefix: Optional[str] = None ) -> 'TestNetworkBuilder': """ Add a new `ConductingEquipment` to the network and connect it to the current network pointer, updating the network pointer to the new @@ -434,10 +439,13 @@ def to_other( :param connectivity_node_mrid: Optional id of the connectivity node used to connect this `ConductingEquipment` to the previous item. Will only be used if the previous item is not already connected. :param action: An action that accepts the new `ConductingEquipment` to allow for additional initialisation. + :param default_mrid_prefix: mRID prefix to use for the new `ConductingEquipment` :return: This `TestNetworkBuilder` to allow for fluent use. """ - it = self._create_other(mrid, creator, nominal_phases, num_terminals) + if mrid and default_mrid_prefix: + raise ValueError('cant specify both mrid and default_mrid_prefix as your intention is unclear') + it = self._create_other(mrid, creator, nominal_phases, num_terminals, default_mrid_prefix=default_mrid_prefix) self._connect(self._current, it, connectivity_node_mrid) action(it) self._current = it @@ -759,9 +767,10 @@ def _create_other( mrid: Optional[str], creator: Union[OtherCreator, Type[ConductingEquipment]], nominal_phases: PhaseCode, - num_terminals: Optional[int] + num_terminals: Optional[int], + default_mrid_prefix: Optional[str] = None ) -> ConductingEquipment: - o = creator(mrid=self._next_id(mrid, "o")) + o = creator(mrid=self._next_id(mrid, default_mrid_prefix or "o")) for i in range(1, (num_terminals if num_terminals is not None else 2) + 1): self._add_terminal(o, i, nominal_phases) From a5467c68d951b57598061a5ab8906c3a1d5879c9 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 3 Jun 2025 14:07:19 +1000 Subject: [PATCH 46/47] Type hint fixes Signed-off-by: Max Chesterfield --- .../evolve/testing/test_network_builder.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/zepben/evolve/testing/test_network_builder.py b/src/zepben/evolve/testing/test_network_builder.py index 26655a1a3..7c327c373 100644 --- a/src/zepben/evolve/testing/test_network_builder.py +++ b/src/zepben/evolve/testing/test_network_builder.py @@ -5,16 +5,14 @@ from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing -try: - from typing import Protocol, Any -except ImportError: - Protocol = object -from typing import Optional, Callable, List, Union, Type +from typing import Optional, Callable, List, Union, Type, TypeVar, Protocol from zepben.evolve import ConductingEquipment, NetworkService, PhaseCode, EnergySource, AcLineSegment, Breaker, Junction, Terminal, Feeder, LvFeeder, \ PowerTransformerEnd, PowerTransformer, EnergyConsumer, PowerElectronicsConnection, BusbarSection, Clamp, Cut, Site +SubclassesConductingEquipment = TypeVar('SubclassesConductingEquipment', bound=ConductingEquipment) + def null_action(_): """ @@ -27,7 +25,7 @@ def null_action(_): class OtherCreator(Protocol): """Type hint class""" - def __call__(self, mrid: str, *args, **kwargs) -> ConductingEquipment: Any + def __call__(self, mrid: str, *args, **kwargs) -> ConductingEquipment: ... class TestNetworkBuilder: @@ -390,11 +388,11 @@ def to_busbar_section( def from_other( self, - creator: Union[OtherCreator, Type[ConductingEquipment]], + creator: Union[OtherCreator, Type[SubclassesConductingEquipment]], nominal_phases: PhaseCode = PhaseCode.ABC, num_terminals: Optional[int] = None, mrid: Optional[str] = None, - action: Callable[[ConductingEquipment], None] = null_action, + action: Callable[[SubclassesConductingEquipment], None] = null_action, default_mrid_prefix: Optional[str] = None ) -> 'TestNetworkBuilder': """ @@ -419,12 +417,12 @@ def from_other( def to_other( self, - creator: Union[OtherCreator, Type[ConductingEquipment]], + creator: Union[OtherCreator, Type[SubclassesConductingEquipment]], nominal_phases: PhaseCode = PhaseCode.ABC, num_terminals: Optional[int] = None, mrid: Optional[str] = None, connectivity_node_mrid: Optional[str] = None, - action: Callable[[ConductingEquipment], None] = null_action, + action: Callable[[SubclassesConductingEquipment], None] = null_action, default_mrid_prefix: Optional[str] = None ) -> 'TestNetworkBuilder': """ @@ -765,11 +763,11 @@ def _create_energy_consumer(self, mrid: Optional[str], nominal_phases: PhaseCode def _create_other( self, mrid: Optional[str], - creator: Union[OtherCreator, Type[ConductingEquipment]], + creator: Union[OtherCreator, Type[SubclassesConductingEquipment]], nominal_phases: PhaseCode, num_terminals: Optional[int], default_mrid_prefix: Optional[str] = None - ) -> ConductingEquipment: + ) -> SubclassesConductingEquipment: o = creator(mrid=self._next_id(mrid, default_mrid_prefix or "o")) for i in range(1, (num_terminals if num_terminals is not None else 2) + 1): self._add_terminal(o, i, nominal_phases) From 803697f445393044e32eb2e184c715075512d786 Mon Sep 17 00:00:00 2001 From: Anthony Charlton Date: Tue, 3 Jun 2025 14:42:08 +1000 Subject: [PATCH 47/47] Fixed terminal test. Signed-off-by: Anthony Charlton --- test/cim/iec61970/base/core/test_terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cim/iec61970/base/core/test_terminal.py b/test/cim/iec61970/base/core/test_terminal.py index fdc30c801..8f0d1c3cb 100644 --- a/test/cim/iec61970/base/core/test_terminal.py +++ b/test/cim/iec61970/base/core/test_terminal.py @@ -18,7 +18,7 @@ "sequence_number": integers(min_value=MIN_32_BIT_INTEGER, max_value=MAX_32_BIT_INTEGER), "normal_feeder_direction": sampled_from(FeederDirection), "current_feeder_direction": sampled_from(FeederDirection), - "traced_phases": builds(TracedPhases), + "traced_phases": builds(TracedPhases, phase_status=integers(min_value=0, max_value=15)), "connectivity_node": builds(ConnectivityNode) } @@ -58,7 +58,7 @@ def test_terminal_constructor_kwargs(conducting_equipment, phases, sequence_numb assert t.sequence_number == sequence_number assert t.normal_feeder_direction == normal_feeder_direction assert t.current_feeder_direction == current_feeder_direction - assert t.traced_phases == TracedPhases() + assert t.traced_phases == traced_phases assert t.connectivity_node == connectivity_node