The Finite Volume functionality
HighVoronois most important feature to the user is the automatic generation of a linear system
\[\mathbb A\,\mathbf u = \mathbf b\]
from the PDE-Problem
\[-\nabla\cdot\left(\kappa\nabla u + \kappa u\nabla V\right) = f\,.\]
More abstract, the class VoronoiFVProblem
discretizes the problem
\[\nabla\cdot J(u) = f\,,\tag{Flux-Form}\]
in the bulk (in the domain) where $J(u)$ is a linear differential operator in $u$ and $f$ is a given right hand side. Furthermore, the method can account for periodic, Dirichlet and Neumann boundary conditions, also all at once (on different parts of the boundary).
The function linearVoronoiFVProblem
then adds particular boundary conditions to the abstract discretization in VoronoiFVProblem
and returns a linear equation to be solved.
The VoronoiFVProblem
dataset
The VoronoiFVProblem
is conceptually a black box into which the user throws a list of nodes and a boundary (or a ready-to-use VoronoiGeometry
) as well as a description of $J$ and $f$. Internally, the black box computes the discrete coefficients of $J$ and $f$ and stores them in a way that allows efficient computation of the matrix and right-hand side for given boundary conditions.
We advise the user to first jump to the examples for calculations of fluxes below and afterwards study the following abstract description of VoronoiFVProblem
.
HighVoronoi.VoronoiFVProblem
— MethodVoronoiFVProblem(Geo::VoronoiGeometry; parent = nothing) # first variant
VoronoiFVProblem(points, boundary; integrator = VI_POLYGON, kwargs...) # second variant
Generates Finite Volume data for fluxes and right hand sides given either a VoronoiGeometry
object or a set of points and a boundary, which will serve to internally create a VoronoiGeometry
. Allows for the following parameters:
kwargs...
: arguments that are not listed explicitly below will be passed to the VoronoiGeometry.integrand
will be overwritten.parent
: Ifparent
is generated fromGeo_p
andGeo
is a refined version ofGeo_p
this parameter will initiate the calculation of an $L^{1}$ projection operator between the spaces of piecewise constant functions on the respective Voronoi Tessellations.discretefunctions=nothing
: A named tuple of form(alpha=x->norm(x),f=x->-x,)
. Will be evaluated pointwise.integralfunctions=nothing
: A named tuple of form(alpha=x->norm(x),f=x->-x,)
. It will make the algoritm calculate the integrals over the given functions or it will associate values to the list of functions based on integrated data present inGeo
fluxes=nothing
: this is assumed to be named tuple, e.g. like the following:fluxes = (alpha = f1, beta = f2, eta = f3, zeta = f4, )
and every of the given fluxes
alpha
,beta
,eta
,zeta
, has the following structure: It is one single flux-function accessing the following named data (see also here in the Documentation):x_i, x_j, para_i, para_j, para_ij, mass_i, mass_j, mass_ij, normal
and returning two values. Functions
f1
,f2
... should hence be defined similar to the following:function f1(;x_i,x_j,para_ij, kwargs...) # some code return something_i, something_j end
Refer to the examples in the documentation.
if the array of Neumann-planes is not provided, the standard list given in boundary
resp. the boundary in Geo
will be used.
rhs_functions=nothing
: same as for fluxes. However, functions can only access the variablesx_i, mass_i, para_i,
HighVoronoi.VoronoiFVProblem
— Typestruct VoronoiFVProblem{...}
after initialization the struct contains the following information:
Geometry
: aVoronoiGeometry
containing mesh and integrated informationCoefficients
: Not advised to be accessed by userParent
: Not advised to be accessed by userprojection_down
: Not advised to be accessed by userprojection_up
: Not advised to be accessed by userparameters
: Not advised to be accessed by user
Examples for the VoronoiFVProblem
Content
- Creating a
VoronoiFVProblem
- Calculating fluxes and rigthand side
- Creating a VoronoiFVProblem from a VoronoiGeometry
- Internal storage of data (For very deep coding only)
Creating a VoronoiFVProblem
and calculating some integrals...
We create a first instance of VoronoiFVProblem
. The following code calculates a VoronoiGeometry
for the given data
of random points. It furthermore calculates the integral of integralfunctions
and pointwise evaluations of discretefunctions
. The latter are not stored but will be internally used for calculations of fluxes or right hand sides (in a later example). To get familiar with the data structure try out the following:
using LinearAlgebra
function myrhs(;para_i,mass_i,kwargs...)
return para_i[:alpha]*mass_i
end
function test_FV(dim,nop)
data = rand(dim,nop)
xs = VoronoiNodes(data)
cube = cuboid(dim,periodic=[1])
VoronoiFVProblem(xs,cube, discretefunctions = (alpha=x->sum(abs,x),),
rhs_functions = (F=myrhs,) )
end
vfvp = test_FV(2,4)
println(vfvp.Coefficients.functions)
The algorithm internally calculutes for each of the four random cells the quantity $\alpha(x_i)*m_i$, where $m_i$ is the mass of cell $i$. The output hence looks like the following:
(F = [0.8899968951003052, 1.6176576528551534, 1.2484331005796414, 0.9868594550457225],)
- At a later stage, we will of course not directly work with
vfvp.Coefficients.functions
... myrhs
could addionally work withx_i
, the coordinates of $x_i$
Calculating fluxes and rigthand side
We write $i\sim j$ if the Voronoi cells of the nodes $x_i$ and $x_j$ are neighbored. Then the discrete version of
\[\nabla\cdot J(u) = f\,,\tag{Flux-Form}\]
in the node $x_i$ is
\[\sum_{j\sim i} J_{i,j}(u) = F_i\,.\tag{Flux-Form-discrete}\]
In the text and in the code hereafter $i$ is the current cell and $j$ is either a neighbor or an index of a part of the boundary.
More precisely, let $f$ and $\kappa$ be scalar functions. If $m_i$ is the mass of cell $i$ and $m_{ij}$ is the mass of the interface between cells $i$ and $j$ and $f_i=f(x_i)$ or $f_i=m_i^{-1}\int_{cell_i}f$ and similarly for $\kappa$ we find the following possible discretization of Fick's law:
\[J(u)=-\kappa \nabla u \qquad\leftrightarrow\qquad J_{ij}(u)\,=\,-\frac{m_{ij}}{h_{ij}}\sqrt{\kappa_i\kappa_j}(u_j-u_i)\,=\,+\frac{m_{ij}}{h_{ij}}\sqrt{\kappa_i\kappa_j}(u_i-u_j)\,,\]
with the right hand side
\[F_i=m_i f_i\,.\]
We can rewrite $J_{ij}(u)$ in the following form:
\[J_{ij}(u)=\frac{m_{ij}}{h_{ij}}\sqrt{\kappa_i\kappa_j}u_i-\frac{m_{ij}}{h_{ij}}\sqrt{\kappa_i\kappa_j}u_j=p_{ij,i}u_i - p_{ij,j}u_j\]
The purpose of VoronoiFVProblem
is to calculate $p_{ij,i}$ and $p_{ij,j}$ using fluxes=...
as well as $F_i$ using rhs_functions
.
We implement the above discretization in myflux_1
and an alternative replacing $\sqrt{\kappa_i\kappa_j}$ by an average over the joint interface of cells $i,j$ in myflux_2
. Here, $\alpha$ is evaluated pointwise in the middle of each cell / interface, while $\kappa$ is averaged over cells and interfaces.
using LinearAlgebra
using SpecialFunctions
function myflux_1(;para_i,para_j,mass_ij,normal,kwargs...)
# kwargs... collects all additional parameters which are not used in the current function.
weight = norm(normal)^(-1) * mass_ij * sqrt(para_i[:kappa]*para_j[:kappa])
return weight, weight
end
function myflux_2(;para_ij,mass_ij,normal,kwargs...)
# kwargs... collects all additional parameters which are not used in the current function.
weight = norm(normal)^(-1) * mass_ij * para_ij[:kappa]
return weight, weight
end
myRHS(;para_i,mass_i,kwargs...) = mass_i * para_i[:f]
function test_FV(dim,nop)
data = rand(dim,nop)
xs = VoronoiNodes(data)
cube = cuboid(dim,periodic=[],neumann=[1,-1]) # cube with preset Neumann BC in dimension 1 and Dirichlet BC all other dimensions
VoronoiFVProblem(xs,cube, discretefunctions = (f=x->sin(2*pi*x[1]),), # evaluate f pointwise
integralfunctions = (kappa=x->1.0+norm(x)^2,), # calculate averages of kappa over cells and interfaces
fluxes = ( j1 = myflux_1, j2 = myflux_2, ),
rhs_functions = (F = myRHS,) )
end
test_FV(2,10)
Creating a VoronoiFVProblem from a VoronoiGeometry
It is also possible to write the following less compact code for test_FV(dim,nop)
. Though it may seem weird to do the extra effort, remember that mesh generation in high dimensions is very time consuming. Hence this approach could be usefull to set up a high dimensional problem from a formerly calculated grid.
function test_FV(dim,nop)
data = rand(dim,nop)
xs = VoronoiNodes(data)
cube = cuboid(dim,periodic=[],neumann=[1,-1])
vg = VoronoiGeometry(xs, cube, integrator=HighVoronoi.VI_POLYGON,
integrand=x->1.0+norm(x)^2)
vfvp = VoronoiFVProblem(vg, discretefunctions = (f=x->sin(2*pi*x[1]),),
integralfunctions = (kappa=x->0.0,),
fluxes = ( j1 = myflux_1, j2 = myflux_2, ),
rhs_functions = (F = myRHS,) )
end
The instatiation of vg
calculates all integrals of x->1.0+norm(x)^2
. The instatiation of vfvp
cimply uses the values stored in vg
and "rebrands" them as :kappa
.
The dimension of integrand
in the instatiation of vg
can be greater or equal than the summed up dimension of all integralfunctions
, but not less!! The definition of :kappa
in VoronoiFVProblem(...)
in the above example does not matter as all values have been calculated before. We strongly advise to have a look at the "intentions of use" section.
Internal storage of data
In the second example, try out the following code:
vfvp = test_FV(2,4)
println(vfvp.Coefficients.functions)
println(vfvp.Coefficients.fluxes)
println(vfvp.Coefficients.rows)
println(vfvp.Coefficients.cols)
The fields rows
and cols
of vfvp
store the row and coloumn coordinates of potentially non-zero entries of a sparse flux matrix. The arrays stored in fluxes
correspondingly store the non-zero values. It is thus possible to directly create SparseMatrix
instances from this data. However, this would not yet properly account for boundary conditions.
Full list of LOCAL PARAMETER names
Functions like myflux_1
and myflux_2
in this example here are evaluated on interfaces between neighboring cells or on the boundary and can take the following arguments
x_i
: coordinates of the current node $i$x_j
: coordinates of the current neighbor $j$ (in case this is an actually existing cell) or the coordinates of a point on the boundary (if this is part of the boundary, seeonboundary
)para_i
andpara_j
: a named tuple container of all pointwise evaluated (discretefunctions
) or averaged (integralfunctions
) functions for either cell $i$ and $j$ respectively.para_ij
: same for the interfacemass_i
andmass_j
: if of cell $i$ and $j$mass_ij
: the mass of the interfacenormal
: Something like $x_j-x_i$. However, in case of periodic nodes with cells "crossing the periodic boundary", it typically holds $x_i+\mathrm{normal}\not=x_j$ but $(x_i+\mathrm{normal})$ is a periodic shift of $x_j$. In any case, it is the correct outer normal vector with length of the "periodized distance".onboundary
: is true if and only ifx_j
is a point on the boudary.
Righthand side functions (bulk functions) like myRHS
are evaluated on nodes have only access to
x_i
para_i
mass_i
- If a function
f
is not provided to eitherdiscretefunctions
orintegralfunctions
the callpara_i[:f]
and alike will cause an error message. - Every name can be used only ONCE. Particularly, a name
f
CANNOT be used both insidediscretefunctions
ANDintegralfunctions
.
Extracting the full FV linear equations including BOUNDARY CONDITIONS
Background
To understand how boundary conditions are implemented in the HighVoronoi
package, multiply equation (Flux-Form) with some function $\varphi$ and use integration by parts to obtain
\[-\int_{domain}J\cdot\nabla\varphi=\int_{domain}f\,\varphi-\int_{boundary}\varphi\,J\cdot \nu\]
where $\nu$ is the outer normal vector.
Furthermore, assume we want to prescribe $u=u_0$ on some part of the boundary. We can write $u=\tilde u +u_0$ where $\tilde u$ has boundary value $0$. Then (Flux-Form-discrete) reads
\[\sum_{j\sim i} J_{i,j}(\tilde u + u_0) = F_i\,.\]
However, since we work in a discrete setting, we can make the following assumptions:
- The function $u_0$ is a discrete function taking value $0$ on every node inside the domain, but might be non-zero on the boundary. $\tilde u$ is a discrete function which is zero on all Dirichlet-parts of the boundary.
- The function
J_0
is a discrete function on the boundary which mimics $J_0=J\cdot\nu$. In particular, we think ofJ_0(i,j)=m_ij*J_0(x_ij)
.
linearVoronoiFVProblem
HighVoronoi.linearVoronoiFVProblem
— MethodlinearVoronoiFVProblem(vd::VoronoiFVProblem;flux)
Takes a VoronoiFVProblem
and a flux::Symbol
and creates a linear problem. flux
has to refer to a flux created in vd
.
Optional arguments
rhs
: aSymbol
referring to one of therhs_functions
or a vector ofFloat64
of the same length as the number of nodes. If not provided, the system assumesrhs=zeros(Float64,number_of_nodes)
.Neumann
: Can be provided in the forms(Int,Function,Int[.],Function,(Int[.],Function),...)
. EveryFunction
depends on(;kwargs...)
Int
or multiple planesInt[.]
.Function
then it takes the Neumann boundaries given by the definition of the boundary of the domain, unless nothing is reinterpreted as Dirichlet boundary.
Dirichlet
: Can be provided in the form(Int,Function,Int[.],Function,(Int[.],Function),...)
. EveryFunction
depends on(;kwargs...)
and represents $u_0$ on a single boundary planeInt
or multiple planesInt[.]
.
Use FVevaluate_boundary(f)
if you simply want f
to be evaluated pointwise at the boundary nodes.
The algorithm will take zero Neumann resp. zero Dirichlet as standard in case no other information is provided by the user. However, it is the users responsibility to make sure there are no double specifications given in Neumann
and Dirichlet
.
Return values
rows, cols, vals, rhs = linearVoronoiFVProblem(vd::VoronoiFVProblem;flux,kwargs...)
rows, cols, vals
are the row and coloumn indeces of values. Create e.g.A=sparse(rows,cols,vals)
and sovle $A*u=rhs$
No Dirichlet condition: Ambiguity
In case the boundary conditions consist only of periodic and/or Neumann conditions, the solution is unique only up to a constant. This is taken into account by providing linearVoronoiFVProblem
with the parameter
enforcement_node=1
: This picks out a node where the solution is forced to be $0$. If the user wants another condition, such as average value $0$, this can be achieved after solving the linear problem, as the library provides enough tools to calculate the respective integrals in the aftermath.
Examples
Let us look at the following example:
using SparseArrays
myrhs(;para_i,mass_i,kwargs...) = mass_i*para_i[:alpha]
function myflux_2(;para_ij,mass_ij,normal,kwargs...)
weight = norm(normal)^(-1) * mass_ij * para_ij[:alpha]
return weight, weight
end
xs = VoronoiNodes(rand(2,6))
cube = cuboid(2,periodic=[1])
vfvp = VoronoiFVProblem(xs, cube, discretefunctions = (alpha=x->sum(abs,x),),
rhs_functions=(F=myrhs,),
fluxes=(j1=myflux_2,) )
har = FVevaluate_boundary(x->0.0) # turn a function into the format HighVoronoi needs
one = FVevaluate_boundary(x->1.0)
r,c,v,f = linearVoronoiFVProblem(vfvp, flux = :j1, Neumann = (3,har), Dirichlet = (4,one))
A = sparse(r,c,v) # a sparse matrix with rows `r`, coloumns `c` and values `v`
# solution_u = somelinearsolver(A,f)
As we see, the output of the algorithm is a matrix A
and a right hand side f
which can be plugged into a linear solver method from some suitable package.