Peano
Loading...
Searching...
No Matches
single-black-hole.py
Go to the documentation of this file.
1"""
2
3Single black hole benchmark for the ExaGRyPE Python API
4
5A single Schwarzschild black hole at the origin of the domain [-243,243]^3,
6as described in README.dox. The initial data stems from TwoPunctures, the
7boundaries are Sommerfeld boundaries. The script uses exagrype.api, i.e. the
8CCZ4 solver of the new Python API. Everything that matters for the benchmark
9is configurable on the command line:
10
11- the numerical scheme (--scheme, --rk-order, --ko-sigma, --lopsided),
12- the adaptive mesh refinement (--amr-levels, --amr-radius),
13- the task graph compiler (--compiler),
14- the build variant (--variant), which picks the compile mode and the size of
15 the setup, and
16- the final time (--end-time).
17
18The build variants are
19
20- debug: Peano's debug build with a tiny setup to check that the code works,
21- profile: Peano's build with statistics counters and a reduced setup
22 (domain 172, volumes of size 1) which is small enough to profile on one
23 node, and
24- production: the release build with the setup of README.dox (domain 486,
25 volumes of size 0.4, t=100). This one is far too big for a workstation.
26
27Each variant fixes the defaults of --domain-size, --volume-size and
28--end-time. Pass them explicitly to override them.
29
30Adaptive mesh refinement respects the constraints of README.dox: the
31maximum volume size (--volume-size) is a hard bound, i.e. the mesh is never
32coarser than the regular mesh of this size. The refinement only ever
33subdivides cells around the black hole further, by --amr-levels levels of
34factor three each within --amr-radius of the puncture. It is available for
35both schemes. The fd4 scheme starts on the regular mesh and refines it while
36it time steps; it cannot combine AMR with the Kreiss-Oliger dissipation yet.
37
38It requires a Peano build with
39
40 --enable-exahype --enable-blockstructured --enable-loadbalancing --enable-exagrype --with-gsl
41
42A run of the fourth-order Finite Differences with lopsided advection and
43the Kreiss-Oliger dissipation looks like
44
45 python3 single-black-hole.py --scheme fd4 --rk-order 4 --lopsided --ko-sigma 8.0 \
46 --compiler producer-consumer-fusion --variant profile
47
48and Finite Volumes with two additional levels of adaptive mesh refinement
49around the black hole are
50
51 python3 single-black-hole.py --scheme fv --amr-levels 2 --variant profile
52
53The script writes two kinds of output into the output directory: binary
54checkpoints (--checkpoint-interval, every 10 time units by default) holding
55the whole solver state, and visualisation snapshots in Peano's patch file
56format (--plot-interval, every 1 time unit by default), i.e.
57solution-CCZ4.peano-patch-file and its companion files. The snapshots
58contain all unknowns. probe-single-black-hole.py extracts profiles from
59them. Pass 0 to switch either kind off.
60
61"""
62
63import argparse
64import os
65
66import peano4.api
67import exahype2.api
68import exagrype.api
70import toolbox.loadbalancing.api.strategies.SplitTrees as SplitTrees
71import toolbox.loadbalancing.api.strategies.Decomposition as Decomposition
72import toolbox.loadbalancing.api.output.Hardcoded as Hardcoded
73
74
75Compilers = {
76 "sequential": peano4.api.taskgraph.compiler.Sequential.translate,
77 "fusion-only": peano4.api.taskgraph.compiler.FuseSteps.translate,
78 "producer-consumer-sequential": peano4.api.taskgraph.compiler.CellTaskProducerConsumer.translate_with_sequential,
79 "producer-consumer-fusion": peano4.api.taskgraph.compiler.CellTaskProducerConsumer.translate_with_fusion,
80}
81
82EnforceModes = {
83 mode.name.lower(): mode
84 for mode in exagrype.api.CCZ4Solver.EnforceCCZ4Constraint
85}
86
87
88Variants = {
89 "debug": dict(
90 mode=peano4.api.output.CompileMode.DEBUG,
91 domain_size=18.0,
92 volume_size=1.2,
93 end_time=0.5,
94 ),
95 "profile": dict(
96 mode=peano4.api.output.CompileMode.STATS,
97 domain_size=172.0,
98 volume_size=1.2,
99 end_time=1.0,
100 ),
101 "production": dict(
102 mode=peano4.api.output.CompileMode.RELEASE,
103 domain_size=486.0,
104 volume_size=0.4,
105 end_time=100.0,
106 ),
107}
108
109SplitModes = {
110 "bottom-up": Hardcoded.BottomUp,
111 "top-down": Hardcoded.TopDown,
112 "inferred": None,
113}
114
115Verbosities = {
116 mode.name.lower(): mode for mode in peano4.api.LoggerVerbosity
117}
118
119
121 parser = argparse.ArgumentParser(
122 description="Single Schwarzschild black hole with ExaGRyPE",
123 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
124 )
125
126 scheme = parser.add_argument_group("numerical scheme")
127 scheme.add_argument(
128 "--scheme",
129 choices=["fv", "fd4"],
130 default="fd4",
131 help="fv: Rusanov Finite Volumes with explicit Euler. fd4: fourth-order Finite Differences with Runge-Kutta.",
132 )
133 scheme.add_argument(
134 "--rk-order",
135 type=int,
136 choices=[1, 2, 3, 4],
137 default=4,
138 help="Order of the Runge-Kutta time integrator (fd4 only).",
139 )
140 scheme.add_argument(
141 "--ko-sigma",
142 type=float,
143 default=0.0,
144 help="Scaling of the Kreiss-Oliger dissipation (fd4 only). 0 switches the dissipation off.",
145 )
146 scheme.add_argument(
147 "--ko-quantities",
148 type=int,
149 nargs="+",
150 default=None,
151 metavar="Q",
152 help="Indices of the unknowns which are damped by the Kreiss-Oliger dissipation. Default: all.",
153 )
154 scheme.add_argument(
155 "--lopsided",
156 action="store_true",
157 help="Add the lop-sided advection terms along the shift (fd4 only).",
158 )
159 scheme.add_argument(
160 "--enforce-constraints",
161 choices=list(EnforceModes),
162 default="no",
163 help="When to push the solution back onto the algebraic CCZ4 constraints.",
164 )
165
166 mesh = parser.add_argument_group("domain and mesh")
167 mesh.add_argument(
168 "--domain-size",
169 type=float,
170 default=None,
171 help="Edge length of the domain. Default: depends on --variant.",
172 )
173 mesh.add_argument(
174 "--volume-size",
175 type=float,
176 default=None,
177 help="Maximum size of a volume or grid cell. The mesh is a regular 3-adic grid, i.e. the actual size is smaller. Default: depends on --variant.",
178 )
179 mesh.add_argument(
180 "--amr-levels",
181 type=int,
182 default=0,
183 help="Number of additional mesh refinement levels (factor 3 each) around the black hole. 0 yields the regular mesh.",
184 )
185 mesh.add_argument(
186 "--amr-radius",
187 type=float,
188 default=12.0,
189 help="Radius around the puncture within which the mesh is refined to the finest level.",
190 )
191 mesh.add_argument(
192 "--patch-size",
193 type=int,
194 default=6,
195 help="Volumes/grid cells per axis per mesh cell. Has to be even, so no volume centre hits the puncture.",
196 )
197 mesh.add_argument("--mass", type=float, default=1.0, help="Mass of the black hole.")
198
199 time = parser.add_argument_group("time and checkpoints")
200 time.add_argument(
201 "--end-time",
202 type=float,
203 default=None,
204 help="Final time. Default: depends on --variant.",
205 )
206 time.add_argument(
207 "--checkpoint-interval",
208 type=float,
209 default=10.0,
210 help="Time between two binary checkpoints. 0 switches checkpointing off.",
211 )
212 time.add_argument(
213 "--plot-interval",
214 type=float,
215 default=1.0,
216 help="Time between two visualisation snapshots. 0 switches the snapshots off.",
217 )
218 time.add_argument(
219 "--first-checkpoint",
220 type=float,
221 default=0.0,
222 help="Time stamp of the first checkpoint and of the first snapshot.",
223 )
224 time.add_argument(
225 "--checkpoint-precision",
226 type=int,
227 default=10,
228 help="Significant digits per value in the visualisation snapshots. Checkpoints are lossless.",
229 )
230
231 balancing = parser.add_argument_group("load balancing")
232 balancing.add_argument(
233 "--load-balancing",
234 choices=["hardcoded", "spread-out"],
235 default="hardcoded",
236 help="hardcoded: decomposition of the regular mesh, computed offline. spread-out: SpreadOutOnceGridStagnates decides at runtime.",
237 )
238 balancing.add_argument(
239 "--decomposition",
240 choices=["equidistant", "quantised"],
241 default="equidistant",
242 help="Chunk sizes of the hardcoded load balancing. equidistant: every tree gets the same number of cells. quantised: chunk sizes are multiples of 3^(k*d) cells, i.e. trees hold (almost) complete subtrees and few virtual cells, but they might be imbalanced.",
243 )
244 balancing.add_argument(
245 "--split-mode",
246 choices=["bottom-up", "top-down", "inferred"],
247 default="bottom-up",
248 help="How Peano picks the cells of a split (hardcoded load balancing only).",
249 )
250 balancing.add_argument(
251 "--trees",
252 type=int,
253 default=os.cpu_count(),
254 help="Number of trees (subdomains) of the hardcoded load balancing. They all live on rank 0, i.e. one tree per thread.",
255 )
256
257 build = parser.add_argument_group("build")
258 build.add_argument(
259 "--compiler",
260 choices=list(Compilers),
261 default="producer-consumer-fusion",
262 help="Task graph compiler, i.e. how time step tasks are mapped onto mesh traversals.",
263 )
264 build.add_argument(
265 "--variant",
266 choices=list(Variants),
267 default="production",
268 help="Build variant: compile mode and default size of the setup.",
269 )
270 build.add_argument(
271 "--verbosity", choices=list(Verbosities), default="text"
272 )
273 build.add_argument(
274 "--output-directory",
275 default=".",
276 help="Directory the code is generated into and the checkpoints are written to.",
277 )
278 build.add_argument(
279 "--peano-directory",
280 default="../../../",
281 help="Root of the Peano installation.",
282 )
283 build.add_argument(
284 "--no-build",
285 action="store_true",
286 help="Generate the code but do not compile it.",
287 )
288 build.add_argument(
289 "--no-clean",
290 action="store_true",
291 help="Do not run make clean before the build.",
292 )
293
294 arguments = parser.parse_args()
295
296 for key, value in Variants[arguments.variant].items():
297 if key != "mode" and getattr(arguments, key) is None:
298 setattr(arguments, key, value)
299
300 if arguments.patch_size % 2 != 0:
301 parser.error("--patch-size has to be even")
302 if arguments.scheme == "fv" and (arguments.ko_sigma > 0.0 or arguments.lopsided):
303 parser.error("--ko-sigma and --lopsided require --scheme fd4")
304 if arguments.ko_quantities is not None and arguments.ko_sigma <= 0.0:
305 parser.error("--ko-quantities requires --ko-sigma > 0")
306 if arguments.amr_levels < 0:
307 parser.error("--amr-levels must not be negative")
308 if arguments.amr_levels > 0 and arguments.scheme == "fd4" and arguments.ko_sigma > 0.0:
309 parser.error("--amr-levels with --scheme fd4 does not support --ko-sigma yet")
310 if arguments.load_balancing == "hardcoded" and arguments.trees < 2:
311 parser.error("--trees has to be at least 2 for the hardcoded load balancing")
312 if arguments.checkpoint_interval < 0.0 or arguments.plot_interval < 0.0:
313 parser.error("--checkpoint-interval and --plot-interval must not be negative")
314 return arguments
315
316
317def create_numerical_scheme(solver, arguments):
318 """! Assign numerical scheme, grid construction and checkpoint plotter to solver"""
319 if arguments.scheme == "fv":
320 package = exahype2.api.fv
321 solver.numerical_scheme = exahype2.api.fv.RusanovWithExplicitEuler(
322 solver,
323 arguments.patch_size,
324 min_volume_size=min_volume_size(arguments),
325 max_volume_size=arguments.volume_size,
326 )
327 else:
328 package = exahype2.api.rkfd4
329 Kernel = exahype2.api.rkfd4.CentralDifferencesWithRungeKutta.RightHandSideKernel
330 options = {}
331 if arguments.lopsided:
332 options = dict(
333 right_hand_side_kernel=Kernel.CentralDifferencesWithLopsidedAdvection,
334 advection_velocity=exagrype.api.CCZ4Solver.ADVECTION_VELOCITY,
335 advected_quantities=solver.lopsided_advection_quantities(),
336 )
337 if arguments.ko_sigma > 0.0:
338 solver.numerical_scheme = exagrype.api.FD4WithKODissipation(
339 solver,
340 arguments.patch_size,
341 rk_order=arguments.rk_order,
342 kreiss_oliger_sigma=arguments.ko_sigma,
343 quantities=arguments.ko_quantities,
344 **options,
345 )
346 else:
347 solver.numerical_scheme = (
348 exahype2.api.rkfd4.CentralDifferencesWithRungeKutta(
349 solver,
350 arguments.patch_size,
351 rk_order=arguments.rk_order,
352 min_volume_size=min_volume_size(arguments),
353 max_volume_size=arguments.volume_size,
354 **options,
355 )
356 )
357
358 if arguments.scheme == "fv":
359 solver.grid_construction = package.SetupSimulationWithUserCodedRefinementCriterion(
360 solver,
361 volume_size=arguments.volume_size,
362 min_volume_size=min_volume_size(arguments),
363 )
364 else:
365 # fd4 has no refinement in the setup: It starts on the regular mesh
366 # and the time stepping refines it
367 solver.grid_construction = package.SetupSimulationOverRegularGrid(
368 solver, volume_size=arguments.volume_size
369 )
370 if arguments.checkpoint_interval > 0.0:
371 solver.add_plotter(
372 package.WriteCheckpoint(
373 solver,
374 first_plot_time_stamp=arguments.first_checkpoint,
375 time_in_between_plots=arguments.checkpoint_interval,
376 output_path=arguments.output_directory,
377 )
378 )
379 if arguments.plot_interval > 0.0:
380 solver.add_plotter(
381 package.PlotAsVTKFile(
382 solver,
383 first_plot_time_stamp=arguments.first_checkpoint,
384 time_in_between_plots=arguments.plot_interval,
385 plotter_precision=arguments.checkpoint_precision,
386 output_path=arguments.output_directory,
387 )
388 )
389
390
391def actual_volume_size(arguments):
392 """! Size of a volume/grid cell the regular 3-adic mesh yields"""
393 cell_size = arguments.domain_size
394 while cell_size >= arguments.volume_size * arguments.patch_size:
395 cell_size /= 3.0
396 return cell_size / arguments.patch_size
397
398
399def min_volume_size(arguments):
400 """! Volumes of this size or bigger may still be refined.
401
402 The regular mesh of --volume-size is the coarsest mesh the benchmark
403 allows (README.dox), so the AMR levels sit on top of its actual volume
404 size. Refining a cell yields volumes strictly smaller than the bound, so
405 the bound is the volume size of the second-finest level, slightly
406 reduced to be robust against rounding. Without AMR levels, the bound
407 would exceed the --volume-size. It is capped there, which is still above the
408 volume size of the regular mesh, i.e. the mesh stays regular.
409 """
410 return min(
411 actual_volume_size(arguments) / 3.0 ** (arguments.amr_levels - 1) * 0.99,
412 arguments.volume_size,
413 )
414
415
416def regular_mesh_levels(arguments):
417 """! Number of refinement levels of the regular 3-adic mesh (see actual_volume_size())"""
418 levels = 0
419 cell_size = arguments.domain_size
420 while cell_size >= arguments.volume_size * arguments.patch_size:
421 cell_size /= 3.0
422 levels += 1
423 return levels
424
425
426def create_load_balancing(arguments):
427 """! Name and constructor arguments of the load balancing strategy
428
429 The hardcoded strategy splits the regular mesh into arguments.trees trees
430 of the same number of cells, i.e. the outcome SpreadOutOnceGridStagnates
431 aims for. The splits are computed offline on a synthetic regular tree
432 along the space-filling curve, and all trees live on rank 0. AMR levels
433 do not enter, as the splits fire once the mesh has reached the regular
434 mesh and are bottom-up.
435 """
436 if arguments.load_balancing == "spread-out":
437 return (
438 "toolbox::loadbalancing::strategies::SpreadOutOnceGridStagnates",
439 "new ::exahype2::LoadBalancingConfiguration()",
440 )
441
442 tree = RegularGridGenerator.RegularGridGenerator(
443 max_level=regular_mesh_levels(arguments), dimensions=3
444 )
445 if arguments.decomposition == "quantised":
446 decomposition = Decomposition.quantised_splitting(
447 peano_tree=tree, number_of_subtrees=arguments.trees, dimension=3
448 )
449 else:
450 decomposition = Decomposition.equidistant_splitting(
451 peano_tree=tree, number_of_subtrees=arguments.trees
452 )
453 SplitTrees.split_tree(
454 tree, decomposition=decomposition, periodic_BC=[False, False, False]
455 )
456 destination_ranks = Hardcoded.assign_trees_to_ranks_round_Robin(
457 number_of_subtrees=tree.number_of_subtrees, max_ranks=1
458 )
459 configuration, _ = Hardcoded.create_constructor_initialisation_string(
460 tree, destination_ranks, modes=SplitModes[arguments.split_mode]
461 )
462 return "toolbox::loadbalancing::strategies::Hardcoded", configuration
463
464
465def main():
466 arguments = parse_arguments()
467
468 project = exahype2.api.Project(
469 namespace=["benchmarks", "exagrype", "singleblackhole"],
470 project_name="single-black-hole",
471 directory=arguments.output_directory,
472 executable="single-black-hole",
473 )
474
475 solver = exagrype.api.CCZ4Solver(
476 "CCZ4",
477 parameters={"CCZ4bs": 1.0, "CCZ4sk": 1.0},
478 initial_condition=exagrype.api.initialvalues.single_black_hole(
479 mass=arguments.mass
480 ),
481 )
482 solver.enforce_ccz4_constraint(EnforceModes[arguments.enforce_constraints])
483 solver.add_pde_term(
484 exahype2.api.SommerfeldBoundaryConditions(
485 implementation="::exagrype::initialvalues::flat(Q, x, 0.0);"
486 )
487 )
488 # The criterion never asks for an erase. The mesh is a function of the
489 # position only, i.e. it is static, and it never coarsens below the
490 # regular mesh anyway. Without AMR levels, the volume size bound keeps
491 # the mesh regular.
492 solver.add_pde_term(
493 exahype2.api.RefineErasePointwisely(
494 implementation=f"""
495 const double r2 = tarch::la::norm2Squared(x);
496 if (r2 < {arguments.amr_radius ** 2}) *refinementCommand = ::exahype2::RefinementCommand::Refine;
497"""
498 )
499 )
500 create_numerical_scheme(solver, arguments)
501 project.add_solver(solver)
502
503 project.compiler = Compilers[arguments.compiler]
504
505 half_size = arguments.domain_size / 2.0
506 project.set_global_simulation_parameters(
507 dimensions=3,
508 offset=[-half_size] * 3,
509 size=[arguments.domain_size] * 3,
510 min_end_time=arguments.end_time,
511 max_end_time=arguments.end_time,
512 periodic_BC=[False, False, False],
513 )
514
515 project.set_load_balancing(*create_load_balancing(arguments))
516
517 project.set_Peano4_installation(
518 arguments.peano_directory, Variants[arguments.variant]["mode"]
519 )
520 peano4_project = project.generate_Peano4_project(
521 verbosity=Verbosities[arguments.verbosity]
522 )
523 peano4_project.generate()
524
525 h = actual_volume_size(arguments)
526 print(
527 f"Volume size {h:.6g} ({round(arguments.domain_size / h)} volumes per axis), "
528 f"scheme {arguments.scheme}, {arguments.amr_levels} AMR levels, "
529 f"variant {arguments.variant}, compiler {arguments.compiler}, "
530 f"end time {arguments.end_time}"
531 )
532
533 if not arguments.no_build:
534 peano4_project.build(make_clean_first=not arguments.no_clean)
535
536
537if __name__ == "__main__":
538 main()
Controls how much detail Logger.log()/new_section() print to the console.
Definition Logger.py:20
This file is part of the ExaGRyPE project.
Definition __init__.py:1
create_numerical_scheme(solver, arguments)
Assign numerical scheme, grid construction and checkpoint plotter to solver.