Peano
Loading...
Searching...
No Matches
522_single-black-hole.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 PNG illustrations of the tutorial 522_single-black-hole.ipynb
6
7The script builds the single black hole setup of the tutorial, runs it,
8extracts the lapse and the conformal factor along the x-axis with Peano's
9PointProbe, and writes all pictures into the directory of this script. It
10runs three setups, one after the other, so their runtimes can be compared:
11
12- plain: the main run of the tutorial, i.e. exahype2.api.fv.RusanovWithExplicitEuler
13 with 6x6x6 patches and without load balancing;
14- decomposed: the run of the section "Speeding up", i.e. 10x10x10 patches,
15 a domain decomposition and four OpenMP threads;
16- decomposed-constrained: the same with the CCZ4 constraint enforcement
17 after each time step (section "Improving the physics").
18
19The pictures of the initial data and of the lapse stem from the plain run.
20522_single-black-hole-speeding-up.png compares the plain and the decomposed
21run, 522_single-black-hole-constraints.png the two decomposed runs. Call it
22from anywhere:
23
24 python3 tutorials/exahype2/exagrype/illustrations/522_single-black-hole.py
25
26Peano has to be configured with --with-gsl, as the initial data stems from
27TwoPunctures. All builds and outputs go into a temporary directory (or
28--work-directory). If you pass a work directory, a run which has completed
29before is reused, i.e. you can rerun the script to redraw the pictures
30without recomputing.
31
32"""
33import argparse
34import contextlib
35import csv
36import io
37import os
38import subprocess
39import sys
40import tempfile
41import time
42
43Here = os.path.dirname(os.path.abspath(__file__))
44PeanoRoot = os.path.abspath(os.path.join(Here, "..", "..", "..", ".."))
45sys.path.insert(0, os.path.join(PeanoRoot, "src"))
46
47import matplotlib
48
49matplotlib.use("Agg")
50import matplotlib.pyplot as plt
51import numpy as np
52
53import peano4.api
54import exahype2.api
55import exagrype.api
57
58
59PatchFile = "solution-CCZ4.peano-patch-file"
60Mass = 1.0
61DomainOffset = -9.0
62DomainSize = 18.0
63CellsPerAxis = 9
64PatchSize = 6
65VolumesPerAxis = CellsPerAxis * PatchSize
66TimeInBetweenPlots = 0.5
67
68
70LineOffset = DomainSize / VolumesPerAxis / 2.0
71
72
73def psi(r):
74 """! Conformal factor of the Schwarzschild black hole in isotropic coordinates"""
75 return 1.0 + Mass / 2.0 / r
76
77
78
81DecomposedPatchSize = 10
82DecomposedThreads = 4
83
84
85Runs = [
86 ("plain", PatchSize, False, None, exagrype.api.CCZ4Solver.EnforceCCZ4Constraint.No),
87 (
88 "decomposed",
89 DecomposedPatchSize,
90 True,
91 DecomposedThreads,
92 exagrype.api.CCZ4Solver.EnforceCCZ4Constraint.No,
93 ),
94 (
95 "decomposed-constrained",
96 DecomposedPatchSize,
97 True,
98 DecomposedThreads,
99 exagrype.api.CCZ4Solver.EnforceCCZ4Constraint.OnlyFinalSolution,
100 ),
101]
102
103
105 directory,
106 peano_installation,
107 end_time,
108 scheme,
109 enforce,
110 patch_size=PatchSize,
111 load_balancing=False,
112 threads=None,
113):
114 """!
115
116 Generate, compile and run the tutorial setup within directory
117
118 @param patch_size: int
119 Number of volumes per axis within one patch. The mesh always has
120 CellsPerAxis cells per axis.
121
122 @param load_balancing: bool
123 If True, decompose the domain with
124 toolbox::loadbalancing::strategies::SpreadOutOnceGridStagnates.
125
126 @param threads: int or None
127 If not None, run with OMP_NUM_THREADS set to this value.
128
129 @param scheme: type
130 Numerical scheme, i.e. a Finite Volume class from exahype2.api.fv.
131
132 @param enforce: exagrype.api.CCZ4Solver.EnforceCCZ4Constraint
133 Constraint enforcement.
134
135 Returns the runtime of the executable in seconds. If the directory holds
136 the result of a previous run, we do not rerun but return the stored
137 runtime.
138
139 """
140 runtime_file = os.path.join(directory, "runtime.txt")
141 if os.path.exists(runtime_file):
142 with open(runtime_file) as f:
143 return float(f.read())
144
145 os.makedirs(directory, exist_ok=True)
146 print(
147 "build and run single black hole until t={} in {}".format(end_time, directory)
148 )
149 project = exahype2.api.Project(
150 namespace=["tutorials", "exagrype", "singleblackhole"],
151 project_name="SingleBlackHole",
152 directory=directory,
153 executable="single-black-hole",
154 )
156 "CCZ4",
157 parameters={"CCZ4bs": 1.0, "CCZ4sk": 1.0},
158 initial_condition=exagrype.api.initialvalues.single_black_hole(mass=Mass),
159 )
160 solver.add_pde_term(
161 exahype2.api.DirichletBoundaryConditions(
162 implementation="::exagrype::initialvalues::singleBlackHole(Q, x, t, {});".format(
163 Mass
164 )
165 )
166 )
167 solver.numerical_scheme = scheme(solver, patch_size)
168 solver.enforce_ccz4_constraint(enforce)
169 solver.grid_construction = exahype2.api.fv.SetupSimulationOverRegularGrid(
170 solver, volume_size=1.01 * DomainSize / CellsPerAxis / patch_size
171 )
172 solver.plotter = exahype2.api.fv.PlotAsVTKFile(
173 solver,
174 first_plot_time_stamp=0.0,
175 time_in_between_plots=TimeInBetweenPlots,
176 plotter_precision=10,
177 )
178 solver.plotter.add_plot_filter(
179 offset=[DomainOffset, 0.1, 0.1], size=[DomainSize, 0.1, 0.1]
180 )
181 project.add_solver(solver)
182 project.set_global_simulation_parameters(
183 dimensions=3,
184 offset=[DomainOffset] * 3,
185 size=[DomainSize] * 3,
186 min_end_time=end_time,
187 max_end_time=end_time,
188 periodic_BC=[False, False, False],
189 )
190 if load_balancing:
191 project.set_load_balancing(
192 "toolbox::loadbalancing::strategies::SpreadOutOnceGridStagnates",
193 "new ::exahype2::LoadBalancingConfiguration()",
194 )
195 project.set_Peano4_installation(
196 peano_installation, peano4.api.output.CompileMode.RELEASE
197 )
198 with contextlib.redirect_stdout(io.StringIO()):
199 peano4_project = project.generate_Peano4_project(
200 verbosity=peano4.api.LoggerVerbosity.TEXT
201 )
202 peano4_project.generate()
203
204 subprocess.run(
205 ["make", "-j"],
206 cwd=directory,
207 check=True,
208 stdout=subprocess.DEVNULL,
209 stderr=subprocess.DEVNULL,
210 )
211 start = time.time()
212 with open(os.path.join(directory, "run.log"), "w") as log:
213 environment = dict(os.environ)
214 if threads is not None:
215 environment["OMP_NUM_THREADS"] = str(threads)
216 subprocess.run(
217 ["./single-black-hole"],
218 cwd=directory,
219 check=True,
220 stdout=log,
221 env=environment,
222 )
223 runtime = time.time() - start
224 with open(runtime_file, "w") as f:
225 f.write(str(runtime))
226 return runtime
227
228
229def probe(directory, point, output_file):
230 """!
231
232 Probe all snapshots at one point
233
234 Returns the time stamps, the lapse and the conformal factor phi.
235
236 """
238 file_name=os.path.join(directory, PatchFile),
239 point=point,
240 output_file=os.path.join(directory, output_file),
241 start=0,
242 end=sys.maxsize,
243 )
244 with contextlib.redirect_stdout(io.StringIO()):
245 point_probe.display()
246 with open(os.path.join(directory, output_file)) as csv_file:
247 rows = list(csv.DictReader(csv_file))
248 return (
249 np.array([float(row["time"]) for row in rows]),
250 np.array([float(row["lapse"]) for row in rows]),
251 np.array([float(row["phi"]) for row in rows]),
252 )
253
254
255def profile(directory, volumes_per_axis=VolumesPerAxis):
256 """!
257
258 Probe lapse and phi in the centre of each volume along the line through the
259 centres of the volumes next to the puncture, i.e. along y=z=h/2
260
261 Returns the x coordinates, the time stamps and two 2d arrays
262 lapse[snapshot,x] and phi[snapshot,x].
263
264 """
265 h = DomainSize / volumes_per_axis
266 x = DomainOffset + (np.arange(volumes_per_axis) + 0.5) * h
267 lapse, phi = [], []
268 for i, x_i in enumerate(x):
269 t, lapse_over_time, phi_over_time = probe(
270 directory, [x_i, h / 2.0, h / 2.0], "profile-{}.csv".format(i)
271 )
272 lapse.append(lapse_over_time)
273 phi.append(phi_over_time)
274 return x, t, np.array(lapse).T, np.array(phi).T
275
276
277def radius(x, line_offset=LineOffset):
278 return np.sqrt(np.asarray(x) ** 2 + 2.0 * line_offset**2)
279
280
281def time_steps(directory):
282 """!
283
284 Number of time steps and average wall clock time per time step
285
286 Both stem from the final report of peano4::DefaultStepReport in run.log,
287 i.e. from the line "TimeStep: ...s (avg=...,#measurements=...".
288
289 """
290 with open(os.path.join(directory, "run.log")) as log:
291 for line in log:
292 if "reportFinalResults" in line and "TimeStep:" in line:
293 statistics = line.split("(avg=")[1]
294 average = float(statistics.split(",")[0])
295 count = int(statistics.split("#measurements=")[1].split(",")[0])
296 return count, average
297 raise RuntimeError(f"no time step statistics in {directory}/run.log")
298
299
300def plot_speeding_up(coarse, fine):
301 """! Lapse next to the puncture over time, and along the x-axis at the end, for both meshes"""
302 fig, axes = plt.subplots(1, 2, figsize=(11, 4))
303 for (label, (x, t, lapse, volumes_per_axis)), marker in zip(
304 [("54 volumes per axis", coarse), ("90 volumes per axis", fine)], ["o", "s"]
305 ):
306 h = DomainSize / volumes_per_axis
307 centre = volumes_per_axis // 2
308 axes[0].plot(
309 t,
310 lapse[:, centre],
311 marker + "-",
312 markersize=3,
313 linewidth=1,
314 label="{}, r={:.2f}".format(label, radius(x[centre], h / 2.0)),
315 )
316 axes[1].plot(x, lapse[-1], marker + "-", markersize=2, linewidth=1, label=label)
317 axes[0].set_xlabel("t")
318 axes[0].set_ylabel("lapse")
319 axes[0].set_title("Lapse next to the puncture")
320 axes[1].set_xlabel("x")
321 axes[1].set_ylabel("lapse")
322 axes[1].set_title("Lapse along the x-axis at t={:.0f}".format(coarse[1][-1]))
323 axes[0].legend(loc="lower right")
324 axes[1].legend(loc="lower right")
325 fig.savefig(
326 os.path.join(Here, "522_single-black-hole-speeding-up.png"),
327 dpi=100,
328 bbox_inches="tight",
329 )
330 plt.close(fig)
331
332
333def plot_initial_data(x, lapse, phi):
334 """! Initial lapse and conformal factor along the x-axis against the closed form"""
335 h = DomainSize / VolumesPerAxis
336 x_fine = np.linspace(DomainOffset, DomainOffset + DomainSize, 2000)
337
338 fig, ax = plt.subplots(figsize=(8, 4))
339 ax.plot(
340 x_fine,
341 1.0 / psi(radius(x_fine)),
342 "-",
343 color="tab:blue",
344 linewidth=1,
345 label=r"$1/\psi$",
346 )
347 ax.plot(
348 x_fine,
349 psi(radius(x_fine)) ** (-2),
350 "-",
351 color="tab:orange",
352 linewidth=1,
353 label=r"$\psi^{-2}$",
354 )
355 ax.plot(x, lapse[0], "o", color="tab:blue", markersize=3, label="lapse (ExaGRyPE)")
356 ax.plot(x, phi[0], "s", color="tab:orange", markersize=3, label="phi (ExaGRyPE)")
357 for cell in range(CellsPerAxis + 1):
358 ax.axvline(
359 DomainOffset + cell * PatchSize * h,
360 color="tab:red",
361 linewidth=0.5,
362 linestyle="--",
363 )
364 ax.plot(
365 [], [], color="tab:red", linewidth=0.5, linestyle="--", label="patch boundaries"
366 )
367 ax.set_xlabel("x (along y=z={:.3f})".format(LineOffset))
368 ax.set_ylabel("initial value")
369 ax.set_title("Initial data of a single black hole with M={}".format(Mass))
370 ax.legend(loc="upper left", bbox_to_anchor=(1.01, 1.0))
371 fig.savefig(
372 os.path.join(Here, "522_single-black-hole-initial-data.png"),
373 dpi=100,
374 bbox_inches="tight",
375 )
376 plt.close(fig)
377
378
379def plot_lapse_profile(x, t, lapse):
380 """! Lapse along the x-axis for several snapshots"""
381 fig, ax = plt.subplots(figsize=(8, 4))
382 snapshots = sorted(set([0, len(t) // 4, len(t) // 2, 3 * len(t) // 4, len(t) - 1]))
383 colours = plt.cm.viridis(np.linspace(0.0, 0.9, len(snapshots)))
384 for snapshot, colour in zip(snapshots, colours):
385 ax.plot(
386 x,
387 lapse[snapshot],
388 "o-",
389 color=colour,
390 markersize=2,
391 linewidth=1,
392 label="t={:.2f}".format(t[snapshot]),
393 )
394 ax.set_xlabel("x (along y=z={:.3f})".format(LineOffset))
395 ax.set_ylabel("lapse")
396 ax.set_title("Lapse along the x-axis over time")
397 ax.legend(loc="upper left", bbox_to_anchor=(1.01, 1.0))
398 fig.savefig(
399 os.path.join(Here, "522_single-black-hole-lapse-profile.png"),
400 dpi=100,
401 bbox_inches="tight",
402 )
403 plt.close(fig)
404
405
406def plot_lapse_over_time(x, t, lapse):
407 """! Lapse over time at a few distances from the puncture"""
408 fig, ax = plt.subplots(figsize=(8, 4))
409 for x_probe in [LineOffset, 1.0 + LineOffset, 3.0 + LineOffset, 8.0 + LineOffset]:
410 i = np.argmin(np.abs(x - x_probe))
411 ax.plot(
412 t,
413 lapse[:, i],
414 "o-",
415 markersize=2,
416 linewidth=1,
417 label="r={:.2f}".format(radius(x[i])),
418 )
419 ax.set_xlabel("t")
420 ax.set_ylabel("lapse")
421 ax.set_title("Lapse over time at several distances from the puncture")
422 ax.legend(loc="upper left", bbox_to_anchor=(1.01, 1.0))
423 fig.savefig(
424 os.path.join(Here, "522_single-black-hole-lapse-over-time.png"),
425 dpi=100,
426 bbox_inches="tight",
427 )
428 plt.close(fig)
429
430
431def plot_constraints(unconstrained, constrained):
432 """! Lapse and phi next to the puncture over time with and without constraint enforcement"""
433 fig, axes = plt.subplots(1, 2, figsize=(11, 4))
434 for (label, run), marker in zip(
435 [("without enforcement", unconstrained), ("with enforcement", constrained)],
436 ["o", "s"],
437 ):
438 centre = run["volumes_per_axis"] // 2
439 axes[0].plot(
440 run["t"],
441 run["lapse"][:, centre],
442 marker + "-",
443 markersize=3,
444 linewidth=1,
445 label=label,
446 )
447 axes[1].plot(
448 run["t"],
449 run["phi"][:, centre],
450 marker + "-",
451 markersize=3,
452 linewidth=1,
453 label=label,
454 )
455 centre = unconstrained["volumes_per_axis"] // 2
456 r = radius(unconstrained["x"][centre], unconstrained["h"] / 2.0)
457 for ax, quantity in zip(axes, ["lapse", "phi"]):
458 ax.set_xlabel("t")
459 ax.set_ylabel(quantity)
460 ax.set_title("{} next to the puncture (r={:.2f})".format(quantity, r))
461 axes[1].legend(loc="lower right")
462 fig.savefig(
463 os.path.join(Here, "522_single-black-hole-constraints.png"),
464 dpi=100,
465 bbox_inches="tight",
466 )
467 plt.close(fig)
468
469
470if __name__ == "__main__":
471 parser = argparse.ArgumentParser(
472 description="Create the PNGs of the tutorial 522_single-black-hole.ipynb"
473 )
474 parser.add_argument(
475 "--work-directory",
476 default=None,
477 help="Directory for builds and outputs (default: temporary directory)",
478 )
479 parser.add_argument(
480 "--peano-root",
481 default=PeanoRoot,
482 help="Configured (--with-gsl) and built Peano installation to link against (default: the repository holding this script)",
483 )
484 parser.add_argument(
485 "--end-time", type=float, default=10.0, help="Simulated time span (default: 10)"
486 )
487 args = parser.parse_args()
488
489 with contextlib.ExitStack() as stack:
490 work_directory = args.work_directory or stack.enter_context(
491 tempfile.TemporaryDirectory()
492 )
493 runs = {}
494 for name, patch_size, load_balancing, threads, enforce in Runs:
495 directory = os.path.join(os.path.abspath(work_directory), name)
496 runtime = build_and_run(
497 directory,
498 os.path.abspath(args.peano_root),
499 args.end_time,
501 enforce,
502 patch_size=patch_size,
503 load_balancing=load_balancing,
504 threads=threads,
505 )
506 volumes_per_axis = CellsPerAxis * patch_size
507 x, t, lapse, phi = profile(directory, volumes_per_axis)
508 count, average = time_steps(directory)
509 h = DomainSize / volumes_per_axis
510 runs[name] = dict(
511 runtime=runtime,
512 x=x,
513 t=t,
514 lapse=lapse,
515 phi=phi,
516 h=h,
517 volumes_per_axis=volumes_per_axis,
518 )
519 centre = volumes_per_axis // 2
520 print(
521 "{}: runtime {:.1f}s, {} time steps of {:.3f}s, {} volumes, {:.3e}s per volume update".format(
522 name,
523 runtime,
524 count,
525 average,
526 volumes_per_axis**3,
527 average / volumes_per_axis**3,
528 )
529 )
530 print(
531 "{}: next to the puncture (r={:.3f}) lapse {:.4f} -> {:.4f}, phi {:.4f} -> {:.4f}, NaN: {}".format(
532 name,
533 radius(x[centre], h / 2.0),
534 lapse[0, centre],
535 lapse[-1, centre],
536 phi[0, centre],
537 phi[-1, centre],
538 np.isnan(lapse).any(),
539 )
540 )
541 print(
542 "max deviation of decomposed-constrained from decomposed: lapse {:.3e}, phi {:.3e}".format(
543 np.max(
544 np.abs(
545 runs["decomposed-constrained"]["lapse"]
546 - runs["decomposed"]["lapse"]
547 )
548 ),
549 np.max(
550 np.abs(
551 runs["decomposed-constrained"]["phi"]
552 - runs["decomposed"]["phi"]
553 )
554 ),
555 )
556 )
558 (
559 runs["plain"]["x"],
560 runs["plain"]["t"],
561 runs["plain"]["lapse"],
562 VolumesPerAxis,
563 ),
564 (
565 runs["decomposed"]["x"],
566 runs["decomposed"]["t"],
567 runs["decomposed"]["lapse"],
568 runs["decomposed"]["volumes_per_axis"],
569 ),
570 )
571 plot_constraints(runs["decomposed"], runs["decomposed-constrained"])
572
573 x, t, lapse, phi = (runs["plain"][key] for key in ["x", "t", "lapse", "phi"])
574 print("snapshots at t =", np.round(t, 3))
575 for i in [
576 VolumesPerAxis // 2,
577 VolumesPerAxis // 2 + 3,
578 VolumesPerAxis // 2 + 9,
579 VolumesPerAxis - 1,
580 ]:
581 print(
582 "r={:.3f}: lapse(t=0)={:.4f} (1/psi={:.4f}), lapse(t={:.2f})={:.4f}, phi(t=0)={:.4f} (psi^-2={:.4f})".format(
583 radius(x[i]),
584 lapse[0, i],
585 1.0 / psi(radius(x[i])),
586 t[-1],
587 lapse[-1, i],
588 phi[0, i],
589 psi(radius(x[i])) ** (-2),
590 )
591 )
592 print("NaN in lapse:", np.isnan(lapse).any())
593
594 plot_initial_data(x, lapse, phi)
595 plot_lapse_profile(x, t, lapse)
596 plot_lapse_over_time(x, t, lapse)
Solver for the Einstein equations in the first-order CCZ4 formulation.
Definition CCZ4Solver.py:16
ExaHyPE 2 project.
Definition Project.py:15
Plotting counterpart to RusanovWithExplicitEuler: maps the patch data of a Finite Volume scheme with ...
Finite Volumes with a Rusanov Riemann solver and explicit Euler time stepping, realised over a block-...
Setup phase counterpart to RusanovWithExplicitEuler: sets the initial condition for a Finite Volume s...
Probe the data within a Peano patch file at one point over time.
Definition PointProbe.py:37
plot_lapse_over_time(x, t, lapse)
Lapse over time at a few distances from the puncture.
radius(x, line_offset=LineOffset)
psi(r)
Conformal factor of the Schwarzschild black hole in isotropic coordinates.
profile(directory, volumes_per_axis=VolumesPerAxis)
Probe lapse and phi in the centre of each volume along the line through the centres of the volumes ne...
plot_constraints(unconstrained, constrained)
Lapse and phi next to the puncture over time with and without constraint enforcement.
build_and_run(directory, peano_installation, end_time, scheme, enforce, patch_size=PatchSize, load_balancing=False, threads=None)
Generate, compile and run the tutorial setup within directory.
plot_speeding_up(coarse, fine)
Lapse next to the puncture over time, and along the x-axis at the end, for both meshes.
plot_lapse_profile(x, t, lapse)
Lapse along the x-axis for several snapshots.
time_steps(directory)
Number of time steps and average wall clock time per time step.
plot_initial_data(x, lapse, phi)
Initial lapse and conformal factor along the x-axis against the closed form.
exahype2.api.InitPointWisely single_black_hole(float mass=1.0)
Single Schwarzschild black hole at the origin.
This file is part of the ExaGRyPE project.
Definition __init__.py:1
-lift-drop-statistics
-single-black-hole