sandbox/acastillo/filaments/input_fields/filaments.h
- Curved vortex filaments
- Representation of vortex filaments in Basilisk
- References
#ifndef BASILISK_HEADER_4
#define BASILISK_HEADER_4
#line 1 "./../../input_fields/filaments.h"
Curved vortex filaments
This section borrows from the development for a general curved vortex presented by Callegari and Ting (1978) and notes from the book by Maurice Rossi, Stéphane Le Dizès, and others.

Let us define a curvilinear orthogonal basis associated with a reference space-curve \mathcal{C}(s,t). Here s is the arc-length along the initial curve measured from a given reference point.
Each point along \mathcal{C}(s,t) is characterized by its position vector \boldsymbol{x}_c, local curvature \kappa(s,t), local torsion \tau(s,t), and the corresponding Frenet–Serret frame composed of three vectors (\boldsymbol{\hat{t}}, \boldsymbol{\hat{n}}, \boldsymbol{\hat{b}}) \begin{aligned} \boldsymbol{\hat{t}}(s) \equiv \frac{d\boldsymbol{x}_c}{ds}, \quad \boldsymbol{\hat{n}}(s) \equiv \frac{1}{\kappa}\frac{d\boldsymbol{\hat{T}}}{ds}, \quad \boldsymbol{\hat{b}}(s) \equiv \boldsymbol{\hat{T}}\times\boldsymbol{\hat{N}} \end{aligned} which are related to one another as: \begin{aligned} \frac{d}{ds} \begin{bmatrix} \boldsymbol{\hat{t}} \\ \boldsymbol{\hat{n}} \\ \boldsymbol{\hat{b}} \end{bmatrix} = \begin{bmatrix} 0 & \kappa(s) & 0 \\ -\kappa(s) & 0 & \tau(s) \\ 0 & -\tau(s) & 0 \end{bmatrix} \begin{bmatrix} \boldsymbol{\hat{t}} \\ \boldsymbol{\hat{n}} \\ \boldsymbol{\hat{b}} \end{bmatrix} \end{aligned}
The position of a given point M can be expressed as two coordininates in the plane \mathcal{A}(s,t) locally perpendicular to \mathcal{C}(s,t) and one coordinate s along \mathcal{C}(s,t). This way, we may introduce a local radial and angular coordinates (\rho,\varphi) and write the position vector of a point M as: \begin{aligned} \boldsymbol{x} = \boldsymbol{x}_c (s) + \rho\boldsymbol{e}_\rho(s,\varphi) \end{aligned} where the unit vectors \boldsymbol{e}_\rho and \boldsymbol{e}_\varphi are defined as \begin{aligned} \boldsymbol{\hat{e}}_\rho &=& \cos\varphi\boldsymbol{\hat{n}} + \sin\varphi\boldsymbol{\hat{b}} \\ \boldsymbol{\hat{e}}_\varphi &=& -\sin\varphi\boldsymbol{\hat{n}} + \cos\varphi\boldsymbol{\hat{b}} \end{aligned}
Note that (\rho,\varphi,s) are curvilinear coordinates but they are not orthogonal in general. However, if we define an angle \theta \begin{aligned} \theta = \varphi - \theta_0(s), \quad \text{ where } \quad \frac{d\theta_0}{ds} = -\tau(s) \end{aligned} then (\rho,\theta,s) form a set of curvilinear orthogonal coordinates. In this system, the infinitesimal increment reads as \begin{aligned} d\boldsymbol{x} &=& d\rho ~\boldsymbol{\hat{e}}_\rho(s,\varphi) + \rho d\varphi ~\boldsymbol{\hat{e}}_\varphi(s,\varphi) + h_s ds ~\boldsymbol{\hat{T}}(s) \end{aligned} where \begin{aligned} h_s = (1-\kappa(s)\rho\cos(\theta+\theta_0(s))) \end{aligned}
Representation of vortex filaments in Basilisk
vortex_filament: defines the properties of a vortex filament
This structure represents a vortex filament. It includes properties such as the number of segments, a scalar quantity, and various vectors representing the geometry and orientation of the filament.
- nseg
- number of segments
- t
- arbitrary of parameter, usually \xi_n
- C
- Cartesian coordinates describing the curve, \boldsymbol{x}_c \in \mathcal{C}(s(\xi_n))
- Tvec
- tangent unit vector, \boldsymbol{\hat{T}}(s(\xi_n))
- Nvec
- normal unit vector, \boldsymbol{\hat{N}}(s(\xi_n))
- Bvec
- binormal unit vector, \boldsymbol{\hat{B}}(s(\xi_n))
- s
- arclenght coordinate, s(\xi_n) or \ell(\xi_n)
- pcar
- placeholder for a Cartesian coordinate
- sigma
- local stretching factor, \sigma(s(\xi_n)) (optional)
- kappa
- local curvature, \kappa(s(\xi_n)) (optional)
- tau
- local torsion, \tau(s(\xi_n)) (optional)
- theta0
- cumulative torsion, \varphi_0(s(\xi_n)) (optional)
#include "PointTriangle.h"
struct vortex_filament{
int nseg; // number of segments
double* phi; // arbitrary parametrisation of C
* C; // cartesian coords of filament
coord* Tvec; // unit tangent vector
coord* Nvec; // unit normal vector
coord* Bvec; // unit binormal vector
coorddouble* s; // arc-length coordinate
; // current point in Cartesian coordinates
coord pcardouble* sigma; // Stretching factor
double* kappa; // Curvature
double* tau; // Torsion
double* theta0; // Cumulative torsion
double* a; // Core size
* dC; //
coord* d2C; //
coord* d3C; //
coord* Ulocal; //
coord* Uauto; //
coord* Umutual; //
coord* U; //
coord* Uprev; //
coorddouble* vol; // Initial volume
};
// Function to allocate memory for the *members* of a vortex_filament struct
// Assumes the struct itself has already been created (e.g., on the stack or
// heap)
void allocate_vortex_filament_members(struct vortex_filament* filament, int nseg) {
if (filament == NULL) {
("Failed to allocate memory for filament");
perrorreturn ;
}
->nseg = nseg;
filament->pcar = (coord) {0.0, 0.0, 0.0};
filament
// Allocate memory for the double arrays
->phi = malloc(nseg * sizeof(double));
filament->s = malloc(nseg * sizeof(double));
filament->sigma = malloc(nseg * sizeof(double));
filament->kappa = malloc(nseg * sizeof(double));
filament->tau = malloc(nseg * sizeof(double));
filament->a = malloc(nseg * sizeof(double));
filament->vol = malloc(nseg * sizeof(double));
filament->theta0 = malloc(nseg * sizeof(double));
filament
// Allocate memory for the coord arrays
->C = malloc(nseg * sizeof(coord));
filament->dC = malloc(nseg * sizeof(coord));
filament->d2C = malloc(nseg * sizeof(coord));
filament->d3C = malloc(nseg * sizeof(coord));
filament->Tvec = malloc(nseg * sizeof(coord));
filament->Nvec = malloc(nseg * sizeof(coord));
filament->Bvec = malloc(nseg * sizeof(coord));
filament
->Ulocal = malloc(nseg * sizeof(coord));
filament->Uauto = malloc(nseg * sizeof(coord));
filament->Umutual = malloc(nseg * sizeof(coord));
filament->U = malloc(nseg * sizeof(coord));
filament->Uprev = malloc(nseg * sizeof(coord));
filament}
// Function to free memory for the *members* of a vortex_filament struct
// Assumes the struct itself will NOT be freed by this function
void free_vortex_filament_members(struct vortex_filament* filament) {
if (filament == NULL) {
// Nothing to free
return;
}
// Free the double arrays
free(filament->phi); filament->phi = NULL; // Set to NULL after freeing
free(filament->s); filament->s = NULL;
free(filament->sigma); filament->sigma = NULL;
free(filament->kappa); filament->kappa = NULL;
free(filament->tau); filament->tau = NULL;
free(filament->a); filament->a = NULL;
free(filament->vol); filament->vol = NULL;
free(filament->theta0); filament->theta0 = NULL;
// Free the coord arrays
free(filament->C); filament->C = NULL;
free(filament->dC); filament->dC = NULL;
free(filament->d2C); filament->d2C = NULL;
free(filament->d3C); filament->d3C = NULL;
free(filament->Tvec); filament->Tvec = NULL;
free(filament->Nvec); filament->Nvec = NULL;
free(filament->Bvec); filament->Bvec = NULL;
free(filament->Ulocal); filament->Ulocal = NULL;
free(filament->Uauto); filament->Uauto = NULL;
free(filament->Umutual); filament->Umutual = NULL;
free(filament->U); filament->U = NULL;
free(filament->Uprev); filament->Uprev = NULL;
}
struct local_filament{
bool near; // flag based on distance from C
double phi; // arbitrary parametrisation of C
; // cartesian coords of filament
coord C; // unit tangent vector
coord Tvec; // unit normal vector
coord Nvec; // unit binormal vector
coord Bvecdouble s; // arc-length coordinate
; // current point in Cartesian coordinates
coord pcardouble sigma; // Stretching factor
double kappa; // Curvature
double tau; // Torsion
double theta0; // Cumulative torsion
double a; // Core size
; //
coord Mcar; //
coord Mraddouble rho; //
double theta; //
};
Interpolate Values Using Cubic Splines
gsl_interp1d_vector(): performs 1D interpolation on a vector using cubic splines
This function performs 1D interpolation on a vector using cubic
splines from the GNU Scientific Library (GSL). It takes discretized
coord values V0
at given spatial coordinate
phi0
and interpolates to find the corresponding value at a
coordinate phiq
.
The arguments and their descriptions are:
- nseg
- number of segments
- phi0
- array with the coordinates \phi_0
- V0
- array with the coord values V_0
- phiq
- target value at which to interpolate V_q
#pragma autolink -lgsl -lgslcblas
#include <gsl/gsl_spline.h>
gsl_interp1d_vector( int nseg, double* phi0, coord * V0, double phiq){
coord ;
coord Vq*acc = gsl_interp_accel_alloc();
gsl_interp_accel
foreach_dimension(){
double *V_x;
= malloc(sizeof(double)*nseg);
V_x for (int i = 0; i < nseg; i++)
[i] = V0[i].x;
V_x
*spline_x = gsl_spline_alloc(gsl_interp_cspline, nseg);
gsl_spline (spline_x, phi0, V_x, nseg);
gsl_spline_init.x = gsl_spline_eval (spline_x, phiq, acc);
Vq(spline_x);
gsl_spline_free free(V_x);
}
(acc);
gsl_interp_accel_free return Vq;
}
gsl_interp1d_scalar(): performs 1D interpolation on a scalar using cubic splines
This function performs 1D interpolation on a scalar using cubic
splines from the GNU Scientific Library (GSL). It takes discretized
scalar values P0
at given spatial coordinate
phi0
and interpolates to find the corresponding value at a
coordinate phiq
.
The arguments and their descriptions are:
- nseg
- number of segments
- phi0
- array with the coordinates \phi_0
- P0
- array with the scalar values P_0
- phiq
- target value at which to interpolate P_q
double gsl_interp1d_scalar( int nseg, double* phi0, double * P0, double phiq){
double Pq;
*acc = gsl_interp_accel_alloc();
gsl_interp_accel *spline_x = gsl_spline_alloc(gsl_interp_cspline, nseg);
gsl_spline (spline_x, phi0, P0, nseg);
gsl_spline_init= gsl_spline_eval (spline_x, phiq, acc);
Pq (spline_x);
gsl_spline_free (acc);
gsl_interp_accel_free return Pq;
}
Project a Point onto the Frenet Frame of a Vortex Filament
frenet_projection(): projects a point onto the Frenet frame
This function calculates the projection of a point M onto the Frenet-Serret frame of a vortex filament at a specified position s(\xi_q). The Frenet frame (\boldsymbol{\hat{T}}, \boldsymbol{\hat{N}}, \boldsymbol{\hat{B}}) is interpolated at this position.
The function returns the dot product of the vector from the origin \boldsymbol{O}(s(\xi_q)) to the point \boldsymbol{M} with the tangent vector \boldsymbol{\hat{T}}(s(\xi_q)):
(\boldsymbol{M} - \boldsymbol{O}(s(\xi_q))) \cdot \boldsymbol{\hat{T}}(s(\xi_q)).
If the point \boldsymbol{M} lies within the plane \mathcal{A}(s(\xi_q)), the output of this function will be zero. This property will be utilized as the objective function to minimize when projecting points onto the Frenet-Serret frame.
The arguments and their descriptions are:
- tq
- double representing the position along the vortex filament where the projection is to be computed.
- params
-
pointer to a
struct vortex_filament
, which contains the properties of the vortex filament, including the number of segments, coordinates, and Frenet frame vectors.
double frenet_projection (double phiq, void *params){
struct vortex_filament *p = (struct vortex_filament *) params;
, frenet[3];
coord ccar= gsl_interp1d_vector( p->nseg, p->phi, p->C, phiq);
ccar
[0] = gsl_interp1d_vector( p->nseg, p->phi, p->Tvec, phiq);
frenet[1] = gsl_interp1d_vector( p->nseg, p->phi, p->Nvec, phiq);
frenet[2] = gsl_interp1d_vector( p->nseg, p->phi, p->Bvec, phiq);
frenet
return vecdot(vecdiff(p->pcar, ccar), frenet[0]);
}
frenet_projection_min(): find the position with the minimum projection on the Frenet frame
This function solves a minimization problem to find the position
along a vortex filament where the projection of a point
pcar
onto the tangent vector of the Frenet-Serret frame is
minimized. In other words, we search for \xi_q such that :
(\boldsymbol{M} - \boldsymbol{O}(s(\xi_q))) \cdot
\boldsymbol{\hat{T}}(s(\xi_q)) = 0
It uses the Brent method from the GNU Scientific Library (GSL) to find the root of the projection function within a specified interval.
The arguments and their descriptions are:
- params
-
pointer to a
struct vortex_filament
, which contains the properties of the vortex filament, including the number of segments, coordinates, and Frenet frame vectors. - r
- an initial guess for \xi_q
#include <gsl/gsl_roots.h>
#include <gsl/gsl_errno.h>
double frenet_projection_min(struct vortex_filament params, double r) {
int status, verbose = 0;
int iter = 0, max_iter = 100;
const gsl_root_fsolver_type *T;
*s;
gsl_root_fsolver
double x_lo = r - 0.25, x_hi = r + 0.25;
;
gsl_function F
.function = &frenet_projection;
F.params = ¶ms;
F
= gsl_root_fsolver_brent;
T = gsl_root_fsolver_alloc (T);
s ();
gsl_set_error_handler_off(s, &F, x_lo, x_hi);
gsl_root_fsolver_set
if (verbose == 1) {
printf ("using %s method\n", gsl_root_fsolver_name (s));
printf ("%5s [%9s, %9s] %9s %10s %9s\n", "iter", "lower", "upper", "root", "err", "err(est)");
}
do {
++;
iter= gsl_root_fsolver_iterate (s);
status = gsl_root_fsolver_root (s);
r = gsl_root_fsolver_x_lower (s);
x_lo = gsl_root_fsolver_x_upper (s);
x_hi = gsl_root_test_interval (x_lo, x_hi, 0, 1e-8);
status
if ((status == GSL_SUCCESS) && (verbose == 1)){
printf ("Converged:\n");
printf ("%5d [%.7f, %.7f] %.7f %.7f\n", iter, x_lo, x_hi, r, x_hi - x_lo);
}
}
while (status == GSL_CONTINUE && iter < max_iter);
(s);
gsl_root_fsolver_free
return r;
}
get_local_coordinates(): computes the coordinates in the local frame
This function computes the local coordinates required for the vorticity field. Each point M is projected into the local Frenet-Serret frame to obtain a set of local coordinates, such that:
\begin{aligned} \textit{i)} \quad\quad & (\boldsymbol{M} - \boldsymbol{O}(s(\xi_q))) \cdot \boldsymbol{\hat{T}}(s(\xi_q)) = 0 \\ \textit{ii)} \quad\quad & (\boldsymbol{M} - \boldsymbol{O}(s(\xi_q))) \cdot \boldsymbol{\hat{N}}(s(\xi_q)) = x_N \\ \textit{iii)} \quad\quad & (\boldsymbol{M} - \boldsymbol{O}(s(\xi_q))) \cdot \boldsymbol{\hat{B}}(s(\xi_q)) = x_B \end{aligned}
This requires finding the value of s(\xi_q) along each space curve that verifies i) through a minization process.
Then, we use the local coordinates (x_N, x_B) to define a local radial and angular coordinates: \begin{aligned} x_\rho = \sqrt{x_N^2 + x_B^2}, && x_\phi = \arctan(x_B,x_N) \end{aligned}
struct local_filament get_local_coordinates(int spatial_period, double max_distance=L0, struct vortex_filament *vortex) {
struct local_filament local_coordinates = {0};
, radial_coord;
coord cart_coorddouble min_distance = 1e30, min_phi = 0;
double local_radial_coord, local_angular_coord;
// Iterate through each segment to find the segment closest to the point
// this should give us a good starting point
for (int i = 0; i < vortex->nseg; i++) {
double current_distance = vecdist2(vortex->pcar, vortex->C[i]);
if (current_distance < min_distance) {
= current_distance;
min_distance = vortex->phi[i];
min_phi }
}
// Adjust the initial phi guess if the curve has periodicity
if (spatial_period != 0)
= fmod(min_phi + spatial_period * 2 * pi, spatial_period * 2 * pi);
min_phi
// If the point is close enough to the vortex, refine the initial guess
if (min_distance < max_distance) {
// Find the value of xi
double phi = frenet_projection_min(*vortex, min_phi);
// Find the Cartesian coordinates of point O(xi)
= gsl_interp1d_vector(vortex->nseg, vortex->phi, vortex->C, phi);
coord Ocar
// Compute the Frenet-Serret frame vectors at the projected phi
, Nvec, Bvec;
coord Tvec= gsl_interp1d_vector(vortex->nseg, vortex->phi, vortex->Tvec, phi);
Tvec = gsl_interp1d_vector(vortex->nseg, vortex->phi, vortex->Nvec, phi);
Nvec = gsl_interp1d_vector(vortex->nseg, vortex->phi, vortex->Bvec, phi);
Bvec
// Compute local coordinates in the Frenet-Serret frame
.x = vecdot(vecdiff(vortex->pcar, Ocar), Tvec); // x_T (must be zero)
cart_coord.y = vecdot(vecdiff(vortex->pcar, Ocar), Nvec); // x_N
cart_coord.z = vecdot(vecdiff(vortex->pcar, Ocar), Bvec); // x_B
cart_coord
// Convert local coordinates to radial coordinates
= sqrt(vecdot(cart_coord, cart_coord));
local_radial_coord = atan2(cart_coord.z, cart_coord.y);
local_angular_coord
// Compute the torsion angle
double torsion_angle = gsl_interp1d_scalar(vortex->nseg, vortex->phi, vortex->theta0, phi);
// Set radial coordinates
.x = cart_coord.x;
radial_coord.y = local_radial_coord;
radial_coord.z = local_angular_coord - torsion_angle;
radial_coord
// Then, we compute the other properties
double s = gsl_interp1d_scalar( vortex->nseg, vortex->phi, vortex->s, phi);
double sigma = gsl_interp1d_scalar( vortex->nseg, vortex->phi, vortex->sigma, phi);
double kappa = gsl_interp1d_scalar( vortex->nseg, vortex->phi, vortex->kappa, phi);
double tau = gsl_interp1d_scalar( vortex->nseg, vortex->phi, vortex->tau, phi);
double a = gsl_interp1d_scalar( vortex->nseg, vortex->phi, vortex->a, phi);
= (struct local_filament){1, phi, Ocar, Tvec, Nvec, Bvec,
local_coordinates , vortex->pcar, sigma, kappa, tau, torsion_angle, a, cart_coord, radial_coord,
s, local_angular_coord - torsion_angle};
local_radial_coord}
return local_coordinates;
}
double get_min_distance(int spatial_period, double max_distance=L0, struct vortex_filament *vortex) {
double min_distance = 1e30;
// Iterate through each segment to find the segment closest to the point
// this should give us a good starting point
for (int i = 0; i < vortex->nseg; i++) {
double current_distance = vecdist2(vortex->pcar, vortex->C[i]);
if (current_distance < min_distance) {
= current_distance;
min_distance }
}
return min_distance;
}
Compute the Finite Difference Derivative of a Coordinate Array
fd_derivative(): calculates the first derivative of a coordinate array using finite differences
This function computes the first derivative of a coordinate array
X
using a central finite difference scheme. It handles the
interior points using a standard central difference formula and applies
a periodic boundary condition with an optional shift to the
endpoints.
The arguments and their descriptions are:
- n
- integer representing the number of points in the coordinate array.
- dphi
- double representing the spacing between consecutive points in the array, used as the step size in the finite difference calculation.
- shift
-
a
coord
structure representing an optional shift applied to the periodic boundary condition. - X
-
pointer to a
coord
array representing the input coordinate values. - dX
-
pointer to a
coord
array where the computed derivatives will be stored.
void fd_derivative( int n, double dphi, coord shift, coord *X, coord *dX){
for (int i = 1; i < n-1; i++){
foreach_dimension(){
[i].x = (X[i+1].x - X[i-1].x)/(2*dphi);
dX}
}
foreach_dimension(){
[0].x = (X[1].x - X[n-2].x + shift.x)/(2*dphi);
dX[n-1].x = (X[1].x - X[n-2].x + shift.x)/(2*dphi);
dX}
}
Finally, we create a macro so we can initialize the vortex filaments more easily
macro initialize_filaments (struct vortex_filament filament, int nseg, double dphi, double* phi, double* a, coord* C, coord xshift, coord dxshift)
{
for (int i = 0; i < nseg; i++){
.a[i] = a[i];
filament.phi[i] = phi[i];
filamentforeach_dimension(){
.C[i].x = C[i].x;
filament}
}
// Find the 1st, 2nd, and 3rd derivatives of C
fd_derivative(nseg, dphi, xshift, filament.C, filament.dC);
fd_derivative(nseg, dphi, dxshift, filament.dC, filament.d2C);
fd_derivative(nseg, dphi, dxshift, filament.d2C, filament.d3C);
// Compute the Frenet-Serret frame, curvature, and torsion
for (int i = 0; i < nseg; i++){
foreach_dimension(){
.Tvec[i].x = filament.dC[i].x/sqrt(vecdot( filament.dC[i], filament.dC[i]));
filament.Nvec[i].x = filament.d2C[i].x/sqrt(vecdot(filament.d2C[i], filament.d2C[i]));
filament}
.sigma[i] = sqrt(vecdot( filament.dC[i], filament.dC[i]));
filament.Bvec[i] = vecdotproduct(filament.Tvec[i], filament.Nvec[i]);
filament
.kappa[i] = sqrt(vecdot(filament.d2C[i], filament.d2C[i]))/sq(filament.sigma[i]);
filament= vecdotproduct(filament.dC[i], filament.d2C[i]);
coord var1 .tau[i] = vecdot(var1, filament.d3C[i])/vecdot(var1,var1);
filament}
// Compute the arc-lenght coordinate and cumulative torsion
(filament.s, 0, nseg*sizeof (double));
memset (filament.theta0, 0, nseg*sizeof (double));
memset for (int i = 0; i < nseg-1; i++){
.s[i+1] = filament.s[i] + filament.sigma[i+1]*dphi;
filament.theta0[i+1] = filament.theta0[i] - filament.sigma[i+1]*filament.tau[i+1]*dphi;
filament}
}
Compute the curvature terms at next to leading order for a Batchelor vortex
At the next to leading order, corrections due to curvature are obtained by solving a second- order differential equation \mathbf{L} \psi = R
In the functions below, we solve numerically \psi and compute the corrections at this order.
#pragma autolink -lgsl -lgslcblas
#include <gsl/gsl_integration.h>
#include <gsl/gsl_errno.h>
Struct to pass parameters (a, U_c) to the GSL integration functions
typedef struct {
double a;
double U_c;
} integration_params;
Struct to hold the results: \psi(\rho) and its first two derivatives
typedef struct {
double A; // Psi(rho)
double A_p; // First derivative Psi'(rho)
double A_pp; // Second derivative Psi''(rho)
} integration_results;
f(\rho,a) = \exp(-\rho^2/a^2)
double gauss(double s, const integration_params *params) {
return exp(-s * s / (params->a * params->a));
}
R = u^{(0)}_\theta + 2\rho\omega^{(0)}_\xi - 2\rho\omega^{(0)}_\theta u^{(0)}_\xi/u^{(0)}_\theta Here, \boldsymbol{u}^{(0)} and \boldsymbol{\omega}^{(0)} are the leading order solutions. Subscripts (\rho,\theta,\xi) indicate the local radial, azimuthal, and axial components, respectively.
double right_hand_side(double s, void *p) {
integration_params *params = (integration_params *)p;
const double a = params->a;
const double U_c = params->U_c;
const double tol = 1e-18;
if (fabs(s) < tol) return 0.0;
double gs = gauss(s, params);
double u_th = (1.0 / s) * (1.0 - gs);
double u_xi = U_c * gs;
double w_th = U_c * (2.0 * s / sq(a)) * gs;
double w_xi = (2.0 / sq(a)) * gs;
double R = 2.0 * s * w_xi + u_th - 2.0 * s * w_th * u_xi / (u_th + tol);
return R;
}
The solution of \psi is obtained using the method of variation of constants \psi = y_1 \int_0^\rho G(s)/(s y_1^2(s)) ds where the integrand G(s) reads G(\rho) = \int_0^\rho s y_1(s) R(s) ds and y_1 is the fundamental solution y_1 = u^{(0)}_\theta = \frac{1}{\rho} (1 - f(\rho,a))
double integrand_G(double s, void *p) {
integration_params *params = (integration_params *)p;
return (1.0 - gauss(s, params)) * right_hand_side(s, p);
}
double integrand_Psi(double s, void *p) {
integration_params *params = (integration_params *)p;
if (fabs(s) < 1e-9) return 0.0;
*w = gsl_integration_workspace_alloc(1000);
gsl_integration_workspace double G_s, error;
;
gsl_function F_G.function = &integrand_G;
F_G.params = p;
F_G
(&F_G, 0, s, 0, 1e-7, 1000, GSL_INTEG_GAUSS61, w, &G_s, &error);
gsl_integration_qag(w);
gsl_integration_workspace_free
double gs = gauss(s, params);
double denominator = sq(1.0 - gs);
if (fabs(denominator) < 1e-18) return 0.0;
return (s * G_s) / denominator;
}
We perform the numerical integration using GLS and evaluate the first and second derivatives with respect to \rho
void compute_A_with_derivatives(double rho, double a, double U_c, integration_results *results) {
const double tol = 1e-18;
if (rho < tol){
->A = 0.;
results->A_p = 0.;
results->A_pp = 0.;
resultsreturn;
}
integration_params params = {a, U_c};
// --- Step 1: Compute the necessary integrals ---
*w = gsl_integration_workspace_alloc(1000);
gsl_integration_workspace double G_rho, I_psi_rho, error;
, F_psi;
gsl_function F_G.function = &integrand_G;
F_G.params = ¶ms;
F_G.function = &integrand_Psi;
F_psi.params = ¶ms;
F_psi
// Temporarily turn off default GSL error handler
*old_handler = gsl_set_error_handler_off();
gsl_error_handler_t
// Integral G(rho)
(&F_G, 0, rho, 0, 1e-7, 1000, GSL_INTEG_GAUSS61, w, &G_rho, &error);
gsl_integration_qag
// Integral for A(rho), which we call I_psi(rho)
(&F_psi, 0, rho, 0, 1e-7, 1000, GSL_INTEG_GAUSS61, w, &I_psi_rho, &error);
gsl_integration_qag
(old_handler); // Restore handler
gsl_set_error_handler(w);
gsl_integration_workspace_free
// --- Step 2: Compute intermediate values at rho ---
double g_rho = gauss(rho, ¶ms);
double R_rho = right_hand_side(rho, ¶ms);
// --- Step 3: Calculate A, A', and A'' using analytical formulas ---
->A = (1.0 / rho) * (1.0 - g_rho) * I_psi_rho;
results
// First derivative A'(rho)
double C1 = -(1.0 - g_rho) / sq(rho) + (2.0 * g_rho) / sq(a);
->A_p = C1 * I_psi_rho + G_rho / (1.0 - g_rho);
results
// Second derivative A''(rho)
double one_minus_g_sq = sq(1.0 - g_rho);
double Ipsip_rho = (rho * G_rho) / one_minus_g_sq; // This is I_psi'(rho)
double C1p = 2.0 * (1.0 - g_rho) / cube(rho)
- (2.0 * g_rho) / (sq(a) * rho)
- (4.0 * rho * g_rho) / (sq(a) * sq(a));
double C2p = R_rho - (2.0 * rho * g_rho * G_rho) / (sq(a) * one_minus_g_sq);
->A_pp = C1p * I_psi_rho + C1 * Ipsip_rho + C2p;
results}
Having solved \psi we may compute the velocity and vorticity terms
// Struct to hold the final calculated output values.
typedef struct {
double u0_r; // Velocity at leading order
double u0_th;
double u0_xi;
double w0_r; // Vorticity at leading order
double w0_th;
double w0_xi;
double u1_r; // Velocity at the following order
double u1_th;
double u1_xi;
double w1_r; // Vorticity at the following order
double w1_th;
double w1_xi;
} batchelor_vortex;
At leading-order, the velocity reads u^{(0)}_\rho = 0, \quad u^{(0)}_\theta = \frac{1}{\rho}(1 - f(\rho,a)), \quad u^{(0)}_\xi = U_c f(\rho,a) while the vorticity reads \omega^{(0)}_\rho = 0, \quad \omega^{(0)}_\theta = (2 U_c \rho / a^2) f(\rho,a), \quad \omega^{(0)}_\xi = (2/a^2) f(\rho,a)
while the next-to-leading-order terms are functions of \psi, \kappa, \varphi and the leading order terms.
batchelor_vortex calculate_vortex_flow(struct local_filament* vortex, double U_c,
const integration_results* corr) {
batchelor_vortex results;
// Extracting values for readability
double rho = vortex->rho;
double a = vortex->a;
double kappa = vortex->kappa;
double phi = vortex->theta + vortex->theta0;
double A = corr->A;
double dA = corr->A_p;
double d2A = corr->A_pp;
// Intermediate calculations
double g_rho = exp(-sq(rho / a));
double rho2 = sq(rho);
double a2 = sq(a);
// --- Zeroth-Order Base Flow (u0, w0) ---
.u0_r = 0.0;
results.u0_th = (1.0 / rho) * (1.0 - g_rho);
results.u0_xi = U_c * g_rho;
results
.w0_r = 0.0;
results.w0_th = U_c * (2.0 * rho / a2) * g_rho;
results.w0_xi = (2.0 / a2) * g_rho;
results
// --- Intermediate terms needed for first-order terms ---
// --- Swirl number at leading order
double S0 = rho * results.w0_th / results.u0_th;
double denom = 1.0 - g_rho;
double dS0_dr = (3.0 * rho2 - 2.0 * sq(rho2) / a2) * g_rho / denom
- (2.0 * sq(rho2) / a2) * g_rho / (denom * denom);
*= 2.0 * U_c / a2;
dS0_dr
// --- First-Order Perturbations (u1, w1) ---
double cos_phi = cos(phi);
double sin_phi = sin(phi);
.u1_r = -A/rho2;
results.u1_th = results.u0_th - dA/rho;
results.u1_xi = results.u0_xi + S0*A/rho2;
results
.w1_r = -S0*A/cube(rho);
results.w1_th = results.w0_th + S0*A/cube(rho) - S0*dA/rho2 - dS0_dr*A/rho2;
results.w1_xi = results.u0_th/rho + results.w0_xi - d2A/rho - dA/rho2 + A/cube(rho);
results
.u1_r *= kappa * rho * sin_phi;
results.u1_th *= kappa * rho * cos_phi;
results.u1_xi *= kappa * rho * cos_phi;
results.w1_r *= kappa * rho * sin_phi;
results.w1_th *= kappa * rho * cos_phi;
results.w1_xi *= kappa * rho * cos_phi;
results
return results;
}
References
[blanco2015internal] |
Francisco J Blanco-Rodríguez, Stéphane Le Dizès, Can Selçuk, Ivan Delbende, and Maurice Rossi. Internal structure of vortex rings and helical vortices. Journal of Fluid Mechanics, 785:219–247, 2015. |
[callegari1978] |
AJ Callegari and Lu Ting. Motion of a curved vortex filament with decaying vortical core and axial velocity. SIAM Journal on Applied Mathematics, 35(1):148–175, 1978. |
#endif