Skip to content

Commit b037f37

Browse files
authored
[DEV-2296] Do not find LvFeeders starting with open switches in the Site (#181)
Signed-off-by: Max Chesterfield <max.chesterfield@zepben.com> Co-authored-by: Max Chesterfield <max.chesterfield@zepben.com>
1 parent 02bc855 commit b037f37

9 files changed

Lines changed: 85 additions & 38 deletions

File tree

changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* None.
1111

1212
### Fixes
13-
* None.
13+
When finding `LvFeeders` in the `Site` we will now exclude `LvFeeders` that start with an open `Switch`
1414

1515
### Notes
1616
* None.

src/zepben/evolve/model/cim/iec61970/base/core/equipment_container.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024 Zeppelin Bend Pty Ltd
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
22
# This Source Code Form is subject to the terms of the Mozilla Public
33
# License, v. 2.0. If a copy of the MPL was not distributed with this
44
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
@@ -8,7 +8,8 @@
88
from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable
99

1010
if TYPE_CHECKING:
11-
from zepben.evolve import Equipment, Terminal, Substation, LvFeeder, ConductingEquipment, NetworkStateOperators
11+
from zepben.evolve import Equipment, Terminal, Substation, LvFeeder, NetworkStateOperators
12+
from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment
1213

1314
from zepben.evolve.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer
1415
from zepben.evolve.util import nlen, ngen, safe_remove_by_id
@@ -445,10 +446,11 @@ class Site(EquipmentContainer):
445446
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.
446447
"""
447448

448-
def find_lv_feeders(self, lv_feeder_start_points: Iterable[ConductingEquipment], state_operators: NetworkStateOperators) -> Generator[LvFeeder]:
449+
def find_lv_feeders(self, lv_feeder_start_points: Iterable[ConductingEquipment], state_operators: NetworkStateOperators) -> Generator[LvFeeder, None, None]:
450+
from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment
449451
for ce in state_operators.get_equipment(self):
450-
if isinstance(ConductingEquipment, ce):
452+
if isinstance(ce, ConductingEquipment):
451453
if ce in lv_feeder_start_points:
452-
if not state_operators.is_open(ce):
454+
if not state_operators.is_open(ce): # Exclude any open switch that might be energised by a different feeder on the other side
453455
for lv_feeder in ce.lv_feeders(state_operators):
454456
yield lv_feeder

src/zepben/evolve/services/network/network_service.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024 Zeppelin Bend Pty Ltd
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
22
# This Source Code Form is subject to the terms of the Mozilla Public
33
# License, v. 2.0. If a copy of the MPL was not distributed with this
44
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
@@ -11,7 +11,7 @@
1111
import logging
1212
from enum import Enum
1313
from pathlib import Path
14-
from typing import TYPE_CHECKING, Dict, List, Union, Iterable, Optional, Generator
14+
from typing import TYPE_CHECKING, Dict, List, Union, Iterable, Optional, Generator, Set
1515

1616
from zepben.evolve.util import ngen
1717

@@ -282,9 +282,9 @@ def aux_equipment_by_terminal(self) -> Dict[Terminal, List[AuxiliaryEquipment]]:
282282
return eq_by_term
283283

284284
@property
285-
def feeder_start_points(self) -> Generator[ConductingEquipment, None, None]:
286-
return ngen(feeder.normal_head_terminal.conducting_equipment for feeder in self.objects(Feeder) if feeder.normal_head_terminal)
285+
def feeder_start_points(self) -> Set[ConductingEquipment]:
286+
return {it.normal_head_terminal.conducting_equipment for it in self.objects(Feeder) if it.normal_head_terminal}
287287

288288
@property
289-
def lv_feeder_start_points(self) -> Generator[ConductingEquipment, None, None]:
290-
return ngen(lv_feeder.normal_head_terminal.conducting_equipment for lv_feeder in self.objects(LvFeeder) if lv_feeder.normal_head_terminal)
289+
def lv_feeder_start_points(self) -> Set[ConductingEquipment]:
290+
return {it.normal_head_terminal.conducting_equipment for it in self.objects(LvFeeder) if it.normal_head_terminal}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# Copyright 2024 Zeppelin Bend Pty Ltd
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
22
# This Source Code Form is subject to the terms of the Mozilla Public
33
# License, v. 2.0. If a copy of the MPL was not distributed with this
44
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
from collections.abc import Collection
6-
from typing import Iterable, Generator, Union, List, Dict, Any
6+
from typing import Iterable, Generator, Union, List, Dict, Any, Set
77

88
from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch, Equipment, LvFeeder
99
from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment
@@ -72,7 +72,7 @@ def _feeder_energizes(self, feeders: Iterable[Union[LvFeeder, Feeder]], lv_feede
7272
for lv_feeder in lv_feeders:
7373
self.network_state_operators.associate_energizing_feeder(feeder, lv_feeder)
7474

75-
def _feeder_try_energize_lv_feeders(self, feeders: Iterable[Feeder], lv_feeder_start_points: Generator[ConductingEquipment, None, None], to_equipment: PowerTransformer):
75+
def _feeder_try_energize_lv_feeders(self, feeders: Iterable[Feeder], lv_feeder_start_points: Set[ConductingEquipment], to_equipment: PowerTransformer):
7676
sites = []
7777
for eq in to_equipment:
7878
sites.extend(eq.sites)
@@ -114,8 +114,8 @@ async def run(self,
114114

115115
async def run_with_feeders(self,
116116
terminal: Terminal,
117-
feeder_start_points: Generator[ConductingEquipment, None, None],
118-
lv_feeder_start_points: Generator[ConductingEquipment, None, None],
117+
feeder_start_points: Set[ConductingEquipment],
118+
lv_feeder_start_points: Set[ConductingEquipment],
119119
terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]],
120120
feeders_to_assign: List[Feeder]):
121121

@@ -132,8 +132,8 @@ async def run_with_feeders(self,
132132

133133
async def _create_trace(self,
134134
terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]],
135-
feeder_start_points: Generator[ConductingEquipment, None, None],
136-
lv_feeder_start_points: Generator[ConductingEquipment, None, None],
135+
feeder_start_points: Set[ConductingEquipment],
136+
lv_feeder_start_points: Set[ConductingEquipment],
137137
feeders_to_assign: List[Feeder]) -> NetworkTrace[Any]:
138138

139139
def _reached_lv(ce: ConductingEquipment):
@@ -159,7 +159,7 @@ async def _process(self,
159159
step_path: NetworkTraceStep.Path,
160160
step_context: StepContext,
161161
terminal_to_aux_equipment: Dict[Terminal, Collection[AuxiliaryEquipment]],
162-
lv_feeder_start_points: Generator[ConductingEquipment, None, None],
162+
lv_feeder_start_points: Set[ConductingEquipment],
163163
feeders_to_assign: List[Feeder]):
164164

165165
if step_path.traced_internally and not step_context.is_start_item:

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
# Copyright 2024 Zeppelin Bend Pty Ltd
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
22
# This Source Code Form is subject to the terms of the Mozilla Public
33
# License, v. 2.0. If a copy of the MPL was not distributed with this
44
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5-
from collections.abc import Iterable
6-
from typing import Collection, List, Generator, TypeVar, Dict
75

6+
from typing import Collection, List, Generator, TypeVar, Dict, Set
87

98
from zepben.evolve import Switch, AuxiliaryEquipment, ProtectedSwitch
109
from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment
@@ -64,6 +63,7 @@ async def run(self,
6463
for feeder in head_equipment.feeders(self.network_state_operators):
6564
self.network_state_operators.associate_energizing_feeder(feeder, lv_feeder)
6665

66+
# We can run from each LV feeder as we process them, as being associated with their energizing feeders is not a requirement of the trace.
6767
await self.run_with_feeders(lv_feeder.normal_head_terminal,
6868
lv_feeder_start_points,
6969
terminal_to_aux_equipment,
@@ -77,7 +77,7 @@ async def run(self,
7777

7878
async def run_with_feeders(self,
7979
terminal: Terminal,
80-
lv_feeder_start_points: Iterable[ConductingEquipment],
80+
lv_feeder_start_points: Set[ConductingEquipment],
8181
terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]],
8282
lv_feeders_to_assign: List[LvFeeder]):
8383

@@ -89,12 +89,12 @@ async def run_with_feeders(self,
8989
if isinstance(start_ce, Switch) and self.network_state_operators.is_open(start_ce):
9090
self._associate_equipment_with_containers(lv_feeders_to_assign, [start_ce])
9191
else:
92-
traversal = await self._create_trace(terminal_to_aux_equipment, lv_feeder_start_points, lv_feeders_to_assign)
92+
traversal = self._create_trace(terminal_to_aux_equipment, lv_feeder_start_points, lv_feeders_to_assign)
9393
await traversal.run(terminal, False)
9494

95-
async def _create_trace(self,
95+
def _create_trace(self,
9696
terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]],
97-
lv_feeder_start_points: Iterable[ConductingEquipment],
97+
lv_feeder_start_points: Set[ConductingEquipment],
9898
lv_feeders_to_assign: List[LvFeeder]) -> NetworkTrace[T]:
9999

100100
def _reached_hv(ce: ConductingEquipment):
@@ -123,19 +123,18 @@ async def _process(self,
123123
found_lv_feeder: bool,
124124
step_context: StepContext,
125125
terminal_to_aux_equipment: Dict[Terminal, Collection[AuxiliaryEquipment]],
126-
lv_feeder_start_points: Iterable[ConductingEquipment],
126+
lv_feeder_start_points: Set[ConductingEquipment],
127127
lv_feeders_to_assign: List[LvFeeder]):
128128

129129
if step_path.traced_internally and not step_context.is_start_item:
130130
return
131131

132132
if found_lv_feeder:
133-
found_lv_feeders = self._find_lv_feeders(step_path.to_equipment, lv_feeder_start_points)
133+
found_lv_feeders = list(self._find_lv_feeders(step_path.to_equipment, lv_feeder_start_points))
134134

135-
energizing_feeders = list(self.network_state_operators.get_energizing_feeders(it) for it in found_lv_feeders)
136-
137-
for feeder_group in (lv_feeders_to_assign, found_lv_feeders):
138-
self._feeder_energizes(feeder_group, energizing_feeders)
135+
for energizing_feeder in (self.network_state_operators.get_energizing_feeders(it) for it in found_lv_feeders):
136+
for feeder_group in (lv_feeders_to_assign, found_lv_feeders):
137+
self._feeder_energizes(feeder_group, energizing_feeder)
139138

140139
try:
141140
aux_equip_for_this_terminal = terminal_to_aux_equipment[step_path.to_terminal]
@@ -148,7 +147,7 @@ async def _process(self,
148147
if isinstance(step_path.to_equipment, ProtectedSwitch):
149148
self._associate_relay_systems_with_containers(lv_feeders_to_assign, step_path.to_equipment)
150149

151-
def _find_lv_feeders(self, ce: ConductingEquipment, lv_feeder_start_points: Iterable[ConductingEquipment]) -> Generator[LvFeeder, None, None]:
150+
def _find_lv_feeders(self, ce: ConductingEquipment, lv_feeder_start_points: Set[ConductingEquipment]) -> Generator[LvFeeder, None, None]:
152151
sites = list(ce.sites)
153152
if sites:
154153
for site in sites:
@@ -159,4 +158,4 @@ def _find_lv_feeders(self, ce: ConductingEquipment, lv_feeder_start_points: Iter
159158
yield feeder
160159

161160
def _lv_feeders_from_terminal(self, terminal: Terminal) -> List[LvFeeder]:
162-
return terminal.conducting_equipment.lv_feeders(self.network_state_operators)
161+
return list(terminal.conducting_equipment.lv_feeders(self.network_state_operators))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Tracing:
1919
def network_trace(network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL,
2020
action_step_type: NetworkTraceActionType=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT,
2121
queue: TraversalQueue[NetworkTraceStep[T]]=TraversalQueue.depth_first(),
22-
compute_data: ComputeData[T]=None
22+
compute_data: Union[ComputeData[T], Callable]=None
2323
) -> NetworkTrace[T]:
2424
"""
2525
Creates a `NetworkTrace` that computes contextual data for every step.

src/zepben/evolve/services/network/tracing/phases/phase_inferrer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def _has_none_phase(self, terminal: Terminal) -> bool:
7575

7676
@staticmethod
7777
def _has_xy_phases(terminal: Terminal) -> bool:
78-
return any(p in terminal.phases for p in (SinglePhaseKind.X, SinglePhaseKind.Y))
78+
return SinglePhaseKind.Y in terminal.phases.single_phases or SinglePhaseKind.X in terminal.phases.single_phases
7979

8080
def _find_terminal_at_start_of_missing_phases(
8181
self,

src/zepben/evolve/testing/test_network_builder.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,9 @@ def _create_feeder(self, mrid: Optional[str], head_equipment: ConductingEquipmen
612612
)
613613

614614
f.add_equipment(head_equipment)
615+
f.add_current_equipment(head_equipment)
615616
head_equipment.add_container(f)
617+
head_equipment.add_current_container(f)
616618

617619
self.network.add(f)
618620
return f
@@ -624,7 +626,9 @@ def _create_lv_feeder(self, mrid: Optional[str], head_equipment: ConductingEquip
624626
)
625627

626628
lvf.add_equipment(head_equipment)
629+
lvf.add_current_equipment(head_equipment)
627630
head_equipment.add_container(lvf)
631+
head_equipment.add_current_container(lvf)
628632

629633
self.network.add(lvf)
630634
return lvf

test/cim/iec61970/base/core/test_site.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
# This Source Code Form is subject to the terms of the Mozilla Public
33
# License, v. 2.0. If a copy of the MPL was not distributed with this
44
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
import pytest
56
from hypothesis import given
67

8+
from build.lib.zepben.evolve.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators
79
from cim.iec61970.base.core.test_equipment_container import equipment_container_kwargs, verify_equipment_container_constructor_default, \
810
verify_equipment_container_constructor_kwargs, verify_equipment_container_constructor_args, equipment_container_args
9-
from zepben.evolve import Site
11+
from zepben.evolve import Site, TestNetworkBuilder, Equipment, AssignToLvFeeders, LvFeeder
1012

1113
site_kwargs = equipment_container_kwargs
1214
site_args = equipment_container_args
@@ -23,3 +25,43 @@ def test_site_constructor_kwargs(**kwargs):
2325

2426
def test_site_constructor_args():
2527
verify_equipment_container_constructor_args(Site(*site_args))
28+
29+
@pytest.mark.asyncio
30+
async def test_find_lv_feeders_excludes_open_switches():
31+
#
32+
# tx0 21 b1(lvf5) 21--c2--2
33+
# 21 b3(lvf6) 21--c4--2
34+
#
35+
site = Site()
36+
network = (TestNetworkBuilder()
37+
.from_power_transformer(action=lambda pt: _add_to_site(pt, site)) # tx0
38+
.from_breaker(is_normally_open=True, is_open=True, action=lambda b: _add_to_site(b, site)) # b1
39+
.to_acls() # c2
40+
.branch_from('tx0')
41+
.to_breaker(is_normally_open=False, is_open=False, action=lambda b: _add_to_site(b, site)) # b3
42+
.to_acls() # c4
43+
.add_lv_feeder('b1') # lvf5
44+
.add_lv_feeder('b3') # lvf6
45+
).network
46+
47+
assign_to_lv_feeders = AssignToLvFeeders()
48+
49+
await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.NORMAL, start_terminal=network.get('b1-t2'))
50+
await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.NORMAL, start_terminal=network.get('b3-t2'))
51+
await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.CURRENT, start_terminal=network.get('b1-t2'))
52+
await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.CURRENT, start_terminal=network.get('b3-t2'))
53+
54+
lvf6 = network.get('lvf6', LvFeeder)
55+
normal_lv_feeders = list(site.find_lv_feeders(network.lv_feeder_start_points, NetworkStateOperators.NORMAL))
56+
assert normal_lv_feeders == [lvf6]
57+
58+
current_lv_feeders = list(site.find_lv_feeders(network.lv_feeder_start_points, NetworkStateOperators.CURRENT))
59+
assert current_lv_feeders == [lvf6]
60+
61+
62+
def _add_to_site(equipment: Equipment, site: Site):
63+
site.add_equipment(equipment)
64+
equipment.add_container(site)
65+
site.add_current_equipment(equipment)
66+
equipment.add_current_container(site)
67+

0 commit comments

Comments
 (0)