Menu

testCases/regridTube.c

Restart regridding contract for the Bretherton tube solver

This is a bounded software test of the transfer operators used when a saved production state is refined by one or two levels. It restores an actual Basilisk dump, rebuilds the same embedded axisymmetric tube, applies the same film-floor geometry as bretherton.c, and stops before time integration.

The test reports axisymmetric dispersed volume and centroid, mixture momentum, kinetic energy, exact straight-tube geometry errors, solid contamination and the discrete leaf face-flux divergence before and after one projection. On an adaptive embedded tree this direct leaf readback is a diagnostic, not the multilevel norm controlled by the Poisson solver. The projection uses the operator-normalised value dt = 1; it is not a substitute for a normal production timestep with the complete solver event sequence and does not by itself establish pointwise incompressibility. Running the same command with serial and MPI builds supplies the parallel agreement check. These measurements establish only the restart/regrid implementation contract; they do not establish grid convergence or physical stationarity.

qcc -I../src-local -O2 -Wall -disable-dimensions regridTube.c -o regridTube -lm
./regridTube SNAPSHOT LEVELS RTUBE FILM CA RHO1 RHO2 [TOLERANCE]

LEVELS must be one or two. FILM is the dimensional central-film thickness used by the source checkpoint (for example Rtube*(b/Rtube)). The remaining arguments reproduce the production boundary conditions and density law.

#include "embed.h"
#include "axi.h"
#include "navier-stokes/centered.h"
#include "two-phase.h"
#include "navier-stokes/conserving.h"
#include "tension.h"
#include "embed-vof-tube.h"
#include <errno.h>
#include <float.h>

static const char * snapshotFile;
static int levelIncrement, sourceLevel, targetLevel;
static double Rtube, filmThickness, Ca, tRamp = 1.;
static int failed = 0;

#define INLET_RAMP (tRamp > 0. ? min (t/tRamp, 1.) : 1.)47

u.n[left] = dirichlet (y < Rtube ?
                       2.*Ca*INLET_RAMP*(1. - sq(y/Rtube)) : 0.);
u.t[left] = dirichlet (0.);
p[left]   = neumann (0.);
pf[left]  = neumann (0.);
f[left]   = dirichlet (0.);

u.n[right] = neumann (0.);
u.t[right] = neumann (0.);
p[right]   = dirichlet (0.);
pf[right]  = dirichlet (0.);

u.n[embed] = dirichlet (0.);
u.t[embed] = dirichlet (0.);

typedef struct {
  long leaves, mixed, cut;
  int level_min, level_max, mixed_level_min, mixed_level_max;
  double volume, centroid_x, centroid_y;
  double momentum_x, momentum_y, kinetic_energy;
  double tube_volume_error, inlet_area_error, outlet_area_error;
  double cs_error, fs_error, cm_error, fm_error, solid_fraction_max;
  double leaf_flux_divergence_diagnostic;
  double physical_divergence_full_max, physical_divergence_cut_max;
  double minimum_positive_cm;
} RegridMetrics;

static bool parse_double (const char * text, double * value)
{
  errno = 0;
  char * end = NULL;
  *value = strtod (text, &end);
  return errno == 0 && end != text && *end == '\0' && isfinite (*value);
}

static bool parse_increment (const char * text, int * value)
{
  errno = 0;
  char * end = NULL;
  long parsed = strtol (text, &end, 10);
  if (errno || end == text || *end != '\0' || (parsed != 1 && parsed != 2))
    return false;
  *value = (int) parsed;
  return true;
}

Reconstruct the face flux exactly as centered.h does after user init.

static void rebuild_face_flux (void)
{
  event ("properties");
  trash ({uf});
  foreach_face()
    uf.x[] = fm.x[]*face_value (u.x, 0);
  boundary ((scalar *){uf});
}

static void divergence_metrics (RegridMetrics * metrics)
{
  double rawMaximum = 0., fullMaximum = 0., cutMaximum = 0., minimumCm = HUGE;
  foreach (serial, noauto)
    if (cm[] > 0.) {
      double divergence = 0.;
      foreach_dimension()
        divergence += uf.x[1] - uf.x[];
      const double raw = fabs (divergence/Delta);
      const double physical = raw/cm[];
      rawMaximum = max (rawMaximum, raw);
      minimumCm = min (minimumCm, cm[]);
      if (cs[] >= 1.)
        fullMaximum = max (fullMaximum, physical);
      else if (cs[] > 0.)
        cutMaximum = max (cutMaximum, physical);
    }
#if _MPI
  mpi_all_reduce (rawMaximum, MPI_DOUBLE, MPI_MAX);
  mpi_all_reduce (fullMaximum, MPI_DOUBLE, MPI_MAX);
  mpi_all_reduce (cutMaximum, MPI_DOUBLE, MPI_MAX);
  mpi_all_reduce (minimumCm, MPI_DOUBLE, MPI_MIN);
#endif
  metrics->leaf_flux_divergence_diagnostic = rawMaximum;
  metrics->physical_divergence_full_max = fullMaximum;
  metrics->physical_divergence_cut_max = cutMaximum;
  metrics->minimum_positive_cm = minimumCm < HUGE ? minimumCm : 0.;
}

Compare the live tube fractions and metrics with the exact straight tube.

static void geometry_errors (RegridMetrics * metrics)
{
  double tubeVolume = 0., inlet = 0., outlet = 0.;
  double ecs = 0., efs = 0., ecm = 0., efm = 0., fsolid = 0.;

  scalar cmv = cm;
  face vector fmv = fm;
  foreach (reduction(+:tubeVolume) reduction(max:ecs)
           reduction(max:ecm) reduction(max:fsolid)) {
    tubeVolume += dv();
    double csExact = clamp ((Rtube - y)/Delta + .5, 0., 1.);
    double cmExact = tube_axial_face_metric (y, Delta);
    ecs = max (ecs, fabs (cs[] - csExact));
    ecm = max (ecm, fabs (cmv[] - cmExact));
    if (cs[] <= 0.)
      fsolid = max (fsolid, fabs (f[]));
  }
  foreach_face (x, reduction(max:efs) reduction(max:efm)) {
    double exact = clamp ((Rtube - y)/Delta + .5, 0., 1.);
    efs = max (efs, fabs (fs.x[] - exact));
    efm = max (efm, fabs (fmv.x[] - tube_axial_face_metric(y, Delta)));
  }
  foreach_face (y, reduction(max:efs) reduction(max:efm)) {
    double fsExact = y < Rtube ? 1. : 0.;
    double fmExact = fsExact ? max(y, 1e-20) : 0.;
    efs = max (efs, fabs (fs.y[] - fsExact));
    efm = max (efm, fabs (fmv.y[] - fmExact));
  }
  foreach_boundary (left, reduction(+:inlet))
    inlet += fm.x[]*Delta;
  foreach_boundary (right, reduction(+:outlet))
    outlet += fm.x[1]*Delta;

  const double exactVolume = L0*sq(Rtube)/2.;
  const double exactArea = sq(Rtube)/2.;
  metrics->tube_volume_error = fabs (tubeVolume - exactVolume);
  metrics->inlet_area_error = fabs (inlet - exactArea);
  metrics->outlet_area_error = fabs (outlet - exactArea);
  metrics->cs_error = ecs;
  metrics->fs_error = efs;
  metrics->cm_error = ecm;
  metrics->fm_error = efm;
  metrics->solid_fraction_max = fsolid;
}

static RegridMetrics measure_state (void)
{
  RegridMetrics metrics = {.level_min = 30, .mixed_level_min = 30};
  double volume = 0., mx = 0., my = 0., px = 0., py = 0., ke = 0.;
  long leaves = 0, mixed = 0, cut = 0;
  int levelMin = 30, levelMax = 0, mixedMin = 30, mixedMax = 0;

  foreach (reduction(+:volume) reduction(+:mx) reduction(+:my)
           reduction(+:px) reduction(+:py) reduction(+:ke)
           reduction(+:leaves) reduction(+:mixed) reduction(+:cut)
           reduction(min:levelMin) reduction(max:levelMax)
           reduction(min:mixedMin) reduction(max:mixedMax)) {
    const double element = 2.*pi*dv();
    const double dispersed = f[]*element;
    const double density = rho(f[]);
    leaves++;
    levelMin = min (levelMin, level);
    levelMax = max (levelMax, level);
    if (f[] > 1e-6 && f[] < 1. - 1e-6) {
      mixed++;
      mixedMin = min (mixedMin, level);
      mixedMax = max (mixedMax, level);
    }
    if (cs[] > 0. && cs[] < 1.)
      cut++;
    volume += dispersed;
    mx += x*dispersed;
    my += y*dispersed;
    px += density*u.x[]*element;
    py += density*u.y[]*element;
    ke += 0.5*density*(sq(u.x[]) + sq(u.y[]))*element;
  }

  metrics.leaves = leaves;
  metrics.mixed = mixed;
  metrics.cut = cut;
  metrics.level_min = levelMin;
  metrics.level_max = levelMax;
  metrics.mixed_level_min = mixed ? mixedMin : -1;
  metrics.mixed_level_max = mixed ? mixedMax : -1;
  metrics.volume = volume;
  metrics.centroid_x = volume > 0. ? mx/volume : nodata;
  metrics.centroid_y = volume > 0. ? my/volume : nodata;
  metrics.momentum_x = px;
  metrics.momentum_y = py;
  metrics.kinetic_energy = ke;
  divergence_metrics (&metrics);
  geometry_errors (&metrics);
  return metrics;
}

static void print_metrics (const char * stage, RegridMetrics m)
{
  if (pid() != 0)
    return;
  // iter is the global event counter declared by Basilisk's grid/events.h.
  printf ("%s,%d,%.17g,%d,%ld,%d,%d,%ld,%d,%d,%ld,"
          "%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,"
          "%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,"
          "%.17g,%.17g,%.17g\n",
          stage, npe(), t, iter, m.leaves, m.level_min, m.level_max,
          m.mixed, m.mixed_level_min, m.mixed_level_max, m.cut,
          m.volume, m.centroid_x, m.centroid_y,
          m.momentum_x, m.momentum_y, m.kinetic_energy,
          m.tube_volume_error, m.inlet_area_error, m.outlet_area_error,
          m.cs_error, m.fs_error, m.cm_error, m.fm_error,
          m.solid_fraction_max, m.leaf_flux_divergence_diagnostic,
          m.physical_divergence_full_max, m.physical_divergence_cut_max,
          m.minimum_positive_cm);
  fflush (stdout);
}

int main (int argc, char ** argv)
{
  if (argc != 8 && argc != 9) {
    fprintf (stderr, "usage: %s SNAPSHOT LEVELS RTUBE FILM CA RHO1 RHO2 "
             "[TOLERANCE]\n", argv[0]);
    return 2;
  }
  snapshotFile = argv[1];
  TOLERANCE = 1e-4;
  if (!parse_increment(argv[2], &levelIncrement) ||
      !parse_double(argv[3], &Rtube) || !parse_double(argv[4], &filmThickness) ||
      !parse_double(argv[5], &Ca) || !parse_double(argv[6], &rho1) ||
      !parse_double(argv[7], &rho2) ||
      (argc == 9 && !parse_double(argv[8], &TOLERANCE)) ||
      Rtube <= 0. || filmThickness <= 0. || Ca <= 0. ||
      rho1 <= 0. || rho2 <= 0. || TOLERANCE <= 0.) {
    fprintf (stderr, "FAIL invalid argument\n");
    return 2;
  }

  mu1 = mu2 = 1.;
  f.sigma = 1.;
  DT = 1e-2;
  init_grid (1 << 4);
  run();
  return failed;
}

event init (t = 0)
{
  if (!restore (file = snapshotFile)) {
    if (pid() == 0)
      fprintf (stderr, "FAIL cannot restore %s\n", snapshotFile);
    failed = 1;
    return 1;
  }

  int restoredMaximumLevel = 0;
  foreach (reduction(max:restoredMaximumLevel))
    restoredMaximumLevel = max (restoredMaximumLevel, level);
  sourceLevel = restoredMaximumLevel;
  targetLevel = sourceLevel + levelIncrement;
  if (targetLevel > 30) {
    if (pid() == 0)
      fprintf (stderr, "FAIL target level %d is unsupported\n", targetLevel);
    failed = 1;
    return 1;
  }

  tube_solid (Rtube);
  vof_solid_cleanup (f);
  boundary ({f, u, cs, fs, cm, fm});
  rebuild_face_flux();

  if (pid() == 0) {
    printf ("stage,ranks,t,iter,leaves,level_min,level_max,mixed,"
            "mixed_level_min,mixed_level_max,cut,volume,centroid_x,centroid_y,"
            "momentum_x,momentum_y,kinetic_energy,tube_volume_error,"
            "inlet_area_error,outlet_area_error,cs_error,fs_error,cm_error,"
            "fm_error,solid_fraction_max,leaf_flux_divergence_diagnostic,"
            "physical_divergence_full_max,physical_divergence_cut_max,"
            "minimum_positive_cm\n");
    fprintf (stderr, "# source_level=%d target_level=%d L0=%.17g "
             "Rtube=%.17g film=%.17g Ca=%.17g rho1=%.17g rho2=%.17g\n",
             sourceLevel, targetLevel, L0, Rtube, filmThickness, Ca,
             rho1, rho2);
  }
  print_metrics ("restored", measure_state());

  double front = -HUGE, rear = HUGE;
  scalar xpos[];
  position (f, xpos, {1, 0});
  front = statsf(xpos).max;
  rear = statsf(xpos).min;
  if (!(front > rear)) {
    if (pid() == 0)
      fprintf (stderr, "FAIL cannot reconstruct interface tips\n");
    failed = 1;
    return 1;
  }

  const double wall0 = rear - Rtube, wall1 = front + Rtube;
  const double band = 2.*filmThickness;
  tube_refinement_geometry_begin();
  refine ((level < targetLevel && cs[] > 0. &&
           f[] > 1e-6 && f[] < 1. - 1e-6) ||
          (level < targetLevel && cs[] > 0. &&
           x > wall0 && x < wall1 && y + Delta/2. > Rtube - band));
  tube_refinement_geometry_end();
  embed_axi_metric_sync();
  vof_solid_cleanup (f);
  boundary ({f, u, cs, fs, cm, fm});
  rebuild_face_flux();
  print_metrics ("regridded_pre_projection", measure_state());

dt = 1 makes the multilevel projection residual directly comparable with TOLERANCE. The direct leaf divergence and its physical value divided by cm remain useful AMR diagnostics, but are not the convergence norm used by Basilisk’s tree Poisson operator.

  const double projectionDt = 1.;
  mgstats projection = project (uf, p, alpha, projectionDt, 0);
  RegridMetrics projected = measure_state();
  print_metrics ("regridded_post_projection", projected);

  int achieved = 0;
  foreach (reduction(max:achieved))
    if (f[] > 1e-6 && f[] < 1. - 1e-6)
      achieved = max (achieved, level);
  if (pid() == 0)
    fprintf (stderr, "# standalone_projection dt=%g resb=%.17g resa=%.17g "
             "iterations=%d nrelax=%d target=%d mixed_max=%d "
             "post_leaf_flux_divergence=%.17g\n",
             projectionDt, projection.resb, projection.resa, projection.i,
             projection.nrelax, targetLevel, achieved,
             projected.leaf_flux_divergence_diagnostic);
  const double roundoffAllowance =
    64.*DBL_EPSILON*max (1., projection.resa);
  const bool fieldsFinite =
    isfinite (projected.volume) && isfinite (projected.centroid_x) &&
    isfinite (projected.centroid_y) && isfinite (projected.momentum_x) &&
    isfinite (projected.momentum_y) && isfinite (projected.kinetic_energy) &&
    isfinite (projected.leaf_flux_divergence_diagnostic);
  const bool geometryOK =
    projected.tube_volume_error <= 5e-12 &&
    projected.inlet_area_error <= 5e-12 &&
    projected.outlet_area_error <= 5e-12 &&
    projected.cs_error <= 5e-12 && projected.fs_error <= 5e-12 &&
    projected.cm_error <= 5e-12 && projected.fm_error <= 5e-12 &&
    projected.solid_fraction_max == 0.;
  const bool projectionOK =
    isfinite (projection.resb) && isfinite (projection.resa) &&
    projection.resa <= TOLERANCE + roundoffAllowance;
  if (achieved != targetLevel || !fieldsFinite || !geometryOK || !projectionOK) {
    if (pid() == 0)
      fprintf (stderr, "FAIL regrid/projection predicate: tolerance=%.17g "
               "roundoff_allowance=%.17g\n", TOLERANCE,
               roundoffAllowance);
    failed = 1;
  }
  return 1;
}