Peano
Loading...
Searching...
No Matches
SplitTrees.py
Go to the documentation of this file.
1# This file is part of the ExaHyPE2 project. For conditions of decomposition and
2# use, please see the copyright notice at www.peano-framework.org
3
4import yaml
5from ..Tree import Tree
6from .Utils import no_split_result
7
8from .SFC import Peano_to_Cartesian_2d
9from .SFC import Peano_to_Cartesian_3d
10
11
13 peano_tree: Tree,
14 decomposition,
15 traversal: str = "bfs_finest_first",
16 enable_propagate_bottom_up: bool = False,
17 enable_optimize_hierarchy: bool = False,
18 require_all_children_for_hierarchy_optimization: bool = True,
19 periodic_BC: list[bool] | None = None,
20# enable_extract_split_instructions: bool = False,
21# destination_ranks: list[int] | None = None,
22 verbose: bool = False,
23) -> tuple[str, str]:
24 """
25 Split a tree using a staged load-balancing workflow.
26
27 Stages:
28 1. distribute_leaves_bottom_up() or distribute_leaves_top_down(): Redistribute leaf nodes across subtrees.
29 2. _propagate_bottom_up(): Propagate subtree ownership to internal nodes.
30 3. _optimize_hierarchy() (optional): Further reduce split complexity by
31 promoting complete child sets up the hierarchy.
32 4. create_constructor_initialisation_string() (optional): Derive full
33 hardcoded constructor configuration from the transformed tree.
34
35 Args:
36 peano_tree: Tree to split (modified in place).
37 number_of_subtrees: Total number of subtrees to create.
38 traversal: Stage I traversal strategy. Supported values:
39 - "bfs": level-order leaf collection
40 - "bfs_finest_first": level-order leaf collection within each level,
41 but levels processed from finest to coarsest
42 - "dfs": depth-first leaf collection
43 enable_propagate_bottom_up: If True, enable Stage II bottom-up subtree
44 propagation after leaf redecomposition.
45 enable_optimize_hierarchy: If True, enable Stage III hierarchical
46 optimisation to minimise split operations through hierarchy lifting.
47 periodic_BC: One flag per axis. Peano never hands over cells along a
48 periodic boundary, so these cells stay on tree 0. If you pass the
49 periodic boundary conditions of your experiment, Stage I keeps
50 the leaves along periodic boundaries on tree 0, too, and counts
51 them towards tree 0's share. None means no periodic boundaries.
52 require_all_children_for_hierarchy_optimization: Stage III guard for
53 parent promotion. If True (default), a parent is only considered if
54 all 3^d children exist. If False, Stage III may also promote mixed
55 parents with partial child sets.
56 enable_extract_split_instructions: If True, run Stage IV and return the
57 derived `(hardcoded_config, hardcoded_mode)`.
58 destination_ranks: Optional destination rank list for Stage IV. If not
59 provided, it defaults to target subtree IDs.
60 verbose: If True, Stage IV prints extracted hardcoded parameters.
61
62 Notes:
63 - All stages after Stage I are optional.
64 - The tree is modified in place by the enabled transformation stages.
65 - If number_of_subtrees <= 1, no transformation stage runs.
66 - Stage III requires Stage II to have been applied first for correctness.
67
68
69 @todo Dmitry We have to update the docu once we have converged the codes.
70 So this can wait, but it remains a todo. Also, there has to
71 be a different way to split up the leaves:
72
73 @todo Dmitry At the moment, I do not see how you determine how many cells
74 you assign to each tree? I assume there's a helper somewhere.
75 I'm not sure if we also want to have splittings, where we can
76 configure the min number of cells per tree. Not sure how to
77 best inject that, i.e. will need your advise.
78
79
80 """
81 traversal_mode = traversal.lower()
82 if traversal_mode == "bfs":
83 has_leaves_to_split, moved_nodes = distribute_leaves_bottom_up(
84 peano_tree, decomposition=decomposition, finest_level_first=False, periodic_BC=periodic_BC
85 )
86 elif traversal_mode == "bfs_finest_first":
87 has_leaves_to_split, moved_nodes = distribute_leaves_bottom_up(
88 peano_tree, decomposition=decomposition, finest_level_first=True, periodic_BC=periodic_BC
89 )
90 elif traversal_mode == "dfs":
91 has_leaves_to_split, moved_nodes = distribute_leaves_top_down(
92 peano_tree, decomposition=decomposition, periodic_BC=periodic_BC
93 )
94 else:
95 raise ValueError(
96 f"Unsupported traversal '{traversal}'. Use 'bfs', 'bfs_finest_first' or 'dfs'."
97 )
98
99 if not has_leaves_to_split:
100 return no_split_result("No leaf nodes found in subtree 0 to split.")
101
102# if enable_propagate_bottom_up:
103# _propagate_bottom_up(peano_tree)
104
105 if enable_optimize_hierarchy:
107 peano_tree,
108 require_all_children=require_all_children_for_hierarchy_optimization,
109 )
110
111
112 # Update tree metadata after all stages
113 peano_tree._subtree_count = len(decomposition)
114 peano_tree.update_split_pattern()
115
116 if verbose:
117 print(f"Splitting complete. Subtree count: {peano_tree._subtree_count}")
118 print(f"Leaf decomposition: {moved_nodes}")
119 print(f"Split pattern: {peano_tree._split_pattern}")
120
121 #if enable_extract_split_instructions:
122 # from toolbox.loadbalancing.api.output.Hardcoded import create_constructor_initialisation_string##
123
124 # if destination_ranks is None:
125 # _, _, _, subtrees = extract_split_instructions(peano_tree)
126 # destination_ranks = [int(subtree) for subtree in subtrees]
127
128 # return create_constructor_initialisation_string(
129 # peano_tree,
130 # destination_ranks,
131 # verbose=verbose,
132 # )
133
134 #return ("", "")
135
136
137def _is_along_periodic_boundary(level: int, coordinates, periodic_BC) -> bool:
138 """
139 Does a cell touch a periodic boundary?
140
141 Args:
142 level: Level of the cell (0-based w.r.t. the API tree arrays).
143 coordinates: Integer coordinates of the cell on this level.
144 periodic_BC: One flag per axis, or None if there are no periodic
145 boundaries.
146 """
147 if periodic_BC is None:
148 return False
149 size = 3 ** (level + 1)
150 return any(
151 is_periodic and (coordinate == 0 or coordinate == size - 1)
152 for coordinate, is_periodic in zip(coordinates, periodic_BC)
153 )
154
155
156def _assign_leaves(peano_tree: Tree, leaf_nodes, decomposition, periodic_BC=None) -> list[int]:
157 """
158 Assign leaves to subtrees in the order given
159
160 Leaves along periodic boundaries stay on subtree 0 and count towards its
161 share, as Peano never hands over such cells. The remaining leaves are
162 handed out in the order of leaf_nodes: Subtree 0 gets the first ones until
163 its share is reached, then subtree 1, and so forth.
164
165 Args:
166 peano_tree: Tree to update (modified in place).
167 leaf_nodes: Leaves as (level, a, b) or (level, a, b, c) tuples.
168 decomposition: Number of leaves per subtree.
169 periodic_BC: One flag per axis, or None.
170
171 Returns:
172 Number of leaves per subtree.
173 """
174 def assign(leaf, subtree):
175 level, *coordinates = leaf
176 if peano_tree._dimension == 2:
177 a, b = coordinates
178 peano_tree._tree_arrays[level][a][b] = (1, subtree)
179 else:
180 a, b, c = coordinates
181 peano_tree._tree_arrays[level][a][b][c] = (1, subtree)
182
183 moved_nodes = [0] * len(decomposition)
184 remaining_leaves = []
185 for leaf in leaf_nodes:
186 if _is_along_periodic_boundary(leaf[0], leaf[1:], periodic_BC):
187 assign(leaf, 0)
188 moved_nodes[0] += 1
189 else:
190 remaining_leaves.append(leaf)
191
192 current_subtree = 0
193 for leaf in remaining_leaves:
194 while (
195 moved_nodes[current_subtree] >= decomposition[current_subtree]
196 and current_subtree < len(decomposition) - 1
197 ):
198 current_subtree += 1
199 assign(leaf, current_subtree)
200 moved_nodes[current_subtree] += 1
201
202 return moved_nodes
203
204
206 peano_tree: Tree,
207 decomposition,
208 finest_level_first: bool = False,
209 periodic_BC: list[bool] | None = None,
210) -> tuple[bool, list[int]]:
211 """
212 Stage I: BFS-style leaf redecomposition over Peano SFC order.
213
214 This strategy runs through the mesh level by level. It first assigns all
215 leaves on the finest level to partitions, then the leaves on the next
216 coarser level, and so forth. Within each level, cells are visited in
217 Peano space-filling-curve order, so the redecomposition follows the same
218 ordering as the SFC traversal used elsewhere in the load-balancing API.
219
220 @image html PeanoSFC.png
221
222 The goal is to keep the finest-mesh cells contiguous while respecting the
223 hierarchical tree structure. In practice, that makes the routine well
224 suited for solvers that spend most of their work on the finest cells, such
225 as limiter-based or multigrid-style workflows.
226
227 The lexicographic order that used to be documented here now lives in
228 `strategies/Lexicographic.py`; this function intentionally uses Peano SFC
229 order instead.
230
231 Leaf definition:
232 - Any existing cell on the finest level, or
233 - Any existing coarser-level cell with no children on the next level.
234
235 Args:
236 peano_tree: Tree to update (modified in place).
237 number_of_subtrees: Total number of subtrees to create.
238 finest_level_first: If `False` (default), process levels from coarsest
239 to finest (`0..max_level-1`). If `True`, process levels from finest
240 to coarsest while keeping BFS order inside each level.
241
242 Returns:
243 A tuple `(has_leaves_to_split, moved_nodes_per_subtree)`:
244 - `has_leaves_to_split`: `False` if no eligible leaves were found.
245 - `moved_nodes_per_subtree`: Per-subtree counts of reassigned leaves.
246
247 Notes:
248 - Only leaves in subtree 0 are redistributed.
249 - Node existence flags (`weight`) are preserved.
250 - This stage does not update non-leaf assignments.
251 """
252 # Collect all leaf nodes (at all levels) that belong to subtree 0.
253 # The level sweep can be either coarse->fine (legacy) or fine->coarse.
254
255 leaf_nodes = []
256
257 level_range = range(peano_tree._max_level)
258 for level in level_range:
259 size = 3**(level + 1)
260
261 if peano_tree._dimension == 2:
262 for peano_index in range(size * size):
263 a, b = Peano_to_Cartesian_2d(peano_index, size)
264 current_node = peano_tree._tree_arrays[level][a][b]
265 if current_node[0] == 1 and current_node[1] == 0: # Node exists and belongs to subtree 0
266 # Check if this is a leaf node
267 is_leaf = False
268
269 if level == peano_tree._max_level - 1:
270 # Nodes at finest level are always leaves
271 is_leaf = True
272 else:
273 # Check if node has no children (not refined further)
274 child_a, child_b = 3*a, 3*b
275 if (child_a < 3**(level+2) and child_b < 3**(level+2) and
276 peano_tree._tree_arrays[level+1][child_a][child_b][0] == 0):
277 is_leaf = True
278
279 if is_leaf:
280 leaf_nodes.append((level, a, b))
281
282 elif peano_tree._dimension == 3:
283 for peano_index in range(size * size * size):
284 a, b, c = Peano_to_Cartesian_3d(peano_index, size)
285 current_node = peano_tree._tree_arrays[level][a][b][c]
286 if current_node[0] == 1 and current_node[1] == 0: # Node exists and belongs to subtree 0
287 # Check if this is a leaf node
288 is_leaf = False
289
290 if level == peano_tree._max_level - 1:
291 # Nodes at finest level are always leaves
292 is_leaf = True
293 else:
294 # Check if node has no children (not refined further)
295 child_a, child_b, child_c = 3*a, 3*b, 3*c
296 if (child_a < 3**(level+2) and child_b < 3**(level+2) and child_c < 3**(level+2) and
297 peano_tree._tree_arrays[level+1][child_a][child_b][child_c][0] == 0):
298 is_leaf = True
299
300 if is_leaf:
301 leaf_nodes.append((level, a, b, c))
302
303 moved_nodes = _assign_leaves(peano_tree, leaf_nodes, decomposition, periodic_BC)
304
305 return (True, moved_nodes)
306
307
308def distribute_leaves_top_down(peano_tree: Tree,
309 decomposition,
310 periodic_BC: list[bool] | None = None,
311 ) -> tuple[bool, list[int]]:
312 """
313 Stage I: DFS-style leaf redecomposition over the Peano SFC.
314
315 Collect leaf cells currently assigned to subtree 0 and redistribute them
316 across `number_of_subtrees` subtrees as evenly as possible.
317 The redecomposition logic is identical to `distribute_leaves_bottom_up()`, but leaf
318 discovery order follows DFS instead of level-order scanning. Both component-root
319 discovery and per-parent child traversal use Peano SFC order.
320
321 Args:
322 peano_tree: Tree to update (modified in place).
323 number_of_subtrees: Total number of subtrees to create.
324
325 Returns:
326 A tuple `(has_leaves_to_split, moved_nodes_per_subtree)`:
327 - `has_leaves_to_split`: `False` if no eligible leaves were found.
328 - `moved_nodes_per_subtree`: Per-subtree counts of reassigned leaves.
329
330 Both return values can be ignored if you want to.
331 """
332 leaf_nodes = []
333 visited = set()
334 max_level = peano_tree._max_level
335
336 def _node_exists(level: int, a: int, b: int, c: int | None = None) -> bool:
337 if peano_tree._dimension == 2:
338 return peano_tree._tree_arrays[level][a][b][0] == 1
339 return peano_tree._tree_arrays[level][a][b][c][0] == 1
340
341 def _node_subtree(level: int, a: int, b: int, c: int | None = None) -> int:
342 if peano_tree._dimension == 2:
343 return peano_tree._tree_arrays[level][a][b][1]
344 return peano_tree._tree_arrays[level][a][b][c][1]
345
346 def _children_2d(level: int, a: int, b: int) -> tuple[bool, list[tuple[int, int]]]:
347 if level == max_level - 1:
348 return (False, [])
349
350 children = []
351 has_existing_children = False
352 for peano_index in range(9):
353 local_a, local_b = Peano_to_Cartesian_2d(peano_index, 3)
354 child_a = 3 * a + local_a
355 child_b = 3 * b + local_b
356 if _node_exists(level + 1, child_a, child_b):
357 has_existing_children = True
358 if _node_subtree(level + 1, child_a, child_b) == 0:
359 children.append((child_a, child_b))
360
361 return (has_existing_children, children)
362
363 def _children_3d(level: int, a: int, b: int, c: int) -> tuple[bool, list[tuple[int, int, int]]]:
364 if level == max_level - 1:
365 return (False, [])
366
367 children = []
368 has_existing_children = False
369 for peano_index in range(27):
370 local_a, local_b, local_c = Peano_to_Cartesian_3d(peano_index, 3)
371 child_a = 3 * a + local_a
372 child_b = 3 * b + local_b
373 child_c = 3 * c + local_c
374 if _node_exists(level + 1, child_a, child_b, child_c):
375 has_existing_children = True
376 if _node_subtree(level + 1, child_a, child_b, child_c) == 0:
377 children.append((child_a, child_b, child_c))
378
379 return (has_existing_children, children)
380
381 def _dfs_2d(level: int, a: int, b: int) -> None:
382 key = (level, a, b)
383 if key in visited:
384 return
385 visited.add(key)
386
387 has_existing_children, children = _children_2d(level, a, b)
388 if not has_existing_children:
389 leaf_nodes.append((level, a, b))
390 return
391
392 for child_a, child_b in children:
393 _dfs_2d(level + 1, child_a, child_b)
394
395 def _dfs_3d(level: int, a: int, b: int, c: int) -> None:
396 key = (level, a, b, c)
397 if key in visited:
398 return
399 visited.add(key)
400
401 has_existing_children, children = _children_3d(level, a, b, c)
402 if not has_existing_children:
403 leaf_nodes.append((level, a, b, c))
404 return
405
406 for child_a, child_b, child_c in children:
407 _dfs_3d(level + 1, child_a, child_b, child_c)
408
409 # Start DFS from all subtree-0 nodes that are roots of subtree-0 components.
410 # A component root is a level-0 node or a node whose parent is not in subtree 0.
411 # Within each level, roots are discovered in Peano SFC order.
412 for level in range(max_level):
413 size = 3 ** (level + 1)
414
415 if peano_tree._dimension == 2:
416 for peano_index in range(size * size):
417 a, b = Peano_to_Cartesian_2d(peano_index, size)
418 if not _node_exists(level, a, b):
419 continue
420 if _node_subtree(level, a, b) != 0:
421 continue
422
423 is_component_root = level == 0
424 if not is_component_root:
425 pa, pb = a // 3, b // 3
426 parent_exists = _node_exists(level - 1, pa, pb)
427 parent_subtree0 = parent_exists and _node_subtree(level - 1, pa, pb) == 0
428 is_component_root = not parent_subtree0
429
430 if is_component_root:
431 _dfs_2d(level, a, b)
432
433 elif peano_tree._dimension == 3:
434 for peano_index in range(size * size * size):
435 a, b, c = Peano_to_Cartesian_3d(peano_index, size)
436 if not _node_exists(level, a, b, c):
437 continue
438 if _node_subtree(level, a, b, c) != 0:
439 continue
440
441 is_component_root = level == 0
442 if not is_component_root:
443 pa, pb, pc = a // 3, b // 3, c // 3
444 parent_exists = _node_exists(level - 1, pa, pb, pc)
445 parent_subtree0 = (
446 parent_exists and _node_subtree(level - 1, pa, pb, pc) == 0
447 )
448 is_component_root = not parent_subtree0
449
450 if is_component_root:
451 _dfs_3d(level, a, b, c)
452
453 moved_nodes = _assign_leaves(peano_tree, leaf_nodes, decomposition, periodic_BC)
454
455 return (True, moved_nodes)
456
457
458def propagate_bottom_up(peano_tree: Tree) -> None:
459 """
460
461 Propagate information bottom-up
462
463 This correponds to the second stage of the staged load-balancing workflow.
464
465 Propagate subtree assignments from leaves to internal nodes in a bottom-up pass.
466 For each existing non-leaf node, if all existing children belong to the same
467 subtree, assign the parent to that subtree.
468
469 ## Implementation
470
471 We work from finest to the coarsest level. On each level, we trigger an
472 iterative process (while loop) for each colour (tree number) `k` that we
473 find:
474
475 ### Check if all kids of one cell belong to the same tree
476
477 For a cell on level `level`, we run over all 3^d children (counter `child_a`
478 and `child_b`). There is a set, and we add all the child tree numbers to this
479 set. Once we have visited all children, we can check the set cardinality:
480 If the set cardinality equals 1, all children are assigned to the same
481 tree. We therefore can assign the parent to this tree number, too.
482
483 ### Make decision if to assign the parent
484
485 If all kids belong to the same tree, we can propagate this ownership one
486 level up. However, we do this for a tree `k` if and only if the following
487 tests pass:
488
489 There could be multiple nodes `k` within the tree that span all together a
490 forest of small trees. Let `max_k` be the maximum level of these root nodes
491 within the forest, and let `min_k` be the minimum level.
492
493 We assign an arbitrary node from another tree colour
494 the colour `k` if and only if
495
496 - `max_k - min_k` becomes smaller through the assignment, or;
497 - `max_k - min_k = 0` and we could group all root nodes of `k` into
498 k/3^d coarser nodes.
499
500 ### Rationale
501
502 The reason is that we don't want to propagate too aggressively bottom-up.
503 Eventually, our goal is to introduce a hierarchy over the trees. For this,
504 it helps us if all the root nodes of tree `k` are on the same level. In this
505 case, we can determine (likely) one parent `k' and then tell `k' to fork x
506 times where x is the number of roots within the forest of `k`.
507
508 Therefore, we try to ensure that `max_k - min_k` becomes smaller and smaller.
509 This implies that, in an adaptive mesh, we try to group all the fine mesh
510 parts together first.
511
512 Eventually, all root nodes of the forest `k` will end up on the one and same
513 level. It could now be that we could move all of them one level up. This
514 is covered by the second case distinction.
515
516 @todo Dmitry the check above is not implemented yet an causes the algorithm
517 to fail later on
518 Args:
519 peano_tree: Tree to update (modified in place).
520
521 Returns:
522 None.
523
524 Notes:
525 - Operates from finest internal level to root.
526 - Preserves node existence flags (`weight`).
527 - Parents with mixed-child subtree assignments remain unchanged.
528 - Intended to run after Stage I (`distribute_leaves_bottom_up()` or `distribute_leaves_top_down()`).
529 """
530 print("Updating non-leaf node assignments to maintain tree topology...")
531
532 for level in range(peano_tree._max_level - 2, -1, -1): # Skip finest level (only leaves)
533 size = 3**(level + 1)
534
535 if peano_tree._dimension == 2:
536 for a in range(size):
537 for b in range(size):
538 current_node = peano_tree._tree_arrays[level][a][b]
539 if current_node[0] == 1: # Node exists
540 # Check children assignments
541 child_subtrees = set()
542 child_size = 3**(level + 2)
543
544 # Collect subtree IDs of all existing children
545 for child_a in range(3*a, min(3*(a+1), child_size)):
546 for child_b in range(3*b, min(3*(b+1), child_size)):
547 child_node = peano_tree._tree_arrays[level+1][child_a][child_b]
548 if child_node[0] == 1: # Child exists
549 child_subtrees.add(child_node[1])
550
551 # If all children belong to the same subtree, assign parent to that subtree
552 if len(child_subtrees) == 1:
553 target_subtree = list(child_subtrees)[0]
554 peano_tree._tree_arrays[level][a][b] = (1, target_subtree)
555
556 elif peano_tree._dimension == 3:
557 for a in range(size):
558 for b in range(size):
559 for c in range(size):
560 current_node = peano_tree._tree_arrays[level][a][b][c]
561 if current_node[0] == 1: # Node exists
562 # Check children assignments
563 child_subtrees = set()
564 child_size = 3**(level + 2)
565
566 # Collect subtree IDs of all existing children
567 for child_a in range(3*a, min(3*(a+1), child_size)):
568 for child_b in range(3*b, min(3*(b+1), child_size)):
569 for child_c in range(3*c, min(3*(c+1), child_size)):
570 child_node = peano_tree._tree_arrays[level+1][child_a][child_b][child_c]
571 if child_node[0] == 1: # Child exists
572 child_subtrees.add(child_node[1])
573
574 # If all children belong to the same subtree, assign parent to that subtree
575 if len(child_subtrees) == 1:
576 target_subtree = list(child_subtrees)[0]
577 peano_tree._tree_arrays[level][a][b][c] = (1, target_subtree)
578
579 print("Non-leaf node assignment update completed.")
580
581def _optimize_hierarchy(peano_tree: Tree, require_all_children: bool = True) -> None:
582 """
583 Stage III of the staged load-balancing workflow.
584
585 Minimise the total number of split operations whilst preserving load balance
586 established by Stage II. The algorithm lifts split decisions up the tree hierarchy
587 by promoting a parent on level L-1 to the dominant subtree among its children
588 on level L.
589
590 Hierarchy lifting is applicable when:
591 - All children of a parent exist (count equals 3^d, where d is the spatial
592 dimensions count: d=2 -> 9 children, d=3 -> 27 children)
593 - Children are mixed across at least two subtrees
594 - A dominant child subtree M exists (largest child count)
595 - The dominant subtree differs from the current parent subtree and yields a
596 reduction of lower-level split pressure
597
598 When these conditions are met, the parent is reassigned to the dominant subtree.
599 Remaining child subtrees stay on level L, but their source parent subtree is now
600 the updated one (which effectively moves part of splitting upwards).
601
602 The algorithm operates bottom-up from finest to coarsest level(s), iteratively
603 lifting applicable promotions until no further reductions are possible.
604
605 Args:
606 peano_tree: Tree to update (modified in place).
607 Required precondition: Stage II (bottom-up propagation) must have been
608 applied first to ensure non-leaf subtree consistency.
609 require_all_children: If True (default), only promote parents whose
610 full 3^d child set exists. If False, also allow promotions for mixed
611 partial child sets.
612
613 Returns:
614 None.
615
616 Notes:
617 - This function is intended to run after `_propagate_bottom_up()`.
618 - Preserves node existence flags (`weight`).
619 - Modifies only subtree assignments to reduce split operation count.
620 - Multi-pass iteration allows cascading promotions up the tree.
621 - The "all children in one subtree" case is already resolved by Stage II;
622 Stage III primarily targets mixed-child parents.
623 """
624 print("Stage III: Optimising hierarchy to minimise split operations...")
625
626 dimensions = peano_tree._dimension
627 max_level = peano_tree._max_level
628 children_per_parent = 3 ** dimensions
629
630 def _dominant_subtree(child_subtrees: list[int], parent_subtree: int) -> int | None:
631 """
632 Pick a promotion target from mixed child subtree assignments.
633
634 Returns the dominant subtree id when promotion is beneficial, otherwise None.
635 """
636 if require_all_children and len(child_subtrees) != children_per_parent:
637 return None
638
639 counts: dict[int, int] = {}
640 for subtree in child_subtrees:
641 counts[subtree] = counts.get(subtree, 0) + 1
642
643 if len(counts) <= 1:
644 # Stage II already covers this case.
645 return None
646
647 # Deterministic tie-break: larger count first, then larger subtree id.
648 ranked = sorted(counts.items(), key=lambda item: (item[1], item[0]), reverse=True)
649 dominant_subtree, dominant_count = ranked[0]
650
651 if dominant_subtree == parent_subtree:
652 return None
653
654 # Do not move ownership "backward" to an earlier subtree.
655 if dominant_subtree < parent_subtree:
656 return None
657
658 parent_count = counts.get(parent_subtree, 0)
659 if dominant_count <= parent_count:
660 return None
661
662 return dominant_subtree
663
664 # Iterate until no more optimisations can be made
665 made_changes = True
666 pass_count = 0
667
668 while made_changes:
669 made_changes = False
670 pass_count += 1
671
672 # Work from finest internal level to root (bottom-up)
673 for level in range(max_level - 2, -1, -1):
674 size = 3 ** (level + 1)
675
676 if dimensions == 2:
677 for a in range(size):
678 for b in range(size):
679 parent_node = peano_tree._tree_arrays[level][a][b]
680 if parent_node[0] != 1: # Parent doesn't exist
681 continue
682
683 # Collect all children's subtree assignments
684 child_subtrees = []
685 child_size = 3 ** (level + 2)
686
687 for child_a in range(3 * a, min(3 * (a + 1), child_size)):
688 for child_b in range(3 * b, min(3 * (b + 1), child_size)):
689 child_node = peano_tree._tree_arrays[level + 1][child_a][child_b]
690 if child_node[0] == 1: # Child exists
691 child_subtrees.append(child_node[1])
692
693 target_subtree = _dominant_subtree(
694 child_subtrees=child_subtrees,
695 parent_subtree=parent_node[1],
696 )
697
698 if target_subtree is not None:
699 peano_tree._tree_arrays[level][a][b] = (1, target_subtree)
700 made_changes = True
701
702 elif dimensions == 3:
703 for a in range(size):
704 for b in range(size):
705 for c in range(size):
706 parent_node = peano_tree._tree_arrays[level][a][b][c]
707 if parent_node[0] != 1: # Parent doesn't exist
708 continue
709
710 # Collect all children's subtree assignments
711 child_subtrees = []
712 child_size = 3 ** (level + 2)
713
714 for child_a in range(3 * a, min(3 * (a + 1), child_size)):
715 for child_b in range(3 * b, min(3 * (b + 1), child_size)):
716 for child_c in range(3 * c, min(3 * (c + 1), child_size)):
717 child_node = peano_tree._tree_arrays[level + 1][
718 child_a][child_b][child_c]
719 if child_node[0] == 1: # Child exists
720 child_subtrees.append(child_node[1])
721
722 target_subtree = _dominant_subtree(
723 child_subtrees=child_subtrees,
724 parent_subtree=parent_node[1],
725 )
726
727 if target_subtree is not None:
728 peano_tree._tree_arrays[level][a][b][c] = (
729 1, target_subtree)
730 made_changes = True
731
732 print(
733 f"Hierarchy optimisation completed in {pass_count} pass(es). "
734 f"Total split operations reduced."
735 )
736
737def generate_tree_yaml(peano_tree: Tree, output_yaml_file: str) -> None:
738 """
739 Generate a YAML file describing the complete tree structure with spatial layout.
740
741 ## YAML Structure
742
743 The output contains:
744 - **Tree Metadata**: Name, dimensions, domain boundaries, leaf counts
745 - **Split Summary**: Distribution statistics across subtrees
746 - **Level Data**: Complete spatial arrays for each refinement level
747
748 ## Spatial Array Format
749
750 Each level contains a spatial representation:
751 - **2D**: Row-indexed arrays with comma-separated (weight,subtree_id) tuples
752 - **3D**: Plane-row indexed arrays with nested structure
753 - **Indexing**: 0-based indexing for both rows and planes
754
755 ## Data Format Example
756
757 ```yaml
758 tree_structure:
759 - level: 1
760 size: 3
761 data:
762 0: "(1,0), (1,0), (1,1)"
763 1: "(1,1), (1,1), (1,2)"
764 2: "(1,2), (1,2), (1,2)"
765 ```
766
767 ## Integration with Peano
768
769 The YAML format is designed for:
770 - Direct import into Peano's hardcoded load balancer
771 - Visualization tools and analysis scripts
772 - Checkpoint/restart mechanisms for offline decomposition
773
774 ## Technical Details
775
776 - Preserves exact spatial relationships from tree arrays
777 - Maintains type consistency (all values converted to int)
778 - Uses safe YAML dumping to prevent formatting issues
779
780 Args:
781 peano_tree: Tree object to generate YAML from
782 output_yaml_file: Path to the output YAML file
783
784 Returns:
785 None
786
787 Raises:
788 IOError: If file cannot be written due to permissions or disk space
789
790 Note:
791 Large trees may generate substantial YAML files. Consider file system
792 constraints when processing high-resolution adaptive meshes.
793 """
794 # Convert tree arrays to readable format
795 tree_levels = []
796
797 for level in range(peano_tree._max_level):
798 size = 3**(level + 1)
799
800 # Create dictionary with row numbers as keys
801 level_data = {}
802
803 if peano_tree._dimension == 2:
804 # Convert 2D array to numbered rows (0-indexed)
805 for a in range(size):
806 row_data = []
807 for b in range(size):
808 weight, subtree_id = peano_tree._tree_arrays[level][a][b]
809 row_data.append(f"({int(weight)},{int(subtree_id)})")
810 level_data[a] = ", ".join(row_data) # Keep 0-indexed rows
811
812 elif peano_tree._dimension == 3:
813 # Convert 3D array to numbered planes and rows (0-indexed)
814 for a in range(size):
815 plane_data = {}
816 for b in range(size):
817 row_data = []
818 for c in range(size):
819 weight, subtree_id = peano_tree._tree_arrays[level][a][b][c]
820 row_data.append(f"({int(weight)},{int(subtree_id)})")
821 plane_data[b] = ", ".join(row_data) # Changed to 0-indexed rows
822 level_data[a] = plane_data # Changed to 0-indexed planes
823
824 tree_levels.append({
825 'level': level + 1,
826 'size': size,
827 'data': level_data
828 })
829
830 # Create the complete YAML structure
831 split_data = {
832 'title': f"Complete Tree Structure from {peano_tree._name}",
833 'description': "Spatial representation of tree with (weight, subtree_id) tuples",
834 'version': "2.0",
835 'tree_info': {
836 'name': peano_tree._name,
837 'dimension': peano_tree._dimension,
838 'max_level': peano_tree._max_level,
839 'domain_offset': list(peano_tree._domain_offset),
840 'domain_size': list(peano_tree._domain_size),
841 'total_leaf_count': int(peano_tree._leaf_count),
842 'subtree_count': peano_tree._subtree_count
843 },
844 'split_summary': {
845 'subtree_decomposition': [int(count) for count in peano_tree._split_pattern]
846 },
847 'tree_structure': tree_levels
848 }
849
850 try:
851 with open(output_yaml_file, 'w') as file:
852 yaml.dump(split_data, file, default_flow_style=False, sort_keys=False)
853 print(f"Complete tree structure written to {output_yaml_file}")
854 except IOError as e:
855 print(f"Error writing to YAML file: {e}")
856
857def reset_tree_to_single_subtree(peano_tree: Tree) -> Tree:
858 """
859 Reset all tree nodes to belong to subtree 0, effectively erasing all splitting.
860
861 ## Use Cases
862
863 - **Algorithm Testing**: Reset to known state between experiments
864 - **Re-splitting**: Clear previous decomposition before applying new strategy
865 - **Debugging**: Return to simple single-subtree configuration
866
867 ## Implementation Details
868
869 - Preserves all node weights (existence flags)
870 - Only modifies subtree assignments (sets all to 0)
871 - Updates tree metadata (subtree_count, split_pattern) consistently
872
873 Args:
874 peano_tree: Tree object to reset
875
876 Returns:
877 Modified Tree object with all nodes assigned to subtree 0
878
879 Note:
880 This operation is irreversible - previous subtree assignments are lost.
881 Consider generating YAML backup before reset if preservation is needed.
882 """
883 print(f"Resetting tree '{peano_tree._name}' - moving all nodes to subtree 0")
884
885 # Reset all nodes in all levels to subtree 0
886 for level in range(peano_tree._max_level):
887 size = 3**(level + 1)
888
889 if peano_tree._dimension == 2:
890 for a in range(size):
891 for b in range(size):
892 current_node = peano_tree._tree_arrays[level][a][b]
893 if current_node[0] == 1: # Node exists (weight = 1)
894 # Reset to subtree 0 while preserving weight
895 peano_tree._tree_arrays[level][a][b] = (1, 0)
896
897 elif peano_tree._dimension == 3:
898 for a in range(size):
899 for b in range(size):
900 for c in range(size):
901 current_node = peano_tree._tree_arrays[level][a][b][c]
902 if current_node[0] == 1: # Node exists (weight = 1)
903 # Reset to subtree 0 while preserving weight
904 peano_tree._tree_arrays[level][a][b][c] = (1, 0)
905
906 # Update tree metadata after reset
907 peano_tree._subtree_count = 1
908 peano_tree.update_split_pattern()
909
910 print(f"Reset completed. All {int(peano_tree._leaf_count)} nodes now belong to subtree 0")
911 print(f"New split pattern: {peano_tree._split_pattern}")
912
913 return peano_tree
914
916 peano_tree: Tree,
917) -> tuple[list[int], list[int], list[int], list[bool]]:
918 """
919 Stage IV of the staged load-balancing workflow.
920
921 Derive the enumerated splits for Peano's hardcoded load balancer from a split tree.
922
923 The function scans the tree arrays and reconstructs a sequence of splits
924 which replays the observed decomposition with
925 toolbox::loadbalancing::strategies::Hardcoded. Entry k of each returned
926 list describes the kth split. The C++ strategy decides itself when to
927 issue the kth split, i.e. there is no notion of mesh sweeps here.
928
929 A split is emitted whenever, on some level, a cell with `subtree_id > 0` exists
930 whose parent exists and has a different subtree id (i.e. the subtree gets introduced
931 across a parent–child edge). Level 0 has no parent; any cells with `subtree_id > 0`
932 on level 0 are treated as introduced from subtree 0.
933
934 **BottomUp vs AggressiveTopDown extraction**
935
936 The level at which a split is emitted depends on how many distinct introduction
937 levels the subtree has:
938
939 - *AggressiveTopDown* (single introduction level): a split is emitted at that one
940 level.
941 - *BottomUp* (multiple introduction levels): the subtree's forest of root cells does
942 not all reside on the same level. In BottomUp mode, Peano propagates the split
943 upward from the finest level automatically, so there must be no split instruction
944 higher up the tree. A **single** split is therefore emitted at the **finest**
945 introduction level (the deepest level at which any root cell of the subtree
946 borders a parent of a different subtree). Coarser introduction levels are
947 suppressed.
948
949 The splits are ordered from coarse to fine emission level and, within a level,
950 by subtree id.
951
952 Mapping:
953 - `splittingTrees` is the unique parent subtree id for all introduced cells on the
954 emitted level, or 0 if ambiguous.
955 - `numberOfCells` is the count of introduced cells on the emitted level.
956 - `subTrees` are subtree ids (not MPI ranks); map them to destination ranks
957 separately.
958 - `bottomUpFlags` is a per-split boolean that is `True` when the corresponding
959 subtree has multiple introduction levels (BottomUp) and `False` otherwise
960 (AggressiveTopDown).
961
962 Args:
963 peano_tree: Input tree with subtree assignments.
964
965 Returns:
966 Tuple `(splittingTrees, numberOfCells, subTrees, bottomUpFlags)`
967 suitable for building a `Hardcoded` configuration.
968
969 Note:
970 This helper is intended for *offline replay* of an already-computed split pattern.
971 It does not modify the tree.
972 """
973 if peano_tree._subtree_count <= 1:
974 print("No splits to extract - tree has only one subtree")
975 return ([], [], [], [])
976
977 dim = peano_tree._dimension
978 max_level = peano_tree._max_level
979
980 def count_cells_on_level(subtree_id: int, level: int) -> int:
981 """Count existing cells (weight==1) on a given level that belong to subtree_id."""
982 size = 3 ** (level + 1)
983 count = 0
984 if dim == 2:
985 for a in range(size):
986 for b in range(size):
987 w, s = peano_tree._tree_arrays[level][a][b]
988 if w == 1 and s == subtree_id:
989 count += 1
990 else:
991 for a in range(size):
992 for b in range(size):
993 for c in range(size):
994 w, s = peano_tree._tree_arrays[level][a][b][c]
995 if w == 1 and s == subtree_id:
996 count += 1
997 return count
998
999 def parent_subtrees_for_introduced_cells(subtree_id: int, level: int) -> tuple[int, set[int]]:
1000 """
1001 Return (count, parent_ids) for cells of subtree_id at given level that are
1002 'introduced' wrt their parent: parent exists and parent_subtree != subtree_id.
1003 """
1004 if level <= 0:
1005 return (0, set())
1006
1007 size = 3 ** (level + 1)
1008 count = 0
1009 parent_ids: set[int] = set()
1010
1011 if dim == 2:
1012 for a in range(size):
1013 for b in range(size):
1014 w, s = peano_tree._tree_arrays[level][a][b]
1015 if w != 1 or s != subtree_id:
1016 continue
1017 pa, pb = a // 3, b // 3
1018 pw, ps = peano_tree._tree_arrays[level - 1][pa][pb]
1019 if pw == 1 and ps != subtree_id:
1020 count += 1
1021 parent_ids.add(ps)
1022 else:
1023 for a in range(size):
1024 for b in range(size):
1025 for c in range(size):
1026 w, s = peano_tree._tree_arrays[level][a][b][c]
1027 if w != 1 or s != subtree_id:
1028 continue
1029 pa, pb, pc = a // 3, b // 3, c // 3
1030 pw, ps = peano_tree._tree_arrays[level - 1][pa][pb][pc]
1031 if pw == 1 and ps != subtree_id:
1032 count += 1
1033 parent_ids.add(ps)
1034
1035 return (count, parent_ids)
1036
1037 split_operations = []
1038
1039 for target_subtree in range(1, peano_tree._subtree_count):
1040 # ------------------------------------------------------------------
1041 # Collect *all* levels at which this subtree is "introduced", i.e.
1042 # levels where at least one cell of the subtree borders a parent cell
1043 # that belongs to a different subtree (or level 0, which has no parent).
1044 # ------------------------------------------------------------------
1045 introductions: dict[int, tuple[int, set[int]]] = {}
1046
1047 # Level 0: no parent edge – any existing cells are introduced from subtree 0.
1048 moved_level0 = count_cells_on_level(target_subtree, level=0)
1049 if moved_level0 > 0:
1050 introductions[0] = (moved_level0, {0})
1051
1052 # Levels >= 1: introduced across parent–child edges.
1053 for level in range(1, max_level):
1054 moved_cells, parent_ids = parent_subtrees_for_introduced_cells(target_subtree, level)
1055 if moved_cells > 0:
1056 introductions[level] = (moved_cells, parent_ids)
1057
1058 if not introductions:
1059 continue
1060
1061 # ------------------------------------------------------------------
1062 # Decide the emission level.
1063 #
1064 # AggressiveTopDown – subtree roots all live on one level: emit there.
1065 #
1066 # BottomUp – roots span multiple levels (bottom-up propagation left
1067 # some coarser cells partially assigned). In BottomUp mode Peano
1068 # propagates the split upward by itself, so the forest of the new
1069 # subtree must appear only once – at the finest introduction level.
1070 # Coarser introduction levels are therefore suppressed.
1071 # ------------------------------------------------------------------
1072 is_bottom_up = len(introductions) > 1
1073 emit_level = max(introductions.keys()) if is_bottom_up else next(iter(introductions.keys()))
1074
1075 num_cells, parent_ids = introductions[emit_level]
1076 source_subtree = next(iter(parent_ids)) if len(parent_ids) == 1 else 0
1077
1078 split_operations.append(
1079 {
1080 "source_subtree": source_subtree,
1081 "target_subtree": target_subtree,
1082 "num_cells": num_cells,
1083 "level": emit_level,
1084 "is_bottom_up": is_bottom_up,
1085 }
1086 )
1087
1088 split_operations.sort(key=lambda op: (op["level"], op["target_subtree"]))
1089
1090 splittingTrees = [op["source_subtree"] for op in split_operations]
1091 numberOfCells = [op["num_cells"] for op in split_operations]
1092 subTrees = [op["target_subtree"] for op in split_operations]
1093 bottomUpFlags = [op["is_bottom_up"] for op in split_operations]
1094
1095 return (splittingTrees, numberOfCells, subTrees, bottomUpFlags)
tuple[str, str] split_tree(Tree peano_tree, decomposition, str traversal="bfs_finest_first", bool enable_propagate_bottom_up=False, bool enable_optimize_hierarchy=False, bool require_all_children_for_hierarchy_optimization=True, list[bool]|None periodic_BC=None, bool verbose=False)
Split a tree using a staged load-balancing workflow.
Definition SplitTrees.py:23
tuple[list[int], list[int], list[int], list[bool]] extract_split_instructions(Tree peano_tree)
Stage IV of the staged load-balancing workflow.
list[int] _assign_leaves(Tree peano_tree, leaf_nodes, decomposition, periodic_BC=None)
Assign leaves to subtrees in the order given.
tuple[bool, list[int]] distribute_leaves_top_down(Tree peano_tree, decomposition, list[bool]|None periodic_BC=None)
Stage I: DFS-style leaf redecomposition over the Peano SFC.
None propagate_bottom_up(Tree peano_tree)
Propagate information bottom-up.
None _optimize_hierarchy(Tree peano_tree, bool require_all_children=True)
Stage III of the staged load-balancing workflow.
bool _is_along_periodic_boundary(int level, coordinates, periodic_BC)
Does a cell touch a periodic boundary?
None generate_tree_yaml(Tree peano_tree, str output_yaml_file)
Generate a YAML file describing the complete tree structure with spatial layout.
Tree reset_tree_to_single_subtree(Tree peano_tree)
Reset all tree nodes to belong to subtree 0, effectively erasing all splitting.
tuple[bool, list[int]] distribute_leaves_bottom_up(Tree peano_tree, decomposition, bool finest_level_first=False, list[bool]|None periodic_BC=None)
Stage I: BFS-style leaf redecomposition over Peano SFC order.