Peano
Loading...
Searching...
No Matches
test_splittrees_leaf_distribution.py
Go to the documentation of this file.
1import itertools
2import pytest
3
4from toolbox.loadbalancing.api.input.RegularGridGenerator import RegularGridGenerator
5import toolbox.loadbalancing.api.strategies.SplitTrees as split_strategy
6
7
8def _balanced_decomposition(total_leaves, number_of_subtrees):
9 """Build an even decomposition list expected by Stage I helpers."""
10 leaves_per_subtree = total_leaves // number_of_subtrees
11 remaining_leaves = total_leaves % number_of_subtrees
12 decomposition = []
13 for i in range(number_of_subtrees):
14 decomposition.append(leaves_per_subtree + (1 if i < remaining_leaves else 0))
15 return decomposition
16
17
18@pytest.mark.parametrize(
19 "dimensions,number_of_subtrees",
20 [
21 (2, 1),
22 (2, 3),
23 (3, 1),
24 (3, 3),
25 ],
26)
27def test_leaf_distribution_balanced_across_subtrees(dimensions, number_of_subtrees):
28 """
29 Stage I ("Leaf Distribution") invariant test in 2D and 3D.
30
31 Verify that leaves from subtree 0 are distributed as evenly as possible
32 across `number_of_subtrees` subtrees.
33 """
34 # Arrange: tree with 2 levels; all cells initially in subtree 0
36 name=f"pytest-tree-stage-i-{dimensions}d", max_level=2, dimensions=dimensions
37 )
38
39 # Collect all leaf nodes before Stage I
40 leaf_count_before = tree._leaf_count
41
42 # Act: run Stage I only (BFS traversal, default)
43 decomposition = _balanced_decomposition(leaf_count_before, number_of_subtrees)
44 has_leaves, moved_nodes = split_strategy.distribute_leaves_bottom_up(tree, decomposition)
45
46 # Assert: leaves were found and redistribution occurred
47 assert has_leaves
48 assert sum(moved_nodes) == leaf_count_before
49 assert len(moved_nodes) == number_of_subtrees
50
51 # Assert: distribution is balanced (within 1 leaf per subtree)
52 max_leaves = max(moved_nodes)
53 min_leaves = min(moved_nodes)
54 assert max_leaves - min_leaves <= 1, (
55 f"Leaf distribution unbalanced: max={max_leaves}, min={min_leaves}, "
56 f"distribution={moved_nodes}"
57 )
58
59 # Assert: no subtree is empty if possible
60 if leaf_count_before >= number_of_subtrees:
61 assert all(count > 0 for count in moved_nodes), (
62 f"Some subtrees are empty: {moved_nodes}"
63 )
64
65
66@pytest.mark.parametrize(
67 "dimensions",
68 [2, 3],
69)
71 """
72 Stage I invariant: non-leaf nodes (internal nodes with children) must NOT
73 be modified by Stage I leaf distribution.
74 """
75 # Arrange: tree with 3 levels; all cells initially in subtree 0
77 name=f"pytest-tree-nonleaf-check-{dimensions}d", max_level=3, dimensions=dimensions
78 )
79
80 # Capture non-leaf node assignments before Stage I
81 non_leaf_assignments_before = {}
82 for level in range(tree._max_level - 1): # Skip finest level (only leaves)
83 size = 3 ** (level + 1)
84 if dimensions == 2:
85 for a in range(size):
86 for b in range(size):
87 node = tree._tree_arrays[level][a][b]
88 if node[0] == 1: # Node exists
89 # Check if it has children
90 child_a, child_b = 3 * a, 3 * b
91 if (child_a < 3 ** (level + 2) and child_b < 3 ** (level + 2) and
92 tree._tree_arrays[level + 1][child_a][child_b][0] == 1):
93 # This is a non-leaf node
94 non_leaf_assignments_before[(level, a, b)] = node[1]
95
96 elif dimensions == 3:
97 for a in range(size):
98 for b in range(size):
99 for c in range(size):
100 node = tree._tree_arrays[level][a][b][c]
101 if node[0] == 1: # Node exists
102 # Check if it has children
103 child_a, child_b, child_c = 3 * a, 3 * b, 3 * c
104 if (child_a < 3 ** (level + 2) and child_b < 3 ** (level + 2) and
105 child_c < 3 ** (level + 2) and
106 tree._tree_arrays[level + 1][child_a][child_b][child_c][0] == 1):
107 # This is a non-leaf node
108 non_leaf_assignments_before[(level, a, b, c)] = node[1]
109
110 # Act: run Stage I
111 decomposition = _balanced_decomposition(tree._leaf_count, 2)
112 split_strategy.distribute_leaves_bottom_up(tree, decomposition)
113
114 # Assert: all non-leaf nodes remain unchanged
115 for level_coords, subtree_before in non_leaf_assignments_before.items():
116 if dimensions == 2:
117 level, a, b = level_coords
118 node_after = tree._tree_arrays[level][a][b]
119 else:
120 level, a, b, c = level_coords
121 node_after = tree._tree_arrays[level][a][b][c]
122
123 assert node_after[1] == subtree_before, (
124 f"Non-leaf node {level_coords} changed from subtree {subtree_before} "
125 f"to subtree {node_after[1]} during Stage I"
126 )
127
128
129@pytest.mark.parametrize(
130 "dimensions",
131 [2, 3],
132)
134 """
135 Stage I invariant: only leaves in subtree 0 are redistributed. Leaves already
136 in other subtrees must NOT be modified.
137 """
138 # Arrange: tree with 2 levels
140 name=f"pytest-tree-subtree0-only-{dimensions}d", max_level=2, dimensions=dimensions
141 )
142
143 # Manually assign some leaves to subtree 1 (mark a corner block)
144 finest_level = tree._max_level - 1
145 size = 3 ** (finest_level + 1)
146
147 # Mark leaves in top-right corner as subtree 1
148 corner_leaves_subtree_1 = []
149 if dimensions == 2:
150 for a in range(size // 2, size):
151 for b in range(size // 2, size):
152 tree._tree_arrays[finest_level][a][b] = (1, 1)
153 corner_leaves_subtree_1.append((a, b))
154
155 elif dimensions == 3:
156 for a in range(size // 2, size):
157 for b in range(size // 2, size):
158 for c in range(size // 2, size):
159 tree._tree_arrays[finest_level][a][b][c] = (1, 1)
160 corner_leaves_subtree_1.append((a, b, c))
161
162 # Act: run Stage I
163 decomposition = _balanced_decomposition(tree._leaf_count, 1)
164 split_strategy.distribute_leaves_bottom_up(tree, decomposition)
165
166 # Assert: leaves in subtree 1 remain in subtree 1
167 if dimensions == 2:
168 for a, b in corner_leaves_subtree_1:
169 node_after = tree._tree_arrays[finest_level][a][b]
170 assert node_after[1] == 1, (
171 f"Leaf ({a},{b}) in subtree 1 was incorrectly modified to subtree {node_after[1]}"
172 )
173
174 elif dimensions == 3:
175 for a, b, c in corner_leaves_subtree_1:
176 node_after = tree._tree_arrays[finest_level][a][b][c]
177 assert node_after[1] == 1, (
178 f"Leaf ({a},{b},{c}) in subtree 1 was incorrectly modified to subtree {node_after[1]}"
179 )
180
181
182@pytest.mark.parametrize(
183 "dimensions",
184 [2, 3],
185)
187 """
188 Stage I invariant: node existence flags (weights) must NOT be modified.
189 Stage I only changes subtree assignments, not node existence.
190 """
191 # Arrange: tree with 2 levels
193 name=f"pytest-tree-weights-{dimensions}d", max_level=2, dimensions=dimensions
194 )
195
196 # Capture all node weights before Stage I
197 weights_before = {}
198 for level in range(tree._max_level):
199 size = 3 ** (level + 1)
200 if dimensions == 2:
201 for a in range(size):
202 for b in range(size):
203 weight, subtree = tree._tree_arrays[level][a][b]
204 weights_before[(level, a, b)] = weight
205
206 elif dimensions == 3:
207 for a in range(size):
208 for b in range(size):
209 for c in range(size):
210 weight, subtree = tree._tree_arrays[level][a][b][c]
211 weights_before[(level, a, b, c)] = weight
212
213 # Act: run Stage I
214 decomposition = _balanced_decomposition(tree._leaf_count, 2)
215 split_strategy.distribute_leaves_bottom_up(tree, decomposition)
216
217 # Assert: all weights remain unchanged
218 for level_coords, weight_before in weights_before.items():
219 if dimensions == 2:
220 level, a, b = level_coords
221 weight_after, _ = tree._tree_arrays[level][a][b]
222 else:
223 level, a, b, c = level_coords
224 weight_after, _ = tree._tree_arrays[level][a][b][c]
225
226 assert weight_after == weight_before, (
227 f"Node {level_coords} weight changed from {weight_before} to {weight_after}"
228 )
229
230
231@pytest.mark.parametrize(
232 "dimensions",
233 [2, 3],
234)
236 """
237 Stage I invariant: the returned `moved_nodes` list must accurately reflect
238 how many leaves were assigned to each subtree.
239 """
240 # Arrange: tree with 2 levels
242 name=f"pytest-tree-move-count-{dimensions}d", max_level=2, dimensions=dimensions
243 )
244
245 number_of_subtrees = 2
246 finest_level = tree._max_level - 1
247
248 # Act: run Stage I
249 decomposition = _balanced_decomposition(tree._leaf_count, number_of_subtrees)
250 has_leaves, moved_nodes = split_strategy.distribute_leaves_bottom_up(tree, decomposition)
251
252 # Assert: moved_nodes list length is correct
253 assert len(moved_nodes) == number_of_subtrees
254
255 # Assert: count leaves actually assigned to each subtree and verify
256 size = 3 ** (finest_level + 1)
257 actual_counts = [0] * (number_of_subtrees)
258
259 if dimensions == 2:
260 for a in range(size):
261 for b in range(size):
262 weight, subtree = tree._tree_arrays[finest_level][a][b]
263 if weight == 1: # Leaf exists
264 if subtree < number_of_subtrees:
265 actual_counts[subtree] += 1
266
267 elif dimensions == 3:
268 for a in range(size):
269 for b in range(size):
270 for c in range(size):
271 weight, subtree = tree._tree_arrays[finest_level][a][b][c]
272 if weight == 1: # Leaf exists
273 if subtree < number_of_subtrees:
274 actual_counts[subtree] += 1
275
276 # Compare reported counts with actual counts
277 assert actual_counts == moved_nodes, (
278 f"Reported moved_nodes {moved_nodes} does not match actual counts {actual_counts}"
279 )
_balanced_decomposition(total_leaves, number_of_subtrees)
Build an even decomposition list expected by Stage I helpers.
test_leaf_distribution_does_not_modify_non_leaf_nodes(dimensions)
Stage I invariant: non-leaf nodes (internal nodes with children) must NOT be modified by Stage I leaf...
test_leaf_distribution_preserves_node_existence_weights(dimensions)
Stage I invariant: node existence flags (weights) must NOT be modified.
test_leaf_distribution_only_redistributes_subtree_0(dimensions)
Stage I invariant: only leaves in subtree 0 are redistributed.
test_leaf_distribution_balanced_across_subtrees(dimensions, number_of_subtrees)
Stage I ("Leaf Distribution") invariant test in 2D and 3D.
test_leaf_distribution_returns_correct_moved_nodes_count(dimensions)
Stage I invariant: the returned moved_nodes list must accurately reflect how many leaves were assigne...