6. Testing the
tracer_advection() function
A qualitative test
We can now start to put together our previous lessons to setup a
tracer advection test case. For any advection problem, we have a
velocity field and since we are preparing for a full Navier-Stokes
equations solver, it makes sense to declare the fields for its
components in the ns.h file.
# include "common.h"
double ux[N][N], uy[N][N];
double t, dt = 1;
void tracer_advection (...We have also introduced global variables for the time
(t) and the time step size (dt), whose value
we can alter in the test file test_advection.c. In this
file, we will setup the test, which consists of including the relevant
header files and declare a scalar field. In
test_advection.c :
#include "ns.h"
#include "visual.h"
// Scalar field
double s[N][N];
int main() {
// setup domain size
L0 = 10;
X0 = Y0 = -L0/2;
}All OK? We can continue by initializing the relevant field values.
For s we could reuse the compact functions from the
previous chapter. For the velocity field, we can use a simple
translation. The case is fully defined by setting an end time that
corresponds to a complete cycle though the periodic domain.
...
X0 = Y0 = -L0/2.;
// Initlauze s, ux and uy
foreach() {
val (s, 0, 0) = exp(-sq(x + 2) - sq(y + 2))
- exp(-sq(x - 1) - sq(y - 2));
val (ux, 0, 0) = -2.;
val (uy, 0, 0) = 0.;
}
// end time
double t_end = 5;
}Now we setup the time loop (c.f. Chapt. 2) with
a small value for dt.
...
// end time
double t_end = 5;
double dt = 0.05;
// step counter
int iter = 0;
// Time loop
for (t = 0; t < t_end; t += dt) {
// Tendency
double ds_dt[N][N];
tracer_advection (s, ux, uy, ds_dt);
// Advance
foreach()
val(s, 0, 0) += dt*val(ds_dt, 0, 0);
// Visualize
char fname[99];
sprintf (fname, "s-%04d.ppm", iter);
output_ppm (s, fname, -0.7, 0.7);
// Increment step counter
iter++;
}
printf ("# Solver finished after %d steps at t = %g\n",
iter, t);
}Most of the code above should look familiar from the previous chapters. After some cleaning up, we can compile and run our test.
$ rm s-*.ppm
$ gcc test_advection.c -lm
$ ./a.out
# Solver finished after 101 steps at t = 5.05
Great! Lets generate a movie,
$ convert s-*.ppm s_advect.mp4
If you have arrived at this point, you should realize that a lot has
gone well. Indeed the blobs move(!), in the expected direction(!)
(ux < 0), and the expected distance(!) (single cycle).
All is well then?
Well… lets push our solver a bit, and increase the resolution.
$ rm s-*.ppm
$ gcc -DN=400 test_advection.c -lm
$ ./a.out
# Solver finished after 101 steps at t = 5.05
$ convert s-*.ppm s_400.mp4
Disaster!
A stability criterion
We have obtained an nonphysical solution due to the rapidly accelerating growth of errors. Indeed, our upwinding strategy does not yield unconditionally stable time integration. Even before the advent of the digital computer it was known that the time-step size cannot be chosen freely for such advection problems. Rather, for explicit time-integration (such as forward Euler) it must be chosen small enough so that the cells do not “over flow” in a single step. This is formalized by the so-called CFL condition, named after its “inventors”, Richard Courant, Kurt Friedrichs, and Hans Lewy. In can be interpreted as follows: There exists a dimensionless number () that compares the time-step size () against the mesh-element size (), based on the (maximum) velocity in the domain.
For any explicit time integration method, there exists a maximum critical finite value for which the solution will remain stable. Typically, . In practice, this entails selecting a time-step size based on this so-called CFL condition,
Notice that this stability criterion is separate from any accuracy criterion. Our test in the second chapter was therefore not representative for the time integration of flow problems. Further, the CFL criterion directly relates the spatial discretization parameter () to one for time. This is somewhat natural when doing a convergence study for a spatio-temporal problem.
For now, we have to extent our code in ns.h with a
function that returns the CFL-based limit. Notice that the
C language does not come with a simple function that finds the absolute
maximum of an array. So we need to code it ourselves. In
ns.h,
...
double t, dt = 1;
double CFL = 0.7;
void tracer_advection (...) {
...
}
double dt_CFL () {
double max_v = -1.;
foreach() {
if (fabs(val(ux, 0, 0)) > max_v)
max_v = fabs(val(ux, 0, 0));
if (fabs(val(uy, 0, 0)) > max_v)
max_v = fabs(val(uy, 0, 0));
}
if (max_v > 0) // Dont divide by zero
return CFL*Delta/max_v;
return 1e30;
}After setting a safe value for CFL, the new function
without input determines the absolute maximum value of the velocity
component fields and computes an appropriate time step. A special check
is performed for the case where max_v = 0, to prevent
dividing by zero. Note that once a return statement is
executed, the control loop stops further function evaluation. I.e. this
function either returns with the CFL-based limit or with
.
Finally, I will admit that it is somewhat debatable and inconsistent to
set ux and uy as input to the function
tracer_advection(), and rely on the global variables in
dt_CFL() for the velocity data. You may choose to do
otherwise.
While we are in ns.h, I would also like our solver to
stop at t = t_end, and not at t = t_end + dt,
which was the case in our previous experiment (see the terminal output).
Once the time parameter t is close to t_end,
t + dt should be smaller or equal to t_end,
not to over step. For this purpose, we add a new function in
ns.h:
...
return 1e30;
}
double dt_next (double t_end) {
double DT = dt_CFL();
if (t + DT > t_end)
return t_end - t;
return DT;
}We use this to update our time stepping in
test_advection.c
...
// end time
double t_end = 5; //L0/ux
// step counter
int iter = 0;
// Time loop
for (t = 0; t < t_end; t += dt) {
// compute timestep size
dt = dt_next(t_end);
// Tendency
double ds_dt[N][N];
...Now test
$ rm s-*.ppm
$ gcc -DN=400 test_advection.c -lm
$ ./a.out
# Solver finished after 572 steps at t = 5
$ convert s-*.ppm s_400.mp4
It appears to solver took more steps and ends at to correct time. You
can verify the contents of s.mp4 yourself. To test our
upwinding implementation, it would also be good to redo the experiment
with positive and negative values for both ux and
uy.
This chapter continues with two exercises.
(1) A quantitative test
It would be good to not only visually inspect the solution, but also
diagnose the convergence rate of our formulations. For this purpose, you
could setup a workflow as in Chapt. 3 using the
initialized solution as the reference analytical solution after one full
cycle. Indeed, pure translation should not alter the shape of the
solution. For this case, the domain size needs to be enlarged as the
initialized solution is not consistent with the periodic boundaries.
This effect is exponentially reduced by increasing L0. Note
that when analyzing your results, both spatial and temporal
discretization are only first-order accurate.
(2) A Well-behaved solver?
Although the speed performance of our solver is not a critical concern, we should verify if the time efficiency at least behaves well. This entails verifying the scaling of the time effort against the expected scaling of the iteration effort. For this purpose we turn of the output routine,
...
// output_ppm (...
...so that the disk-writing performance does not affect our wall-clock
time to solution. Next, we generate 3 executables with increasingly
finer grids. In order to not overwrite a previously generated program,
we name them differently using the -o option, to name the
output programs other than the default a.out.
$ gcc -DN=100 test_advection.c -lm -o ta-100
$ gcc -DN=200 test_advection.c -lm -o ta-200
$ gcc -DN=400 test_advection.c -lm -o ta-400
We can use a stopwatch that is build into most shells to measure the time spend on the execution of the programs. I get (using bash for illustrative purposes):
$ time ./ta-100
# Solver finished after 143 steps at t = 5
real 0m0.136s
...
$ time ./ta-200
# Solver finished after 286 steps at t = 5
real 0m0.885s
...
$ time ./ta-400
# Solver finished after 572 steps at t = 5
real 0m6.819s
...
As we increase the resolution by a factor of two, the effort required to run the simulation increased by a factor of eight! This somewhat worrying scaling behavior is in fact the expected result, and so far, our solver is well behaved. Can you see why?
Lets continue with the next term in the Navier-Stokes equations in chapter 7