simulationCases/bretherton.c
bretherton.c
Axisymmetric two-phase simulation of a long drop or bubble translating in a liquid-filled capillary tube, after Bretherton (1961).
The tube wall is an embedded boundary
(embed.h), fully wetted by the continuous
phase: the drop/bubble never touches the wall and rides
on a thin lubricating film. The interface is tracked
with VOF (two-phase.h). The embed/VOF
couplings that Basilisk does not handle automatically
are collected in
src-local/embed-vof-tube.h.
Non-dimensionalisation
Repeating variables: surface tension \(\sigma\), continuous-phase dynamic viscosity \(\mu_c\), and the volume-equivalent drop/bubble radius \(R = (3V/4\pi)^{1/3}\). Hence
- length scale \(R\),
- velocity scale \(V_\mu = \sigma/\mu_c\) (visco-capillary velocity),
- time scale \(\tau = \mu_c R/\sigma\),
- pressure scale \(\sigma/R\).
In these units the dimensionless bubble tip velocity is itself the capillary number \(Ca_b = \mu_c U_b/\sigma\).
Control parameters (all runtime keys):
Ca: imposed mean inlet velocity \(\mu_c U/\sigma\) (capillary number of the driving flow),La: Laplace number \(\rho_c \sigma R/\mu_c^2\) (sets the continuous phase density; Bretherton’s analysis assumes the visco-capillary limit, so keep \(La\,Ca \ll 1\)),muR: viscosity ratio \(\mu_d/\mu_c\) (bubble: \(10^{-2}\); drop: \(\geq 1\)),rhoR: density ratio \(\rho_d/\rho_c\) (bubble: \(10^{-3}\); drop: 1),Rtube: tube radius in units of \(R\) (must be \(< 1\) for a confined, elongated drop/bubble),Rb0frac: initial capsule radius as a fraction ofRtube,xRear: initial distance of the rear meniscus tip from the inlet.
Validation targets (small \(Ca_b\)): film thickness \(b/R_{tube} \simeq 1.34\,Ca_b^{2/3}\) and speed excess \(W = (U_b - U)/U_b \simeq 1.29\,(3 Ca_b)^{2/3}\) [Bretherton (1961), eq. 2]; at moderate \(Ca_b\) compare with the Aussillous & Quéré (2000) fit \(b/R_{tube} = 1.34\,Ca_b^{2/3}/(1 + 2.5\cdot1.34\,Ca_b^{2/3})\).
Input parameters
Runtime keys via src-local/params.h
(defaults in brackets): CaseNo [1000],
MAXlevel [10], MINlevel [4],
Ca [0.05], La [1],
muR [0.01], rhoR [0.001],
Rtube [0.7], Rb0frac [0.8],
xRear [1.0], Ldomain [16],
travelR [8], tmax
[travelR/Ca],
tsnap [tmax/200],
tRamp [1], dtmax [0.01],
bTol [2e-3], advWin [0.25],
advMin [1.0], convHold [3],
uRel [1e-2], dRel [1e-2],
csErr [1e-2], filmBins [128],
filmCoverage [0.95],
filmFlatTol [0.05],
freshFracMin [0.98], speedTol
[0.02], shapeTol [0.02],
filmCells [4], filmMinLevel
[0], requireShapeSteady [true],
filmSampleDt [derived] and
solverTol [1e-4]. Restart controls are
restartBurnR [0], regridBurnR
[0] and freshFront [the analytic initial
front].
tmax and tsnap derive from
the capillary number by default. The bubble advances at
\(U = Ca\), so a fixed
run time gives a different travel distance at every
\(Ca\), and the film
only reaches its steady thickness after several bubble
lengths of advance. travelR sets that
advance in units of \(R\). The run stops when the
front tip reaches the outlet buffer at \(L_0 - 2R_{tube}\), which
leaves about 9.0 radii of usable advance, so
travelR is capped below that. In practice
the run normally ends earlier, after the central
deposited film and front speed pass their
observation-window gates (and, by default, the complete
travelling shape diagnostic also passes).
tmax is a failure cap rather than the
expected duration.
#include "embed.h"
#include "axi.h"
#include "navier-stokes/centered.h"
#include "two-phase.h"
#include "navier-stokes/conserving.h"
#include "tension.h"
#include "params.h"
#include "embed-vof-tube.h"
#include "central-film.h"
#include <errno.h>
#include <sys/stat.h>Adaptivity controls
#define fErr (1e-3) // error tolerance in f VOF
#define KErr (1e-4) // error tolerance in VOF curvatureThe velocity and strain-rate tolerances are
relative to the imposed scales, not absolute.
The velocity scale here is \(U
= Ca\), which spans 0.002 to 0.05 across the
campaign, so a fixed tolerance inherited from problems
where \(U \sim 1\) is
inert at the low-\(Ca\)
end: at \(Ca = 0.005\)
an absolute 1e-2 exceeds 45% of the peak
speed, and the mesh follows the interface alone while
the bulk velocity and dissipation fields stay coarse.
That under-resolution grows monotonically as \(Ca\) falls.
uRel is the velocity tolerance as a
fraction of \(U\), and
dRel the strain-rate tolerance as a
fraction of the bulk shear rate \(U/R_{tube}\).
Global runtime variables
int MAXlevel, MINlevel, CaseNo;
double Ca, La, muR, rhoR, Rtube, Rb0frac, xRear, Ldomain;tsnap must be non-zero
statically: Basilisk classifies event
expressions (t += tsnap vs conditions)
before main() assigns the runtime
parameters, and a zero increment is misread as a second
condition.
double tmax = 200., tsnap = 1., tRamp = 1., travelR = 10.;
double Rb0, Lcyl, Xb0, vol0;
double bTol, advWin, advMin, uRel, dRel, VelErr, DErr, csErr;
double filmCoverage, filmFlatTol, freshFracMin, speedTol, shapeTol;
double filmCells, filmSampleDt, solverTol;
double restartBurnR, regridBurnR, freshFront;
double observationStartFront = 0., observationBurnDistance = 0.;
int convHold, filmBins, filmMinLevel;
bool requireShapeSteady;
bool restoredRun = false, logNeedsHeader = false, logHasContent = false;
bool restartFilePresent = false;
int runFailed = 0;
double latestSafetyKE = 0.;
#define CENTRAL_FILM_MAX_BINS 512134
CentralFilmObserver filmObserver;
CentralFilmConfig filmConfig;
CentralFilmMeasurement latestFilmMeasurement;
bool latestFilmValid = false;
double nextFilmSampleTime = 0.;
char nameOut[128], dumpFile[128], logFile[128];Reconstructs the upper VOF interface at uniformly
spaced axial stations. Each MPI rank contributes local
intersections, followed by element-wise maximum
reductions. The resolution screen uses the largest leaf
intersected by each film column, so
film/Delta_max is a conservative lower
bound on the number of cells across that column.
static bool measure_central_film (double xRearNow, double xFrontNow,
double centroidNow,
CentralFilmMeasurement * measurement)
{
if (!(xFrontNow > xRearNow) || filmBins < 1)
return false;
double radius[CENTRAL_FILM_MAX_BINS], localRadius[CENTRAL_FILM_MAX_BINS];
double maxDelta[CENTRAL_FILM_MAX_BINS];
const double dx = (xFrontNow - xRearNow)/filmBins;
for (int j = 0; j < filmBins; j++)
radius[j] = localRadius[j] = -HUGE, maxDelta[j] = 0.;
foreach (serial)
if (f[] > 1e-6 && f[] < 1. - 1e-6) {
coord normal = interface_normal (point, f), segment[2];
double alpha = plane_alpha (f[], normal);
if (facets (normal, alpha, segment) == 2) {
double x0 = x + segment[0].x*Delta;
double y0 = y + segment[0].y*Delta;
double x1 = x + segment[1].x*Delta;
double y1 = y + segment[1].y*Delta;
if (fabs(x1 - x0) > 1e-14) {
int first = max(0, (int) ceil((min(x0, x1) - xRearNow)/dx - .5));
int last = min(filmBins - 1,
(int) floor((max(x0, x1) - xRearNow)/dx - .5));
for (int j = first; j <= last; j++) {
double xj = xRearNow + (j + .5)*dx;
double rj = y0 + (xj - x0)*(y1 - y0)/(x1 - x0);
localRadius[j] = max(localRadius[j], rj);
}
}
}
}
memcpy (radius, localRadius, filmBins*sizeof(double));
#if _MPI
mpi_all_reduce_array (radius, MPI_DOUBLE, MPI_MAX, filmBins);
#endifFind the coarsest cell actually crossed between interface and wall.
foreach (serial)
if (cs[] > 0.)
for (int j = 0; j < filmBins; j++)
if (radius[j] > -HUGE/2.) {
double xj = xRearNow + (j + .5)*dx;
if (xj >= x - Delta/2. && xj < x + Delta/2. &&
y + Delta/2. > radius[j] && y - Delta/2. < Rtube)
maxDelta[j] = max(maxDelta[j], Delta);
}
#if _MPI
mpi_all_reduce_array (maxDelta, MPI_DOUBLE, MPI_MAX, filmBins);
#endif
double films[CENTRAL_FILM_MAX_BINS];
int expected = 0, covered = 0, fresh = 0, count = 0;
double minimumCells = HUGE;
for (int j = 0; j < filmBins; j++) {
double xi = (j + .5)/filmBins;
if (xi <= .3 || xi >= .7)
continue;
expected++;
if (radius[j] <= -HUGE/2. || radius[j] >= Rtube)
continue;
covered++;
double thickness = Rtube - radius[j];
films[count++] = thickness;
if (xRearNow + (j + .5)*dx > freshFront)
fresh++;
if (maxDelta[j] > 0.)
minimumCells = min(minimumCells, thickness/maxDelta[j]);
}
if (!count || !expected)
return false;
double sorted[CENTRAL_FILM_MAX_BINS];
memcpy (sorted, films, count*sizeof(double));
double median = central_film_median (sorted, count);
memcpy (sorted, films, count*sizeof(double));
double q05 = central_film_quantile (sorted, count, .05);
memcpy (sorted, films, count*sizeof(double));
double q95 = central_film_quantile (sorted, count, .95);
*measurement = (CentralFilmMeasurement) {
.time = t, .front = xFrontNow, .rear = xRearNow,
.centroid = centroidNow, .film = median,
.spatial_spread = median > 0. ? (q95 - q05)/median : HUGE,
.coverage = (double) covered/expected,
.fresh_fraction = covered ? (double) fresh/covered : 0.,
.minimum_cells = minimumCells < HUGE ? minimumCells : 0.
};
return true;
}Applies two independent controls.
filmMinLevel floors the complete bubble
interface and its wall-facing liquid band, so changing
it is a genuine grid study of deposition and both
menisci. filmCells may demand a still finer
level in the measured central film. Neither control
refines unrelated parts of the square domain or the
solid exterior.
static void refine_measured_film (void)
{
if (!latestFilmValid || (filmCells <= 0. && filmMinLevel <= 0) ||
!(latestFilmMeasurement.film > 0.))
return;
int measuredTarget = 0;
if (filmCells > 0.)
measuredTarget = (int) ceil
(log(L0*filmCells/latestFilmMeasurement.film)/log(2.));
measuredTarget = max(MINlevel, min(MAXlevel, measuredTarget));
int floorLevel = max(MINlevel, min(MAXlevel, filmMinLevel));
const double length = latestFilmMeasurement.front - latestFilmMeasurement.rear;
const double central0 = latestFilmMeasurement.rear + .25*length;
const double central1 = latestFilmMeasurement.rear + .75*length;
const double wall0 = latestFilmMeasurement.rear - Rtube;
const double wall1 = latestFilmMeasurement.front + Rtube;
const double band = 2.*latestFilmMeasurement.film;
tube_refinement_geometry_begin();
refine ((filmMinLevel > 0 && level < floorLevel && cs[] > 0. &&
f[] > 1e-6 && f[] < 1. - 1e-6) ||
(filmMinLevel > 0 && level < floorLevel && cs[] > 0. &&
x > wall0 && x < wall1 && y + Delta/2. > Rtube - band) ||
(filmCells > 0. && level < measuredTarget && cs[] > 0. &&
x > central0 && x < central1 &&
y + Delta/2. > Rtube - 1.5*latestFilmMeasurement.film));
tube_refinement_geometry_end();
}Returns interface tips, global minimum gap and dispersed-phase centroid.
static bool current_drop_geometry (double * xFrontNow, double * xRearNow,
double * minimumFilm,
double * centroidNow)
{
scalar xpos[], ypos[];
position (f, xpos, {1, 0});
position (f, ypos, {0, 1});
*xFrontNow = statsf(xpos).max;
*xRearNow = statsf(xpos).min;
*minimumFilm = Rtube - statsf(ypos).max;
double volume = 0., moment = 0.;
foreach (reduction(+:volume) reduction(+:moment)) {
double element = f[]*dv();
volume += element;
moment += x*element;
}
*centroidNow = volume > 0. ? moment/volume : 0.;
return *xFrontNow > *xRearNow && volume > 0.;
}
static bool close_parameter (double a, double b)
{
return fabs(a - b) <= 1e-12*max(1., max(fabs(a), fabs(b)));
}An existing log may only be continued when its case metadata and central-film schema match. Missing or empty logs receive exactly one header.
static bool prepare_case_log (void)
{
struct stat status;
if (stat(logFile, &status) != 0) {
if (errno == ENOENT) {
logNeedsHeader = true;
return true;
}
return false;
}
if (status.st_size == 0) {
logNeedsHeader = true;
return true;
}
FILE * fp = fopen (logFile, "r");
if (!fp)
return false;
char metadata[512] = "", columns[512] = "";
bool readOK = fgets(metadata, sizeof(metadata), fp) &&
fgets(columns, sizeof(columns), fp);
fclose (fp);
int caseNumber = 0, maximumLevel = 0, minimumLevel = 0;
double ca = 0., la = 0., viscosityRatio = 0., densityRatio = 0., radius = 0.;
double domain = 0., tolerance = 0.;
bool metadataOK = readOK &&
sscanf(metadata, "# CaseNo %d, MAXlevel %d, MINlevel %d, Ca %lf, "
"La %lf, muR %lf, rhoR %lf, Rtube %lf, Ldomain %lf, "
"solverTol %lf", &caseNumber, &maximumLevel, &minimumLevel, &ca,
&la, &viscosityRatio, &densityRatio, &radius, &domain,
&tolerance) == 10 &&
caseNumber == CaseNo && maximumLevel == MAXlevel &&
minimumLevel == MINlevel &&
close_parameter(ca, Ca) && close_parameter(la, La) &&
close_parameter(viscosityRatio, muR) &&
close_parameter(densityRatio, rhoR) && close_parameter(radius, Rtube) &&
close_parameter(domain, Ldomain) && close_parameter(tolerance, solverTol) &&
strstr(columns, "bCentral") && strstr(columns, "minCells");
if (!metadataOK)
return false;
logNeedsHeader = false;
logHasContent = true;
return true;
}An init event which returns after
restore() leaves Basilisk to reschedule
events with the restored clock and an unset next
timestep. Terminate directly instead. All init
predicates below are collective; the barrier lets rank
zero flush the exact terminal reason before MPI aborts.
The existing checkpoint is never rewritten on this
path.
static void terminate_init_failure (void)
{
fflush (NULL);
#if _MPI
MPI_Abort (MPI_COMM_WORLD, 1);
#endif
exit (EXIT_FAILURE);
}Boundary conditions
Poiseuille inflow of the continuous phase on the left
(mean velocity Ca in capillary units,
ramped over tRamp), outflow on the right,
no-slip on the embedded tube wall. The bottom boundary
is the axis of symmetry (handled by
axi.h).
#define INLET_RAMP (tRamp > 0. ? min (t/tRamp, 1.) : 1.)378
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.);main()
Loads runtime parameters, sets fluid properties from the dimensionless groups and enters the event loop.
int main (int argc, char const *argv[])
{
params_init_from_argv (argc, argv);
CaseNo = param_int ("CaseNo", 1000);
MAXlevel = param_int ("MAXlevel", 10);
MINlevel = param_int ("MINlevel", 4);
Ca = param_double ("Ca", 0.05);
La = param_double ("La", 1.);
muR = param_double ("muR", 1e-2);
rhoR = param_double ("rhoR", 1e-3);
Rtube = param_double ("Rtube", 0.7);
Rb0frac = param_double ("Rb0frac", 0.8);
xRear = param_double ("xRear", 1.0);
Ldomain = param_double ("Ldomain", 16.);The film reaches its steady thickness only after the
bubble has travelled several of its own lengths, and the
mean inlet velocity is \(U =
Ca\) in these units, so one fixed
tmax can only ever be right at one
capillary number. The default therefore derives from a
travel distance: advancing travelR
radii takes \(t =
travelR/Ca\). An explicit tmax still
wins. The snapshot cadence follows it, so a case writes
a fixed number of snapshots whatever its duration rather
than 5000 of them at low \(Ca\).
Convergence stop. Uniform central-film measurements
are averaged over successive windows of
advWin radii of front-tip advance. Film and
front speed must remain within tolerance for
convHold windows after at least
advMin of newly observed advance.
tmax remains a hard cap.
bTol = param_double ("bTol", 2e-3);
advWin = param_double ("advWin", 0.25);
advMin = param_double ("advMin", 1.0);
convHold = param_int ("convHold", 3);
filmBins = param_int ("filmBins", 128);
filmCoverage = param_double ("filmCoverage", .95);
filmFlatTol = param_double ("filmFlatTol", .05);
freshFracMin = param_double ("freshFracMin", .98);
speedTol = param_double ("speedTol", .02);
shapeTol = param_double ("shapeTol", .02);
filmCells = param_double ("filmCells", 4.);
filmMinLevel = param_int ("filmMinLevel", 0);
requireShapeSteady = param_bool ("requireShapeSteady", true);
restartBurnR = param_double ("restartBurnR", 0.);
regridBurnR = param_double ("regridBurnR", 0.);
uRel = param_double ("uRel", 1e-2);
dRel = param_double ("dRel", 1e-2);
csErr = param_double ("csErr", 1e-2);
VelErr = uRel*Ca;
DErr = dRel*Ca/Rtube;
travelR = param_double ("travelR", 8.);
tmax = param_double ("tmax", travelR/Ca);
tsnap = param_double ("tsnap", tmax/200.);
tRamp = param_double ("tRamp", 1.);
filmSampleDt = param_double
("filmSampleDt", min(tsnap, advWin/(8.*Ca)));The time-step cap goes into DT:
centered.h resets dtmax = DT
every iteration, so assigning dtmax
directly has no effect.
DT = param_double ("dtmax", 1e-2);
solverTol = param_double ("solverTol", 1e-4);The initial shape is a capsule (cylinder of length
Lcyl with hemispherical caps of radius
Rb0) whose volume equals that of the unit
volume-equivalent sphere: \(L_{cyl} = \tfrac{4}{3}(1 -
R_{b0}^3)/R_{b0}^2\).
Rb0 = Rb0frac*Rtube;
Lcyl = 4.*(1. - cube(Rb0))/(3.*sq(Rb0));
Xb0 = xRear + Rb0 + Lcyl/2.;
vol0 = 4.*pi/3.;
freshFront = param_double ("freshFront", Xb0 + Lcyl/2. + Rb0);
const double finiteParameters[] = {
Ca, La, muR, rhoR, Rtube, Rb0frac, xRear, Ldomain, travelR,
tmax, tsnap, DT, tRamp, bTol, advWin, advMin, uRel, dRel, csErr,
filmCoverage, filmFlatTol, freshFracMin, speedTol, shapeTol,
filmCells, filmSampleDt, solverTol, restartBurnR, regridBurnR, freshFront
};
for (unsigned int j = 0; j < sizeof(finiteParameters)/sizeof(double); j++)
if (!isfinite(finiteParameters[j])) {
fprintf (ferr, "ERROR: Runtime parameters must be finite.\n");
return 1;
}
if (CaseNo < 1000 || MAXlevel <= 0 || MINlevel <= 0 ||
MINlevel > MAXlevel || Ca <= 0. || La <= 0. || muR <= 0. ||
rhoR <= 0. || Rtube <= 0. || Rtube >= 1. ||
Rb0frac <= 0. || Rb0frac >= 1. || xRear <= 0. || Ldomain <= 0. ||
tmax <= 0. || tsnap <= 0. || DT <= 0. || tRamp < 0. ||
travelR <= 0. || bTol <= 0. || advWin <= 0. || advMin < 0. ||
convHold < 1 || uRel <= 0. || dRel <= 0. || csErr <= 0. ||
filmBins < 32 || filmBins > CENTRAL_FILM_MAX_BINS ||
filmCoverage <= 0. || filmCoverage > 1. || filmFlatTol <= 0. ||
freshFracMin < 0. || freshFracMin > 1. || speedTol <= 0. ||
shapeTol <= 0. || filmCells < 0. || filmMinLevel < 0 ||
filmMinLevel > MAXlevel || filmSampleDt <= 0. || solverTol <= 0. ||
restartBurnR < 0. || regridBurnR < 0. || !isfinite(freshFront)) {
fprintf (ferr, "ERROR: Invalid runtime parameters.\n");
return 1;
}
if (Xb0 + Lcyl/2. + Rb0 >= Ldomain) {
fprintf (ferr, "ERROR: initial capsule crosses the outlet; "
"increase Ldomain or reduce xRear.\n");
return 1;
}
if (Xb0 + Lcyl/2. + Rb0 > Ldomain - 4.*Rtube)
fprintf (ferr, "WARNING: little travel room ahead of the front tip; "
"increase Ldomain or reduce xRear.\n");A run asking for more advance than the domain holds
would drive the bubble into the outlet before
tmax.
double room = (Ldomain - 2.*Rtube) - (Xb0 + Lcyl/2. + Rb0);
if (Ca*tmax > room)
fprintf (ferr, "WARNING: requested advance %g R exceeds the %g R of "
"travel room ahead of the front tip; the bubble reaches the "
"outlet before tmax = %g.\n", Ca*tmax, room, tmax);
L0 = Ldomain;
init_grid (1 << MINlevel);
int directoryError = 0;
if (pid() == 0 && mkdir ("intermediate", 0777) != 0) {
int mkdirError = errno;
struct stat status;
if (mkdirError != EEXIST || stat ("intermediate", &status) != 0 ||
!S_ISDIR(status.st_mode)) {
errno = mkdirError;
perror ("intermediate");
directoryError = 1;
}
}
#if _MPI
mpi_all_reduce (directoryError, MPI_INT, MPI_MAX);
#endif
if (directoryError)
return 1;
sprintf (dumpFile, "restart");
sprintf (logFile, "c%d-log", CaseNo);
int restartPresent = 0;
if (pid() == 0) {
struct stat restartStatus;
restartPresent = stat(dumpFile, &restartStatus) == 0 &&
restartStatus.st_size > 0;
}
int logSetupError = 0;
if (pid() == 0 && !prepare_case_log()) {
fprintf (ferr, "HARDFAIL_LOG_SCHEMA: existing log %s is incompatible with this case "
"or cannot be read; refusing to append a mixed case log.\n",
logFile);
logSetupError = 1;
}
int headerNeeded = logNeedsHeader, existingLog = logHasContent;
#if _MPI
mpi_all_reduce (restartPresent, MPI_INT, MPI_MAX);
mpi_all_reduce (logSetupError, MPI_INT, MPI_MAX);
mpi_all_reduce (headerNeeded, MPI_INT, MPI_MAX);
mpi_all_reduce (existingLog, MPI_INT, MPI_MAX);
#endif
restartFilePresent = restartPresent;
logNeedsHeader = headerNeeded;
logHasContent = existingLog;
if (logSetupError)
return 1;Fluid 1 (f = 1) is the dispersed
drop/bubble; fluid 2 (f = 0) is the
continuous wetting phase. With \(\sigma = \mu_c = R = 1\):
\(\rho_c = La\), \(\rho_d = La\,\rho_R\),
\(\mu_d = \mu_R\).
rho1 = La*rhoR; mu1 = muR;
rho2 = La; mu2 = 1.;
f.sigma = 1.;
filmConfig = (CentralFilmConfig) {
.advance_window = advWin,
.minimum_observed_advance = advMin,
.film_relative_tolerance = bTol,
.front_speed_relative_tolerance = speedTol,
.minimum_coverage = filmCoverage,
.maximum_spatial_spread = filmFlatTol,
.minimum_fresh_fraction = freshFracMin,
.minimum_cells = filmCells,
.shape_relative_tolerance = shapeTol,
.hold_windows = convHold
};
central_film_observer_init (&filmObserver, filmConfig);
TOLERANCE = solverTol;
CFL = 0.5;
if (pid() == 0) {
fprintf (ferr, "CaseNo=%d MAXlevel=%d MINlevel=%d Ca=%g La=%g muR=%g "
"rhoR=%g Rtube=%g Rb0=%g Lcyl=%g tmax=%g solverTol=%g\n",
CaseNo, MAXlevel, MINlevel, Ca, La, muR, rhoR, Rtube, Rb0,
Lcyl, tmax, solverTol);
fprintf (ferr, "Central film: bins=%d sampleDt=%g coverage>=%g "
"spread<=%g fresh>=%g cells>=%g filmMinLevel=%d "
"requireShapeSteady=%d restartBurnR=%g regridBurnR=%g "
"freshFront=%g\n", filmBins, filmSampleDt,
filmCoverage, filmFlatTol, freshFracMin, filmCells,
filmMinLevel, requireShapeSteady, restartBurnR, regridBurnR,
freshFront);
fprintf (ferr, "Logging to %s\n", logFile);
}
run();
return runFailed ? 1 : 0;
}Event: initialisation
Restores from restart when present;
otherwise refines the tube interior, embeds the wall and
initialises the capsule interface. The axi+embed metric
must be resynchronised in both branches (see
src-local/embed-vof-tube.h).
event init (t = 0)
{
restoredRun = restore (file = dumpFile);
if (!restoredRun && restartFilePresent) {
if (pid() == 0)
fprintf (ferr, "HARDFAIL_RESTORE: restart exists but could not be "
"restored; refusing to replace it with a fresh case.\n");
terminate_init_failure();
}
if (!restoredRun && logHasContent) {
if (pid() == 0)
fprintf (ferr, "HARDFAIL_LOG_REUSE: %s contains an existing case log "
"but no compatible restart was restored.\n", logFile);
terminate_init_failure();
}
if (!restoredRun) {
refine (y < 1.05*Rtube && level < MAXlevel - 2);
refine (y < 1.05*Rtube &&
x > Xb0 - Lcyl/2. - Rb0 - 4.*Rtube &&
x < Xb0 + Lcyl/2. + Rb0 + 4.*Rtube && level < MAXlevel);
tube_solid (Rtube);
fraction (f, Rb0 - sqrt (sq (max (fabs (x - Xb0) - Lcyl/2., 0.))
+ sq(y)));
vof_solid_cleanup (f);
}
else {
if (!close_parameter(L0, Ldomain)) {
if (pid() == 0)
fprintf (ferr, "HARDFAIL_DOMAIN_MISMATCH: restart L0=%g differs "
"from requested Ldomain=%g; a dump cannot be extended or "
"shrunk in place.\n", L0, Ldomain);
terminate_init_failure();
}
// Dumps contain cell fields, not the embedded face fractions. Rebuild
// the stationary wall before the solver initializes face fluxes.
tube_solid (Rtube);
#if TREE && AXI
// VOF's face scan also visits inactive full-fluid parent cells.
// Initialize their exact axisymmetric metric, not only active leaves.
foreach_cell()
if (cs[] >= 1.)
cm[] = y;
#endif
vof_solid_cleanup (f);
}A warm restart may request a finer film floor or a
measured cell target. Reconstruct the restored geometry,
then perform one bounded recursive refinement to the
requested level (never beyond MAXlevel).
Observation history is intentionally not restored: the
run must cover new advance windows before any terminal
success.
double front, rear, minimumFilm, centroid;
if (!current_drop_geometry (&front, &rear, &minimumFilm, ¢roid)) {
if (pid() == 0)
fprintf (ferr, "HARDFAIL_GEOMETRY: cannot reconstruct the dispersed "
"phase after initialisation.\n");
terminate_init_failure();
}
observationStartFront = front;
observationBurnDistance = restoredRun ? max(restartBurnR, regridBurnR) : 0.;
central_film_observer_reset (&filmObserver);
bool initialFilmOK = measure_central_film
(rear, front, centroid, &latestFilmMeasurement);
if (initialFilmOK)
latestFilmValid = true;
else if (filmMinLevel > 0 || filmCells > 0.) {
if (pid() == 0)
fprintf (ferr, "HARDFAIL_REGRID_SAMPLE: cannot measure the film needed "
"to apply filmMinLevel/filmCells before integration.\n");
terminate_init_failure();
}
refine_measured_film();
embed_axi_metric_sync();
vof_solid_cleanup (f);
if (pid() == 0 && restoredRun)
fprintf (ferr, "# restart observation burn: startFront=%g distance=%g "
"freshFront=%g\n", observationStartFront,
observationBurnDistance, freshFront);
}Cheap per-step corruption guard. Interface reconstruction stays on the bounded diagnostic cadence, while nonfinite primary fields, time-step failure and kinetic-energy blow-up terminate before several corrupt steps accumulate.
event solverSafety (i++)
{
double ke = 0.;
double nonfinite = (!isfinite(t) || !isfinite(dt) || dt <= 0.) ? 1. : 0.;
foreach (reduction(+:ke) reduction(+:nonfinite)) {
if (!isfinite(f[]) || !isfinite(u.x[]) || !isfinite(u.y[]) ||
!isfinite(p[]) || !isfinite(cs[]) || !isfinite(cm[]))
nonfinite += 1.;
ke += 2.*pi*cm[]*0.5*rho(f[])*(sq(u.x[]) + sq(u.y[]))*sq(Delta);
}
latestSafetyKE = ke;
if (nonfinite || !isfinite(ke)) {
if (pid() == 0)
fprintf (ferr, "HARDFAIL_NONFINITE: nonfinite solver state at i=%d "
"t=%g dt=%g.\n", i, t, dt);
runFailed = 1;
if (pid() == 0)
fprintf (ferr, "# last good restart preserved at %s.\n", dumpFile);
return 1;
}
if (ke > 1e3 && i > 10) {
if (pid() == 0)
fprintf (ferr, "HARDFAIL_ENERGY: kinetic energy %.6e exceeds 1e3 "
"at i=%d t=%g.\n", ke, i, t);
runFailed = 1;
dump (file = "hardfail-energy");
return 1;
}
}Adaptive mesh refinement
Adapts on the interface, its curvature, the embedded fraction (which keeps the wall and film region refined) and the velocity, then restores the axi+embed metric and the solid-cell volume fraction.
event adapt (i++)
{
scalar KAPPA[];
curvature (f, KAPPA);The strain-rate magnitude \(\sqrt{\mathbf{D}\!:\!\mathbf{D}}\) is adapted on directly, so the mesh follows viscous dissipation rather than only the interface and the velocity. In the film the shear is far above the bulk \(U/R_{tube}\), which is exactly where the resolution is wanted.
scalar Dmag[];
foreach() {
double D11 = (u.y[0,1] - u.y[0,-1])/(2.*Delta);
double D22 = (y > 1e-10) ? u.y[]/y : D11; // axis limit is du_r/dr
double D33 = (u.x[1,0] - u.x[-1,0])/(2.*Delta);
double D13 = 0.5*((u.y[1,0] - u.y[-1,0] + u.x[0,1] - u.x[0,-1])/(2.*Delta));
Dmag[] = sqrt (sq(D11) + sq(D22) + sq(D33) + 2.*sq(D13));
}
adapt_wavelet ((scalar *){f, KAPPA, cs, u.x, u.y, Dmag},
(double[]){fErr, KErr, csErr, VelErr, VelErr, DErr},
MAXlevel, MINlevel);
refine_measured_film();
embed_axi_metric_sync();
vof_solid_cleanup (f);
}Event: writingFiles
Restart dump plus time-stamped snapshots in
intermediate/.
event writingFiles (t = 0; t += tsnap; t <= tmax)
{
dump (file = dumpFile);
sprintf (nameOut, "intermediate/snapshot-%5.4f", t);
dump (file = nameOut);
}Event: logWriting
Bounded-cadence diagnostics: kinetic energy, dispersed-phase volume error, front/rear/centroid positions, the legacy global minimum gap \(b_{min} = R_{tube} - \max_y(\text{interface})\), and the uniformly sampled central film. The window diagnostics directly report front, rear and centroid speeds.
event logWriting (i++)
{Interface reconstruction is deliberately sampled at a bounded cadence.
if (t + 1e-12 < nextFilmSampleTime)
return 0;
nextFilmSampleTime = t + filmSampleDt;
double ke = latestSafetyKE;
double vol = 2.*pi*statsf(f).sum;
double xTipF, xTipR, bFilm, centroid;
bool geometryOK = current_drop_geometry
(&xTipF, &xTipR, &bFilm, ¢roid);
bool centralOK = geometryOK && measure_central_film
(xTipR, xTipF, centroid, &latestFilmMeasurement);
if (centralOK)
latestFilmValid = true;
else
central_film_observer_reset (&filmObserver);
double central = centralOK ? latestFilmMeasurement.film : NAN;
double spread = centralOK ? latestFilmMeasurement.spatial_spread : NAN;
double coverage = centralOK ? latestFilmMeasurement.coverage : 0.;
double fresh = centralOK ? latestFilmMeasurement.fresh_fraction : 0.;
double cells = centralOK ? latestFilmMeasurement.minimum_cells : 0.;
int logError = 0;
if (pid() == 0) {
FILE * fp = fopen (logFile, "a");
if (fp == NULL) {
fprintf (ferr, "HARDFAIL_LOG_IO: cannot open log file %s\n", logFile);
logError = 1;
}
else {
if (logNeedsHeader) {
fprintf (fp, "# CaseNo %d, MAXlevel %d, MINlevel %d, Ca %.17g, "
"La %.17g, muR %.17g, rhoR %.17g, Rtube %.17g, "
"Ldomain %.17g, solverTol %.17g\n", CaseNo, MAXlevel,
MINlevel, Ca, La, muR, rhoR, Rtube, Ldomain, solverTol);
fprintf (fp, "# i dt t ke dVol/Vol0 xTipF xTipR bFilm "
"bCentral spatialSpread coverage freshFraction minCells "
"centroid length\n");
logNeedsHeader = false;
}
fprintf (fp, "%d %.6e %.6e %.6e %.6e %.6e %.6e %.6e "
"%.6e %.6e %.6e %.6e %.6e %.6e %.6e\n",
i, dt, t, ke, (vol - vol0)/vol0, xTipF, xTipR, bFilm,
central, spread, coverage, fresh, cells, centroid,
xTipF - xTipR);
fclose (fp);
}
fprintf (ferr, "%d %.6e %.6e %.6e %.6e %.6e %.6e %.6e "
"%.6e %.6e %.6e %.6e %.6e %.6e %.6e\n",
i, dt, t, ke, (vol - vol0)/vol0, xTipF, xTipR, bFilm,
central, spread, coverage, fresh, cells, centroid,
xTipF - xTipR);
}
if (logError) {
#if _MPI
// A rank-zero I/O failure must end this MPI job, without adding a
// collective reduction to every successful diagnostic step.
MPI_Abort (MPI_COMM_WORLD, 1);
#endif
runFailed = 1;
return 1;
}Hard failure guards: kinetic-energy blow-up, loss of the wetting film (interface reaching within one fine cell of the wall) or the front meniscus approaching the outlet.
if (bFilm < 4.*L0/(1 << MAXlevel) && i > 10 && pid() == 0)
fprintf (ferr, "WARNING: film resolved by fewer than 4 cells at t=%g; "
"increase MAXlevel.\n", t);
if (xTipF > L0 - 2.*Rtube) {
if (pid() == 0)
fprintf (ferr, "INCOMPLETE_OUTLET: front tip reached the outlet "
"buffer at t=%g.\n", t);
runFailed = 1;
dump (file = dumpFile);
return 1;
}Central-film stationarity is based on uniformly sampled 30–70% profiles, successive advance windows and a separately measured front-speed plateau. Whole-shape stationarity compares front, rear and centroid speeds plus length drift; it is a distinct milestone and can optionally be required.
bool observationEligible = centralOK && t >= tRamp &&
xTipF >= observationStartFront + observationBurnDistance;
if (!observationEligible)
central_film_observer_reset (&filmObserver);
if (observationEligible) {
CentralFilmWindow window;
if (central_film_observer_push
(&filmObserver, latestFilmMeasurement, &window)) {
if (pid() == 0)
fprintf (ferr, "# central-film window: advance=%.3f b=%.6e "
"filmDrift=%.3e frontSpeed=%.6e speedDrift=%.3e "
"coverage=%.3f spread=%.3e fresh=%.3f minCells=%.3f "
"shapeMismatch=%.3e lengthDrift=%.3e filmHold=%d/%d "
"shapeHold=%d/%d\n",
window.advance, window.film, window.film_drift,
window.front_speed, window.front_speed_drift,
window.minimum_coverage, window.maximum_spatial_spread,
window.minimum_fresh_fraction, window.minimum_cells,
window.shape_speed_mismatch, window.length_drift,
window.hold_count, convHold, window.shape_hold_count,
convHold);
if (filmCells > 0. && window.minimum_cells < filmCells && pid() == 0)
fprintf (ferr, "# central-film resolution insufficient: measured "
"minimum %.3f cells, requested %.3f; refinement is capped "
"at MAXlevel=%d.\n", window.minimum_cells, filmCells,
MAXlevel);
if (window.film_converged) {
if (pid() == 0)
fprintf (ferr, "Central deposited-film steady: b=%.6e "
"(b/Rtube=%.6e), front speed %.6e. Whole-shape status: "
"%s (%d/%d consecutive windows).\n", window.film,
window.film/Rtube,
window.front_speed,
window.shape_converged ? "steady" : "still evolving",
window.shape_hold_count, convHold);
if (!requireShapeSteady || window.shape_converged) {
if (pid() == 0)
fprintf (ferr, "SUCCESS: %s milestone passed at t=%g.\n",
requireShapeSteady ? "central-film and whole-shape" :
"central deposited-film", t);
dump (file = dumpFile);
return 1;
}
}
}
}
}Event: stopSimulation
event stopSimulation (t = tmax)
{
runFailed = 1;
if (pid() == 0)
fprintf (ferr, "INCOMPLETE_TMAX: case %d reached tmax without the requested "
"stationarity milestone: Ca %g, La %g, muR %g, rhoR %g.\n",
CaseNo, Ca, La, muR, rhoR);
dump (file = dumpFile);
return 1;
}