Functions and kernels#
Caution
If you are not familiar with C++ syntax, we recommend you to see C++ syntax before reading this page.
The Function
class#
In order to deal with functions with parameters of any kind it is necessary to use an object function (Function
class) which is related to a Parameters
object (a list of parameters, see The Parameter and Parameters classes). This approach allows passing friendly, at low level of the code, some user’s functions, say functions defined in the main program.
User function and object function#
When you want to deal with the integral term:
where the loop of finite element computation requires the computation of the function \(f(x)=e^{ikx}\) on quadrature points, it is necessary to pass the function \(f\) to the low-level code where the finite element loop is implemented. Most functions are function of a point or a list of points. So, only four kinds of function are concerned:
Function of an n-dimensional point (see The Point class for the class
Point
);function of two n-dimensional points, usually named kernel;
function of a vector of n-dimensional point;
function of two vectors of n-dimensional points.
The functions may also have a Parameters
as input argument if necessary (default value);
for example, the real \(k\) in the next example. The output argument may be of any type (real,
complex, vector, matrix, …), but has to be compatible which the type required in computation.
The way to define such a function is the following. First, define a standard C++ function,
for instance:
Complex f(const Point & P, Parameters& pa = defaultParameters)
{
Real k=pa(1); // k is the first parameter of the parameter's list pa
// Real k=pa("k"); is available if you have a parameter with name "k"
Real x=P(1); // x is the first coordinate of the point P
return exp(i*k*x); // return a complex value result,
//return exp(i*pa(1)*P(1)); is also possible
}
Then create in your main program a Parameters
object, a Function
object from C++ function and you Parameters
object:
Parameters pars(1, "k"); // function parameters
Function F(f, pars); // Function object
If you have to deal with the integral term involving a real value matrix (with no parameter involved in this example):
you may define:
Matrix<Real> A(const Point & P, Parameters& pa = defaultParameters)
{
Matrix<Real> vA(3, 3);
...
return vA;
}
Warning
Note that even if your function does not involve some parameters, the second argument of type Parameters is mandatory in the function definition.
For a kernel type function, it is quite similar. You have to specify two points as input arguments:
Complex Green_Helmholtz_3D(const Point& M, const Point& P, Parameters& pa = defaultParameters)
{
Real r=distance(M, P); // we assume a distance function exists
Real k=pa(1);
Real eps=pa(2);
if (r>eps) return exp(i*k*r)/r;
else ...
}
The vector form of a Function
is a function working with a vector of points and returning a vector of results (values on each point). It may be useful when computing n values together is really faster than computing \(n\) times a single value. Such a function should be declared, for instance, as follows:
Vector<Complex> vf(const Vector<Point> & vP, Parameters& pa = defaultParameters)
{
Real k=pa(1); //k is the first parameter of the parameter's list pa
uint n=vP.size();
Vector<Complex> res(n);
for(int j=1;j<=n;j++) res(j)=exp(i*k*vP(j)(1));
return res;
}
Note that the function has to return a Vector
object. For a function involving a couple
of vectors of points, the syntax of the declaration of the function is:
Vector<Complex> vf(const Vector<Point> & vP, const Vector<Point> & vQ, Parameters& pa = defaultParameters)
The C++ functions defined by the user may be used directly as argument of some functions of the library. But in most of cases it is necessary to define explicitly the object Function
associated to the user function. This is the way to do this:
Parameters par;
par<<2.<<.000001; // k and eps values, inserted in a parameter list
Function funcf(f, par); // define a scalar function object using parameters
Function funcA(A); // define a matrix function object
Function funcG(Green_Hemholtz_3D, par); // define a scalar kernel
Function funcvf(vf, par); // define a scalar function in its vector form
Warning
Do not confuse the vector form of a Function
and a function which returns a vector!
The vector form means a function which computes a quantity (scalar, vector, matrix) on a set
of a points or bipoints, the result being a vector of scalars, vectors or matrices. For most
applications, scalar form of Function
are sufficient. Vector
form is an extension
allowing the user to compute the function more efficiently in the case of multiple evaluations.
When the user wants to associate some parameters to his function, it is mandatory to define
the object Function
because it stores the list of parameters. To understand the role
of the object Function
, note that if P is a Point
, the two following instructions are allowed and give the same result:
r=f(P, par); // call directly the user function f
funcf(P, r); // call the user function f using the object function funcf
In other words, the object Function
shadows
the Parameters
object. In this example, using an object function seems to be artificial.
The object function has a real interest when internal computational routines requires user’s
functions, because it is easier to send one type of object encapsulating various type of function
rather than many objects.
When you have to pass an object Function
to a function which requires such an object,
it is possible to use the constructor syntax Function(f,param)
, avoiding the explicit creation
of the object Function
:
Parameters par;
par<<2.<<.000001; // value of k and eps
compute(Function(f, par)); // compute requiring an object function
Tip
When using a function returning a vector or a matrix, because XLiFE++ checks the dimension(s) of the returned object by using a fake point or a normal vector, it may occur some consistency problems with the dimension of points or normals that is set to 3 by default.
You can change it by specifying explicitly the dimension of the function when building it:
Function f(dnuinc, pars);
f.dimPoint=2; // change the point dimension
// or
Function f(dnuinc, 2, pars); // specifying the point dimension when building
It works in the same way for kernels.
Dealing with normal vectors#
Sometimes, user functions have to deal with some normal vectors and obviously the normal vectors will depend on the point where the function is evaluated. For instance, a function computing the normal derivative of a given incident field (e.g. a plane wave \(e^{ikx}\)), will look like:
Complex dnuinc(const Point& P, Parameters& pa = defaultParameters)
{
Real x=P(1), k=pa("k"); // get k from parameters
Reals n=getN(); // get the normal vector at P
return i_*k*exp(i_*k*x)*n(1);
}
When this function is passed to FE computation routines, the normal vector will be evaluated and transmitted to the function only if the Function
object associated to the function, has declared to use the normal vector. It is done by the following instructions in the main program:
Parameters pars(1, "k"); // declare k in the parameters
Function f(dnuinc,pars); // associate parameters to Function object
f.require(_n); // tell the function will use the normal
TermVector B(intg(Sigma,f*v)); // use function f
The normal vector refers to the domain on which the linear or bilinear form (involving the function) acts. The orientation of normal vector is described in section Dealing with normals.
If you want to call a function involving a normal vector out of the context of FE computations, you have to “transmit” explicitly the normal vectors required by your function. The following code shows how to do this:
Real f(const Point& x, Parameters& pa = defaultParameters)
{
Vector<Real>& n=getN(); // get the normal vector
return n(1);
}
...
Function F(f); F.require(_n); //say the function is using normal vector
Point P(1.,0.); //point belonging to gamma
Vector<Real> n1=gamma.getNormal(P); //get the normal vector to gamma at P
setNx(n1); //pass the normal vector to the context
Real r = F(P,r); //compute F at P using n1
Note that the normal vector returned by the getN()
function, is not interpolated. As a consequence, normal vector on an edge of the mesh is the normal vector of one
of the elements supporting the edge!
The setNx()
is thread dependent, so it can be used in a multithreading context.
Hint
Other data are available in user function, depending on the computation context :
the x/y-tangent vector:
getTx()
,getTy()
(first tangent vector for 3D surface)the x/y-bitangent vector:
getBx()
,getBy()
(second tangent vector for 3D surface only)the current element:
getElement()
the current Dof:
getDof()
the domains
getDomain()
,getDomainx()
,getDomainy()
All this data are returned as references.
Dealing with spectral family#
A spectral family is defined either from a XLiFE++ function or from a collection of TermVector
. When it is defined from a function, the function has to propose the computation for each basis index, say \(n\). This basis index is managed by the XLiFE++ computation algorithms and transmitted to a global data structure (thread safe). To retrieve it as a Number
, users have to call the global function getBasisIndex()
:
Real cosny(const Point& P, Parameters& pa = defaultParameters)
{
Real y=P(2);
Real h=pa("h"); // get the parameter h (user definition)
Number n=getBasisIndex()-1; // get the index using the global function
if (n==0) return std::sqrt(1./h);
else return std::sqrt(2./h)*std::cos(n*pi_*(y+h*0.5)/h);
}
Warning
Note that basis index, set by internal algorithms, starts from 1!
Caution
Manipulating basis indexes does not work currently when two functions are involved at the same time with different indexes.
The basis index is automatically set by internal algorithms in their computation loops. But, in user context, it may be set by calling the global function setBasisIndex()
:
Function cos(cosny, params);
TermVectors cos_int(N);
for s(Number n=1; n<=N; n++)
{
setBasisIndex(n); // update the basis index
cos_int(n)=TermVector(u, sigmaP, cos, "c"+tostring(n-1));
}
Advanced user#
Delaying computations#
It may occur that the function you plan to use is a very complex one, involving some heavy computations that you want to compute only once by storing some intermediate results somewhere.
In order to allow flexibility to the user, it is advised to use the capabilities of Parameters
object to store void pointers. For instance, the first time the function is called, you can compute some reusable quantities and store them in any structure with dynamic memory allocation and store the pointer of this structure in the Parameters
object.
The next time the function is called, as you have an access to this void pointer (do not forget to recast it), you can recover your data.
Calling a Function
object#
If you have to compute the values of the function via the object Function
, there are
mainly two ways to do it:
-
using an alias to the pointer function (requires that you know the type of function and arguments)
Point P=Point(0, 0), Q=Point(1, 1); Complex c=funcf.funSC(P); //a function returning a complex scalar (funSC) Complex g=funcG.kerSC(P, Q); //a kernel returning a complex scalar (kerSC) Matrix<Real> m=funcA.funMR(P); //a function returning a real matrix scalar(funMR)
As this method uses a recasting of a void pointer with no checking, it can cause segmentation errors if there is a misfit between the type of function required and the real function stored in the void pointer! It is possible to check the type of arguments by using the utility functions
typeReturned
,structReturned
,typeFunction
andtypeArg
. This direct method is offered to developers in order to have the best performance. -
using the safe overloaded operator (), allowing to deal with point or vector of points
Point P=Point(0, 0), Q=Point(1, 1); Complex c; // complex to store the result funcf.checkTypeOn(); // activate checking mode funcf(P, c); // compute a complex scalar function at point P // checking mode is disabled after the computation Vector<Point> pts; // a vector of points pts(1)=P;pts(2)=Q; vector<Complex> vc; // vector to store the result funcf.checkTypeOn(); // activate the checking mode funcf(Pts, vc); // compute function at a vector of points
This method does not require the knowledge of the exact type of the function (the output argument must be compatible !). It allows scalar or vector form independently of the form of the user function. Note that, contrary to the first method, this method uses a reference to return the values, so you have to manage its memory allocation. When the function is called with a vector of points as input, the vector result is resized if it is too small. Using the
checkTypeOn
function, it is possible to activate the checking of the type of argument. After computation, the checkType variable is reset to false in order to avoid unnecessary rechecking. As the checking process invokes RTTI functions (expensive in time), activate wisely this option. So, if you have to evaluate many times the function, activate the checking only for the first evaluation. Note that when the checking process is deactivated, this method is still slightly more expensive than the first one.
The Kernel
class#
The Function
class allows defining kernel type function, say function of two points x and y. But, to deal with integral equation, more information are required. It is the purpose of the Kernel
class. A Kernel
object manages mainly:
Function kernel, gradx, grady, gradxy, ndotgradx, ndotgrady, curlx, curly,
curlxy, divx, divy, divxy, dx1, dx2, dx3;
Kernel* singPart; // singular part of kernel
Kernel* regPart; // regular part of kernel
Dimen dimPoint; // dimension of points
SingularityType singularType; // singularity (_notsingular, _r, _logr, _loglogr)
Real singularOrder; // order of singularity
Complex singularCoefficient; // coefficient of singularity
SymType symmetry; // kernel symmetry: _noSymmetry, _symmetric, ...
String name; // kernel name
Parameters userData; // to store some additional information
Setting the kernel
data is mandatory, but derivatives of kernel are often required.
These can be provided by directly defining the data gradx
, grady
, … (Function
) or the dx1
, dx2
, dx1
to obtain all first derivative operators.
Integral calculations involving singular kernels can use quadrature methods handling singular and regular parts of the kernel, which are provided by singPart
, regPart
(Function
pointers).
Note
By default, the dimension of points of a Kernel is 3. When you define a 2D kernel, it may happen some troubles with point dimensions, in particular if the kernel function involves some operations sensitive to the point dimension. You can cure this problem by testing point dimensions in the kernel function or by specifying the point dimension when building Kernel:
Real ker(const Point& x, const Point& x, Parameters& pa=defaultParameters)
{...}
...
Kernel K(ker); K.dimPoint=2;
//or
Kernel K(ker, 2);
Some classical Green’s kernel as offered by XLiFE++ as Kernel
objects, see Available Green’s kernels.
Besides, users can define their own Kernel
objects, see User kernels.
Tensor kernel#
Tensor kernels are kernels of the following form:
with \(\mathbb{A}\) a \(M\times N\) matrix. Such kernel is involved, for instance, in Dirichlet to Neumann method using spectral expansion. In many cases, the matrix is a diagonal one:
In the meaning of XLiFE++, the families \((\psi_m)_{1\le m\le M}\) and \((\psi_n)_{1\le n\le N}\) may be considered as some basis of spectral space, say SpectralBasis
object. To define a TensorKernel
, you have to specify either one or two Function
defining the spectral families, one or two SpectralBasis
, one or two Unknown
defined on a spectral Space
or one or two TermVectors
handling numerical spectral family. The matrix \(\mathbb{A}\) must be also given as a Matrix
or a Vector
if \(\mathbb{A}\) is diagonal.
Real cosny(const Point& P, Parameters& pa = defaultParameters) // spectral functions
{
Real y=P(2);
Real h=pa("h"); //get the parameter h (user definition)
Int n=basis_index(); //get the index of function to compute
if(n==0) return sqrt(1./h);
else return sqrt(2./h)*std::cos(n*pi_*(y+h*0.5)/h);
}
...
Real k=2.; Number N=10;
Parameters params(1.,"h");
Vector<Complex> A(N);
for(Number n=0; n<N; n++)
A[n]=sqrt(Complex(k*k-n*n*pi_*pi_/(h*h))); // matrix A
Function cosN(cosny, params); // spectral Function's
TensorKernel tkp(cosN, A); // define TensorKernel
In an alternate way, you can also construct the TensorKernel
as follows:
Space Sp(_domain=sigma, _basis=cosN, _dim=N, _name="vect{cos(n*pi*y)}");
Unknown phi(Sp, "phi"); // spectral unknown
TensorKernel tkp(phi, A);
Doing this, a spectral unknown is available to deal with problem involving such unknown. All the following constructors are available:
template <typename T>
TensorKernel(SpectralBasis&, vector<T>&, bool conj = false);
TensorKernel(SpectralBasis&, Matrix<T>&, bool conj = false);
TensorKernel(SpectralBasis&, vector<T>&, SpectralBasis&);
TensorKernel(SpectralBasis&, Matrix<T>&, SpectralBasis&);
TensorKernel(Function, vector<T>&, bool conj = false);
TensorKernel(Function, Matrix<T>&, bool conj = false);
TensorKernel(Function, vector<T>&, Function);
TensorKernel(Function, Matrix<T>&, Function);
TensorKernel(Unknown&, vector<T>&, bool conj = false);
TensorKernel(Unknown&, Matrix<T>&, bool conj = false);
TensorKernel(Unknown&, vector<T>&, Unknown&);
TensorKernel(Unknown&, Matrix<T>&, Unknown&);
TensorKernel(vector<TermVector>&, vector<T>&, bool conj = false);
TensorKernel(vector<TermVector>&, Matrix<T>&, bool conj = false);
TensorKernel(vector<TermVector>&, vector<T>&, vector<TermVector>&);
TensorKernel(vector<TermVector>&, Matrix<T>&, vector<TermVector>&);
By default, the second family (may be same as the first one) is not conjugated in computation. By activating the option conj to true, the second family will be conjugated in computation.
It is possible to associate some geometric maps (\(f,g\)) to a tensor kernel:
Vector<Real> f(const Point& P, Parameters& pa = defaultParameters){...}
Vector<Real> g(const Point& P, Parameters& pa = defaultParameters){...}
...
TensorKernel tkp(cosN, A);
tkp.xmap=Function(f);
tkp.ymap=Function(g);
When TensorKernel
is defined from vector spectral families (i.e. \(\boldsymbol{\phi}_n\) is a vector function), the vector tensor kernel reads
with \(\mathbb{K}\) a matrix dim \((\boldsymbol{\phi}_n)\times\) dim \((\boldsymbol{\psi}_m)\). This vector kernel can be used in bilinear form as follows:
with dim( \(\boldsymbol{u})=\) dim \((\boldsymbol{\phi}_n)\) and dim( \(\boldsymbol{v})=\) dim \((\boldsymbol{\psi}_m)\) and \((.|.)\) the inner product. In terms of FE (\(\boldsymbol{w}_j\) and \(\boldsymbol{\tau}_i\) basis functions related respectively to \(u\) and \(v\)), XLiFE++ produces the matrix
Dealing with normal vectors#
For Kernel
objects, one can pass to some kernel functions either _nx vector or _ny vector or both using the same method as one used for Function
:
Complex G(const Point& P, const Point& Q, Parameters& pa = defaultParameters)
{
Reals nx= getNx(); // get the normal vector at P
Reals ny= getNy(); // get the normal vector at Q
...
}
...
Parameters pars(1, "k"); // declare k in the parameters
Kernel K(g, pars); // associate parameters to Kernel object
K.require(_nx); // declare that kernel uses x-normal
K.require(_ny); // declare that kernel uses y-normal
TermMatrix B(intg(Sigma, Sigma, u*K*v)); // use kernel K
Kernel functions defined in XLiFE++ managed the normal vectors, so the user has not to deal with.
Tabulated functions: the Tabular
class#
The Tabular
class allows managing tabulated values of a function. More precisely, it handles the values of a function on a uniform grid of any dimension, mainly providing some tools to create the table, to save the table to a file, to load a table from a file, and to evaluate the values of the function at some points by interpolating from the values on the grid.
Up to now, only scalar (T is Real
or Complex
) functions of 1,2 or 3 real coordinates, may be with a Parameters
, are handled:
T f1 (Real);
T f1p (Real, Parameters&);
T f2 (Real, Real);
T f2p (Real, Real, Parameters&);
T f3 (Real, Real, Real);
T f3p (Real, Real, Real, Parameters&);
To create a Tabular
define your function and give the grid parameters (the starting coordinate x0, the step dx, the number of steps nbx, an optional name) along each coordinate:
Real f1(Real x){return sin(x);}
...
Tabular<Real> t1(0., 0.01, 100, f1);
Now, you can print the Tabular
, save the Tabular
to a file and evaluate the tabulated function at any point inside the grid domain:
theCout<<t1;
t1.saveToFile("f1.tab", "my optional comment");
Real y=t1(0.5);
If you saved a Tabular
to a file, you can load it in a new Tabular
of the same type:
Tabular<Real> t1("f1.tab");
To deal with a 2D or 3D grid, it is quite similar:
Real f2(Real x, Real y){return sin(x*y);}
...
Tabular<Real> t2(0., 0.01, 100, 0., 0.005, 50, f2);
theCout<<t2;
t2.saveToFile("f2.tab", "my optional comment");
Real y=t2(0.5, 0.5);
The general form of Tabular
constructor and evaluation are ([ ] meaning an optional argument):
Tabular<T> tab(x0, dx, nx, [y0, dy, ny], [z0, dz, nz],
fun, ["name_x"], ["name_y"], ["name_z"]);
T r = tab(x, [y], [z]);
Tabular
class inherits from std::vector<Real>
. On a grid \(\small x_i=x_0+i*dx,\ [y_j=y_0+j*dy,\ [z_k=z_0+k*dz]]\), \(\small i=0,n_x,\ [j=0,n_y,\ [k=0,n_z]]\),
tabulated values \(\small f(x_i, [y_j, [z_k]])\) are stored in vector at place \(\small [[k*nx*ny]+j*nx]+i\).
Tip
If you need a grid of dimension greater than 3, contact the developers. But be care, a 4D grid with 100 steps in each direction requires to store \(10^8\) values, almost 1 Go of memory for double values.
Tabulated Function#
In order to use tabulated function in FE computation, it is possible to associate a Tabular to a Function:
Real f(Real x, Real y){return sin(x*y);}
...
Tabular<Real> tab(0., 0.01, 100, 0., 0.005, 50, f); // create tabular
Function F(tab, 2); //associate tabular to f
To be consistent, the dimension of the input Point
of the function has to be specified to the dimension of the Tabular
! Up to now, Tabular
dimension is limited to 3.
It may be of interest to use a Tabular
with a dimension less than the Point
dimension; for instance a Tabular
defined only for the radial coordinate. In that case, a Function
relating Point
to radial coordinates must be defined:
Real f(Real x, Real y){return sin(x*y);}
Real radial(const Point& X, Parameters& pars=defaultParameters) {return norm(X);}
...
Tabular<Real> tab(0., 0.01, 100, 0., 0.005, 50, f); // create tabular from f
Function R(radial); // create R function from radial function
Function F(tab, 2, R); // associate R map
Note that it is also possible to associate a Tabular
to the function itself:
Real f(const Point& X, Parameters& pars=defaultParameters){return sin(x*y);}
...
Tabular<Real> tab(0., 0.01, 100, 0., 0.005, 50, f); // create tabular from f
Function F(f, 2); // create Function
F.createTabular(0., 0.01, 100, 0., 0.005, 50); // F is now tabulated
Once a Tabular
is handled by a Function, it will be used in any circumstances!
Warning
Tabulated Function
works only for scalar functions !
The SymbolicFunction
class#
Some finite element constructions require passing as argument a SymbolicFunction
object, that is a symbolic expression of a function. Such object is built in by writting any expression involving
some constants (real or complex)
some variables :
x_1
,x_2
,x_3
some algebraic operators :
+
,-
,*
,/
,^
(power)some boolean operators :
&&
,||
,<
,<=
,>
,>=
,==
,!=
,!
(not)some mathematical functions :
abs
,realPart
,imagPart
,sqrt
,squared
,sin
,cos
,tan
,asin
,acos
,atan
,sinh
,cosh
,tanh
,asinh
,acosh
,atanh
,exp
,log
,log10
The result of a boolean expression is either 0 (false) or 1 (true).
Tip
the functions asinh
, acosh
, atanh
are only available when compiling with C++ 2011 standard.
To deal with a symbolic function is very easy. For instance consider the function
To define it as a symbolic function, write:
SymbolicFunction fs = (1-x_1+cos(x_2))^3;
To evaluate it, write:
Real r = fs(-1, 0);
Complex c = fs(i_, 0);
You can mix algebraic and boolean expressions. Consider the following function:
To define it, write:
SymbolicFunction fs = (x_1<=0)*exp(x_1)+(x_1>0);
Symbolic function can be derived:
SymbolicFunction fs = power(sin(x_1),cos(x_2));
SymbolicFunction dx1fs = derivative(fs,_x1);
SymbolicFunction dx2fs = derivative(fs,_x2);
/* gives :
((cos(x2)*cos(x1))/sin(x1))*pow(sin(x1),cos(x2))
-((sin(x2)*log(sin(x1)))*pow(sin(x1),cos(x2))) */