Dam break over triangular bottom topography

In this tutorial we will use the shallow water equations to simulate a dam break over triangular bottom topography with wetting and drying and compare the results to experimental data. The test case is based on a dam break experiment and has been discussed in:

The tutorial will cover:

  • Set up a SWE solver for wet/dry transitions
  • Create custom initial conditions and source terms
  • Save solution data at gauge points
  • Visualization with Makie.jl

Load required packages

Before we start, we need to load the required packages. Besides TrixiShallowWater.jl, we require Trixi.jl for the spatial discretization and OrdinaryDiffEqSSPRK.jl for time integration. In addition to that CairoMakie.jl is used for visualization and CSV.jl and DataFrames.jl will be used to load the experimental data.

Standard packages

using OrdinaryDiffEqSSPRK
using Trixi
using TrixiShallowWater

Packages for visualization

using CairoMakie
using DataFrames
using CSV

Prepare and run the problem setup

In the first step we will set up the equation system. In this example we want to solve the one-dimensional shallow water equations, so we will use the ShallowWaterEquationsWetDry1D and specify the gravitational acceleration to gravity = 9.812. In contrast to the Trixi.ShallowWaterEquations1D type, this equation type contains additional parameters and methods that are needed to handle wetting and drying.

equations = ShallowWaterEquationsWetDry1D(gravity = 9.812)
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ShallowWaterEquationsWetDry1D                                                                    │
│ ═════════════════════════════                                                                    │
│ #variables: ………………………………………………… 3                                                                │
│ │ variable 1: …………………………………………… h                                                                │
│ │ variable 2: …………………………………………… h_v                                                              │
│ │ variable 3: …………………………………………… b                                                                │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

We then create a function to supply the initial condition for the simulation. Note, in the last step of this function the water height needs to be shifted by a small value to avoid division by zero.

function initial_condition_dam_break_triangular(x, t,
                                                equations::ShallowWaterEquationsWetDry1D)
    b = 0.0  # Bottom topography
    h = 0.0  # Water height
    v = 0.0  # Velocity

    if x[1] <= 15.5
        h = 0.75  # Water height in the left reservoir
    elseif 25.5 < x[1] && x[1] <= 28.5
        b = (x[1] - 25.5) * 0.4 / 3.0  # Rising slope of the triangular bottom
    elseif x[1] > 28.5 && x[1] < 31.5
        b = 0.4 - (x[1] - 28.5) * 0.4 / 3.0  # Falling slope of the triangular bottom
    end

    H = h + b  # Total water height
    if x[1] > 28.5
        H = max(H, 0.15)  # Water height in the right reservoir
    end

    # It is mandatory to shift the water level at dry areas to make sure the water height h
    # stays positive. The system would not be stable for h set to a hard zero due to division by h in
    # the computation of velocity, e.g., (h v) / h. Therefore, a small dry state threshold
    # with a default value of 5*eps() ≈ 1e-13 in double precision, is set in the constructor above
    # for the ShallowWaterEquations and added to the initial condition if h = 0.
    # This default value can be changed within the constructor call depending on the simulation setup.
    H = max(H, b + equations.threshold_limiter)
    return prim2cons(SVector(H, v, b), equations)
end

initial_condition = initial_condition_dam_break_triangular;

As we want to compare the results to experimental data, we also need to account for bottom friction. For this we create a new source term, which adds a Manning friction term to the momentum equation.

@inline function source_term_manning_friction(u, x, t,
                                              equations::ShallowWaterEquationsWetDry1D)
    h, hv, _ = u

    n = 0.0125  # friction coefficient
    h = (h^2 + max(h^2, 1e-8)) / (2 * h) # desingularization procedure

    return SVector(0.0, -equations.gravity * n^2 * h^(-7 / 3) * abs(hv) * hv, 0.0)
end
source_term_manning_friction (generic function with 1 method)

Now we can set up the DG approximation space. We use the discontinuous Galerkin spectral element method (DGSEM), with a volume integral in flux differencing formulation. For this, we first need to specify fluxes for both volume and surface integrals. Since the system contains nonconservative terms the fluxes are provided in form of a tuple flux = (conservative flux, nonconservative_flux). To ensure well-balancedness and positivity a reconstruction procedure is applied for the surface fluxes and a shock-capturing scheme with modified indicator function is used to compute the volume integrals.

volume_flux = (flux_wintermeyer_etal, flux_nonconservative_wintermeyer_etal)
surface_flux = (FluxHydrostaticReconstruction(flux_hll_chen_noelle,
                                              hydrostatic_reconstruction_chen_noelle),
                flux_nonconservative_chen_noelle)

basis = LobattoLegendreBasis(4)

indicator_sc = IndicatorHennemannGassnerShallowWater(equations, basis,
                                                     alpha_max = 0.5,
                                                     alpha_min = 0.001,
                                                     alpha_smooth = true,
                                                     variable = waterheight)
volume_integral = VolumeIntegralShockCapturingHG(indicator_sc;
                                                 volume_flux_dg = volume_flux,
                                                 volume_flux_fv = surface_flux)

solver = DGSEM(basis, surface_flux, volume_integral)
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DG{Float64}                                                                                      │
│ ═══════════                                                                                      │
│ basis: ……………………………………………………………… LobattoLegendreBasis{Float64}(polydeg=4)                         │
│ mortar: …………………………………………………………… LobattoLegendreMortarL2{Float64}(polydeg=4)                      │
│ surface integral: ………………………………… SurfaceIntegralWeakForm                                          │
│ │ surface flux: ……………………………………… (Trixi.FluxHydrostaticReconstruc…ux_nonconservative_chen_noelle) │
│ volume integral: …………………………………… VolumeIntegralShockCapturingHG                                   │
│ │ volume flux DG: ………………………………… (Trixi.flux_wintermeyer_etal, Tr…nconservative_wintermeyer_etal) │
│ │ volume flux FV: ………………………………… (Trixi.FluxHydrostaticReconstruc…ux_nonconservative_chen_noelle) │
│ │ indicator: ……………………………………………… IndicatorHennemannGassnerShallowWater                            │
│ │ │ indicator variable: ………………… waterheight                                                      │
│ │ │ max. α: ………………………………………………… 0.5                                                              │
│ │ │ min. α: ………………………………………………… 0.001                                                            │
│ │ │ smooth α: …………………………………………… yes                                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

Then the mesh is created using the TreeMesh type. The computational domain spans from coordinates_min to coordinates_max and is initialized with 2^8 = 256 elements on a non-periodic domain.

coordinates_min = 0.0
coordinates_max = 38.0

mesh = TreeMesh(coordinates_min, coordinates_max,
                initial_refinement_level = 8,
                n_cells_max = 10_000,
                periodicity = false)
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ TreeMesh{1, Trixi.SerialTree{1, Float64}}                                                        │
│ ═════════════════════════════════════════                                                        │
│ center: …………………………………………………………… [19.0]                                                           │
│ length: …………………………………………………………… 38.0                                                             │
│ periodicity: ……………………………………………… (false,)                                                         │
│ current #cells: ……………………………………… 511                                                              │
│ #leaf-cells: ……………………………………………… 256                                                              │
│ maximum #cells: ……………………………………… 10000                                                            │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

The semi-discretization object combines the mesh, equations, initial condition, solver, boundary conditions, and source terms into a single object. This object represents the spatial discretization of the problem and is complemented with the required time interval to define an ODE problem for time integration.

semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver,
                                    boundary_conditions = boundary_condition_slip_wall,
                                    source_terms = source_term_manning_friction)
tspan = (0.0, 40.0)
ode = semidiscretize(semi, tspan);

Callbacks are used to monitor the simulation, save results, and control the time step. Below, we define several callbacks for different purposes.

AnalysisCallback

The AnalysisCallback is used to analyze the solution at regular intervals. Extra analysis quantities such as conservation errors can be added to the callback.

analysis_callback = AnalysisCallback(semi, interval = 5000,
                                     extra_analysis_errors = (:conservation_error,))
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ AnalysisCallback                                                                                 │
│ ════════════════                                                                                 │
│ interval: ……………………………………………………… 5000                                                             │
│ analyzer: ……………………………………………………… LobattoLegendreAnalyzer{Float64}(polydeg=8)                      │
│ │ error 1: …………………………………………………… l2_error                                                         │
│ │ error 2: …………………………………………………… linf_error                                                       │
│ │ error 3: …………………………………………………… conservation_error                                               │
│ │ integral 1: …………………………………………… entropy_timederivative                                           │
│ save analysis to file: …………………… no                                                               │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

Time Series Callback

The TimeSeriesCallback is used to extract time series data at a specific gauge location.

time_series = TimeSeriesCallback(semi, [(19.5)])
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ TimeSeriesCallback                                                                               │
│ ══════════════════                                                                               │
│ #points: ………………………………………………………… 1                                                                │
│ interval: ……………………………………………………… 1                                                                │
│ solution_variables: …………………………… cons2cons                                                        │
│ output_directory: ………………………………… out                                                              │
│ filename: ……………………………………………………… time_series.h5                                                   │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

Stepsize Callback

The StepsizeCallback calculates the time step based on a CFL condition.

stepsize_callback = StepsizeCallback(cfl = 0.5)
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ StepsizeCallback                                                                                 │
│ ════════════════                                                                                 │
│ CFL number: ………………………………………………… 0.5                                                              │
│ Interval: ……………………………………………………… 1.0                                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

All the defined callbacks are then combined into a single CallbackSet.

callbacks = CallbackSet(analysis_callback,
                        time_series,
                        stepsize_callback);

Finally, we can go ahead an solve the ODE problem using a strong stability-preserving Runge-Kutta (SSPRK) method. The PositivityPreservingLimiterShallowWater is used as a stage limiter to ensure positivity of the water height during the simulation. The SSPRK43 integrator supports adaptive timestepping, but since we use a CFL-based time step we set (adaptive = false). For visualization purposes, we also use the saveat option to the solution at specific times.

stage_limiter! = PositivityPreservingLimiterShallowWater(variables = (waterheight,))

sol = solve(ode, SSPRK43(stage_limiter!); dt = 1.0,
            ode_default_options()..., callback = callbacks, adaptive = false,
            saveat = (0, 3.5, 40));

────────────────────────────────────────────────────────────────────────────────────────────────────
 Simulation running 'ShallowWaterEquationsWetDry1D' with DGSEM(polydeg=4)
────────────────────────────────────────────────────────────────────────────────────────────────────
 #timesteps:                  0                run time:       7.01000000e-07 s
 Δt:             1.00000000e+00                └── GC time:    0.00000000e+00 s (0.000%)
 sim. time:      0.00000000e+00 (0.000%)       time/DOF/rhs!:         NaN s
                                               PID:                   Inf s
 #DOFs per field:          1280                alloc'd memory:        165.366 MiB
 #elements:                 256

 Variable:       h                h_v              b
 L2 error:       9.73019569e-03   0.00000000e+00   1.15014447e-05
 Linf error:     3.55093396e-01   0.00000000e+00   3.92987526e-04
 |∑U - ∑U₀|:     0.00000000e+00   0.00000000e+00   0.00000000e+00
 ∑∂S/∂U ⋅ Uₜ :  -1.97004377e-01
────────────────────────────────────────────────────────────────────────────────────────────────────


────────────────────────────────────────────────────────────────────────────────────────────────────
 Simulation running 'ShallowWaterEquationsWetDry1D' with DGSEM(polydeg=4)
────────────────────────────────────────────────────────────────────────────────────────────────────
 #timesteps:               5000                run time:       2.11917818e+00 s
 Δt:             5.36347773e-03                └── GC time:    0.00000000e+00 s (0.000%)
 sim. time:      1.96595521e+01 (49.149%)      time/DOF/rhs!:  6.22613279e-08 s
                                               PID:            7.26145873e-08 s
 #DOFs per field:          1280                alloc'd memory:        184.983 MiB
 #elements:                 256

 Variable:       h                h_v              b
 L2 error:       4.19039722e-01   1.44949071e-01   1.15014447e-05
 Linf error:     5.78359913e-01   3.15368463e-01   3.92987526e-04
 |∑U - ∑U₀|:     7.58837437e-14   7.29677093e-02   1.59594560e-16
 ∑∂S/∂U ⋅ Uₜ :  -6.96048486e-03
────────────────────────────────────────────────────────────────────────────────────────────────────


────────────────────────────────────────────────────────────────────────────────────────────────────
 Simulation running 'ShallowWaterEquationsWetDry1D' with DGSEM(polydeg=4)
────────────────────────────────────────────────────────────────────────────────────────────────────
 #timesteps:               8691                run time:       3.47854985e+00 s
 Δt:             5.00910517e-03                └── GC time:    0.00000000e+00 s (0.000%)
 sim. time:      4.00000000e+01 (100.000%)     time/DOF/rhs!:  6.15171540e-08 s
                                               PID:            7.19042402e-08 s
 #DOFs per field:          1280                alloc'd memory:        188.125 MiB
 #elements:                 256

 Variable:       h                h_v              b
 L2 error:       3.33916579e-01   1.33582677e-01   1.15014447e-05
 Linf error:     4.17378531e-01   2.35879494e-01   3.92987526e-04
 |∑U - ∑U₀|:     1.48936419e-13   1.07619576e-01   1.59594560e-16
 ∑∂S/∂U ⋅ Uₜ :  -6.87112004e-04
────────────────────────────────────────────────────────────────────────────────────────────────────

Visualization

After solving the ODE problem, we want to visualize the results. The first plot shows the water height over the spatial domain at different times. The second plot compares the simulation results to experimental data at a gauge point G4 located at $x=19.5\,\text{m}$.

Spatial plot

We first extract the solution data for each saved time using PlotData1D. This allows us to reformat the solution for visualization purposes.

pd_list = [PlotData1D(sol.u[i], semi, reinterpolate = false) for i in 1:length(sol.u)];

Create a figure and axis for the spatial plot

f = Figure(size = (550, 550 / 2.5))
ax = Axis(f[1, 1], xlabel = "x / m", ylabel = "waterheight / m", limits = (0, 38, 0.0, 1.2));

Plot the water height at different time points

for (i, pd) in enumerate(pd_list)
    lines!(ax, pd.x, pd.data[:, 1], label = "t = $(sol.t[i])s")
end

Add the bottom topography to the plot

lines!(ax, pd_list[1].x, pd_list[1].data[:, 3], color = :black, linestyle = :solid)
band!(ax, pd_list[1].x, 0.0, pd_list[1].data[:, 3], color = :gray95);  # Set color for bottom topography

Add a legend to the plot

axislegend(ax, orientation = :horizontal)
f
Example block output

Time series plot

To validate the simulation, we now want to compare the results to experimental data. Therefore, We first download the experimental data for the first gauge location G4 from an external source and load it into a DataFrame format.

G4_data = Trixi.download("https://raw.githubusercontent.com/patrickersing/paper-2024-es_hydrostatic_reconstruction/refs/heads/main/code/DamBreakTriangularBottom/Reference/G4_Experimental.csv",
                         joinpath(@__DIR__, "G4_Experimental.csv"))
pd_G4 = CSV.read(G4_data, DataFrame);

The simulation data at the gauge location has been saved to the time_series variable. To reformat the data for visualization, we use the PlotData1D function.

pd = PlotData1D(time_series, 1);

Create a figure and axis for the comparison plot

f = Figure(size = (550, 550 / 2.5))
ax = Axis(f[1, 1], xlabel = "time / s", ylabel = "waterheight / m",
          limits = (0, 40, 0.0, 0.8));

Add experimental and simulation data to the plot

scatter!(ax, pd_G4.X, pd_G4.Y, color = :orange, strokewidth = 1,
         strokecolor = (:black, 0.5), label = "Experiment")
lines!(ax, pd.x, pd.data[:, 1], label = "Simulation");

Add a legend to the plot

axislegend(ax, orientation = :horizontal)
f
Example block output

This page was generated using Literate.jl.