sandbox/ecipriano/run/burningdroplet.c
Combustion of an axi droplet in microgravity or in any gravity conditions
This simulation file helps setting up numerical simulations of droplet combustion in axial symmetry, in any gravity conditions, and considering different kinetic schemes and chemical species.
In the following animation, the top-left panel shows the temperature field, which rises due to the ignition by a spark. This heat causes the fuel to evaporate, as reflected by the increasing fuel mass fraction in the top-right panel. When the temperature and the local concentrations of fuel and oxygen (bottom-right panel) fall within the flammability limits, combustion begins, leading to the formation of a stable envelope flame surrounding the droplet. The flame front corresponds to the region of peak temperature in the field. The elevated flame temperature further enhances fuel evaporation, which in turn sustains the combustion process while consuming the available oxygen in the surrounding environment. The flame front keeps expanding with respect to the droplet diameter.
Ignition of a microgravity droplet
#include "axi.h"
#include "navier-stokes/low-mach.h"
#define FILTERED 1
#define P_ERR 0.1
#include "two-phase-varprop.h"
#if USE_OPENSMOKE
# include "opensmoke/properties.h"
# include "opensmoke/chemistry.h"
#elif USE_CANTERA
# include "cantera/properties.h"
# include "cantera/chemistry.h"
#endif
#include "pinning.h"
#include "two-phase.h"
#include "tension.h"
#include "gravity.h"
#include "evaporation.h"
#include "spark.h"
#if OPENSMOKE
# include "opensmoke/flame.h"
#endif
#if CHT
# include "conjugate.h"
#endif
#include "defaultvars.h"
#include "maxruntime.h"
#include "view.h"Simulation setups
We define an enum with different simulation setups: in microgravity and in normal gravity conditions. We use this approach because the first configuration place the droplet on the lower-left corner of the domain, in order to exploit the axis of symmetry and to avoid non-physical lateral displacement. In normal gravity, instead, we place the droplet along the edge of the bottom boundary, and we include the droplet suspension strategy. This enum helps managing differences among the two configurations.
enum Setups { microgravity, gravity };
enum Setups setup = microgravity;Input data
We declare a structure containing the input parameters for the simulations. We are resolving not only the fluid dynamics but also the temperature field and many chemical species (we don’t know how many a-priori). Additionally, we need to include a kinetic scheme, and the rules for calculating the material properties in both the liquid and the gas phases. Since too many parameter have to be provided, we gather them in a structure of input data and we read all the parameters from an input file.
struct InputData {
char * kinetics_folder;
char * liquid_properties_folder;
int maxlevel;
double Ytol;
double Ttol;
double Utol;
double TL0;
double TG0;
double P0;
double D0;
double G;
double sigma;
double emissivity;
double rfiber_to_rdrop;
double rgas_to_rdrop;
bool combustion;
bool fick_corrected;
bool molar_diffusion;
bool mass_diffusion_enthalpy;
bool divergence;
char * liq_start;
char * gas_start;
double spark_diameter;
double spark_start;
double spark_time;
double spark_value;
double dump_every;
double movie_every;
} inputdata = {
TOSTRING(KINFOLDER),
TOSTRING(LIQFOLDER),
MAXLEVEL,
YTOL,
TTOL,
UTOL,
TEMPERATURE_DROPLET,
TEMPERATURE,
PRESSURE,
DIAMETER,
GRAVITY,
SIGMA,
EMISSIVITY,
FIBER,
ENVIRONMENT,
COMBUSTION,
true, // fick_corrected
true, // molar_diffusion
true, // mass_diffusion_enthalpy
true, // divergence
TOSTRING(LIQUID),
TOSTRING(GAS),
SPARK_DIAMETER,
SPARK_START,
SPARK_TIME,
SPARK_VALUE,
DUMP_EVERY,
MOVIE_EVERY,
};Parsing input file
We write a function which parse an input file with the simulation data, in case one wants to avoid using compiler macros. We exploit libconfig.
#if USE_LIBCONFIG
#include <libconfig.h>
#pragma autolink -lconfig
#define config_get_string(name, variable) \
config_lookup_string (&cfg, name, (const char **)&variable);
#define config_get_int(name, variable) \
config_lookup_int (&cfg, name, &variable);
#define config_get_bool(name, variable) \
config_lookup_bool (&cfg, name, (int *)&variable);
#define config_get_float(name, variable) \
config_lookup_float (&cfg, name, &variable);
#define config_get(type, name, variable) \
{ \
int success = config_get_##type(name, variable); \
if (success == CONFIG_FALSE) \
fprintf (stdout, "config: variable %s is set to default\n", #name), \
fflush (stdout); \
}
void parse_input_file (char * filename, struct InputData * inputdata) {
config_t cfg;
config_init (&cfg);
if (!config_read_file (&cfg, filename)) {
fprintf (ferr, "error:%s:%d: %s:%d - %s\n",
__FILE__, __LINE__,
config_error_file(&cfg),
config_error_line(&cfg),
config_error_text(&cfg));
config_destroy (&cfg);
abort();
}
// Get strings
config_get (string, "kinetics.kinfolder", inputdata->kinetics_folder);
config_get (string, "kinetics.liqfolder", inputdata->liquid_properties_folder);
config_get (string, "liquid.composition", inputdata->liq_start);
config_get (string, "gas.composition", inputdata->gas_start);
// Get integer values
config_get (int, "domain.maxlevel", inputdata->maxlevel);
// Get booleans
config_get (bool, "physics.combustion", inputdata->combustion);
config_get (bool, "physics.fick_corrected", inputdata->fick_corrected);
config_get (bool, "physics.molar_diffusion", inputdata->molar_diffusion);
config_get (bool, "physics.mass_diffusion_enthalpy", inputdata->mass_diffusion_enthalpy);
config_get (bool, "physics.divergence", inputdata->divergence);
// Get floating point values
config_get (float, "domain.Ytol", inputdata->Ytol);
config_get (float, "domain.Ttol", inputdata->Ttol);
config_get (float, "domain.Utol", inputdata->Utol);
config_get (float, "gas.temperature", inputdata->TG0);
config_get (float, "liquid.temperature", inputdata->TL0);
config_get (float, "gas.pressure", inputdata->P0);
config_get (float, "domain.diameter", inputdata->D0);
config_get (float, "domain.gravity", inputdata->G);
config_get (float, "domain.fiber", inputdata->rfiber_to_rdrop);
config_get (float, "domain.environment", inputdata->rgas_to_rdrop);
config_get (float, "liquid.sigma", inputdata->sigma);
config_get (float, "liquid.emissivity", inputdata->emissivity);
config_get (float, "spark.diameter", inputdata->spark_diameter);
config_get (float, "spark.start", inputdata->spark_start);
config_get (float, "spark.time", inputdata->spark_time);
config_get (float, "spark.value", inputdata->spark_value);
config_get (float, "postprocessing.dump_every", inputdata->dump_every);
config_get (float, "postprocessing.movie_every", inputdata->movie_every);
}
#endifUseful variables
We declare other useful variabiles for this simulations.
int maxlevel, minlevel = 2;
double D0, R0, R, M0, DD02 = 1., tad = 0.;
bool restored = false;
scalar qspark[];If there is conjugate heat transfer, we include a function that dissipates solid temperature due to radiation effects.
#if CHT
void radiation_fiber (scalar i, scalar e) {
double sigma_stefan_boltzmann = 5.670373e-8;
double emissivity = 1.;
double dfiber = 2.*Y0;
foreach()
if (f[] == 0.)
e[] -= 4.*sigma_stefan_boltzmann*emissivity/dfiber*
(pow (TS[], 4.) - pow (TS0, 4.))*cm[]*cw[];
}
#endifSimulation setups
Here we have the main function and the intial event which set the properties needed by the navier-stokes solver and by the phasechange model.
int main (int argc, char ** argv) {This reative simulation is likely to be longer than the walltime of
the usual supercomputers. Therefore, we use the
maxruntime() functions to write the restart
file and stop the simulation smoothly before being killed.
maxruntime (&argc, argv);We read input data from a file, if we prefer to avoid compiler macros.
#if USE_LIBCONFIG
char * inputfile = (argc > 1) ? argv[1] : "burningdroplet.input";
parse_input_file (inputfile, &inputdata);
#endifWe set the kinetics folders, for the gas and for the liquid kinetics. The liquid properties are initialized as well.
kinetics (inputdata.kinetics_folder, &NGS);
kinetics_liquid (inputdata.kinetics_folder, &NLS);
properties_liquid (inputdata.liquid_properties_folder);We specify the species names, in order to facilitate operations on the species when lage kinetic schemes are used (in this context large means more than 4 species).
gas_species = new_species_names (NGS);
liq_species = new_species_names_liquid (NLS);We set default simulation properties which will be overwritten by the variable material properties formulations. We can skip this part but WE NEED TO DEFINE NON-NULL VISCOSITY, because the two-phase-generic.h module creates the variable viscosity field only if mu is non-zero.
mu1 = mu2 = 1;
rho1 = 789.008, rho2 = 1.30161;
mu1 = 0.000523973, mu2 = 1.86309e-05;
lambda1 = 0.199406, lambda2 = 0.0264842;
cp1 = 2544.9, cp2 = 1011.42;
Dmix1 = 0., Dmix2 = 1.e-5;
dhev = 1.1714e+06;We set the initial thermodynamic state of the two-phase system (SI units).
Pref = inputdata.P0*101325.;
TG0 = inputdata.TG0;
TL0 = inputdata.TL0;We set the thermal properties of the solid suspender, together with source/sink functions.
#if CHT
TS0 = min (TL0, TG0);
solid_properties (QUARTZ);
energy_sources = radiation_fiber;
#endifWe use a two-velocities model, and we set additional options for the
phase change model pcm.
nv = 2;
pcm.divergence = inputdata.divergence;
pcm.chemistry = inputdata.combustion;
pcm.fick_corrected = inputdata.fick_corrected;
pcm.molar_diffusion = inputdata.molar_diffusion;
pcm.mass_diffusion_enthalpy = inputdata.mass_diffusion_enthalpy;
pcm.emissivity = inputdata.emissivity;We change the dimensions of the domain according with the simulation setup used.
double RR = inputdata.rgas_to_rdrop;
D0 = inputdata.D0;
L0 = 0.5*RR*D0;
maxlevel = inputdata.maxlevel;Depending on the gravity value, we change the simulation setup.
G.x = inputdata.G;
setup = G.x ? gravity : microgravity;
if (setup == gravity) {
double df = inputdata.rfiber_to_rdrop*D0;
X0 = -0.5*L0, Y0 = 0.5*df;
pinning.ap = sqrt (sq (0.5*D0) - sq (Y0));
pinning.ac = pinning.ap - 2.*L0/(1 << maxlevel);
}
else {
pinning.ap = 1e10;
pinning.ac = 1e10;
pinning_warnings = false;
}We set the surface tension and additional tracers if needed.
f.sigma = inputdata.sigma;Setting up the grid and the pinning point if the setup requires droplet suspension. Finally, we run the simulation.
We de-allocate memory used for the vectors of species names.
free_species_names (NGS, gas_species), gas_species = NULL;
free_species_names (NLS, liq_species), liq_species = NULL;
}
macro number circle (double x, double y, double R,
double x0 = 0., double y0 = 0.)
{
return (sq(R) - sq(x - x0) - sq(y - y0));
}
event init (i = 0) {
if (!restore (file = "restart")) {
#if TREE
# if CHT
refine (circle (x, y, 4.*D0) > 0. && (y <= 3.*Y0) && level < maxlevel);
# else
refine (circle (x, y, 4.*D0) > 0. && level < maxlevel);
# endif // CHT
#endif
fraction (f, circle (x, y, 0.5*D0));We compute initial variables, useful for post-processing.
if (setup == gravity)
R = R0 = pow(3./4./pi*(2.*pi*statsf(f).sum), 1./3.);
else
R = R0 = pow (3.*statsf(f).sum, 1./3.);
scalar rhol = liq->rho;
M0 = 0.;
foreach (reduction(+:M0))
M0 += rhol[]*f[]*dv();
}
else
restored = true;We set the initial thermodynamic state of the phases, and we
overwrite some of their properties. The function
phase_set_thermo_state sets the initial conditions of
temperature, pressure, and mass fraction fields only if they are
uniform. Therefore, if restore was called, the function
should not overwrite the fields. However, it is still important to call
it in order to store the initial thermo state
ThermoState tsl, tsg;
tsl.T = TL0, tsl.P = Pref, tsl.x = NULL;
tsg.T = TG0, tsg.P = Pref, tsg.x = NULL;
phase_set_thermo_state (liq, &tsl, force = !restored);
phase_set_thermo_state (gas, &tsg, force = !restored);
phase_set_composition_from_string (liq, inputdata.liq_start,
sep = "_", force = !restored);
phase_set_composition_from_string (gas, inputdata.gas_start,
sep = "_", force = !restored);
#if RADIAL_PROFILE
if (!restored) {
scalar TL = liq->T, TG = gas->T;
foreach() {
double r = sqrt (sq (x) + sq (y));
TG[] = radialprofile (r, 0.5*D0, 2.*D0, TL0, TG0)*(1. - f[]);
TL[] = TL0*f[];
T[] = TL[] + TG[];
}
}
#endifThe only property that we need to set, and that remains constant throughout the simulation, is the vector with the molecular weight of each species.
double MWLs[NLS], MWGs[NGS];
molecular_weights (NGS, MWGs);
foreach_species_in (liq)
MWLs[i] = MWGs[LSI[i]];
phase_set_properties (liq, MWs = MWLs);
phase_set_properties (gas, MWs = MWGs);
scalar fuel = liq->YList[0];
fuel.antoine = antoine_methanol;
#if OPENSMOKE
antoine = &opensmoke_antoine;
#else
//tp1.rhov = liqprop_density_heptane;
#endifWe set the spark properties.
#if SPARK
spark.T = qspark;
spark.position = (setup == gravity) ? (coord){0., 1.2*D0}
: (coord){0.75*D0, 0.75*D0};
spark.diameter = inputdata.spark_diameter*D0;
spark.time = inputdata.spark_start;
spark.duration = inputdata.spark_time;
spark.temperature = inputdata.spark_value;
spark.phase = gas;
#endif
#if 0
print_thermoprop (&tp1, liq->ts0, liq->n, stderr);
print_thermoprop (&tp2, gas->ts0, gas->n, stderr);
#endifWe define the solid geometry and set the boundary conditions for temperature when CHT is present.
#if CHT
{
solid (cw, fw, (x >= 0.) && y <= 2.*Y0);
scalar TL = liq->T, TG = gas->T;
TS[bottom] = dirichlet (TSB[]);
TL[bottom] = (x >= 0.) ? dirichlet (TLB[]) : neumann (0.);
TG[bottom] = (x >= 0.) ? dirichlet (TGB[]) : neumann (0.);
}
#endif
}Boundary conditions
We set the boundary conditions in the defaults event
because, in navier-stokes/centered-new.h the
boundary conditions for the additional velocity fields are copied from u
and p.
event defaults (i = 0) {
u.n[top] = neumann (0.);
u.t[top] = neumann (0.);
p[top] = dirichlet (0.);
u.n[right] = neumann (0.);
u.t[right] = neumann (0.);
p[right] = dirichlet (0.);
if (setup == gravity) {
u.n[left] = neumann (0.);
u.t[left] = neumann (0.);
p[left] = dirichlet (0.);
u.n[bottom] = dirichlet (0.);
u.t[bottom] = dirichlet (0.);
uf.n[bottom] = 0.;
uf.t[bottom] = 0.;
}
}Mesh adaptation
We adapt the grid according to the fuel mass fraction, the temperature, and the velocity field.
#if TREE
event adapt (i++) {
double Ytol = inputdata.Ytol;
double Ttol = inputdata.Ttol;
double Utol = inputdata.Utol;
adapt_wavelet_leave_interface ({Y,T,u.x,u.y}, {f},
(double[]){Ytol,Ttol,Utol,Utol}, maxlevel, minlevel, 1);
if (setup == gravity)
unrefine (x >= (0.45*L0));
}
#endif
#if 0
double CFL_MAX = 0.5;
event stability (i++) {
if (CFL > CFL_MAX)
CFL = CFL_MAX;
}
#endifLogger
We output the dimensionless droplet radius.
event logger (t += 0.001) {
double R = pow (3./4./pi*(2.*pi*statsf(f).sum), 1./3.);
fprintf (stderr, "%d %g %.5g\n", i, t, R / (0.5*D0));
}Output Files
We write on a file interesting average properties in time, such as the droplet diameter, surface, mass, interface temperature and mass fractions.
event output_data (i += 50) {
if (setup == gravity)
R = pow(3./4./pi*(2.*pi*statsf(f).sum), 1./3.);
else
R = pow (3.*statsf(f).sum, 1./3.);
DD02 = sq (R/R0);
tad = t/sq (D0*1e3);
double TIntAvg = avg_interface (gas_int->T, f, tol = 0.1);
int counter = 0.;
double TDropAvg = 0.;
scalar TL = liq->T;
foreach(reduction(+:TDropAvg) reduction(+:counter)) {
if (f[] > 1.-F_ERR) {
counter++;
TDropAvg += TL[];
}
}
TDropAvg = (counter > 0.) ? TDropAvg/counter : 0.;
char name[80];
sprintf (name, "OutputData-%d", maxlevel);
static FILE * fp = fopen (name, "w");
fprintf (fp, "%g %g %g %g %g %g %g", t, tad, R0, R, DD02,
TIntAvg, TDropAvg);
foreach_species_in (liq_int) {
scalar YGInt = gas_int->YList[LSI[i]];
double YGIntAvg = avg_interface (YGInt, f, tol = 0.1);
fprintf (fp, " %g", YGIntAvg);
}
fprintf (fp, "\n"), fflush (fp);
}Snapshots
We write periodic sanpshots of the simulation in order to restart the simulation if needed, or for post-processing.
#if DUMP
event snapshots (t += inputdata.dump_every) {
if (i > 1) {
char name[80];
sprintf (name, "snapshots-%g", t);
dump (name);
}
}
#endifMovie
We write the animation with the evolution of the fuel mass fraction, the temperature field, the interface position and the flame front.
#if BASILISK_SERVER
event movie (t += inputdata.movie_every) {
if (setup == microgravity) {
clear();
view (fov = 4);
draw_vof ("f", lw = 1.5);
squares ("Y", min = 0., max = 1., linear = true);
//isoline ("zmix - zsto", lw = 1.5, lc = {1.,1.,1.});
mirror ({0,1}) {
draw_vof ("f", lw = 1.5);
squares ("YGO2", min = 0., max = 0.21, linear = true);
//isoline ("zmix - zsto", lw = 1.5, lc = {1.,1.,1.});
}
mirror ({1,0}) {
draw_vof ("f", lw = 1.5);
squares ("T", spread=-1, linear = true);
//isoline ("zmix - zsto", lw = 1.5, lc = {1.,1.,1.});
mirror ({0,1}) {
cells();
draw_vof ("f", lw = 1.5);
//isoline ("zmix - zsto", lw = 1.5, lc = {1.,1.,1.});
}
}
save ("movie.mp4");
}
}
#endifTracing
Check the distribution of the computational time among the different events and functions.
#if TRACE > 1
event profiling (i += 20) {
char name[80];
sprintf (name, "profiling-%d", maxlevel);
static FILE * fp = fopen (name, "w");
trace_print (fp, 1);
}
#endifStopping conditions
We assume that in 50 s the droplet is fully consumed. Alternatively, we stop the simulation when the droplet is almost fully consumed, i.e. when the square diameter decay is small: (D/D_0)^2 < 0.005.
