A semi-implicit Saint-Venant solver

We solve the Saint-Venant equations semi-implicitly to lift the timestep restriction due to gravity waves. This is just a particular application of the “all Mach” semi-implicit solver. We will use the Bell-Collela-Glaz advection scheme to transport mass and momentum.

#include "all-mach.h"
#include "tracer.h"

In addition to the momentum q=hu defined by the all Mach solver, we will need the fluid depth h (i.e. the density ρ) and the topography zb. Both the momentum q and mass h are advected tracers. The acceleration of gravity is G. dry is the fluid depth below which a cell is considered “dry”.

scalar zb[], h[], * tracers = {q,h};
double G = 1.;
double dry = 1e-4;

We need fields to store the (varying) state fields ρc2, α and the acceleration a.

scalar rhoc2v[];
face vector alphav[], av[];

event defaults (i = 0) {
  ρ = h;
  α = alphav;
  a = av;
  rhoc2 = rhoc2v;

We set limiting for q, h.

  for (scalar s in {q,h})
    s.gradient = minmod2;

As well as default values.

    h[] = 1.;

On trees, we ensure that limiting is also applied to prolongation.

  #if TREE
  for (scalar s in {q,h})
    s.prolongation = refine_linear;

  boundary ({h});

After user initialisation we set the initial pressure and apply boundary conditions. The boundary conditions for ρ=h and q are already applied by the all Mach solver.

event init (i = 0) {
    p[] = G*sq(h[])/2.;
  boundary ({zb,p});

By default the timestep is only limited by the CFL condition on the advection velocity field. We add the option to define an “acoustic CFL number” which takes into account the speed of gravity waves.

double CFLa = HUGE;

event stability (i++) {
  if (CFLa < HUGE)
      if (h[] > dry) {
	double dt = CFLa*Δ/sqrt(G*h[]);
	if (dt < dtmax)
	  dtmax = dt;

The properties of the fluid are given by the “Saint-Venant equation of state” i.e. p=gh2/2, ρc2=gh2.

event properties (i++) {
  foreach() {
    rhoc2v[] = G*sq(max(h[],dry));
    ps[] = rhoc2v[]/2.;

The specific volume α=1/ρ is constructed by averaging, taking into account dry states.

  foreach_face() {
    if ((h[] > dry && h[-1] > dry) ||
	(h[] > dry && h[] + zb[] >= zb[-1]) ||
	(h[-1] > dry && h[-1] + zb[-1] >= zb[]))
      alphav.x[] = 2./(max(h[],dry) + max(h[-1],dry));
      alphav.x[] = 0.;

Topographic source term

The acceleration due to the topography is gzb. On regular Cartesian grids we can simply do

#if !TREE
event acceleration (i++) {
    if (α.x[])
      av.x[] -= G*(zb[] - zb[-1])/Δ;
#else // TREE

On trees things are a bit more complicated due to the necessity to verify the “lake-at-rest” condition. For a lake, the pressure gradient must balance the topographic source term i.e. 1hp=a=gzb with a the acceleration. Using the equation of state and introducing η=zb+h the elevation of the free surface (constant for a lake at rest), we get 12hgh2=g(ηh)=gh This identity is obviously verified mathematically, however it is not necessarily verified by the discrete gradient operator. In the case of Cartesian meshes it is simple to show that the naive discrete gradient operator we used above verifies this identity. For tree meshes this is not generally the case due to the prolongation operator used to fill ghost cells at refinement boundaries. Rather than trying to redefine the prolongation operator, we discretise the topographic source term as a=gzb=gη+g2hh2 This is strictly identical mathematically, however if we now use the same discrete operator to estimate p and h2, the discrete lake-at-rest condition becomes η=0 which is much easier to verify.

To do so, we define the following restriction and prolongation operators for η. The main idea is to avoid using the elevation of dry cells. Restriction is done by averaging the elevation of wet cells.

void eta_restriction (Point point, scalar s)
  double sum = 0., n = 0.;
    if (h[] > dry) {
      sum += s[];
  s[] = n > 0 ? sum/n : nodata;

Prolongation uses linear interpolation with a cell-centered gradient computed from simple finite-differences of wet cells.

void eta_prolongation (Point point, scalar s)
  coord g;
  foreach_dimension() {
    if (h[] > dry) {
      if (h[1] > dry && h[-1] > dry)
	g.x = (s[1] - s[-1])/2.;
      else if (h[1] > dry)
	g.x = s[1] - s[];
      else if (h[-1] > dry)
	g.x = s[] - s[-1];
	g.x = 0.;
      g.x = 0.;
  double sc = s[];
  foreach_child() {
    s[] = sc;
      s[] += child.x*g.x/4.;

One can check that the prolongation values constructed using these functions verify η=constant for a lake-at-rest.

event acceleration (i++) {

To compute the acceleration due to topography, we first compute η and h2.

  scalar η[], h2[];
  foreach() {
    h2[] = sq(h[]);
    η[] = zb[] + h[];

We then make sure that η uses our restriction/prolongation functions. h2 uses the same prolongation functions as p by default.

  η.prolongation = eta_prolongation;
  η.restriction = eta_restriction;
  boundary ({η,h2});

We then compute the acceleration as a=gη+gα2h2

    if (α.x[])
      av.x[] += G*(η[-1] - η[] + α.x[]/2.*(h2[] - h2[-1]))/Δ;

#endif // TREE

The utility functions in elevation.h need to know which gradient we are using.

double (* gradient)  (double, double, double) = minmod2;

#include "elevation.h"