Skip to content

Commit 7d38003

Browse files
committed
More tests, again
1 parent a6a25fe commit 7d38003

10 files changed

Lines changed: 229 additions & 20 deletions

File tree

src/zepben/evolve/services/network/tracing/feeder/clear_direction.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
from zepben.evolve.model.cim.iec61970.base.core.terminal import Terminal
1010

11-
from zepben.evolve import FeederDirection, Tracing, WeightedPriorityQueue, Traversal
11+
from zepben.evolve import FeederDirection, Traversal
12+
from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing
13+
from zepben.evolve.services.network.tracing.traversal.weighted_priority_queue import WeightedPriorityQueue
1214
from zepben.evolve.services.network.tracing.networktrace.network_trace import NetworkTrace
1315
from zepben.evolve.services.network.tracing.networktrace.network_trace_action_type import NetworkTraceActionType
1416
from zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators

src/zepben/evolve/services/network/tracing/networktrace/conditions/open_condition.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ def __init__(self, is_open: Callable[[Switch, SinglePhaseKind], bool], phase: Si
2727
self._phase = phase
2828

2929
def should_queue_matched_step(self, next_item: NetworkTraceStep[T], next_context: StepContext, current_item: NetworkTraceStep[T], current_context: StepContext) -> bool:
30-
return not self._is_open(next_item.path.to_equipment, self._phase)
30+
from zepben.evolve.model.cim.iec61970.base.wires.switch import Switch
31+
equip = next_item.path.to_equipment
32+
if isinstance(equip, Switch):
33+
return not self._is_open(equip, self._phase)
34+
else:
35+
return True
3136

3237
def should_queue_start_item(self, item: T) -> bool:
3338
return True

src/zepben/evolve/services/network/tracing/networktrace/operators/open_state_operators.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
if TYPE_CHECKING:
1818
from zepben.evolve.model.cim.iec61970.base.wires.switch import Switch
19+
from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment
1920

2021
T = TypeVar('T')
2122

@@ -27,27 +28,42 @@ class OpenStateOperators(StateOperator):
2728

2829
@staticmethod
2930
@abstractmethod
30-
def is_open(switch: Switch, phase: SinglePhaseKind=None) -> bool:
31+
def is_open_switch(switch: Switch, phase: SinglePhaseKind=None) -> bool:
3132
"""
3233
Checks if the specified switch is open. Optionally checking the state of a specific phase.
3334
3435
`switch` The switch to check open state.
3536
`phase` The specific phase to check, or `null` to check if any phase is open.
36-
Returns `true` if open; `false` otherwise.
37+
Returns `True` if open; `False` otherwise.
3738
"""
3839
raise NotImplementedError()
3940

41+
@classmethod
42+
def is_open(cls, conducting_equipment: ConductingEquipment, phase: SinglePhaseKind=None) -> bool:
43+
"""
44+
Convenience method that checks if the `conducting_equipment` is a `Switch` before checking if its open
45+
46+
:param conducting_equipment: The conducting equipment to check open state
47+
:param phase: The specified phase to check, or 'None' to check if any phase is open
48+
Returns `True` if conducting equipment is a switch and its open; `False` otherwise
49+
"""
50+
from zepben.evolve.model.cim.iec61970.base.wires.switch import Switch # FIXME: circular import
51+
52+
if isinstance(conducting_equipment, Switch):
53+
return cls.is_open_switch(conducting_equipment, phase)
54+
return False
55+
4056
@staticmethod
4157
@abstractmethod
4258
def set_open(switch: Switch, is_open: bool, phase: SinglePhaseKind=None) -> None:
4359
"""
4460
Sets the open state of the specified switch. Optionally applies the state to a specific phase.
4561
4662
`switch` The switch for which to set the open state.
47-
`isOpen` The desired open state (`true` for open, `false` for closed).
48-
`phase` The specific phase to set, or `null` to apply to all phases.
63+
`isOpen` The desired open state (`True` for open, `False` for closed).
64+
`phase` The specific phase to set, or `None` to apply to all phases.
4965
"""
50-
pass
66+
raise NotImplementedError()
5167

5268
@classmethod
5369
def stop_at_open(cls) -> NetworkTraceQueueCondition[T]:
@@ -59,11 +75,8 @@ class NormalOpenStateOperators(OpenStateOperators):
5975
Operates on the normal state of the `Switch`
6076
"""
6177
@staticmethod
62-
def is_open(switch: Switch, phase:SinglePhaseKind=None) -> Optional[bool]:
63-
try:
64-
return switch.is_normally_open(phase)
65-
except AttributeError:
66-
return False
78+
def is_open_switch(switch: Switch, phase:SinglePhaseKind=None) -> Optional[bool]:
79+
return switch.is_normally_open(phase)
6780

6881
@staticmethod
6982
def set_open(switch: Switch, is_open: bool, phase: SinglePhaseKind = None) -> None:
@@ -75,16 +88,13 @@ class CurrentOpenStateOperators(OpenStateOperators):
7588
Operates on the current state of the `Switch`
7689
"""
7790
@staticmethod
78-
def is_open(switch: Switch, phase: SinglePhaseKind = None) -> Optional[bool]:
79-
try:
80-
return switch.is_open(phase)
81-
except AttributeError:
82-
return False
91+
def is_open_switch(switch: Switch, phase: SinglePhaseKind = None) -> Optional[bool]:
92+
return switch.is_open(phase)
8393

8494
@staticmethod
8595
def set_open(switch: Switch, is_open: bool, phase: SinglePhaseKind = None) -> None:
8696
switch.set_open(is_open, phase)
8797

8898

8999
OpenStateOperators.NORMAL = NormalOpenStateOperators()
90-
OpenStateOperators.CURRENT = CurrentOpenStateOperators()
100+
OpenStateOperators.CURRENT = CurrentOpenStateOperators()

src/zepben/evolve/services/network/tracing/networktrace/tracing.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def network_trace(network_state_operators: NetworkStateOperators=NetworkStateOpe
3737
return NetworkTrace.non_branching(network_state_operators, queue, action_step_type, compute_data)
3838

3939
@staticmethod
40-
def network_trace_branching(network_state_operators: NetworkStateOperators,
40+
def network_trace_branching(network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL,
4141
action_step_type: NetworkTraceActionType=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT,
4242
queue_factory: Callable[[], TraversalQueue[NetworkTraceStep[T]]]=lambda: TraversalQueue.depth_first(),
4343
branch_queue_factory: Callable[[], TraversalQueue[NetworkTraceStep[T]]]=lambda: TraversalQueue.breadth_first(),
@@ -57,6 +57,7 @@ def set_direction():
5757

5858
@staticmethod
5959
def clear_direction():
60+
from zepben.evolve.services.network.tracing.feeder.clear_direction import ClearDirection
6061
return ClearDirection()
6162

6263
@staticmethod

test/database/sqlite/network/test_network_database_schema.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
NetworkDatabaseReader, NetworkServiceComparator, LvFeeder, CurrentTransformerInfo, PotentialTransformerInfo, CurrentTransformer, \
2323
PotentialTransformer, SwitchInfo, RelayInfo, CurrentRelay, EvChargingUnit, TapChangerControl, DistanceRelay, VoltageRelay, ProtectionRelayScheme, \
2424
ProtectionRelaySystem, Ground, GroundDisconnector, SeriesCompensator, NetworkService, StreetAddress, TownDetail, StreetDetail, GroundingImpedance, \
25-
PetersenCoil, ReactiveCapabilityCurve, SynchronousMachine, PanDemandResponseFunction, BatteryControl, StaticVarCompensator, Tracing, NetworkStateOperators
25+
PetersenCoil, ReactiveCapabilityCurve, SynchronousMachine, PanDemandResponseFunction, BatteryControl, StaticVarCompensator, Tracing, NetworkStateOperators, \
26+
NetworkTraceStep
2627
from zepben.evolve.model.cim.iec61970.base.wires.clamp import Clamp
2728
from zepben.evolve.model.cim.iec61970.base.wires.cut import Cut
2829
from zepben.evolve.model.cim.iec61970.base.wires.per_length_phase_impedance import PerLengthPhaseImpedance
@@ -48,6 +49,25 @@
4849
from database.sqlite.schema_utils import SchemaNetworks
4950

5051

52+
# FIXME: see Line [305]
53+
54+
class PatchedNetworkTraceStepPath(NetworkTraceStep.Path):
55+
@property
56+
def from_equipment(self):
57+
try:
58+
return super().from_equipment
59+
except AttributeError:
60+
return
61+
62+
@property
63+
def to_equipment(self):
64+
try:
65+
return super().to_equipment
66+
except AttributeError:
67+
return
68+
69+
NetworkTraceStep.Path = PatchedNetworkTraceStepPath
70+
5171
# pylint: disable=too-many-public-methods
5272
class TestNetworkDatabaseSchema(CimDatabaseSchemaCommonTests[NetworkService, NetworkDatabaseWriter, NetworkDatabaseReader, NetworkServiceComparator]):
5373

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
from zepben.evolve import Terminal, FeederDirection
6+
from zepben.evolve.services.network.tracing.networktrace.operators.feeder_direction_state_operations import FeederDirectionStateOperations
7+
8+
9+
class TestFeederDirectionStateOperators:
10+
11+
normal = FeederDirectionStateOperations.NORMAL
12+
current = FeederDirectionStateOperations.CURRENT
13+
14+
def test_get_direction(self):
15+
for operations, attr in ((self.normal, 'normal_feeder_direction'), (self.current, 'current_feeder_direction')):
16+
terminal = Terminal()
17+
setattr(terminal, attr, FeederDirection.UPSTREAM)
18+
assert operations.get_direction(terminal) == FeederDirection.UPSTREAM
19+
20+
21+
def test_set_direction(self):
22+
for operations, attr in ((self.normal, 'normal_feeder_direction'), (self.current, 'current_feeder_direction')):
23+
terminal = Terminal()
24+
setattr(terminal, attr, FeederDirection.NONE)
25+
assert operations.set_direction(terminal, FeederDirection.UPSTREAM)
26+
assert getattr(terminal, attr) == FeederDirection.UPSTREAM
27+
28+
# Attempting to add a direction to the terminal already has should return False
29+
assert not operations.set_direction(terminal, FeederDirection.UPSTREAM)
30+
31+
# Setting direction should replace the existing direction
32+
assert operations.set_direction(terminal, FeederDirection.DOWNSTREAM)
33+
assert getattr(terminal, attr) == FeederDirection.DOWNSTREAM
34+
35+
def test_add_direction(self):
36+
for operations, attr in ((self.normal, 'normal_feeder_direction'), (self.current, 'current_feeder_direction')):
37+
terminal = Terminal()
38+
setattr(terminal, attr, FeederDirection.NONE)
39+
assert operations.add_direction(terminal, FeederDirection.UPSTREAM)
40+
assert getattr(terminal, attr) == FeederDirection.UPSTREAM
41+
42+
# Attempting to add a direction the terminal already has should return False
43+
assert not operations.add_direction(terminal, FeederDirection.UPSTREAM)
44+
45+
# Adding a direction should end up with a combination of the directions
46+
assert operations.add_direction(terminal, FeederDirection.DOWNSTREAM)
47+
assert getattr(terminal, attr) == FeederDirection.BOTH
48+
49+
def test_remove_direction(self):
50+
for operations, attr in ((self.normal, 'normal_feeder_direction'), (self.current, 'current_feeder_direction')):
51+
terminal = Terminal()
52+
setattr(terminal, attr, FeederDirection.BOTH)
53+
assert operations.remove_direction(terminal, FeederDirection.UPSTREAM)
54+
assert getattr(terminal, attr) == FeederDirection.DOWNSTREAM
55+
56+
# Attempting to remove a direction the terminal does not have should return False
57+
assert not operations.remove_direction(terminal, FeederDirection.UPSTREAM)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
from unittest.mock import MagicMock
6+
7+
from zepben.evolve import Equipment
8+
from zepben.evolve.services.network.tracing.networktrace.operators.in_service_state_operators import InServiceStateOperators
9+
10+
11+
class TestInServiceStateOperators:
12+
normal = InServiceStateOperators.NORMAL
13+
current = InServiceStateOperators.CURRENT
14+
15+
def test_is_in_service(self):
16+
for operator, attr in ((self.normal, 'normally_in_service'), (self.current, 'in_service')):
17+
for _bool in (True, False):
18+
equipment = MagicMock(Equipment)
19+
setattr(equipment, attr, _bool)
20+
21+
assert operator.is_in_service(equipment) == _bool
22+
23+
def test_set_in_service(self):
24+
for operator, attr in ((self.normal, 'normally_in_service'), (self.current, 'in_service')):
25+
for _bool in (True, False):
26+
equipment = MagicMock(Equipment)
27+
assert getattr(equipment, attr)
28+
29+
operator.set_in_service(equipment, False)
30+
31+
assert not getattr(equipment, attr)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
from unittest.mock import MagicMock
6+
7+
from zepben.evolve import Switch, SinglePhaseKind
8+
from zepben.evolve.services.network.tracing.networktrace.operators.open_state_operators import OpenStateOperators
9+
10+
11+
class FlipFlopper:
12+
def __init__(self, state):
13+
self.state = state
14+
15+
def __call__(self, *args, **kwargs):
16+
_state = self.state
17+
self.state = not _state
18+
return _state
19+
20+
class TestOpenStateOperators:
21+
22+
normal = OpenStateOperators.NORMAL
23+
current = OpenStateOperators.CURRENT
24+
25+
def test_is_open_check_swith_open_state(self):
26+
for operators, attr in ((self.normal, 'is_normally_open'), (self.current, 'is_open')):
27+
switch = MagicMock(Switch)
28+
flopper = FlipFlopper(False)
29+
setattr(switch, attr, lambda spk: flopper())
30+
31+
assert not operators.is_open(switch, SinglePhaseKind.A)
32+
assert operators.is_open(switch, SinglePhaseKind.A)
33+
34+
def test_set_open(self):
35+
for operators, attr in ((self.normal, 'is_normally_open'), (self.current, 'is_open')):
36+
switch = MagicMock(Switch)
37+
operators.set_open(switch, True, SinglePhaseKind.A)
38+
assert getattr(switch, attr)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
from zepben.evolve import Terminal
6+
from zepben.evolve.services.network.tracing.networktrace.operators.phase_state_operators import PhaseStateOperators
7+
8+
9+
class TestPhaseStateOperators:
10+
11+
normal = PhaseStateOperators.NORMAL
12+
current = PhaseStateOperators.CURRENT
13+
14+
def test_phase_status(self):
15+
for operators, attr in ((self.normal, 'normal_phases'), (self.current, 'current_phases')):
16+
terminal = Terminal()
17+
# FIXME: should be comparing the actual PhaseStatus object, but Terminal makes a new one on every call
18+
assert operators.phase_status(terminal).terminal is getattr(terminal, attr).terminal
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
import pytest
6+
7+
from zepben.evolve.services.network.tracing.networktrace.tracing import Tracing
8+
from zepben.evolve.testing.test_network_builder import TestNetworkBuilder
9+
10+
11+
class TestNetworkTrace:
12+
13+
@pytest.mark.asyncio
14+
async def test_can_run_large_branching_traces(self):
15+
builder = TestNetworkBuilder()
16+
network = builder.network
17+
18+
builder.from_junction(num_terminals=1) \
19+
.to_acls()
20+
21+
for i in range(250):
22+
builder.to_junction(mrid=f'junc-{i}', num_terminals=3) \
23+
.to_acls(mrid=f'acls-{i}-top') \
24+
.from_acls(mrid=f'acls-{i}-bottom') \
25+
.connect(f'junc-{i}', f'acls-{i}-bottom', 2, 1)
26+
27+
await Tracing.network_trace_branching().run(network['j0'].get_terminal_by_sn(1))

0 commit comments

Comments
 (0)