Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
43457ba
.
chestm007 May 18, 2025
e007fb4
more tests, slight optimisation
chestm007 May 18, 2025
dc7870a
Add step path provider, and some tests
chestm007 May 19, 2025
7c4b033
be helpful if the TestNetworkBuilder knew what a Busbar was
chestm007 May 19, 2025
3b69b62
stashing changes
chestm007 May 20, 2025
9b9ce0d
NetworkTraceStepPathProvider seems to do the needful
chestm007 May 20, 2025
3777cde
Network Trace now supports cuts and clamps
chestm007 May 20, 2025
5a6f2d8
Reworking some internal classes to be less boilerplate
chestm007 May 21, 2025
847539e
this class isnt a test...
chestm007 May 21, 2025
a972967
Reworked NetworkStateOperators so auto complete works
chestm007 May 21, 2025
44e083d
I broke some type hinting, and created circular imports. #winning
chestm007 May 21, 2025
cf24250
Pretty bulky change
chestm007 May 21, 2025
a410f72
Reworked the internals of network_trace.Conditions
chestm007 May 22, 2025
0a3a303
.
chestm007 May 22, 2025
e31b95e
get rid of alot of `if isinstance` in favor of singledispatch
chestm007 May 23, 2025
9589cd3
Fixed WeightedPriorityQueue, and rework of network trace/traversal
chestm007 May 24, 2025
fb08464
More typing, and refactoring code to be more pythonic
chestm007 May 24, 2025
81d7f7f
slight refactoring to clean up API
chestm007 May 24, 2025
06c7d4e
Chasing an exponential queueing issue
chestm007 May 26, 2025
6c54dbf
yield != return.
chestm007 May 26, 2025
8b4f434
codebase inline with ewb-sdk-jvm PR#220
chestm007 May 26, 2025
519c0b9
DEV-2568 PR#220, tracing fixes, and more unit tests - 2 unexpected te…
chestm007 May 26, 2025
c80c9be
because ofcourse i accidentally wrote tests that modify global state
chestm007 May 27, 2025
66d0449
pointless code prettifying
chestm007 May 27, 2025
4947e4b
comparing a bound method to an enum will always return False silently
chestm007 May 27, 2025
d4d9d17
[DEV-2573] changes ported from Kotlin SDK (#221)
chestm007 May 27, 2025
84453a9
Busbranch tests fixed, this needs verification that its not dumb
chestm007 May 27, 2025
2510f21
phase inferring is solved 💪
chestm007 May 27, 2025
3c17140
Added action blocks for `TestNetworkBuilder.with_clamp` and `TestNetw…
chestm007 May 27, 2025
bfd7add
[DEV-2592] - added tests from kotlin sdk
chestm007 May 27, 2025
abff444
[DEV-2766] `NetworkTrace`/`Traversal` now correctly respects `can_sto…
chestm007 May 27, 2025
242c10b
rebase off main incase new build system takes a while
chestm007 May 27, 2025
5ebd932
remove version lock for pytest-subtests, it can sort itself out
chestm007 May 27, 2025
5c543ae
DEV-2382
chestm007 May 27, 2025
9dbfb65
should update the changelog.
chestm007 May 27, 2025
e7781b3
log inferred phases properly.
chestm007 May 28, 2025
3946da0
Add debug logging to `Traversal` and traces and exposed them via all …
chestm007 May 28, 2025
7ff79e4
reworked the logger slightly, added heaps of docs to it
chestm007 May 29, 2025
f532d80
Minor formatting fix
chestm007 May 29, 2025
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,5 @@ dmypy.json

/todo.md
test/resources/test-network-database.txt

*.iml
29 changes: 26 additions & 3 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,38 @@
## [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.
* `FeederDirectionStateOperations` have been reworked to take `NetworkStateOperators` as a parameter.



### 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.
* 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:
* `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 `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.
* `AssignToFeeders`/`AssignToLvFeeders` now finds back-fed equipment correctly
* `AssignToFeeders` and `AssignToLvFeeders` will now associate `PowerElectronicUnits` with their `powerElectronicsConnection` `Feeder`/`LvFeeder`.



### Notes
* None.
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"pytest-cov==2.10.1",
"pytest-asyncio==0.19.0",
"pytest-timeout==1.4.2",
'pytest-subtests',
"hypothesis==6.56.3",
"grpcio-testing==1.61.3",
"pylint==2.14.5",
Expand Down
15 changes: 13 additions & 2 deletions src/zepben/evolve/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@
from zepben.evolve.services.network.translator.network_cim2proto import *
from zepben.evolve.services.network.network_service import *

from zepben.evolve.services.network.tracing.traversal.step_context import *
from zepben.evolve.services.network.tracing.networktrace.network_trace_step import *
from zepben.evolve.services.network.tracing.connectivity.connectivity_result import *
from zepben.evolve.services.network.tracing.connectivity.nominal_phase_path import *
from zepben.evolve.services.network.tracing.connectivity.phase_paths import *
Expand All @@ -170,13 +172,23 @@
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 *
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 *
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.traversal.traversal_condition import *
from zepben.evolve.services.network.tracing.feeder.set_direction import *

from zepben.evolve.services.common.meta.data_source import *
Expand Down Expand Up @@ -426,6 +438,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
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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -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...")
Expand All @@ -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]
Expand Down
9 changes: 7 additions & 2 deletions src/zepben/evolve/model/busbranch/bus_branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 __future__ import annotations

import abc
from collections import Counter
from dataclasses import dataclass, field
from functools import reduce
from typing import Set, Tuple, FrozenSet, Dict, Callable, Union, TypeVar, Any, List, Generic, Optional, Iterable
from typing import Set, Tuple, FrozenSet, Dict, Callable, Union, TypeVar, Any, List, Generic, Optional, Iterable, TYPE_CHECKING

from zepben.evolve import Junction, BusbarSection, EquivalentBranch, Traversal, StepContext
from zepben.evolve import Junction, BusbarSection, EquivalentBranch, Traversal
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.aclinesegment import AcLineSegment
Expand All @@ -20,6 +22,9 @@
from zepben.evolve.services.network.network_service import NetworkService
from zepben.evolve.services.network.tracing.busbranch_trace import BusBranchTrace, BusBranchTraceStep

if TYPE_CHECKING:
from zepben.evolve import StepContext

__all__ = [
"BusBranchNetworkCreationValidator",
"BusBranchNetworkCreator",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
12 changes: 5 additions & 7 deletions src/zepben/evolve/model/cim/iec61970/base/core/equipment.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,18 @@
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
from zepben.evolve.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource
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.
Expand Down Expand Up @@ -64,11 +62,11 @@ 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`
"""
if network_state_operators == NetworkStateOperators.NORMAL:
if network_state_operators.NORMAL:
return self.normal_feeders
else:
return self.current_feeders
Expand All @@ -80,11 +78,11 @@ 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`
"""
if network_state_operators == NetworkStateOperators.NORMAL:
if network_state_operators.NORMAL:
return self.normal_lv_feeders
else:
return self.current_lv_feeders
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/zepben/evolve/model/cim/iec61970/base/core/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
6 changes: 6 additions & 0 deletions src/zepben/evolve/model/cim/iec61970/base/wires/clamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ class Clamp(ConductingEquipment):

ac_line_segment: Optional[AcLineSegment] = None
"""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
6 changes: 6 additions & 0 deletions src/zepben/evolve/model/cim/iec61970/base/wires/cut.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ 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."""

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
Loading