sandbox/vheusinkveld/fan.c
Intro
This is a minimal example for implementing a ‘fan’ after geometry examples that can be found here. The mentioned function is updated to be applicable for general phi and theta. Also this script adds a velocity based on a stated fan power. All functions can be found at the end of this file.
Power to velocity forcing and expected exit velocity
The implementation is based on a momentum forcing. An amount of work per time is done on the flow with a certain power, P. Using the work-energy theorem, which states that the work done by all acting forces on a particle is equal the change in kinetic energy, this can be expressed as \Delta E_{kin}=P\Delta t. This directly implies that,
\displaystyle \vec{v}^2_{t+\Delta t} - \vec{v}^2_{t} = \frac{2\textrm{P}\Delta t}{m}\hat{n}^2
which in turn implies for every spatial dimension,
\displaystyle v^2_{i,t+\Delta t} - v^2_{i,t} = \frac{2\textrm{P}\Delta t}{m} n_i^2
From these equations the direction of \hat{n} is not yet defined and |\hat{n}|=1. When it is assumed that the force applied to the flow is normal to the plane in which the fan blades rotate then \hat{n} is the normal vector of this plane. Rearranging and taking the square root results in, \displaystyle \vec{v}_{i,t+\Delta t} = \sqrt{\vec{v}^2_{i,t} + \frac{2\textrm{P}\Delta t}{m}\hat{n}^2_i }
To estimate the velocity to which air is accelerated by this forcing we assume that it is applied over a finite time from t_{start} to t_{end}, moving a distance w in the process: \displaystyle w=\int_{t_{start}}^{t_{end}} v\left(t\right)\textrm{dt} Assuming a zero entrance velocity, v is given at every instance as: \displaystyle \textrm{E}_{\textrm{kin}}=\textrm{P}\Delta t=0.5mv^2 \displaystyle v\left(t\right)=\sqrt{2\textrm{P}t/m} P, w and m are set by the user thus this leaves two equations and two unknowns. Solving for the exit velocity gives: \displaystyle v_{end} = \left(\frac{3\textrm{P}w}{m}\right)^{1/3}
The code
First we include relevant files for 2D navier stokes solving and geometric fun.
//#include "grid/octree.h" // For 3D --> to demanding for server
#include "navier-stokes/centered.h" // Navier stokes
#include "fractions.h" // Needed to compute fan fractions
Required functions and structures
void init_rotor();
void rotor_update();
void rotor_coord();
void rotor_forcing();
struct sRotor {
double rampT; // Time to start up rotor
double P, Prho; // Power, powerdensity
double R, W, A, V; // Diameter, Thickness, Area ,Volume
double diaVol; // Diagnosed rotor volume
double x0, y0, z0; // Origin of rotor
double xt, yt, zt; // Translation of origin per rotate event
double theta, phi; // Polar and Azimuthal angle
double thetat, phit; // Addition to angles per rotate event
double Work; // Work done by rotor
double cu; // Characteristic velocity
bool rotate; // Rotation yes or no
coord nf, nr; // Normal vector fan, rotation
};
Some global vars
int minlevel, maxlevel; // Grid depths
double eps; // Adaptivity criterion
struct sRotor rot; // Rotor details structure
scalar fan[]; // Fan volume fraction
scalar lev[]; // for movie diagnostic level
scalar Ekin[]; // for movie kinetic energy
Starting with the most loved c function
int main() {
minlevel = 3;
maxlevel = 8;
L0 = 10.;
X0 = Y0 = Z0 = 0.;
init_grid(1<<7);
foreach_dimension(){
periodic (left);
u.x.refine = refine_linear; // Momentum conserved
}
fan.prolongation = fraction_refine; // Fan is a volume fraction
p.refine = p.prolongation = refine_linear;
DT = 10E-5; // For poisson solver
TOLERANCE=10E-6; // For poisson solver
CFL = 0.5; // CFL condition
run(); // Start simulation
}
Initialisation
event init(t = 0){
rot.rotate = true; // Turn on rotation
init_rotor(); // See init function for details
fan.prolongation = fraction_refine; // Tell basilisk that fan is volumetric field
refine (fan[] > 0. && level < maxlevel); // Refine where fan is
eps = 0.07*rot.cu; // Wavelet estimator based on flow velocity
}
Reset to defaults for poisson solver
event reset_pois(i = 10){
DT = 1;
TOLERANCE = 10E-3;
}
Adaptivity
Forcing by the rotor
event forcing(i = 1; i++) {
rotor_coord();
rotor_forcing();
}
Rotate the rotor
event rotate(t+=0.1) {
if(rot.rotate) {
// Change center
rot.x0 += rot.xt;
rot.y0 += rot.yt;
rot.z0 += rot.zt;
// Change angles
rot.theta += rot.thetat;
rot.phi += rot.phit;
rotor_update();
}
}
Visual output
event movies(t+=1){
fprintf(stderr, "t=%g\n", t);
foreach(){
lev[] = level;
Ekin[] = sqrt(sq(u.y[]) + sq(u.x[]));
}
boundary({lev, Ekin});
output_ppm (lev, file = "ppm2mp4 level.mp4", n = 512, linear = false, min = minlevel, max = maxlevel);
output_ppm (Ekin, file = "ppm2mp4 ekin.mp4", n = 512, linear = true);
}
End the simulation
Results
Functions
Function returning the sRotor structure, includign default properties
void init_rotor() {
rot.Work = 0.;
if(!rot.rampT)
rot.rampT = 1.; // fan at full thrust after 1 second, linear ramp
if(!rot.R)
rot.R = L0/80.;
if(!rot.W)
rot.W = rot.R/5;
if(!rot.Prho)
rot.Prho = L0/10.;
if(!rot.x0)
rot.x0 = L0/2.;
if(!rot.y0)
rot.y0 = L0/2.;
if(!rot.z0){
#if dimension == 2
rot.z0 = 0.;
#elif dimension == 3
rot.z0 = L0/2.;
#endif
}
if(!rot.theta)
rot.theta = 90.*M_PI/180.; // Polar angle
if(!rot.phi)
rot.phi = 90.*M_PI/180.; // Azimuthal angle
if(rot.rotate) {
// determines for every rotate event how much needs to be added to x, y, z, tehta, phi.
rot.xt = 0;
rot.yt = 0;
rot.zt = 0;
rot.thetat = 0.;
rot.phit = -0.1*M_PI/180.; // note that this rate is linked to the frequency of the rotate event
} else {
rot.xt = 0;
rot.yt = 0;
rot.zt = 0;
rot.thetat = 0.;
rot.phit = 0.;
}
rotor_update();
}
Updating relevant rotor variables
void rotor_update() {
// Normal vectors to the fan plane, note that z and y are turned around for consistency regarding polar coordinates (basilisk uses y for the vertical)
rot.nf.x = sin(rot.theta)*cos(rot.phi);
rot.nf.y = sin(rot.theta)*sin(rot.phi);
rot.nf.z = cos(rot.theta);
rot.nr.x = sin(rot.theta)*cos(rot.phi);
rot.nr.y = sin(rot.theta)*sin(rot.phi);
rot.nr.z = cos(rot.theta);
#if dimension == 2
rot.A = 2*rot.R*rot.W;
#elif dimension == 3
rot.A = sq(rot.R)*M_PI;
#endif
// Volume is slice of a sphere
#if dimension == 2
rot.V = 1.*rot.A;
#elif dimension == 3
rot.V = 4.*M_PI*pow(rot.R,3.)/3. -
2*M_PI*pow(rot.R-rot.W/2., 2.)/3.*(2*rot.R + rot.W/2.);
#endif
rot.P = rot.V*rot.Prho;
rot.cu = pow(3*rot.Prho*rot.W, 1./3.);
}
Function returning the volume fractions of a fan object
void rotor_coord() {
scalar sph[], plnu[], plnd[];
fraction(sph, -sq((x - rot.x0)) - sq((y - rot.y0)) - sq((z - rot.z0)) + sq(rot.R)); // sphere
fraction(plnu, rot.nr.x*(x - rot.x0) + rot.nr.y*(y - rot.y0) + rot.nr.z*(z - rot.z0) + rot.W/2.); // upper plane
fraction(plnd, -rot.nr.x*(x - rot.x0) - rot.nr.y*(y - rot.y0) - rot.nr.z*(z - rot.z0) + rot.W/2.); // lower plane
foreach () {
fan[] = sph[]*plnu[]*plnd[]; // estimate for the overlappnig volumes, not that this is not exact at the boundaries of the 'fan', this is corrected for later in the focring by comparing diagnosed to theoretical volume.
}
boundary({fan}); // update boundaries
}
Function returning new velocities based on a rotor forcing. This is a function of powerdensity, width, direction, ramp-time, and diagnosed volume
void rotor_forcing(){
double tempW = 0.;
double w, wsgn, damp, usgn, utemp, corrP;
foreach(reduction(+:tempW)) {
if(fan[] > 0.) {
foreach_dimension() {
wsgn = sign(rot.nf.x*u.x[]) + (sign(rot.nf.x*u.x[]) == 0)*sign(rot.nf.x); // Get the direction of the flow relative to forcing direction
damp = rot.rampT > t ? t/rot.rampT : 1.; // Linear ramp for starting up the forcing
corrP = rot.diaVol > 0. ? rot.V/rot.diaVol : 1.; // Correction if diagnosed volume differs from theoretical volume
w = wsgn*fan[]*damp*sq(rot.nf.x)*(2./rho[])*(corrP*rot.P/rot.V)*dt; // additional kinetic energy
tempW += 0.5*rho[]*w*dv(); // save work done to asses total work in diagnostics
// New kinetic energy
utemp = sq(u.x[]) + w;
// New sign of the velocity
usgn = 1.*(u.x[] >= 0)*(utemp > 0) +
-1.*(u.x[] >= 0)*(utemp < 0) +
1.*(u.x[] < 0)*(utemp < 0) +
-1.*(u.x[] < 0)*(utemp > 0);
// New velocity
u.x[] = usgn*sqrt(fabs(utemp));
}
}
}
rot.Work += tempW; // Diagnostics
}