Skip to content

Commit d387bc1

Browse files
authored
[DEV-3524] - equipment tree builder leaves (#192)
Signed-off-by: Max Chesterfield <max.chesterfield@zepben.com>
1 parent fd72c18 commit d387bc1

File tree

5 files changed

+169
-40
lines changed

5 files changed

+169
-40
lines changed

changelog.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
* None.
9393

9494
### Enhancements
95-
* None.
95+
* `EquipmentTreeBuilder` will now calculate `leaves` when specified to do so.
9696

9797
### Fixes
9898
* Marked some extensions properties and classes with [ZBEX] that were missing them (might still be more). In addition to the ones moved into the extensions
@@ -127,6 +127,8 @@
127127
* `RegulatingControl.ratedCurrent`
128128
* `Sensor.relayFunctions`
129129
* `UsagePoint.approvedInverterCapacity`
130+
* using `EquipmentTreeBuilder` more then once per interpreter will no longer cause the `roots` to contain more objects then it should due to `_roots` being a
131+
class var
130132

131133
### Notes
132134
* None.

src/zepben/ewb/services/network/tracing/networktrace/actions/equipment_tree_builder.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,40 @@ class EquipmentTreeBuilder(StepActionWithContextValue):
3434
>>> .add_step_action(tree_builder)).run()
3535
"""
3636

37-
_roots: dict[ConductingEquipment, EquipmentTreeNode] = {}
38-
39-
def __init__(self):
37+
def __init__(self, calculate_leaves: bool = False):
4038
super().__init__(key=str(uuid.uuid4()))
4139

40+
self._roots: dict[ConductingEquipment, EquipmentTreeNode] = {}
41+
self._leaves: set[EquipmentTreeNode] = set()
42+
43+
self._calculate_leaves = calculate_leaves
44+
4245
@property
4346
def roots(self) -> Generator[TreeNode[ConductingEquipment], None, None]:
4447
return (r for r in self._roots.values())
4548

49+
def recurse_nodes(self) -> Generator[TreeNode[ConductingEquipment], None, None]:
50+
"""
51+
Returns a generator that will yield every node in the tree structure.
52+
"""
53+
def recurse(node: TreeNode[ConductingEquipment]):
54+
yield node
55+
for child in node.children:
56+
yield from recurse(child)
57+
58+
for root in self._roots.values():
59+
yield from recurse(root)
60+
61+
@property
62+
def leaves(self) -> set[EquipmentTreeNode]:
63+
"""
64+
Return the leaves of the tree structure.
65+
Depending on how the backing trace is configured, there may be extra unexpected leaves in loops.
66+
"""
67+
if not self._calculate_leaves:
68+
raise AttributeError('leaves were not calculated, you must pass calculate_leaves=True to the EquipmentTreeBuilder when creating.')
69+
return set(self._leaves)
70+
4671
def compute_initial_value(self, item: NetworkTraceStep[Any]) -> EquipmentTreeNode:
4772
node = self._roots.get(item.path.to_equipment)
4873
if node is None:
@@ -62,10 +87,18 @@ def compute_next_value(
6287
else:
6388
return TreeNode(next_item.path.to_equipment, current_value)
6489

65-
def _apply(self, item: NetworkTraceStep[Any], context: StepContext):
90+
def _apply(self, _: NetworkTraceStep[Any], context: StepContext):
6691
current_node: TreeNode = self.get_context_value(context)
6792
if current_node.parent:
6893
current_node.parent.add_child(current_node)
6994

95+
if self._calculate_leaves:
96+
self._process_leaf(current_node)
97+
98+
def _process_leaf(self, current_node: TreeNode[ConductingEquipment]):
99+
self._leaves.add(current_node) # add this node to _leaves as it has no children
100+
if current_node.parent:
101+
self._leaves.discard(current_node.parent) # this nodes parent now has a child, it's not a leaf anymore
102+
70103
def clear(self):
71104
self._roots.clear()

src/zepben/ewb/services/network/tracing/networktrace/actions/tree_node.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,31 @@
55

66
__all__ = ['TreeNode']
77

8-
from typing import List, TypeVar, Generic
9-
10-
from zepben.ewb import IdentifiedObject
8+
from typing import TypeVar, Generic, Set
119

1210
T = TypeVar('T')
1311

1412

1513
class TreeNode(Generic[T]):
1614
"""
17-
represents a node in the NetworkTrace tree
15+
Represents a node in the NetworkTrace tree
1816
"""
1917

20-
def __init__(self, identified_object: IdentifiedObject, parent=None):
18+
def __init__(self, identified_object: T, parent=None):
2119
self.identified_object = identified_object
2220
self._parent: TreeNode = parent
23-
self._children: List[TreeNode] = []
21+
self._children: Set[TreeNode] = set()
2422

2523
@property
2624
def parent(self) -> 'TreeNode[T]':
2725
return self._parent
2826

2927
@property
30-
def children(self):
31-
return list(self._children)
28+
def children(self) -> Set['TreeNode[T]']:
29+
return set(self._children)
3230

33-
def add_child(self, child: 'TreeNode'):
34-
self._children.append(child)
31+
def add_child(self, child: 'TreeNode[T]'):
32+
self._children.add(child)
3533

3634
def __str__(self):
3735
return f"{{object: {self.identified_object}, parent: {self.parent or ''}, num children: {len(self.children)}}}"

test/services/network/tracing/networktrace/actions/test_equipment_tree_builder.py

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,41 @@
1616
from zepben.ewb.services.network.tracing.networktrace.actions.tree_node import TreeNode
1717

1818

19+
def test_accessing_leaves_when_not_calculated_raises_exception():
20+
builder = EquipmentTreeBuilder()
21+
with pytest.raises(AttributeError):
22+
builder.leaves
23+
24+
@pytest.mark.asyncio
25+
async def test_equipment_tree_builder_leaves():
26+
n = create_looping_network()
27+
normal = NetworkStateOperators.NORMAL
28+
current = NetworkStateOperators.CURRENT
29+
30+
await Tracing.set_phases().run(n)
31+
feeder_head = n.get("j0", ConductingEquipment)
32+
await Tracing.set_direction().run_terminal(feeder_head, network_state_operators=normal)
33+
await Tracing.set_direction().run_terminal(feeder_head, network_state_operators=current)
34+
await log_directions(n.get('j0', ConductingEquipment))
35+
36+
start = n.get("j1", ConductingEquipment)
37+
assert start is not None
38+
tree_builder = EquipmentTreeBuilder(calculate_leaves=True)
39+
trace = (
40+
Tracing.network_trace_branching(
41+
network_state_operators=normal,
42+
action_step_type=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT
43+
)
44+
.add_condition(downstream())
45+
.add_step_action(tree_builder)
46+
)
47+
48+
await trace.run(start)
49+
50+
for ce in (n['j5'], n['j13']):
51+
assert ce in {l.identified_object for l in tree_builder.leaves}
52+
53+
1954
@pytest.mark.asyncio
2055
async def test_downstream_tree():
2156
n = create_looping_network()
@@ -33,12 +68,15 @@ async def test_downstream_tree():
3368
start = n.get("j1", ConductingEquipment)
3469
assert start is not None
3570
tree_builder = EquipmentTreeBuilder()
36-
trace = Tracing.network_trace_branching(
37-
network_state_operators=normal,
38-
action_step_type=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT) \
39-
.add_condition(downstream()) \
40-
.add_step_action(tree_builder) \
71+
trace = (
72+
Tracing.network_trace_branching(
73+
network_state_operators=normal,
74+
action_step_type=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT
75+
)
76+
.add_condition(downstream())
77+
.add_step_action(tree_builder)
4178
.add_step_action(lambda item, context: visited_ce.append(item.path.to_equipment.mrid))
79+
)
4280

4381
await trace.run(start)
4482

@@ -51,34 +89,39 @@ async def test_downstream_tree():
5189

5290
pprint.pprint(visit_counts)
5391

54-
root = list(tree_builder.roots)[0]
92+
root = tree_builder._roots[start]
5593

5694
assert root is not None
5795
_verify_tree_asset(root, n["j1"], None, [n["ac1"], n["ac3"]])
5896

59-
test_node = root.children[0]
60-
_verify_tree_asset(test_node, n["ac1"], n["j1"], [n["j2"]])
97+
assert len(root.children) == 2
98+
for test_node in root.children:
99+
if test_node.identified_object == n['ac1']:
100+
_verify_tree_asset(test_node, n["ac1"], n["j1"], [n["j2"]])
61101

62-
test_node = test_node.children[0]
63-
_verify_tree_asset(test_node, n["j2"], n["ac1"], [n["ac2"]])
102+
test_node = test_node.children.pop()
103+
_verify_tree_asset(test_node, n["j2"], n["ac1"], [n["ac2"]])
64104

65-
test_node = test_node.children[0]
66-
_verify_tree_asset(test_node, n["ac2"], n["j2"], [n["j3"]])
105+
test_node = test_node.children.pop()
106+
_verify_tree_asset(test_node, n["ac2"], n["j2"], [n["j3"]])
67107

68-
test_node = next(iter(test_node.children))
69-
_verify_tree_asset(test_node, n["j3"], n["ac2"], [n["ac4"]])
108+
test_node = next(iter(test_node.children))
109+
_verify_tree_asset(test_node, n["j3"], n["ac2"], [n["ac4"]])
70110

71-
test_node = next(iter(test_node.children))
72-
_verify_tree_asset(test_node, n["ac4"], n["j3"], [n["j6"]])
111+
test_node = next(iter(test_node.children))
112+
_verify_tree_asset(test_node, n["ac4"], n["j3"], [n["j6"]])
73113

74-
test_node = next(iter(test_node.children))
75-
_verify_tree_asset(test_node, n["j6"], n["ac4"], [])
114+
test_node = next(iter(test_node.children))
115+
_verify_tree_asset(test_node, n["j6"], n["ac4"], [])
116+
break
76117

77-
test_node = list(root.children)[1]
78-
_verify_tree_asset(test_node, n["ac3"], n["j1"], [n["j4"]])
118+
elif test_node.identified_object == n['ac3']:
119+
_verify_tree_asset(test_node, n["ac3"], n["j1"], [n["j4"]])
79120

80-
test_node = next(iter(test_node.children))
81-
_verify_tree_asset(test_node, n["j4"], n["ac3"], [n["ac5"], n["ac6"]])
121+
test_node = next(iter(test_node.children))
122+
_verify_tree_asset(test_node, n["j4"], n["ac3"], [n["ac5"], n["ac6"]])
123+
else:
124+
assert False
82125

83126
assert len(_find_nodes(root, "j0")) == 0
84127
assert len(_find_nodes(root, "ac0")) == 0
@@ -162,8 +205,15 @@ def _verify_tree_asset(
162205
else:
163206
assert tree_node.parent is None
164207

165-
children_nodes = list(c.identified_object for c in tree_node.children)
166-
assert children_nodes == expected_children
208+
children_nodes = [c.identified_object for c in tree_node.children]
209+
try:
210+
for child in expected_children:
211+
assert child in children_nodes
212+
for child in children_nodes:
213+
assert child in expected_children
214+
except AssertionError as e:
215+
e.args = (expected_children, children_nodes)
216+
raise e
167217

168218

169219
def _find_nodes(root: TreeNode[ConductingEquipment], asset_id: str) -> List[TreeNode[ConductingEquipment]]:

test/services/network/tracing/networktrace/test_network_trace.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from services.network.tracing.networktrace.test_network_trace_step_path_provider import PathTerminal, _verify_paths
1515
from zepben.ewb import AcLineSegment, Clamp, Terminal, NetworkTraceStep, Cut, ConductingEquipment, TraversalQueue, Junction, ngen, NetworkTraceActionType, \
16-
Tracing
16+
Tracing, StepActionWithContextValue, EnergyConsumer
1717
from zepben.ewb.testing.test_network_builder import TestNetworkBuilder
1818

1919
Terminal.__add__ = PathTerminal.__add__
@@ -286,3 +286,49 @@ async def validate(start: Tuple[str, str], action_step_type: NetworkTraceActionT
286286
# Can even use bizarre paths, they are just the same as any other external path.
287287
await validate(('c0-t1', 'c2-t1'), NetworkTraceActionType.ALL_STEPS, ["c2-t1", "c2-t2"])
288288
await validate(('c0-t1', 'c2-t1'), NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT, ["c2-t1"])
289+
290+
async def test_context_is_not_shared_between_branches(self):
291+
#
292+
# 1--c0--21--c1-*-21--c2--21--ec3
293+
# 1
294+
# 1--c4--21--ec5
295+
#
296+
297+
ns = (
298+
TestNetworkBuilder()
299+
.from_acls() # c0
300+
.to_acls() # c1
301+
.with_clamp() # c1-clamp1
302+
.to_acls() # c2
303+
.to_energy_consumer() # ec3
304+
.branch_from('c1-clamp1')
305+
.to_acls() # c4
306+
.to_energy_consumer() # ec5
307+
).network
308+
309+
data_capture: list[tuple[str, list]] = []
310+
311+
class StepActionWithContext(StepActionWithContextValue):
312+
def _apply(self, item: NetworkTraceStep, context: List):
313+
print(item)
314+
if isinstance((ec := item.path.to_equipment), EnergyConsumer):
315+
data_capture.append((ec.mrid, self.get_context_value(context)))
316+
317+
def compute_next_value(self, next_item: NetworkTraceStep, current_item: NetworkTraceStep, current_value: List):
318+
nv = list(current_value)
319+
nv.append(next_item.path.from_equipment.mrid)
320+
return nv
321+
322+
def compute_initial_value(self, item: NetworkTraceStep):
323+
return [item.path.from_equipment.mrid]
324+
325+
await (
326+
Tracing.network_trace()
327+
.add_step_action(StepActionWithContext('key'))
328+
).run(ns.get('c0'))
329+
330+
assert len(data_capture) == 2
331+
assert data_capture == [('ec3', ['c0', 'c0', 'c1', 'c1', 'c2', 'c2']),
332+
('ec5', ['c0', 'c0', 'c1', 'c1-clamp1', 'c4', 'c4'])]
333+
334+

0 commit comments

Comments
 (0)