Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* None.

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

### Notes
* None.
Expand Down
Original file line number Diff line number Diff line change
@@ -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/.
Expand All @@ -8,7 +8,8 @@
from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable

if TYPE_CHECKING:
from zepben.evolve import Equipment, Terminal, Substation, LvFeeder, ConductingEquipment, NetworkStateOperators
from zepben.evolve import Equipment, Terminal, Substation, LvFeeder, NetworkStateOperators
from zepben.evolve.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment

from zepben.evolve.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer
from zepben.evolve.util import nlen, ngen, safe_remove_by_id
Expand Down Expand Up @@ -445,10 +446,11 @@ 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]:
def find_lv_feeders(self, lv_feeder_start_points: Iterable[ConductingEquipment], state_operators: 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(ConductingEquipment, ce):
if isinstance(ce, ConductingEquipment):
if ce in lv_feeder_start_points:
if not state_operators.is_open(ce):
if not state_operators.is_open(ce): # Exclude any open switch that might be energised by a different feeder on the other side
for lv_feeder in ce.lv_feeders(state_operators):
yield lv_feeder
12 changes: 6 additions & 6 deletions src/zepben/evolve/services/network/network_service.py
Original file line number Diff line number Diff line change
@@ -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/.
Expand All @@ -11,7 +11,7 @@
import logging
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Union, Iterable, Optional, Generator
from typing import TYPE_CHECKING, Dict, List, Union, Iterable, Optional, Generator, Set

from zepben.evolve.util import ngen

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

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

@property
def lv_feeder_start_points(self) -> Generator[ConductingEquipment, None, None]:
return ngen(lv_feeder.normal_head_terminal.conducting_equipment for lv_feeder in self.objects(LvFeeder) if lv_feeder.normal_head_terminal)
def lv_feeder_start_points(self) -> Set[ConductingEquipment]:
return {it.normal_head_terminal.conducting_equipment for it in self.objects(LvFeeder) if it.normal_head_terminal}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# 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 collections.abc import Collection
from typing import Iterable, Generator, Union, List, Dict, Any
from typing import Iterable, Generator, Union, List, Dict, Any, Set

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

def _feeder_try_energize_lv_feeders(self, feeders: Iterable[Feeder], lv_feeder_start_points: Generator[ConductingEquipment, None, None], to_equipment: PowerTransformer):
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)
Expand Down Expand Up @@ -114,8 +114,8 @@ async def run(self,

async def run_with_feeders(self,
terminal: Terminal,
feeder_start_points: Generator[ConductingEquipment, None, None],
lv_feeder_start_points: Generator[ConductingEquipment, None, None],
feeder_start_points: Set[ConductingEquipment],
lv_feeder_start_points: Set[ConductingEquipment],
terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]],
feeders_to_assign: List[Feeder]):

Expand All @@ -132,8 +132,8 @@ async def run_with_feeders(self,

async def _create_trace(self,
terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]],
feeder_start_points: Generator[ConductingEquipment, None, None],
lv_feeder_start_points: Generator[ConductingEquipment, None, None],
feeder_start_points: Set[ConductingEquipment],
lv_feeder_start_points: Set[ConductingEquipment],
feeders_to_assign: List[Feeder]) -> NetworkTrace[Any]:

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

if step_path.traced_internally and not step_context.is_start_item:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# 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 collections.abc import Iterable
from typing import Collection, List, Generator, TypeVar, Dict

from typing import Collection, List, Generator, TypeVar, Dict, Set

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

# 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.
await self.run_with_feeders(lv_feeder.normal_head_terminal,
lv_feeder_start_points,
terminal_to_aux_equipment,
Expand All @@ -77,7 +77,7 @@ async def run(self,

async def run_with_feeders(self,
terminal: Terminal,
lv_feeder_start_points: Iterable[ConductingEquipment],
lv_feeder_start_points: Set[ConductingEquipment],
terminal_to_aux_equipment: Dict[Terminal, List[AuxiliaryEquipment]],
lv_feeders_to_assign: List[LvFeeder]):

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

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

def _reached_hv(ce: ConductingEquipment):
Expand Down Expand Up @@ -123,19 +123,18 @@ async def _process(self,
found_lv_feeder: bool,
step_context: StepContext,
terminal_to_aux_equipment: Dict[Terminal, Collection[AuxiliaryEquipment]],
lv_feeder_start_points: Iterable[ConductingEquipment],
lv_feeder_start_points: Set[ConductingEquipment],
lv_feeders_to_assign: List[LvFeeder]):

if step_path.traced_internally and not step_context.is_start_item:
return

if found_lv_feeder:
found_lv_feeders = self._find_lv_feeders(step_path.to_equipment, lv_feeder_start_points)
found_lv_feeders = list(self._find_lv_feeders(step_path.to_equipment, lv_feeder_start_points))

energizing_feeders = list(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_feeders)
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)

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

def _find_lv_feeders(self, ce: ConductingEquipment, lv_feeder_start_points: Iterable[ConductingEquipment]) -> Generator[LvFeeder, None, None]:
def _find_lv_feeders(self, ce: ConductingEquipment, lv_feeder_start_points: Set[ConductingEquipment]) -> Generator[LvFeeder, None, None]:
sites = list(ce.sites)
if sites:
for site in sites:
Expand All @@ -159,4 +158,4 @@ def _find_lv_feeders(self, ce: ConductingEquipment, lv_feeder_start_points: Iter
yield feeder

def _lv_feeders_from_terminal(self, terminal: Terminal) -> List[LvFeeder]:
return terminal.conducting_equipment.lv_feeders(self.network_state_operators)
return list(terminal.conducting_equipment.lv_feeders(self.network_state_operators))
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Tracing:
def network_trace(network_state_operators: NetworkStateOperators=NetworkStateOperators.NORMAL,
action_step_type: NetworkTraceActionType=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT,
queue: TraversalQueue[NetworkTraceStep[T]]=TraversalQueue.depth_first(),
compute_data: ComputeData[T]=None
compute_data: Union[ComputeData[T], Callable]=None
) -> NetworkTrace[T]:
"""
Creates a `NetworkTrace` that computes contextual data for every step.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def _has_none_phase(self, terminal: Terminal) -> bool:

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

def _find_terminal_at_start_of_missing_phases(
self,
Expand Down
4 changes: 4 additions & 0 deletions src/zepben/evolve/testing/test_network_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,9 @@ def _create_feeder(self, mrid: Optional[str], head_equipment: ConductingEquipmen
)

f.add_equipment(head_equipment)
f.add_current_equipment(head_equipment)
head_equipment.add_container(f)
head_equipment.add_current_container(f)

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

lvf.add_equipment(head_equipment)
lvf.add_current_equipment(head_equipment)
head_equipment.add_container(lvf)
head_equipment.add_current_container(lvf)

self.network.add(lvf)
return lvf
Expand Down
44 changes: 43 additions & 1 deletion test/cim/iec61970/base/core/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
# 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 pytest
from hypothesis import given

from build.lib.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
from zepben.evolve import Site, TestNetworkBuilder, Equipment, AssignToLvFeeders, LvFeeder

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

def test_site_constructor_args():
verify_equipment_container_constructor_args(Site(*site_args))

@pytest.mark.asyncio
async def test_find_lv_feeders_excludes_open_switches():
#
# tx0 21 b1(lvf5) 21--c2--2
# 21 b3(lvf6) 21--c4--2
#
site = Site()
network = (TestNetworkBuilder()
.from_power_transformer(action=lambda pt: _add_to_site(pt, site)) # tx0
.from_breaker(is_normally_open=True, is_open=True, action=lambda b: _add_to_site(b, site)) # b1
.to_acls() # c2
.branch_from('tx0')
.to_breaker(is_normally_open=False, is_open=False, action=lambda b: _add_to_site(b, site)) # b3
.to_acls() # c4
.add_lv_feeder('b1') # lvf5
.add_lv_feeder('b3') # lvf6
).network

assign_to_lv_feeders = AssignToLvFeeders()

await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.NORMAL, start_terminal=network.get('b1-t2'))
await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.NORMAL, start_terminal=network.get('b3-t2'))
await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.CURRENT, start_terminal=network.get('b1-t2'))
await assign_to_lv_feeders.run(network, network_state_operators=NetworkStateOperators.CURRENT, start_terminal=network.get('b3-t2'))

lvf6 = network.get('lvf6', LvFeeder)
normal_lv_feeders = list(site.find_lv_feeders(network.lv_feeder_start_points, NetworkStateOperators.NORMAL))
assert normal_lv_feeders == [lvf6]

current_lv_feeders = list(site.find_lv_feeders(network.lv_feeder_start_points, NetworkStateOperators.CURRENT))
assert current_lv_feeders == [lvf6]


def _add_to_site(equipment: Equipment, site: Site):
site.add_equipment(equipment)
equipment.add_container(site)
site.add_current_equipment(equipment)
equipment.add_current_container(site)