Peano
Loading...
Searching...
No Matches
The gauge wave: A first numerical relativity experiment with ExaGRyPE
This documentation is autogenerated from ./tutorials/exahype2/exagrype/521_gauge-wave.ipynb. Once you clone the Peano repository, please work against this notebook or study Peano's generic description how to generate web docu from Jupyter notebooks.

This tutorial runs the simplest standard test of numerical relativity codes with ExaGRyPE: the gauge wave. Its exact solution is known, so we can always check what the code computes. The tutorial follows the structure of A first advection problem with the Euler equations, but instead of programming the PDE ourselves, we use the CCZ4 solver which ExaGRyPE's Python API exagrype.api ships.

We run through the following steps:

  • how to create a CCZ4 solver with exagrype.api and discretise it with Finite Volumes;
  • how to keep the output of a three-dimensional simulation with 59 unknowns small;
  • how to use Peano's PointProbe from within Python to extract the wave profile and the time series at one point;
  • how to study the convergence order over different mesh and patch sizes.

Before you start, configure and build Peano as described in Configure ExaGRyPE. You need --enable-exagrype, but not GSL: the gauge wave does not need any black hole initial data.

In this tutorial, we analyse the solution after the simulation has finished by reading the plot files. Later tutorials introduce the in-situ alternatives: tracer particles which record quantities while the code runs, on-the-fly postprocessing such as the evaluation of the constraints, and black hole spacetimes. See Advanced Usage of ExaGRyPE, A single black hole with ExaGRyPE: a first (too) trivial setup and Binary Black Hole Merger.

The gauge wave

The gauge wave is flat (Minkowski) spacetime written down in unusual coordinates. Its line element reads

\begin{eqnarray*} ds^2 = -H \, dt^2 + H \, dx^2 + dy^2 + dz^2, \qquad H(x,t) = 1 - A \sin\left( 2 \pi (x-t) \right) \end{eqnarray*}

with the amplitude \( A=0.1\) . As the spacetime is flat, nothing physical happens. The numerical code however sees a non-trivial lapse \( \alpha = \sqrt{H}\) , a non-trivial metric and extrinsic curvature, and all of them travel with speed one along the x-axis. We simulate the unit cube with periodic boundary conditions. After \( t=1\) , the wave has crossed the domain once and is back at its initial position. The exact lapse is

\begin{eqnarray*} \alpha(x,t) = \sqrt{ 1 - A \sin\left( 2 \pi (x-t) \right) }. \end{eqnarray*}

The gauge wave is one of the standard testbeds for numerical relativity codes. The setup and the equations are discussed in Alcubierre et al.: Toward standard testbeds for numerical relativity, section IV.B. As we know the exact solution, any change of the wave's shape or amplitude is due to numerical effects.

Configuring the environment

The Python API of Peano, ExaHyPE and ExaGRyPE resides within the src directory of your Peano installation. Unless you have installed it via pip install -e ., we add it to the search path manually. We also memorise the Peano root directory, as the generated Makefile has to link against Peano's libraries.

import sys, os
peano_root = os.path.abspath("../../../")
sys.path.insert(0, os.path.join(peano_root, "src"))
import peano4.api
This file is part of the ExaGRyPE project.
Definition __init__.py:1

The unknowns of CCZ4

ExaGRyPE solves the Einstein equations in the first-order CCZ4 formulation. In this formulation, the spacetime is described by 59 unknowns per point: the conformal metric \( \tilde{\gamma}_{ij}\) (six entries, as it is symmetric), the traceless conformal extrinsic curvature \( \tilde{A}_{ij}\) , the Z4 variables \( \Theta\) and \( \hat{\Gamma}^i\) , the lapse \( \alpha\) , the shift \( \beta^i\) and the auxiliary variable \( b^i\) of the gamma driver, the trace \( K\) of the extrinsic curvature and the conformal factor \( \phi\) . Since we use a first-order formulation, the spatial derivatives of lapse, shift, conformal metric and conformal factor become unknowns of their own. exagrype.api names all of them:

first_index = 0
for name, entries in exagrype.api.CCZ4Solver.Unknowns.items():
print("{:>8}: entries {:2} to {:2}".format(name, first_index, first_index + entries - 1))
first_index += entries

The lapse is unknown 16. The plotter writes these names into the output files, so we can later refer to the lapse by its name.

The project

Every ExaHyPE 2 code starts with a project. The project describes the C++ namespace of the generated code, the name of the executable, and the directory into which all of the generated code goes. We use a subdirectory 521_gauge-wave, as the generator produces quite a lot of files and directories.

namespace = ["tutorials", "exagrype", "gaugewave"],
project_name = "GaugeWave",
directory = "521_gauge-wave",
executable = "gauge-wave",
)
ExaHyPE 2 project.
Definition Project.py:15

The solver

In the Euler tutorial, we created a generic exahype2.api.Solver and added the PDE terms one by one. ExaGRyPE does this for us: exagrype.api.CCZ4Solver is an exahype2.api.Solver with the 59 unknowns and all PDE terms of CCZ4 (non-conservative product, source and maximum eigenvalue) pre-registered. The terms call the kernels from the ExaGRyPE library, i.e. we do not have to write any C++ code. We only pick the initial condition. The module exagrype.api.initialvalues offers a couple of analytical setups.

"CCZ4",
)
Solver for the Einstein equations in the first-order CCZ4 formulation.
Definition CCZ4Solver.py:16
exahype2.api.InitPointWisely gauge_wave()
Gauge wave along the x-axis,.

CCZ4 has a number of parameters, e.g. the damping coefficients \( \kappa_1\) , \( \kappa_2\) and \( \kappa_3\) (CCZ4k1, CCZ4k2, CCZ4k3) of the constraint violations. The solver comes along with the defaults of ExaGRyPE's production runs. You can change them through the constructor's parameters argument, e.g. parameters={"CCZ4k1": 0.0}. For the gauge wave, we stick to the defaults:

solver.parameters

Numerical scheme and mesh

We discretise CCZ4 with Finite Volumes, exactly as the Euler equations before: Each cell of the Peano mesh carries a patch of \( 9 \times 9 \times 9\) volumes. The scheme uses a Rusanov flux between neighbouring volumes and explicit Euler in time, i.e. it is a first-order scheme. The time step size follows from the CFL condition with the largest eigenvalue of CCZ4.

For the mesh, we use a regular grid. Three sizes are involved: the size of one mesh cell, the size of one volume, and the maximum volume size volume_size which we hand over to the grid construction. Every mesh cell hosts one patch, i.e. a cell's edge length is the patch size times the edge length \( h\) of one volume. Peano refines every cell as long as its edge length is greater than or equal to volume_size times the patch size. As Peano splits each cell into \( 3 \times 3 \times 3\) cells whenever it refines, a maximum volume size of 0.04 yields \( 3 \times 3 \times 3\) cells of edge length \( 1/3\) , i.e. 27 volumes per axis with a volume size of \( h=1/27\) .

The volume size \( h\) is hence always strictly smaller than the maximum volume size. Be careful if you pass in exactly the volume size that you want to obtain: A maximum volume size of \( 1/27\) asks for cells which are strictly smaller than \( 9 \cdot 1/27 = 1/3\) . Peano refines once more, and you end up with 81 volumes per axis and a code which is 81 times more expensive. Pick a maximum volume size which is slightly bigger than the volume size you want.

patch_size = 9
max_volume_size = 0.04
solver.numerical_scheme = exahype2.api.fv.RusanovWithExplicitEuler(solver, patch_size)
solver.grid_construction = exahype2.api.fv.SetupSimulationOverRegularGrid(solver, volume_size=max_volume_size)
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...

The picture below shows the initial lapse along the line \( y=z=0.5\) . The black line is the exact lapse, the blue steps are the volume averages which the Finite Volume scheme stores. The dashed red lines are the patch boundaries: Three patches with nine volumes each cover the x-axis. We sample the wave with 27 volumes per wavelength.

Initial lapse along the x-axis with volumes and patches

Plotting

A three-dimensional simulation with 59 unknowns produces a lot of data. One snapshot of our coarse mesh holds \( 27^3 \cdot 59 \approx 1.2\) million values. On a mesh with 81 volumes per axis, it is 27 times more. As the gauge wave travels along the x-axis only, we do not need all of these data: We add a plot filter which restricts the output to all patches overlapping a thin box around the line \( y=z=0.5\) . The plotter then writes one row of patches only.

We also increase the plotter's precision. By default, it writes five digits. This is enough to look at the solution, but it would limit the accuracy of our convergence study later on. We plot every 0.05 time units.

solver,
first_plot_time_stamp = 0.0,
time_in_between_plots = 0.05,
plotter_precision = 10,
)
solver.plotter.add_plot_filter(offset=[0.0, 0.49, 0.49], size=[1.0, 0.02, 0.02])
Plotting counterpart to RusanovWithExplicitEuler: maps the patch data of a Finite Volume scheme with ...

Global simulation parameters

We add the solver to the project and specify the domain, the time span we simulate, and the boundary conditions. We simulate exactly one period of the wave, i.e. until \( t=1\) . CCZ4 is three-dimensional only: The generated solver refuses to compile for any other dimension.

project.add_solver(solver)
project.set_global_simulation_parameters(
dimensions = 3,
offset = [0.0, 0.0, 0.0],
size = [1.0, 1.0, 1.0],
min_end_time = 1.0,
max_end_time = 1.0,
periodic_BC = [True, True, True],
)

Lowering into C++

Now we lower the ExaHyPE project into a Peano project, and the Peano project into C++ code plus a Makefile. ExaHyPE has to know where Peano is installed, as the Makefile links against Peano's libraries. We pass an absolute path, as the generated Makefile resides in the subdirectory 521_gauge-wave. We also pick the release mode.

project.set_Peano4_installation(peano_root, peano4.api.output.CompileMode.RELEASE)
peano4_project = project.generate_Peano4_project(verbosity=peano4.api.LoggerVerbosity.TEXT)
peano4_project.generate()

Let's see what we got:

!ls 521_gauge-wave

Compared to the Euler tutorial, there is one difference: There is no solver class CCZ4.cpp for us to fill. All PDE terms are implemented in AbstractCCZ4.cpp, which delegates to the ExaGRyPE library:

!grep "::exagrype::ccz4" 521_gauge-wave/AbstractCCZ4.cpp

Log filter

As in the Euler tutorial, we tell the executable which log messages we are interested in:

%%writefile 521_gauge-wave/exahype2.log-filter
debug tarch -1 black always-on
debug peano4 -1 black always-on
debug exahype2 -1 black always-on
debug exagrype -1 black always-on
debug toolbox -1 black always-on
debug tutorials -1 black always-on
trace tarch -1 black always-on
trace peano4 -1 black always-on
trace exahype2 -1 black always-on
trace exagrype -1 black always-on
trace toolbox -1 black always-on
trace tutorials -1 black always-on
info tarch -1 white always-on
info peano4 -1 white always-on
info exahype2 -1 white always-on
info exagrype -1 white always-on
info toolbox -1 white always-on
info tutorials -1 white always-on

Build and run

We compile with make and run the executable. The code runs for around 180 time steps. On an eight-core workstation, the run takes around 25 seconds, and it writes 21 snapshots.

!cd 521_gauge-wave && make -j
!cd 521_gauge-wave && ./gauge-wave

Probing one point over time

In the Euler tutorial, we called Peano's probe script probe.py to extract the solution at one point over all snapshots. The script is a thin wrapper around the Python class tarch.plotter.api.output.PointProbe. We can use this class directly within our notebook: We create a probe object for the point of interest, and its display() routine writes the values of all unknowns at this point over all snapshots into a csv file. There is no interpolation: The probe takes the value of the volume which contains the point. The csv file holds one row per snapshot: the time stamp followed by all unknowns, named as discussed above.

We wrap everything into a small helper routine which returns the time stamps and the lapse. The probe is chatty, so we silence its output:

import contextlib
import csv
import io
import numpy as np
import matplotlib.pyplot as plt
def probe(point, output_file="probe.csv", directory="521_gauge-wave"):
file_name = os.path.join(directory, "solution-CCZ4.peano-patch-file"),
point = point,
output_file = os.path.join(directory, output_file),
start = 0,
end = sys.maxsize,
)
with contextlib.redirect_stdout(io.StringIO()):
point_probe.display()
with open(os.path.join(directory, output_file)) as csv_file:
rows = list(csv.DictReader(csv_file))
t = np.array([float(row["time"]) for row in rows])
lapse = np.array([float(row["lapse"]) for row in rows])
return t, lapse
def exact_lapse(x, t):
return np.sqrt(1.0 - 0.1 * np.sin(2.0 * np.pi * (np.asarray(x) - t)))
Probe the data within a Peano patch file at one point over time.
Definition PointProbe.py:37
-single-black-hole

We probe the centre of the domain. The exact lapse there is \( \alpha(0.5,t) = \sqrt{1 - 0.1 \sin(2\pi(0.5-t))}\) . Please note that the code does not hit the plot time stamps exactly: It plots after the first time step which passes the next plot time stamp. We therefore compare against the exact solution at the time stamps stored in the csv file.

t, lapse = probe([0.5, 0.5, 0.5])
t_exact = np.linspace(0.0, 1.0, 400)
plt.plot(t_exact, exact_lapse(0.5, t_exact), "k-", label="exact")
plt.plot(t, lapse, "o", markersize=3, label="probe at (0.5,0.5,0.5)")
plt.xlabel("t")
plt.ylabel("lapse")
plt.legend()
plt.show()

You should get the following picture: The observer at the centre sees the wave passing by once. The numerical solution has the right period, but it loses amplitude over time.

Lapse at the centre of the domain over time

Extracting the wave profile

The probe gives us the time series at one point. To get the shape of the wave at one point in time, we place one probe into the centre of every volume along the line \( y=z=0.5\) . Each probe returns the whole time series, so we obtain the wave profile for all snapshots at once. With 27 volumes, this takes a few seconds.

def wave_profile(volumes_per_axis, directory="521_gauge-wave"):
x = (np.arange(volumes_per_axis) + 0.5) / volumes_per_axis
lapse = []
for i, x_i in enumerate(x):
t, lapse_over_time = probe([x_i, 0.5, 0.5], "profile-{}.csv".format(i), directory)
lapse.append(lapse_over_time)
return x, t, np.array(lapse).T
x, t, lapse = wave_profile(27)
x_exact = np.linspace(0.0, 1.0, 400)
for snapshot in [0, len(t) // 2, len(t) - 1]:
line, = plt.plot(x_exact, exact_lapse(x_exact, t[snapshot]), "-", linewidth=1)
plt.plot(x, lapse[snapshot], "o", markersize=3, color=line.get_color(), label="t={:.2f}".format(t[snapshot]))
plt.xlabel("x")
plt.ylabel("lapse")
plt.legend()
plt.show()

The picture shows the lapse at \( t=0\) , \( t=0.5\) and \( t=1\) . The lines are the exact solution, the dots are the values computed by ExaGRyPE. The wave travels at the right speed: After half a period, it has moved by half the domain, and at \( t=1\) it is back at its initial position. However, the scheme smears out the peaks. This is numerical diffusion. The Rusanov flux adds artificial viscosity to keep the scheme stable, and the coarser the mesh, the stronger this effect. After one period, the amplitude has dropped from 0.05 to around 0.013, and the maximum error of the lapse is 0.038.

Lapse profile at three points in time

If you prefer to look at the solution in ParaView, convert the patch files into VTU files with Peano's render script. It relies on the ParaView/VTK Python libraries. See Visualization of ExaGRyPE Output for some remarks on the visualisation of ExaGRyPE's output. cd 521_gauge-wave python3 ../../../../src/tarch/plotter/api/render.py solution-CCZ4.peano-patch-file

Convergence over mesh and patch sizes

The gauge wave is a convergence test: We check that the numerical error shrinks as expected when we refine the mesh. We measure the error of the lapse after one period along the line \( y=z=0.5\) ,

\begin{eqnarray*} e(h) = \max_i \left| \alpha_i - \alpha(x_i,t) \right|, \end{eqnarray*}

where \( \alpha_i\) is the value of volume \( i\) with centre \( x_i\) and \( t \approx 1\) the time stamp of the last snapshot. If we make the volumes three times smaller, the error of a scheme of order \( p\) should go down by a factor of \( 3^p\) . We hence estimate the order from two subsequent meshes via

\begin{eqnarray*} p \approx \frac{\log \left( e(3h) / e(h) \right)}{\log 3}. \end{eqnarray*}

Here, \( h\) is the volume size, i.e. the edge length of one volume, which is one over the number of volumes per axis. With patches, there are two knobs to obtain a certain volume size \( h\) : the patch size and the number of mesh cells. 27 volumes per axis result from \( 9 \times 9 \times 9\) cells with patches of size 3, or from \( 3 \times 3 \times 3\) cells with patches of size 9. Peano always refines the root cell at least once, i.e. there are at least three cells per axis. A patch size of 27 hence requires at least 81 volumes per axis. We study all combinations of the volume sizes \( 1/9\) , \( 1/27\) and \( 1/81\) with the patch sizes 3, 9 and 27 that fit to this constraint. To do so, we wrap all the steps from above into one routine which builds and runs the gauge wave in its own directory and returns the runtime:

import shutil
import subprocess
import time
def run_gauge_wave(max_volume_size, patch_size, directory):
namespace=["tutorials", "exagrype", "gaugewave"],
project_name="GaugeWave",
directory=directory,
executable="gauge-wave",
)
solver = exagrype.api.CCZ4Solver("CCZ4", initial_condition=exagrype.api.initialvalues.gauge_wave())
solver.numerical_scheme = exahype2.api.fv.RusanovWithExplicitEuler(solver, patch_size)
solver.grid_construction = exahype2.api.fv.SetupSimulationOverRegularGrid(solver, volume_size=max_volume_size)
solver, first_plot_time_stamp=0.0, time_in_between_plots=0.05, plotter_precision=10
)
solver.plotter.add_plot_filter(offset=[0.0, 0.49, 0.49], size=[1.0, 0.02, 0.02])
project.add_solver(solver)
project.set_global_simulation_parameters(
dimensions=3, offset=[0.0, 0.0, 0.0], size=[1.0, 1.0, 1.0],
min_end_time=1.0, max_end_time=1.0, periodic_BC=[True, True, True],
)
project.set_Peano4_installation(peano_root, peano4.api.output.CompileMode.RELEASE)
with contextlib.redirect_stdout(io.StringIO()):
peano4_project = project.generate_Peano4_project(verbosity=peano4.api.LoggerVerbosity.TEXT)
peano4_project.generate()
shutil.copy("521_gauge-wave/exahype2.log-filter", directory)
subprocess.run(["make", "-j"], cwd=directory, check=True, stdout=subprocess.DEVNULL)
start = time.time()
subprocess.run(["./gauge-wave"], cwd=directory, check=True, stdout=subprocess.DEVNULL)
return time.time() - start

Again, we pick maximum volume sizes which are slightly bigger than the volume sizes \( 1/9\) , \( 1/27\) and \( 1/81\) that we want to obtain. The runs with 9 and 27 volumes per axis take seconds. The runs with 81 volumes per axis take around 25 to 55 minutes each on an eight-core workstation, as they have 27 times more volumes and need three times more time steps than the runs on the coarse mesh. They are therefore switched off by default:

include_fine_meshes = False
max_volume_sizes = {9: 0.12, 27: 0.04}
if include_fine_meshes:
max_volume_sizes[81] = 0.013
results = {}
for volumes_per_axis, max_volume_size in max_volume_sizes.items():
for patch_size in [3, 9, 27]:
if 3 * patch_size > volumes_per_axis:
continue
directory = "521_gauge-wave-h{}-p{}".format(volumes_per_axis, patch_size)
runtime = run_gauge_wave(max_volume_size, patch_size, directory)
x, t, lapse = wave_profile(volumes_per_axis, directory)
error = np.max(np.abs(lapse[-1] - exact_lapse(x, t[-1])))
results[(volumes_per_axis, patch_size)] = (error, runtime)
print("{:2} volumes per axis, patch size {:2}: error={:.4e}, runtime={:.1f}s".format(
volumes_per_axis, patch_size, error, runtime))

We get the following errors after one period. The estimated order refers to the next coarser mesh with the same patch size:

volumes per axis patch size cells per axis max error at t=1 order runtime
9 3 3 4.84e-2 2 s
27 3 9 3.76e-2 0.23 82 s
81 3 27 1.88e-2 0.63 53 min
27 9 3 3.76e-2 23 s
81 9 9 1.88e-2 0.63 26 min
81 27 3 1.88e-2 31 min
Convergence of the Finite Volume scheme

Two observations stand out.

First, the patch size does not change the solution at all. For a given volume size, all patch sizes yield exactly the same error. This is what we expect: The Finite Volume scheme applies the same stencil to every volume, and the patches only determine how Peano organises the volumes in memory and how it distributes the work. The patch size does however change the runtime. With patches of size 3, there are \( 27^3\) cells on the finest mesh. Each cell comes with some administrative overhead, and each patch has to exchange a halo of one volume with its neighbours, which is expensive relative to the tiny patch. With patches of size 27, there are only 27 cells, i.e. there is little parallelism for the eight cores. Patches of size 9 are the sweet spot here.

Second, the estimated orders at \( t=1\) are well below one, although the scheme is first order. The table below shows the maximum error of the runs with patch size 3 at several points in time:

t h=1/9 h=1/27 h=1/81 order (1/9 to 1/27) order (1/27 to 1/81)
0.05 9.32e-3 3.39e-3 1.18e-3 0.92 0.96
0.1 1.57e-2 6.59e-3 2.34e-3 0.79 0.94
0.2 2.58e-2 1.21e-2 4.57e-3 0.69 0.89
0.5 4.21e-2 2.48e-2 1.05e-2 0.48 0.78
1.0 4.84e-2 3.76e-2 1.88e-2 0.23 0.63

Early on, the error indeed shrinks by a factor of three whenever we make the volumes three times smaller. Over time, the numerical diffusion damps the wave away. The error cannot grow beyond the amplitude of the wave, which is 0.05. On the coarsest mesh, the wave has almost entirely disappeared after one period, i.e. the error has saturated. Such a mesh is not in the asymptotic regime where the error behaves like \( h^p\) , and the order estimates drop. The picture shows the maximum error over the volume size for the four points in time. The lines are the runs with patch size 3. The squares and triangles show that the runs with patch sizes 9 and 27 sit exactly on top of them.

If you want to measure a convergence order, you hence either have to stop early or to refine further until all meshes are in the asymptotic regime. The latter quickly becomes expensive: A run with 243 volumes per axis would need around 81 times as long as a run with 81 volumes per axis.

Exercise:** Compute the convergence order for the L2 error along the line instead of the maximum error, and compare the error of the lapse with the error of another unknown such as the trace of the extrinsic curvature (traceK). The analytical solution of all unknowns is implemented in exagrype::initialvalues::gaugeWave() in src/exagrype/initialvalues/InitialValues.cpp.

The pictures of this tutorial are created by the script tutorials/exahype2/exagrype/illustrations/521_gauge-wave.py, which runs exactly this study.

Outlook

Our setup has a few limitations which you should be aware of:

  • We used Finite Volumes, i.e. a first-order scheme which smears out the wave noticeably. The production runs of ExaGRyPE use fourth-order Finite Differences. They are available through exahype2.api.rkfd4. To switch, replace the numerical scheme, the grid construction and the plotter:
    solver.numerical_scheme  = exahype2.api.rkfd4.CentralDifferencesWithRungeKutta(solver, patch_size, rk_order=4)
    solver.grid_construction = exahype2.api.rkfd4.SetupSimulationOverRegularGrid(solver, volume_size=max_volume_size)
    solver.plotter           = exahype2.api.rkfd4.PlotAsVTKFile(solver, first_plot_time_stamp=0.0, time_in_between_plots=0.05, plotter_precision=10)
    
    On our mesh with 27 grid cells per axis, the maximum error of the lapse after one period (t=1) drops from roughly 4e-2 with Finite Volumes to roughly 5e-5 with fourth-order Finite Differences. Unlike the legacy ExaGRyPE, exahype2.api.rkfd4 supports regular grids only so far.
  • There is no constraint enforcement after the time steps yet, i.e. the solution is not pushed back onto the CCZ4 constraint surface. Long runs hence drift away from it.
  • We analysed the solution after the run through the plot files. This becomes expensive for long runs and fine meshes. Later tutorials introduce tracers, i.e. particles which record the solution at fixed points while the code runs, and on-the-fly postprocessing (see Advanced Usage of ExaGRyPE), before we move on to black holes in A single black hole with ExaGRyPE: a first (too) trivial setup and Binary Black Hole Merger.

To clean up, delete the generated directories. If you rerun the notebook, they are all re-created.

!rm -rf 521_gauge-wave 521_gauge-wave-h*