sandbox/ghigo/src/test-stl/wind-turbine-flow.c
Incompressible flow past a fixed windturbine
#include "grid/octree.h"
#include "../myembed.h"
#include "../mycentered.h"
#include "distance.h"
#include "lambda2.h"
#include "view.h"
#include "../myperfs.h"
Reference solution
#define Re (1000.) // Reynolds number, u*l/nu
#define l (100.) // Characteristic size of the windturbine
#define uref (1.) // Reference velocity
#define tref ((l)/(uref)) // Reference time, tref=l/u
Importing the geometry from an stl file
This function computes the solid and face fractions given a pointer to an STL file, an adaptation criteria (maximum relative error on distance) and a minimum and maximum level.
#define scmin (1.e-14) // Minimun volume and face fraction,
void fraction_from_stl (scalar c, face vector f, FILE * fp, double eps,
int minlevel, int maxlevel)
{
We read the STL file and compute the bounding box of the model.
coord * p = input_stl (fp);
coord min, max;
bounding_box (p, &min, &max);
double maxl = -HUGE;
foreach_dimension()
if (max.x - min.x > maxl)
maxl = max.x - min.x;
We initialize the distance field on the coarse initial mesh and refine it adaptively until the threshold error (on distance) is reached.
scalar d[];
distance (d, p);
#if TREE
while (adapt_wavelet ({d}, (double[]){eps*maxl}, maxlevel, minlevel).nf);
#endif // TREE
We also compute the volume fraction from the distance field. We first construct a vertex field interpolated from the centered field and then call the appropriate VOF functions. We have to be carefull with the orientation of the normals (- sign here).
vertex scalar phi[];
foreach_vertex()
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.;
boundary ({phi});
fractions (phi, c, f);
fractions_cleanup (c, f,
smin = (scmin), cmin = (scmin));
}
Setup
We need a field for viscosity so that the embedded boundary metric can be taken into account.
face vector muv[];
We also define a reference velocity field.
scalar un[];
We define the mesh adaptation parameters.
#define lmin (5) // Min mesh refinement level (l=5 is 3pt/l)
#define lmax (9) // Max mesh refinement level (l=9 is 50pt/l)
#define cmax (1.e-2*(uref)) // Absolute refinement criteria for the velocity field
int main ()
{
The domain is 1024\times 1024\times 1024.
L0 = 1024.;
size (L0);
origin (-(L0)/2., -(L0)/2., 0.); // Centered in x and y, z is the altitude
We set the maximum timestep.
DT = 1.e-2*(tref);
NITERMAX = 200;
We set the tolerance of the Poisson solver.
TOLERANCE = 1.e-4;
TOLERANCE_MU = 1.e-4*(uref);
We initialize the grid.
N = 1 << (lmin);
init_grid (N);
run();
}
Boundary conditions
We use inlet boundary conditions in the y-direction.
u.n[bottom] = dirichlet ((uref));
u.t[bottom] = dirichlet (0);
u.r[bottom] = dirichlet (0);
p[bottom] = neumann (0);
pf[bottom] = neumann (0);
u.n[top] = neumann (0);
u.t[top] = neumann (0);
u.r[top] = neumann (0);
p[top] = dirichlet (0);
pf[top] = dirichlet (0);
The boundary z=0 is the ground with a no-slip boundary condition.
u.n[back] = dirichlet (0);
u.t[back] = dirichlet (0);
u.r[back] = dirichlet (0);
p[back] = neumann (0);
pf[back] = neumann (0);
We give boundary conditions for the face velocity to “potentially” improve the convergence of the multigrid Poisson solver.
uf.n[left] = 0;
uf.n[right] = 0;
uf.n[bottom] = (uref);
uf.n[back] = 0;
uf.n[front] = 0;
Properties
event properties (i++)
{
foreach_face()
muv.x[] = (uref)*(l)/(Re)*fm.x[];
boundary ((scalar *) {muv});
}
Initial conditions
We set the viscosity field in the event properties.
mu = muv;
We use “third-order” face flux interpolation.
#if ORDER2
for (scalar s in {u, p, pf})
s.third = false;
#else
for (scalar s in {u, p, pf})
s.third = true;
#endif // ORDER2
We use a slope-limiter to reduce the errors made in small-cells.
#if SLOPELIMITER
for (scalar s in {u}) {
s.gradient = minmod2;
}
#endif // SLOPELIMITER
#if TREE
When using TREE and in the presence of embedded boundaries, we should also define the gradient of u at the cell center of cut-cells.
#endif // TREE
We initialize the embedded boundary. We read the stl file that contains the network geometry.
FILE * fp = fopen ("../data/wind-turbine/windTurbineGeom.stl", "r");
fraction_from_stl (cs, fs, fp, 1.e-4, (lmin), max (10, (lmax)));
fclose (fp);
#if TREE
When using TREE, we refine the mesh around the embedded boundary. Since we do not have an analytic expression for the level-set function, we perform this operation only once.
adapt_wavelet ({cs}, (double[]) {1.e-30},
maxlevel = (lmax), minlevel = (1));
fractions_cleanup (cs, fs,
smin = (scmin), cmin = (scmin));
#endif // TREE
We also define the volume fraction at the previous timestep csm1=cs.
csm1 = cs;
We define the no-slip boundary conditions for the velocity.
u.n[embed] = dirichlet (0);
u.t[embed] = dirichlet (0);
u.r[embed] = dirichlet (0);
p[embed] = neumann (0);
uf.n[embed] = dirichlet (0);
uf.t[embed] = dirichlet (0);
uf.r[embed] = dirichlet (0);
pf[embed] = neumann (0);
We initialize the velocity.
We finally initialize the reference velocity field.
foreach()
un[] = u.y[];
}
Embedded boundaries
Adaptive mesh refinement
#if TREE
event adapt (i++)
{
adapt_wavelet ({cs,u}, (double[]) {1.e-2,(cmax),(cmax),(cmax)},
maxlevel = (lmax), minlevel = (1));
We do not reset the embedded fractions to avoid interpolation errors on the geometry as we do not have an analytic expression for the level-set function.
fractions_cleanup (cs, fs,
smin = (scmin), cmin = (scmin));
}
#endif // TREE
Outputs
We look for a stationary solution.
double du = change (u.y, un);
stats nx = statsf (u.x), ny = statsf(u.y), nz = statsf (u.z), np = statsf(p);
fprintf (stderr, "%d %g %g %d %d %d %d %d %d %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %ld %g\n",
i, t/(tref), dt/(tref),
mgp.i, mgp.nrelax, mgp.minlevel,
mgu.i, mgu.nrelax, mgu.minlevel,
mgp.resb, mgp.resa,
mgu.resb, mgu.resa,
nx.sum/(nx.volume + SEPS), nx.min, nx.max,
ny.sum/(ny.volume + SEPS), ny.min, ny.max,
nz.sum/(nz.volume + SEPS), nz.min, nz.max,
np.sum/(np.volume + SEPS), np.min, np.max,
du,
grid->tn, perf.t
);
fflush (stderr);
}
Snapshots
event snapshots (t = end)
{
view (fov = 16,
quat = {0.575, -0.191, -0.261, 0.752},
ty = -0.2,
bg = {0.3,0.4,0.6},
width = 800, height = 800);
clear();
draw_vof ("cs", "fs", lw = 0.5);
save ("vof.png");
draw_vof ("cs", "fs", lw = 0.5);
squares ("u.x", n= {1,0,0}, map = cool_warm);
save ("ux.png");
draw_vof ("cs", "fs", lw = 0.5);
squares ("u.y", n= {1,0,0}, map = cool_warm);
save ("uy.png");
draw_vof ("cs", "fs", lw = 0.5);
squares ("u.y", n= {0,1,0}, map = cool_warm);
save ("uy-2.png");
draw_vof ("cs", "fs", lw = 0.5);
squares ("p", n= {1,0,0});
save ("p.png");
scalar l2[];
lambda2 (u, l2);
boundary ({l2});
draw_vof ("cs", "fs", lw = 0.5);
cells (n= {1,0,0});
squares ("u.y", n= {1,0,0}, map = cool_warm);
isosurface ("l2", -0.002);
save ("l2.png");
}
event animations (i += 10)
{
view (fov = 16,
quat = {0.575, -0.191, -0.261, 0.752},
ty = -0.2,
bg = {0.3,0.4,0.6},
width = 800, height = 800);
clear();
cells (n= {1,0,0});
save ("mesh.mp4");
draw_vof ("cs", "fs", lw = 0.5);
squares ("u.x", n= {1,0,0}, map = cool_warm);
save ("ux.mp4");
draw_vof ("cs", "fs", lw = 0.5);
squares ("u.y", n= {1,0,0}, map = cool_warm);
save ("uy.mp4");
draw_vof ("cs", "fs", lw = 0.5);
squares ("u.y", n= {0,1,0}, map = cool_warm);
save ("uy-2.mp4");
draw_vof ("cs", "fs", lw = 0.5);
squares ("p", n= {1,0,0});
save ("p.mp4");
scalar l2[];
lambda2 (u, l2);
boundary ({l2});
draw_vof ("cs", "fs", lw = 0.5);
cells (n= {1,0,0});
squares ("u.y", n= {1,0,0}, map = cool_warm);
isosurface ("l2", -0.002);
save ("l2.mp4");
}
Results
\lambda_2 criteria