Peano
Loading...
Searching...
No Matches
35_offline-load-balancing-split-modes.py
Go to the documentation of this file.
1# This file is part of the Peano project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3"""!
4
5Create the split-mode PNG of the tutorial 35_offline-load-balancing.ipynb
6
7The picture contrasts Peano's two split modes. Its left column sketches how
8a top-down and a bottom-up split hand over cells of a small one-dimensional
9spacetree. Its right column shows what the two modes make out of the
10tutorial's offline decomposition: The script computes the decomposition,
11builds the tutorial's Euler setup twice, once with top-down and once with
12bottom-up splits, runs both with four threads, and reads the subdomains from
13the patch files. Call the script from anywhere:
14
15 python3 tutorials/exahype2/illustrations/35_offline-load-balancing-split-modes.py
16
17All builds and outputs go into a temporary directory (or --work-directory).
18The script needs matplotlib, but no ParaView.
19
20"""
21import argparse
22import importlib.util
23import os
24import re
25import tempfile
26
27import numpy as np
28import matplotlib
29
30matplotlib.use("Agg")
31import matplotlib.pyplot as plt
32from matplotlib.colors import ListedColormap
33from matplotlib.patches import Patch, Rectangle
34
35Here = os.path.dirname(os.path.abspath(__file__))
36
37# The file name of the script starts with a digit, i.e. we cannot import it
38_specification = importlib.util.spec_from_file_location(
39 "offline_load_balancing_illustrations",
40 os.path.join(Here, "35_offline-load-balancing.py"),
41)
42offline_load_balancing = importlib.util.module_from_spec(_specification)
43_specification.loader.exec_module(offline_load_balancing)
44domain_decomposition = offline_load_balancing.domain_decomposition
45
47import toolbox.loadbalancing.api.output.Hardcoded as Hardcoded
48import toolbox.loadbalancing.api.strategies.Decomposition as Decomposition
49import toolbox.loadbalancing.api.strategies.SplitTrees as SplitTrees
50import toolbox.loadbalancing.api.strategies.SFC as SFC
51
52SplitModesPicture = os.path.join(Here, "35_offline-load-balancing-split-modes.png")
53
54TreeColours = ["#4c72b0", "#dd8452", "#55a868", "#c44e52"]
55CellsPerAxis = 27
56DomainOffset = -0.1
57DomainSize = 0.2
58
59
61 """! Arguments of the hardcoded load balancing with the given split modes"""
62 tree = RegularGridGenerator.RegularGridGenerator(max_level=3, dimensions=2)
64 tree,
66 peano_tree=tree, number_of_subtrees=domain_decomposition.NumberOfThreads
67 ),
68 )
69 destination_ranks = Hardcoded.assign_trees_to_ranks_round_Robin(
70 number_of_subtrees=tree.number_of_subtrees, max_ranks=1
71 )
72 configuration, mode = Hardcoded.create_constructor_initialisation_string(
73 tree, destination_ranks, modes=modes
74 )
75 return configuration
76
77
78def read_subdomains(directory):
79 """!
80
81 Subdomain of each fine grid cell, read from the first snapshot's patch files
82
83 The patch files hold one file per tree, but neither the file names nor the
84 order in which the trees write their files tell us the tree number. We
85 only know that tree 0 is the one tree which holds the cells along the
86 periodic boundary. The result therefore holds 0 for tree 0 and 1, 2, ...
87 for the other trees, numbered by the position of their first cell along
88 the space-filling curve. These numbers are not necessarily Peano's tree
89 numbers, as Peano runs through the curve forth and back in alternating
90 mesh sweeps.
91
92 """
93 curve_position = np.zeros((CellsPerAxis, CellsPerAxis), dtype=int)
94 for position in range(CellsPerAxis * CellsPerAxis):
95 x, y = SFC.Peano_to_Cartesian_2d(position, CellsPerAxis)
96 curve_position[y, x] = position
97
98 cells_per_file = []
99 for tree in range(domain_decomposition.NumberOfThreads):
100 file_name = os.path.join(
101 directory, "solution-EulerFV-{}.peano-patch-file".format(tree)
102 )
103 with open(file_name) as patch_file:
104 text = patch_file.read()
105 cells = []
106 for match in re.finditer(r"begin patch\s+offset\s+(\S+)\s+(\S+)", text):
107 x = int(
108 round(
109 (float(match.group(1)) - DomainOffset) / DomainSize * CellsPerAxis
110 )
111 )
112 y = int(
113 round(
114 (float(match.group(2)) - DomainOffset) / DomainSize * CellsPerAxis
115 )
116 )
117 cells.append((y, x))
118 cells_per_file.append(cells)
119
120 def holds_boundary_cell(cells):
121 return any(min(y, x) == 0 or max(y, x) == CellsPerAxis - 1 for (y, x) in cells)
122
123 def first_curve_position(cells):
124 return min(curve_position[y, x] for (y, x) in cells)
125
126 tree_zero = [cells for cells in cells_per_file if holds_boundary_cell(cells)]
127 assert len(tree_zero) == 1, "expected exactly one tree along the periodic boundary"
128 others = sorted(
129 [cells for cells in cells_per_file if not holds_boundary_cell(cells)],
130 key=first_curve_position,
131 )
132
133 owner = -np.ones((CellsPerAxis, CellsPerAxis), dtype=int)
134 for subdomain, cells in enumerate(tree_zero + others):
135 for y, x in cells:
136 owner[y, x] = subdomain
137 return owner
138
139
140def read_cells_per_tree(directory):
141 """! Cells per tree as Peano reports them in the last load balancing statistics of run.log"""
142 with open(os.path.join(directory, "run.log")) as log_file:
143 statistics = [line for line in log_file if "dumpStatistics" in line]
144 return [int(count) for count in re.findall(r"#\d+:(\d+)/", statistics[-1])]
145
146
147def draw_tree_sketch(axis, title, owners, caption):
148 """!
149
150 Draw a one-dimensional spacetree with three levels
151
152 owners holds the owner per level, i.e. a list with 1, 3 and 9 entries.
153
154 """
155 for level, level_owners in enumerate(owners):
156 width = 9 / len(level_owners)
157 for i, owner in enumerate(level_owners):
158 axis.add_patch(
159 Rectangle(
160 (i * width, -level),
161 width,
162 0.7,
163 facecolor=TreeColours[owner],
164 edgecolor="black",
165 lw=0.8,
166 )
167 )
168 axis.annotate(
169 "",
170 xy=(9.1, -2 + 0.35),
171 xytext=(-0.1, -2 + 0.35),
172 arrowprops=dict(arrowstyle="->", color="black", lw=0.8),
173 )
174 axis.set_xlim(-0.3, 9.3)
175 axis.set_ylim(-2.9, 0.9)
176 axis.axis("off")
177 axis.set_title(title, fontsize=10)
178 axis.text(4.5, -2.75, caption, ha="center", va="center", fontsize=8.5)
179
180
181def draw_subdomains(axis, directory, title):
182 """! Draw the subdomains of a run together with Peano's cell counts per tree"""
183 owner = read_subdomains(directory)
184 axis.imshow(
185 owner,
186 origin="lower",
187 cmap=ListedColormap(TreeColours),
188 vmin=0,
189 vmax=3,
190 extent=(0, CellsPerAxis, 0, CellsPerAxis),
191 )
192 for k in range(0, CellsPerAxis + 1, 3):
193 width = 1.0 if k % 9 == 0 else 0.3
194 axis.axhline(k, color="black", lw=width)
195 axis.axvline(k, color="black", lw=width)
196 counts = read_cells_per_tree(directory)
197 axis.set_title(
198 title + "\ncells on tree 0, 1, 2, 3: " + " / ".join(str(c) for c in counts),
199 fontsize=10,
200 )
201 axis.set_xticks([])
202 axis.set_yticks([])
203
204
205def render(top_down_directory, bottom_up_directory, picture=SplitModesPicture):
206 figure, axes = plt.subplots(
207 2, 2, figsize=(9.5, 8.2), gridspec_kw={"width_ratios": [1.25, 1]}
208 )
210 axes[0][0],
211 "Top-down: tree 0 hands 4 cells to tree 1",
212 [[0], [1, 1, 0], [1, 1, 1, 1, 1, 1, 0, 0, 0]],
213 "Whole refined cells move while cells are still owed:\n"
214 "the second coarse cell moves although only 1 cell is owed,\n"
215 "so tree 1 gets 6 instead of 4 fine cells",
216 )
218 axes[1][0],
219 "Bottom-up: tree 0 hands 4 cells to tree 1",
220 [[0], [1, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 0]],
221 "Fine cells move one by one along the curve, and a coarse\n"
222 "cell follows once all of its children have moved:\n"
223 "tree 1 gets exactly 4 fine cells",
224 )
225 draw_subdomains(axes[0][1], top_down_directory, "Tutorial run, top-down")
226 draw_subdomains(axes[1][1], bottom_up_directory, "Tutorial run, bottom-up")
227 figure.legend(
228 handles=[
229 Patch(color=TreeColours[0], label="tree 0"),
230 Patch(color=TreeColours[1], label=""),
231 Patch(color=TreeColours[2], label=""),
232 Patch(
233 color=TreeColours[3],
234 label="trees split off from tree 0 (tree 1 in the sketches)",
235 ),
236 ],
237 loc="lower center",
238 ncol=4,
239 frameon=False,
240 fontsize=9,
241 columnspacing=0.4,
242 )
243 plt.tight_layout(rect=(0, 0.04, 1, 1))
244 plt.savefig(picture, dpi=130)
245
246
247if __name__ == "__main__":
248 parser = argparse.ArgumentParser(
249 description="Create the split-mode PNG of the tutorial 35_offline-load-balancing.ipynb"
250 )
251 parser.add_argument(
252 "--work-directory",
253 default=None,
254 help="Directory for builds and outputs (default: temporary directory)",
255 )
256 parser.add_argument(
257 "--peano-root",
258 default=domain_decomposition.PeanoRoot,
259 help="Peano installation with the compiled libraries (default: repository holding this script)",
260 )
261 args = parser.parse_args()
262
263 work_directory = args.work_directory or tempfile.mkdtemp(
264 prefix="35_offline-load-balancing-split-modes-"
265 )
266 print("build and run in {}".format(work_directory))
267 directories = {}
268 for modes in [Hardcoded.TopDown, Hardcoded.BottomUp]:
269 directories[modes] = os.path.join(work_directory, modes)
270 os.makedirs(directories[modes], exist_ok=True)
271 domain_decomposition.build_and_run(
272 directories[modes],
273 os.path.abspath(args.peano_root),
274 tutorial="35_offline-load-balancing",
275 namespace="offlineloadbalancing",
276 load_balancer="toolbox::loadbalancing::strategies::Hardcoded",
277 load_balancer_arguments=offline_decomposition(modes),
278 )
279 render(directories[Hardcoded.TopDown], directories[Hardcoded.BottomUp])
280 print("wrote {}".format(SplitModesPicture))
offline_decomposition(modes)
Arguments of the hardcoded load balancing with the given split modes.
render(top_down_directory, bottom_up_directory, picture=SplitModesPicture)
draw_subdomains(axis, directory, title)
Draw the subdomains of a run together with Peano's cell counts per tree.
draw_tree_sketch(axis, title, owners, caption)
Draw a one-dimensional spacetree with three levels.
read_cells_per_tree(directory)
Cells per tree as Peano reports them in the last load balancing statistics of run....
read_subdomains(directory)
Subdomain of each fine grid cell, read from the first snapshot's patch files.
equidistant_splitting(Tree peano_tree, int number_of_subtrees, verbose=False)
RegularGridGenerator(name=None, max_level=None, dimensions=None)
Generate an artificial regular grid with uniform refinement for testing purposes.
tuple[int, int] Peano_to_Cartesian_2d(int peano_index, int size)
Inverse of Cartesian_to_Peano_2d.
Definition SFC.py:160
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