sandbox/acastillo/frozen_waves/without_walls/main.c
«Frozen waves» at the interface between non-miscible fluids with strong density contrast with periodic boundary conditions
Preamble
Problem parameters
We consider two non-miscible fluids with densities \rho_1 and \rho_2 (\rho_1 > \rho_2) and viscosities \mu_1 and \mu_2. The interface \eta(x,y,z,t) between the two fluids is characterized by a surface tension coefficient \sigma, assumed constant. Additionally, we consider gravity g and apply a periodic oscillating motion with amplitude a and frequency \omega in the horizontal direction. For a recipient of size W\times H\times D, the system is characterised by five fluid parameters (\rho_1, \rho_2, \mu_1, \mu_2, \sigma) and three forcing parameters (a, \omega, g). If we consider the forcing displacement, the forcing frequency and the capillary length as characteristic lengths of the problem \displaystyle [\text{L}] = a, \quad [\text{T}] = \frac{1}{\omega}, \quad l_c = \sqrt{\frac{\sigma}{(\rho_1 - \rho_2) g}} we identify the following eight dimensionless groups:
| Parameter | Symbol | Definition |
|---|---|---|
| Aspect ratios | {\color{purple}\mathsf{W}} | \displaystyle \frac{W}{a} |
| {\color{purple}\mathsf{H}} | \displaystyle \frac{H}{a} | |
| {\color{purple}\mathsf{D}} | \displaystyle \frac{D}{a} | |
| Reynolds number | {\color{orange}\mathsf{Re}} | \displaystyle \frac{\rho_1 a^2\omega}{\mu_1} |
| Froude number | {\color{red}\mathsf{Fr}} | \displaystyle \frac{a\omega^2}{g} |
| Weber number | {\color{green}\mathsf{We}} | \displaystyle \frac{(\rho_1-\rho_2) a^3 \omega^2}{\sigma} |
| Atwood number | {\color{blue}\mathsf{At}} | \displaystyle \frac{\rho_1 - \rho_2}{\rho_1 + \rho_2} |
| Viscosity ratio | {\color{purple}\Upsilon} | \displaystyle \frac{\nu_2}{\nu_1} |
To complete the problem description, we specify the contact angle and the initial conditions. Here, we consider a static contact angle of \theta = 90^\circ. The interface \eta(x,y,z,t) is initially flat. We may also impose an initial perturbation in the form of a constant amplitude and random phase annular spectrum, following the approach of Dimonte et al. (2004) and Thévenin et al. (2025).
Imposing periodic boundary conditions using the approach of Bunner and Tryggvason (2002)
Frozen waves are subject to confinement effects due to the presence of walls. Sidewalls also induce an horizontal pressure gradient that drive a mean flow and pushes frozen waves to the sides. To avoid these effects, it would be required an infinite domain in the horizontal direction. Instead, we take the approach of Bunner and Tryggvason (2002), in which an additional body force is added to both fluids to ensure that the net momentum flux across the boundaries is zero. Here, the pressure field is assumed of the form : \displaystyle p(\mathbf{x},t) = p_0(\mathbf{x},t) + x {\color{red}\mathcal{P}}\sin(t) The main idea is to pick a value of {\color{red}\mathcal{P}} such that p_0(\mathbf{x},t) is periodic in the x-direction. This term is analogous to the pressure gradient generated by the base of a flow container, which balances the total gravitational force on the fluid. In the context of this problem, Gréa et al. (2025) showed that \displaystyle {\color{red}\mathcal{P}}= 1 - {\color{blue}\mathsf{At}}^2 in the limit of large {\color{purple}\mathsf{H}}, provided that both fluids have equal volumes.
Governing equations
We use the two-phase incompressible Navier–Stokes equations. The volume fraction \phi(\mathbf{x},t) is used to transform the two-phase problem into a one-fluid formulation. If we consider the same characteristic lengths [\text{L}] and [\text{T}] as before, the equations read \begin{aligned} \nabla \cdot \mathbf{u} &= 0 \\ \partial_{t} \mathbf{u} + \nabla \cdot (\mathbf{u}\otimes\mathbf{u}) &= \frac{1}{\rho^*} \left[ -\nabla p_0 + \nabla\cdot(2\mu^*\mathbf{D})+\sigma^*\mathbf{f}_\sigma \right] - a^* \mathbf{e}_z + {\color{red}\left[ \frac{\rho^* - {\color{red}\mathcal{P}}}{\rho^*} \right]} \sin(t)\mathbf{e}_x \\ \partial_{t} \phi + \nabla \cdot (\phi \mathbf{u}) &= 0 \end{aligned}
where
- \mathbf{D} = (\nabla\mathbf{u}+(\nabla^*\mathbf{u})^T)/2 is the deformation tensor,
- \mathbf{f}_\sigma = \kappa \delta_s
\mathbf{n} is the surface tension force, with
- \kappa being the local interface curvature,
- \delta_s being the Dirac function associated with the interface and
- \mathbf{n} being the unit normal vector,
- \sigma^* is the (dimensionless) surface tension coefficient, \displaystyle \sigma^* = \frac{\sigma [T]^2}{\rho_0[L]^3} = 2{\color{cyan}{\color{green}\mathsf{We}}}^{-1}
- a^* is the (dimensionless) acceleration due to gravity. a^* = \frac{g[T]^2}{[L]} = {\color{blue}{\color{red}\mathsf{Fr}}}^{-1}
The equations are coupled via the (dimensionless) density and viscosity fields \begin{aligned} \rho^*(\mathbf{x},t) &= (1+{\color{blue}\mathsf{At}})\phi(\mathbf{x},t) + (1-{\color{blue}\mathsf{At}})(1-\phi(\mathbf{x},t)) \\ \mu^*(\mathbf{x},t) &= {\color{orange}\mathsf{Re}}^{-1}(1+{\color{blue}\mathsf{At}})\phi(\mathbf{x},t) + {\color{orange}\mathsf{Re}}^{-1}{\color{purple}\Upsilon}(1-{\color{blue}\mathsf{At}})(1-\phi(\mathbf{x},t)) \end{aligned}
Additionally, the computational domain is of size {\color{purple}\mathsf{W}}\times {\color{purple}\mathsf{H}}\times {\color{purple}\mathsf{D}}.
Implementation using Basilisk
Include solver blocks
We use a combination of the two-phase incompressible solver with no-slip boundaries.
// Uncomment the following lines to use different solver options
//#define FILTERED 1 // Uncomment to smudge the interface
//#define REDUCED 1 // Uncomment to use the reduced gravity approach
//#define CLSVOF 1 // Uncomment to use CLSVOF
#include "grid/multigrid.h"
#include "navier-stokes/centered.h"
// Two-phase flow configuration
#if CLSVOF
// CLSVOF (Coupled Level Set and Volume of Fluid) method
#include "two-phase-clsvof.h"
#include "integral.h"
#include "curvature.h"
#else
// Standard VOF method
#include "two-phase.h"
#include "tension.h"
#endif
// Momentum conserving advection scheme
#include "navier-stokes/conserving.h"
// Reduced gravity approach (optional)
#ifdef REDUCED
#include "reduced.h"
#endif
#include "maxruntime.h"
#include "sim_parameters.h"Set parameters in the main loop
Parameters are now read from a JSON configuration file using the
sim_parameters.h module. The global params
struct holds all simulation parameters.
SimParams params;
#define BACKUPFREQ 100.0 // Backup frequency (in time units)
#define SNAPFREQ 100.0 // Snapshot frequency (in time units)
#define PROFFREQ (3.14159265359) // Profiles frequency (in time units)
#define ANIMFREQ (3.14159265359) // Animation frequency (in time units)
#define SERIESFREQ (3.14159265359) // Time series frequency (in time units)
#if !CLSVOF
vector h[];
#endif
int main() {- Read parameters from JSON file (or use defaults)
read_sim_params("main.json", ¶ms);
if (pid() == 0)
print_sim_params(¶ms);- Set the domain size
L0 = params.width;
#if dimension == 2
dimensions (nx = params.aspectratio_x, ny = params.aspectratio_y);
X0 = -params.width/2.;
Y0 = -params.height/2.;
#else
dimensions (nx = params.aspectratio_x, ny = params.aspectratio_y, nz = params.aspectratio_z);
X0 = -params.width/2.;
Y0 = -params.depth/2.;
Z0 = -params.height/2.;
#endif
N = (1 << params.level);
init_grid (N);- Assign the fluid properties
rho1 = (1. + params.atwood);
rho2 = (1. - params.atwood);
mu1 = rho1 / params.reynolds;
mu2 = rho2 * params.viscosity / params.reynolds;
#if CLSVOF
const scalar sigma[] = 2. / params.weber;
d.sigmaf = sigma;
#else
f.sigma = 2. / params.weber;
f.height = h;
#endif- Set the gravitational acceleration
#if REDUCED
#if dimension == 2
G.y = -1./params.froude;
#else
G.z = -1./params.froude;
#endif
#endif- Set periodicity in the horizontal direction
- Set numeric parameters
_maxruntime = params.walltime; // Maximum runtime in seconds
p.nodump = false; // Includes pressure in the dump file
CFL = params.cfl;
DT = params.dt;
TOLERANCE = params.tolerance;
NITERMIN = params.nitermin;
run();
}
p[right] = neumann (neumann_pressure(ghost));
p[left] = neumann (- neumann_pressure(0));
#if dimension == 3
p[top] = neumann (neumann_pressure(ghost));
p[bottom] = neumann (- neumann_pressure(0));
#endifWe add the external acceleration
To prevent sloshing, we ramp the acceleration using a sigmoid function.
\displaystyle r(t) = \frac{1}{1 + \exp(-k(t - t_\text{start}))} where t_\text{start} is the time at which the ramp starts and k is the slope of the ramp.
double sigmoid(double t1, double k){
return 1.0 / (1.0 + exp(-k * (t - t1)));
}
double ramp;
event acceleration (i++) {
// We set the parameters for the ramp
double time_start = params.ramp_start;
double time_slope = params.ramp_slope;
ramp = sigmoid(time_start, time_slope);
// Then, we add the acceleration and subtract the pressure gradient
double P_rond = 1. - sq(params.atwood);
face vector av = a;
foreach_face(x)
av.x[] += ramp * sin(t) * (1. - P_rond*alpha.x[]);
// And finally, we add the gravity (if not using the reduced gravity approach)
#if !REDUCED
#if dimension == 2
foreach_face(y)
av.y[] -= 1./params.froude;
#else
foreach_face(z)
av.z[] -= 1./params.froude;
#endif
#endif
}
#include "acastillo/frozen_waves/acceleration_cfl.h"
event set_dtmax (i++,last) {
dtmax = timestep_force (u, DT, ramp);
}Set the initial conditions
We use the initial conditions from Dimonte
(2004) as implemented in my sandbox. Note that the amplitude {\color{cyan}\mathsf{S}} we use here is a
target value, and the actual amplitude of the initial conditions is
computed in initial_condition_dimonte_fft and
initial_condition_dimonte_fft2. In practice, the r.m.s. of
the initial perturbation is
\displaystyle
{\color{cyan}\mathsf{S}}= \frac{\eta_0}{a} = \frac{1}{A} \iint
(\eta(x,y)-\overline{\eta})^2 \, dx \, dy
which is generally small.
#include "view.h"
#include "acastillo/output_fields/vtu/output_vtu.h"
#include "acastillo/output_fields/vtkhdf/output_vtkhdf.h"
void setup_snapshot(char* filename, bool video = false){
char png_filename[256];
char hdf_filename[256];
if (video)
sprintf(png_filename, "%s.mp4", filename);
else {
sprintf(png_filename, "%s.png", filename);
sprintf(hdf_filename, "%s.hdf", filename);
}
#if dimension == 2
view( width=1024, height=1024);
box();
draw_vof("f", filled = -1, fc = {0,0,0} );
#else
view( camera = "iso");
draw_vof("f", color = "z" );
#endif
save(png_filename);
if (!video){
scalar kappa[];
#if CLSVOF
foreach()
kappa[] = distance_curvature (point, d);
#else
curvature (f, kappa);
#endif
output_facets_vtu(f, kappa, filename);
output_vtkhdf({f,p}, {u}, hdf_filename);
}
}
#if dimension == 2
#include "acastillo/input_fields/initial_conditions_dimonte_fft1.h"
#else
#include "acastillo/input_fields/initial_conditions_dimonte_fft2.h"
#endif
event init(i = 0){
if (!restore(file = "./restart")){
#if CLSVOF
#if dimension == 2
initial_condition_dimonte_fft(d, amplitude=params.amplitude, NX=N, kmin=params.kmin, kmax=params.kmax, isvertex=0);
#else
initial_condition_dimonte_fft2(d, amplitude=params.amplitude, NX=N, NY=N, kmin=params.kmin, kmax=params.kmax, isvof=1, isvertex=0);
#endif
#else
vertex scalar phi[];
#if dimension == 2
initial_condition_dimonte_fft(phi, amplitude=params.amplitude, NX=N, kmin=params.kmin, kmax=params.kmax);
#else
initial_condition_dimonte_fft2(phi, amplitude=params.amplitude, NX=N, NY=N, kmin=params.kmin, kmax=params.kmax, isvof=1);
#endif
fractions(phi, f);
#endif
}Then, we visualize the initial conditions
#if CLSVOF
vertex scalar phi[];
foreach_vertex()
#if dimension == 2
phi[] = (d[] + d[-1] + d[0,-1] + d[-1,-1])/4.;
#else
phi[] = (d[] + d[-1] + d[0,-1] + d[-1,-1] + d[0,0,-1] + d[-1,0,-1] + d[0,-1,-1] + d[-1,-1,-1])/8.;
#endif
fractions (phi, f);
#endif
setup_snapshot("init");
}Outputs
Convergence of the multigrid solver
A log to track the convergence of the multigrid solvers.
import numpy as np
import matplotlib.pyplot as plt
filename1 = 'out'
data = np.genfromtxt(filename1, usecols=[1,5,11], skip_header=6)
t = data[:,0]
residual_p = data[:,1]
residual_u = data[:,2]
fig, axes = plt.subplots(1,2, figsize=(4*2, 3))
ax = axes.ravel()
ax[0].semilogy(t, residual_p)
ax[1].semilogy(t, residual_u)
ax[0].set_xlabel('Time')
ax[0].set_ylabel('Residual pressure')
ax[1].set_xlabel('Time')
ax[1].set_ylabel('Residual velocity')
plt.tight_layout()
plt.savefig('plot_residuals.svg', dpi=150)void mg_print (mgstats mg)
{
if (mg.i > 0 && mg.resa > 0.)
fprintf (stdout, " \t - \t %d %g %g %g %d ", mg.i, mg.resb, mg.resa,
mg.resb > 0 ? exp (log (mg.resb/mg.resa)/mg.i) : 0.,
mg.nrelax);
}
event log (i++; t <= params.endtime) {
fprintf (stdout, " %d %g ", i, t);
mg_print (mgp);
mg_print (mgu);
fprintf (stdout, "\n");
fflush (stdout);
}
event finalize(t = end)
dump("backup");
event backups(t += BACKUPFREQ)
dump("backup");Saving videos and snapshots for visualization
event animate (t += ANIMFREQ){
setup_snapshot("animate", true);
}
event snapshot (t += SNAPFREQ){
char name[80];
sprintf(name, "snapshot_t%g", t);
setup_snapshot(name);
}1D Profiles
Let us introduce the horizontal average operator \displaystyle \overline{Q}(z,t) = \frac{1}{A} \iint_{-\infty}^{\infty} Q(\mathbf{x},t) ~dx~dy and its corresponding Reynolds decomposition as: \displaystyle Q(\mathbf{x},t) = \overline{Q}(z,t) + q(\mathbf{x},t)
We may also consider the weighted average of the horizontal profiles \displaystyle \widetilde{Q}(z,t) = \frac{\overline{\rho(\mathbf{x},t) Q(\mathbf{x},t)}}{\overline{\rho(\mathbf{x},t)}} and its corresponding Favre decomposition as: \displaystyle Q(\mathbf{x},t) = \widetilde{Q}(z,t) - q'(z,t)
We compute both kinds of vertical profiles for:
- the primitive variables, \phi, u_x, u_y, \cdots
- second order terms, \phi^2, \phi(1-\phi), \mathbf{u}^2, \phi u_z, \cdots
- dissipative terms, \nabla\mathbf{u}:\nabla\mathbf{u}, \cdots
For instance, an example of the Reynolds averaged profiles can be seen below:
import numpy as np
import matplotlib.pyplot as plt
def count_rows_until_empty(filename):
count = 0
with open(filename, 'r') as f:
for line in f:
if line.strip() == '': # Empty line (or only whitespace)
break
count += 1
return count
filename1 = 'profiles_f1.asc'
filename2 = 'profiles_K1.asc'
filename3 = 'profiles_Eps1.asc'
filename4 = 'profiles_ff1.asc'
filename5 = 'profiles_Y1.asc'
filename6 = 'profiles_F1.asc'
first_row = np.genfromtxt(filename2, max_rows=1, skip_header=1)
num_columns = len(first_row)
n = count_rows_until_empty(filename1) - 1
if num_columns == 2:
data = np.genfromtxt(filename1, skip_header=1, usecols=[0,1,2,3,4])
y = data[:,0].reshape(-1,n).T
fmean = data[:,1].reshape(-1,n).T
mmean = data[:,2].reshape(-1,n).T
umean = data[:,3].reshape(-1,n).T
vmean = data[:,4].reshape(-1,n).T
L = fmean*(1-fmean)
data = np.genfromtxt(filename2, skip_header=1, usecols=[1])
K = data[:].reshape(-1,n).T
data = np.genfromtxt(filename3, skip_header=1, usecols=[1])
Eps = data[:].reshape(-1,n).T
data = np.genfromtxt(filename4, skip_header=1, usecols=[1])
ffmean = data[:].reshape(-1,n).T
data = np.genfromtxt(filename5, skip_header=1, usecols=[1])
Ymean = data[:].reshape(-1,n).T
data = np.genfromtxt(filename6, skip_header=1, usecols=[1])
Fmean = data[:].reshape(-1,n).T
fig, axes = plt.subplots(3,3, figsize=(2*3, 3*3))
ax = axes.ravel()
nmin = 0
nmax = 20
nskip = 2
ax[0].plot(fmean[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[1].plot(umean[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[2].plot(vmean[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[3].plot(ffmean[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[4].plot(K[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[5].plot(Fmean[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[6].plot(L[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[7].plot(Ymean[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
ax[8].plot(Eps[:,nmin:nmax:nskip], y[:,nmin:nmax:nskip])
xlabels = [r'$\overline{\phi}$', r'$\overline{u_x}$', r'$\overline{u_y}$',
r'$\overline{\phi^2}$', r'$\overline{\mathbf{u}^2}$', r'$\overline{\phi u_y}$',
r'$\overline{\phi}(1-\overline{\phi})$', r'$\overline{\phi(1-\phi)}$',
r'$\overline{\nabla\mathbf{u}:\nabla\mathbf{u}}$']
for i in range(9):
ax[i].set_ylim([np.min(y),np.max(y)])
ax[i].set_xlabel(xlabels[i])
ax[i].set_ylabel(r'$y$')
plt.tight_layout()
plt.savefig('plot_profiles.svg', dpi=150)
plt.close()The vertical profiles may also be used to compute relevant 0D quantities, such as :
- the mixing length width \displaystyle L(t) = 6\int_{-\infty}^{\infty} \overline{\phi}(z,t)(1-\overline{\phi}(z,t))~dz
- the 0D turbulent kinetic energy and its dissipation rate \displaystyle \mathcal{K}(t) = \frac{1}{L}\int_{-\infty}^{\infty} \frac{1}{2}\overline{\mathbf{u}^2(z,t)}~dz \displaystyle \mathcal{E}(t) = \frac{1}{L}\int_{-\infty}^{\infty} \nu\overline{\nabla\mathbf{u}:\nabla\mathbf{u}}~dz
- fluctuations of the volume fraction \displaystyle \mathcal{K}_{\phi\phi}(t) = \frac{1}{L}\int_{-\infty}^{\infty} \overline{\phi(\mathbf{x},t)\phi(\mathbf{x},t)}~dz
- the vertical flux of the volume fraction \displaystyle \mathcal{F}(t) = \frac{1}{L}\int_{-\infty}^{\infty} \overline{\phi(\mathbf{x},t)u_z(\mathbf{x},t)}~dz
For instance, an example of the 0D quantities (based on the Reynolds averaged profiles) can be seen below:
import numpy as np
import matplotlib.pyplot as plt
# NumPy compatibility: trapz deprecated in 2.0, replaced by trapezoid
try:
from numpy import trapezoid as integrate
except ImportError:
from numpy import trapz as integrate
def count_rows_until_empty(filename):
count = 0
with open(filename, 'r') as f:
for line in f:
if line.strip() == '': # Empty line (or only whitespace)
break
count += 1
return count
filename1 = 'profiles_f1.asc'
filename2 = 'profiles_K1.asc'
filename3 = 'profiles_Eps1.asc'
filename4 = 'profiles_ff1.asc'
filename5 = 'profiles_Y1.asc'
filename6 = 'profiles_F1.asc'
first_row = np.genfromtxt(filename2, max_rows=1, skip_header=1)
num_columns = len(first_row)
n = count_rows_until_empty(filename1) - 1
profreq = np.pi
if num_columns == 2:
data = np.genfromtxt(filename1, skip_header=1, usecols=[0,1,2,3])
y = data[:,0].reshape(-1,n).T
fmean = data[:,1].reshape(-1,n).T
L = 6*integrate(fmean*(1-fmean), y, axis=0)
t = np.arange(len(L))*profreq
data = np.genfromtxt(filename2, skip_header=1, usecols=[1])
UUmean = data[:].reshape(-1,n).T
K = 0.5*integrate(UUmean, y, axis=0)/L
data = np.genfromtxt(filename3, skip_header=1, usecols=[1])
gradUgradUmean = data[:].reshape(-1,n).T
Epsilon = integrate(gradUgradUmean, y, axis=0)/L
data = np.genfromtxt(filename4, skip_header=1, usecols=[1])
ffmean = data[:].reshape(-1,n).T
Kcc = integrate(ffmean, y, axis=0)/L
data = np.genfromtxt(filename5, skip_header=1, usecols=[1])
Ymean = data[:].reshape(-1,n).T
Theta = integrate(Ymean, y, axis=0)/integrate(fmean*(1-fmean), y, axis=0)
data = np.genfromtxt(filename6, skip_header=1, usecols=[1])
Fmean = data[:].reshape(-1,n).T
F = integrate(Fmean, y, axis=0)/L
fig, axes = plt.subplots(2,3, figsize=(4*3, 3*2))
ax = axes.ravel()
ax[0].plot(t, L)
ax[1].plot(t, K)
ax[2].plot(t, Epsilon)
ax[3].plot(t, Kcc)
ax[4].plot(t, Theta)
ax[5].plot(t, F)
ylabels = [r'$L$', r'$\mathcal{K}$', r'$\mathcal{E}$', r'$\mathcal{K}_{cc}$', r'$\Theta$', r'$\mathcal{F}$']
for i in range(6):
ax[i].set_xlabel(r'$t$')
ax[i].set_xlim(np.min(t), np.max(t))
ax[i].set_ylabel(ylabels[i])
ax[i].ticklabel_format(style='sci', axis='y', scilimits=(0,0))
plt.tight_layout()
plt.savefig('plot_0D.svg', dpi=150)// Macro to simplify repeated profile_foreach_region calls
#include "acastillo/output_fields/profiles_foreach_region.h"
#define SAVE_PROFILE(...) \
profile_foreach_region(__VA_ARGS__, xmin = -L0/4., xmax = L0/4., hmin = hmin, hmax = hmax)
// Function to compute the strain rate for the dissipative term
static inline double strain_rate_sq (Point point, vector u) {
double dudx = (u.x[1] - u.x[-1] )/(2.*Delta);
double dvdx = (u.y[1] - u.y[-1] )/(2.*Delta);
double dudy = (u.x[0,1] - u.x[0,-1] )/(2.*Delta);
double dvdy = (u.y[0,1] - u.y[0,-1] )/(2.*Delta);
double Sxx = dudx;
double Sxy = 0.5*(dudy + dvdx);
double Syx = Sxy;
double Syy = dvdy;
double S2 = sq(Sxx) + sq(Sxy) + sq(Syx) + sq(Syy);
#if dimension == 3
double dwdx = (u.z[1] - u.z[-1] )/(2.*Delta);
double dwdy = (u.z[0,1] - u.z[0,-1] )/(2.*Delta);
double dudz = (u.x[0,0,1] - u.x[0,0,-1])/(2.*Delta);
double dvdz = (u.y[0,0,1] - u.y[0,0,-1])/(2.*Delta);
double dwdz = (u.z[0,0,1] - u.z[0,0,-1])/(2.*Delta);
double Szz = dwdz;
double Sxz = 0.5*(dwdx + dudz);
double Syz = 0.5*(dwdy + dvdz);
double Szx = Sxz;
double Szy = Syz;
S2 += sq(Szz) + sq(Sxz) + sq(Syz) + sq(Szx) + sq(Szy);
#endif
return S2;
}
event save_profiles (t += PROFFREQ){
double del = L0 / (1 << params.level);
double hmin = -params.height/2. + del;
double hmax = params.height/2. - del;
// Volume fraction, mass fraction and velocity
scalar temp[];
foreach(){
temp[] = rho1*f[]/(f[]*(rho1 - rho2) + rho2);
}
scalar * list = {f, temp, u};
SAVE_PROFILE(list, filename="profiles_f1.asc");
SAVE_PROFILE(list, rhov, filename="profiles_f2.asc");
// Kinetic energy
foreach(){
temp[] = 0.;
foreach_dimension(){
temp[] += sq(u.x[]);
}
}
list = (scalar *){temp};
SAVE_PROFILE(list, filename="profiles_K1.asc");
SAVE_PROFILE(list, rhov, filename="profiles_K2.asc");
// Vertical flux
foreach(){
#if dimension == 2
temp[] = f[]*u.y[];
#else
temp[] = f[]*u.z[];
#endif
}
list = (scalar *){temp};
SAVE_PROFILE(list, filename="profiles_F1.asc");
SAVE_PROFILE(list, rhov, filename="profiles_F2.asc");
// Scalar dissipation
foreach(){
temp[] = sq(f[]);
}
list = (scalar *){temp};
SAVE_PROFILE(list, filename="profiles_ff1.asc");
SAVE_PROFILE(list, rhov, filename="profiles_ff2.asc");
// Young parameter
foreach(){
temp[] = f[]*(1-f[]);
}
list = (scalar *){temp};
SAVE_PROFILE(list, filename="profiles_Y1.asc");
SAVE_PROFILE(list, rhov, filename="profiles_Y2.asc");
// Viscous dissipation
foreach(){
temp[] = strain_rate_sq(point, u);
}
list = (scalar *){temp};
SAVE_PROFILE(list, filename="profiles_Eps1.asc");
SAVE_PROFILE(list, rhov, filename="profiles_Eps2.asc");
}
#undef SAVE_PROFILEGlobal Quantities
import numpy as np
import matplotlib.pyplot as plt
filename1 = 'globals.asc'
first_row = np.genfromtxt(filename1, max_rows=1, skip_header=1)
num_columns = len(first_row)
if num_columns == 6:
data = np.genfromtxt(filename1, usecols=[0,1,2,4,5])
else:
data = np.genfromtxt(filename1, usecols=[0,1,2,5,6])
t = data[:,0]
r = data[:,1]
ekin = data[:,2]
epot = data[:,3]
epot -= epot[0]
epsilon = data[:,4]
fig, axes = plt.subplots(2, 2, figsize=(8, 6))
ax = axes.ravel()
ax[0].plot(t, r)
ax[1].plot(t, ekin)
ax[2].plot(t, epot)
ax[3].plot(t, epsilon)
ylabels = [r'$r(t)$', r'$E_{kin}(t)$', r'$E_{pot}(t)$', r'$\epsilon(t)$']
for i in range(4):
ax[i].set_xlabel(r'$t$')
ax[i].set_ylabel(ylabels[i])
ax[i].set_xlim(np.min(t), np.max(t))
ax[i].ticklabel_format(style='sci', axis='y', scilimits=(0,0))
plt.tight_layout()
plt.savefig('plot_globals.svg', dpi=150)event time_series(t += SERIESFREQ){
double ekin = 0., epot[dimension] = {0.}, epsilon = 0.;
foreach(reduction(+:ekin) reduction(+:epot[:dimension]) reduction(+:epsilon)){
epot[0] += dv() * rhov[] * x;
epot[1] += dv() * rhov[] * y;
#if dimension == 3
epot[2] += dv() * rhov[] * z;
#endif
foreach_dimension()
ekin += dv() * 0.5 * rhov[] * sq(u.x[]);
epsilon += dv() * 2 * (mu(f[])/rhov[]) * strain_rate_sq(point, u);
}
if (pid()==0){
FILE * fp = fopen("globals.asc", "a");
fprintf (fp, "%.9g ", t);
fprintf (fp, "%.9g ", ramp);
fprintf (fp, "%.9g ", ekin);
fprintf (fp, "%.9g ", epot[0]);
fprintf (fp, "%.9g ", epot[1]);
#if dimension == 3
fprintf (fp, "%.9g ", epot[2]);
#endif
fprintf (fp, "%.9g \n", epsilon);
fclose (fp);
}
}References
| [grea:2025] |
Benoit-Joseph Gréa, Andrés Castillo-Castellanos, Antoine Briard, Alexis Banvillet, Nicolas Lion, Catherine Canac, Kevin Dagrau, and Pauline Duhalde. Frozen waves in the inertial regime. Journal of Fluid Mechanics, 1021:A12, 2025. [ DOI | http | .pdf ] |
| [thevenin:2025] |
Sébastien Thévenin, Benoît-Joseph Gréa, Gilles Kluth, and Balasubramanya T. Nadiga. Leveraging initial conditions memory for modelling rayleigh–taylor turbulence. J. Fluid Mech., 2025. [ DOI ] |
| [thevenin:2024] |
Sébastien Thevenin. Contribution of machine learning to the modeling of turbulent mixing. Theses, Université Paris-Saclay, November 2024. [ http | .pdf ] |
| [dimonte:2004] |
Guy Dimonte. Dependence of turbulent rayleigh-taylor instability on initial perturbations. Phys. Rev. E, 2004. [ DOI ] |
| [bunner:2002] |
Bernard Bunner and Grétar Tryggvason. Dynamics of homogeneous bubbly flows part 1. rise velocity and microstructure of the bubbles. Journal of Fluid Mechanics, 466:17–52, 2002. |
