Finite-element discretizations of the dynamic Laplacian
These methods rely on the theory outlined by Froyland's Dynamical Laplacian and the Geometric Heat Flow of Karrasch & Keller.
The Laplace-like operators are best discretized by finite-element-based methods, see this paper by Froyland & Junge.
This involves the discretization of the average of a one-parameter family of Laplace operators of the form:
\[\Delta^{dyn} := \sum_{t \in \mathcal T} P_t^* \Delta P_t\]
for a finite series of times $\mathcal T$, where $P_t$ is the transfer-operator for the flow at time $t$ (in volume-preserving flows).
The resulting operator is both symmetric and uniformly elliptic. Eigenfunctions of $\Delta^{dyn}$ can be used to find Lagrangian Coherent Structures.
See the Examples section for examples of how these methods can be used.
Features
CG and TO methods
The standard Galerkin formulation of the weak dynamical Laplace is referred to as the CG-method here, due to the fact that the inverse Cauchy-Green tensor appears in the weak formulation. This gives a bilinear form $\overline a(u,v) := \sum_{t \in \mathcal T}a^t(P_t u, P_t v)$ Here $P_t$ is the Transfer-Operator (or pushforward) to time-$t$, and $a^t$ is the weak-form of the Laplacian on the range of the time-$t$ map being considered.
There is also a range of transfer operator-based approaches implemented here. These approximate the weak form of the Dynamical-Laplace by a bilinear-form:
\[\tilde a_h(u,v) = \sum_{t \in \mathcal T} a^t(I_hP_t u, I_h P_t v)\]
where $I_h$ is a suitable interpolation operator depending on the mesh-width $h$. Options for $I_h$ implemented in this package are:
- collocation (pointwise interpolation):
- points used are mesh points from domain grid ("adaptive TO"),
- points used are arbitrary ("non-adaptive TO");
- the $L^2$-orthogonal projection onto an FEM-space:
- using the forward-flow map (currently gives poor results),
- using the inverse flow map.
Note that the $L^2$-Galerkin methods currently perform very poorly on larger problems.
For more details, see Froyland & Junge, 2018.
Grids
Various types of regular and irregular meshes (with Delaunay triangulation using VoronoiDelaunay.jl
) are supported. These are based on the corresponding elements from Ferrite.jl
and include:
- triangular P1-Lagrange elements in 2D (all methods)
- quadrilateral P1-Lagrange elements in 2D (all methods except adaptive TO)
- triangular and quadrilateral P2-Lagrange elements in 2D (all methods except adaptive TO)
- tetrahedral P1-Lagrange elements in 3D (only CG method tested, non-adaptive TO might work also)
The GridContext
Type
The FEM-based methods of CoherentStructures.jl
rely heavily on the Ferrite.jl
package. This package is very low-level and does not provide point-location/plotting functionality. To be able to more conveniently work with the specific types of grids that we need, all necessary variables for a single grid are combined in a GridContext
structure - including the grid points, the quadrature formula used and the type of element used (e.g. Triangular P1, Quadrilateral P2, etc..). This makes it easier to assemble mass/stiffness matrices, and provides an interface for point-location and plotting.
In this documentation, the variable name ctx
is exclusively used for GridContext
objects.
See also Constructing Grids in the FEM-API section.
Node ordering and dof ordering
Finite Element methods work with degrees of freedom (dof), which are elements of some dual space. For nodal finite elements, these correspond to evaluation functionals at the nodes of the grid.
The nodes of the grid can be obtained in the following way [n.x for n in ctx.grid.nodes]
. However, most of the methods of this package do not return results in this order, but instead use Ferrite.jl
's dof-ordering.
See also the documentation in dof2node
and CoherentStructures.GridContext
When working with (non-natural) Boundary Conditions, the ordering is further changed, due to there being fewer degrees of freedom in total.
Assembly
See Stiffness and Mass Matrices from the FEM-API section.
Evaluating Functions in the Approximation Space
given a series of coefficients that represent a function in the approximation space, to evaluate a function at a point, use the evaluate_function_from_node_or_cellvals
or evaluate_function_from_dofvals
functions.
using Plots, Tensors
ctx, _ = regularP2TriangularGrid((10, 10))
u = zeros(ctx.n)
u[45] = 1.0
Plots.heatmap(range(0, stop=1, length=200), range(0, stop=1, length=200),
(x, y) -> evaluate_function_from_dofvals(ctx, u, Vec(x, y)))
For more details, consult the API: evaluate_function_from_dofvals
, evaluate_function_from_node_or_cellvals
, evaluate_function_from_node_or_cellvals_multiple
Nodal Interpolation
To perform nodal interpolation of a grid, use the nodal_interpolation
function.
Boundary Conditions
To use something other than the natural homogeneous von Neumann boundary conditions, the BoundaryData
type can be used. This currently supports combinations of homogeneous Dirichlet and periodic boundary conditions.
- Homogeneous Dirichlet BCs require rows and columns of the stiffness/mass matrices to be deleted
- Periodic boundary conditions require rows and columns of the stiffness/mass matrices to be added to each other.
This means that the coefficient vectors for elements of the approximation space that satisfy the boundary conditions are potentially smaller and in a different order. Given a bdata
argument, functions like plot_u
will take this into account.
Constructing Boundary Conditions
Natural von-Neumann boundary conditions can be constructed with BoundaryData()
and are generally the default.
Homogeneous Dirichlet boundary conditions can be constructed with the getHomDBCS(ctx[, which="all"])
function. The optional which
parameter is a vector of strings, corresponding to Ferrite
face-sets, e.g. getHomDBCS(ctx, which=["left", "right"])
Periodic boundary conditions are constructed by calling BoundaryData(ctx,predicate,[which_dbc=[]])
. The argument predicate
is a function that should return true
if and only if two points should be identified. Due to floating-point rounding errors, note that using exact comparisons (==
) should be avoided. Only points that are in Ferrite.jl
boundary facesets are considered. If this is too restrictive, use the BoundaryData(dbc_dofs, periodic_dofs_from, periodic_dofs_to)
constructor.
For details, see BoundaryData
.
Example
Here we apply homogeneous DBC to top and bottom, and identify the left and right side:
using CoherentStructures, Distances, Plots
ctx, _ = regularQuadrilateralGrid((10, 10))
predicate = (p1, p2) -> peuclidean(p1, p2, [1.0, Inf]) < 2e-10
bdata = BoundaryData(ctx, predicate, ["top", "bottom"])
u = ones(nBCDofs(ctx, bdata))
u[20] = 2.0; u[38] = 3.0; u[56] = 4.0
plot_u(ctx, u, 200, 200, bdata=bdata, colorbar=:none)
To apply boundary conditions to a stiffness/mass matrix, use the applyBCS
function. Note that both assemble
functions take a bdata
argument that does this internally.
Plotting and Videos
There are some helper functions that exist for making plots and videos of functions on grids. These rely on the Plots.jl library. Plotting recipes are unfortunately not implemented.
The simplest way to plot is using the plot_u
function. Plots and videos of eulerian plots like $f \circ \Phi^0_t$ can be made with the plot_u_eulerian
and eulerian_videos
functions.
Parallelisation
Many of the plotting functions support parallelism internally. Tensor fields can be constructed in parallel, and then passed to assemble(Stiffness(), _)
. For an example that does this, see TODO: Add this example
FEM-API
Stiffness and Mass Matrices
CoherentStructures.assemble
— Functionassemble(Stiffness(), ctx; A=Id, p=nothing, bdata=BoundaryData())
Assemble the stiffness-matrix for a symmetric bilinear form
\[a(u,v) = \int \nabla u(x)\cdot A(x)\nabla v(x)f(x) dx.\]
The integral is approximated using numerical quadrature. A
is a function that returns a SymmetricTensor{2,dim}
object and must have one of the following signatures:
A(x::Vector{Float64})
;A(x::Vec{dim})
;A(x::Vec{dim}, index::Int, p)
. Here,x
is equal toctx.quadrature_points[index]
, andp
is some parameter, think of some precomputed object that is indexed viaindex
.
The ordering of the result is in dof order, except that boundary conditions from bdata
are applied. The default is natural (homogeneous Neumann) boundary conditions.
assemble(Mass(), ctx; bdata=BoundaryData(), lumped=false)
Assemble the mass matrix
\[M_{i,j} = \int \varphi_j(x) \varphi_i(x) f(x)d\lambda^d\]
The integral is approximated using numerical quadrature. The values of f(x)
are taken from ctx.mass_weights
, and should be ordered in the same way as ctx.quadrature_points
.
The result is ordered to be usable with a stiffness matrix with boundary data bdata
.
Returns a lumped mass matrix if lumped=true
.
Example
ctx.mass_weights = map(f, ctx.quadrature_points)
M = assemble(Mass(), ctx)
CoherentStructures.adaptiveTOCollocationStiffnessMatrix
— FunctionadaptiveTOCollocationStiffnessMatrix(ctx, flow_maps, times=nothing; [quadrature_order, on_torus,on_cylinder, LL, UR, bdata, volume_preserving=true, flow_map_mode=0] )
Calculate the matrix-representation of the bilinear form $a(u,v) = 1/N \sum_n^N a_1(I_hT_nu,I_hT_nv)$ where $I_h$ is pointwise interpolation of the grid obtained by doing Delaunay triangulation on images of grid points from ctx
and $T_n$ is the transfer operator for $x \mapsto flow_maps(x, times)[n]$ and $a_1$ is the weak form of the Laplacian on the codomain. Moreover, $N$ in the equation above is equal to length(times)
and $t_n$ ranges over the elements of times
.
If times==nothing
, take $N=1$ above and use the map $x \mapsto flow_maps(x)$ instead of the version with t_n
.
If on_torus
is true, the Delaunay triangulation is done on the torus. If on_cylinder
is true, then triangulation is done on an x-periodic cylinder. In both of these cases we require bdata
for boundary information on the original domain as well as LL
and UR
as lower-left and upper-right corners of the image.
If volume_preserving == false
, add a volumecorrection term to ``a1`` (See paper by Froyland & Junge).
If flow_map_mode=0
, apply flow map to nodal basis function coordinates. If flow_map_mode=1
, apply flow map to nodal basis function index number (allows for precomputed trajectories).
Constructing Grids
There are several helper functions available for constructing grids. The simplest is:
In 1D
CoherentStructures.regular1dP1Grid
— Functionregular1dP1Grid(numnodes, left=0.0, right=1.0; [quadrature_order])
Create a regular grid with numnodes
nodes on the interval [left, right]
in 1d, with P1-Lagrange basis functions.
CoherentStructures.regular1dP2Grid
— Functionregular1dP2Grid(numnodes, left=0.0, right=1.0; [quadrature_order])
Create a regular grid with numnodes
non-interior nodes on the interval [left, right]
, with P2-Lagrange elements.
In 2D
CoherentStructures.regular2dGrid
— Functionregular2dGrid(gridType, numnodes, LL=(0.0,0.0), UR=(1.0,1.0); quadrature_order=default_quadrature_order)
Constructs a regular grid. gridType
should be one from regular2dGridTypes
.
Supported values for the gridType
argument are:
regular2dGridTypes
9-element Vector{String}:
"regular PC triangular grid"
"regular P1 triangular grid"
"regular P2 triangular grid"
"regular PC Delaunay grid"
"regular P1 Delaunay grid"
"regular P2 Delaunay grid"
"regular PC quadrilateral grid"
"regular P1 quadrilateral grid"
"regular P2 quadrilateral grid"
The following functions are conceptually similar:
CoherentStructures.regularTriangularGrid
— FunctionregularTriangularGrid(numnodes, LL, UR; [quadrature_order, PC=false])
Create a regular triangular grid on a rectangle; does not use Delaunay triangulation internally. If
CoherentStructures.regularDelaunayGrid
— FunctionregularDelaunayGrid(numnodes=(25,25), LL=(0.0,0.0), UR=(1.0,1.0); [quadrature_order, on_torus=false, on_cylinder=false, nudge_epsilon=1e-5, PC=false])
Create a regular grid on a square with lower left corner LL
and upper-right corner UR
. Uses Delaunay triangulation internally. If on_torus==true
, uses a periodic Delaunay triangulation in both directions. If on_cylinder==true
uses a periodic Delaunay triangulatin in x direction only. To avoid degenerate special cases, all nodes are given a random nudge
, the strength of which depends on numnodes
and nudge_epsilon
. If PC==true
, returns a piecewise constant grid. Else returns a P1-Lagrange grid.
CoherentStructures.irregularDelaunayGrid
— FunctionirregularDelaunayGrid(nodes_in; [on_torus=false, on_cylinder=false, LL, UR, PC=false, ...])
Triangulate the nodes nodes_in
and return a GridContext
and bdata
for them. If on_torus==true
, the triangulation is done on a torus. If PC==true
, return a mesh with piecewise constant shape-functions, else P1 Lagrange.
CoherentStructures.regularP2TriangularGrid
— FunctionregularP2TriangularGrid(numnodes=(25,25), LL=(0.0,0.0), UR=(1.0,1.0), quadrature_order=default_quadrature_order)
Create a regular P2 triangular grid on a rectangle. Does not use Delaunay triangulation internally.
CoherentStructures.regularP2DelaunayGrid
— FunctionregularP2DelaunayGrid(numnodes=(25,25), LL=(0.0,0.0), UR=(1.0,1.0), quadrature_order=default_quadrature_order)
Create a regular P2 triangular grid with numnodes
being the number of (non-interior) nodes in each direction.
CoherentStructures.regularQuadrilateralGrid
— FunctionregularP2QuadrilateralGrid(numnodes, LL, UR; [quadrature_order, PC=false])
Create a regular P1 quadrilateral grid on a rectangle. If PC==true
, use piecewise constant shape functions, otherwise use P1 Lagrange.
CoherentStructures.regularP2QuadrilateralGrid
— FunctionregularP2QuadrilateralGrid(numnodes=(25,25), LL=(0.0,0.0), UR=(1.0,1.0), quadrature_order=default_quadrature_order)
Create a regular P2 quadrilateral grid on a rectangle.
In 3D we have
CoherentStructures.regularTetrahedralGrid
— FunctionregularTetrahedralGrid(numnodes=(10,10,10), LL=(0.0,0.0,0.0), UR=(1.0,1.0,1.0), quadrature_order=default_quadrature_order3D)
Create a regular P1 tetrahedral grid on a Cuboid in 3D. Does not use Delaunay triangulation internally.
CoherentStructures.regularP2TetrahedralGrid
— FunctionregularP2TetrahedralGrid(numnodes=(10,10,10), LL=(0.0,0.0,0.0), UR=(1.0,1.0,1.0), quadrature_order=default_quadrature_order3D)
Create a regular P2 tetrahedral grid on a Cuboid in 3D. Does not use Delaunay triangulation internally.
All of these methods return a GridContext
object and a BoundaryData
object. The latter is only relevant when using a Delaunay grid with on_torus==true
.
CoherentStructures.GridContext
— Typemutable struct GridContext
Stores everything needed as "context" to be able to work on a FEM grid based on the Ferrite
package. Adds a point-locator API which facilitates plotting functions defined on the grid within Julia.
Fields
grid::FEM.Grid
,ip::FEM.Interpolation
,ip_geom::FEM.Interpolation
,qr::FEM.QuadratureRule
; see theFerrite
package.loc::PointLocator
: object used for point location on the grid.node_to_dof::Vector{Int}
: lookup table for dof index of a node (for Lagrange elements)dof_to_node::Vector{Int}
: inverse of nodetodofcell_to_dof::Vector{Int}
: lookup table for dof index of a cell (for piecewise constant elements)dof_to_cell::Vector{Int}
: inverse of celltodofnum_nodes
: number of nodes on the gridnum_cells
: number of elements (e.g. triangles,quadrilaterals, ...) on the gridn
: number of degrees of freedom (==num_nodes
for Lagrange Elements, and ==num_cells
for piecewise constant elements)quadrature_points::Vector{Vec{dim,Float64}}
: lists of all quadrature points on the grid, in a specific order.mass_weights::Vector{Float64}
: weighting for stiffness/mass matrices.spatialBounds
: if available, the corners of a bounding box of a domain. For regular grids, the bounds are tight.numberOfPointsInEachDirection
: for regular grids, how many (non-interior) nodes make up the regular grid.gridType
: a string describing what kind of grid this is (e.g. "regular triangular grid")no_precompute=false
: whether to precompute objects like quadrature points. Only enable this if you know what you are doing.
Boundary Conditions API
CoherentStructures.BoundaryData
— Typemutable struct BoundaryData
Represent (a combination of) homogeneous Dirichlet and periodic boundary conditions.
Fields
dbc_dofs
: list of dofs that should have homogeneous Dirichlet boundary conditions. Must be sorted.periodic_dofs_from
andperiodic_dofs_to
are bothVector{Int}
. The former must be strictly increasing, both must have the same length.periodic_dofs_from[i]
is identified withperiodic_dofs_to[i]
.periodic_dofs_from[i]
must be strictly larger thanperiodic_dofs_to[i]
. Multiple dofs can be identified with the same dof. If some dof is identified with another dof and one of them is indbc_dofs
, both points must be indbc_dofs
.
CoherentStructures.getHomDBCS
— FunctiongetHomDBCS(ctx, which="all")
Return BoundaryData
object corresponding to homogeneous Dirichlet boundary conditions for a set of facesets. which="all"
is shorthand for ["left", "right", "top", "bottom"]
.
CoherentStructures.undoBCS
— FunctionundoBCS(ctx, u, bdata)
Given a vector u
in dof order with boundary conditions applied, return the corresponding u
in dof order without the boundary conditions.
CoherentStructures.applyBCS
— FunctionapplyBCS(ctx_row, K, bdata_row; [ctx_col, bdata_col, bdata_row, add_vals=true])
Apply the boundary conditions from bdata_row
and bdata_col
to the sparse matrix K
. Only applies boundary conditions accross columns (rows) if bdata_row==nothing
(bdata_col==nothing
). If add_vals==true
(the default), then values in rows that should be cominbed are added. Otherwise, one of the rows is discarded and the values of the other are used.
Helper functions
CoherentStructures.dof2node
— Functiondof2node(ctx,u)
Interprets u
as an array of coefficients ordered in dof order, and reorders them to be in node order.
CoherentStructures.getDofCoordinates
— FunctiongetDofCoordinates(ctx,dofindex)
Return the coordinates of the node corresponding to the dof with index dofindex
.
CoherentStructures.evaluate_function_from_dofvals
— Functionevaluate_function_from_dofvals(ctx, dofvals, x_in; outside_value=NaN,project_in=false)
Evaluate the function at point x_in
with coefficients of dofs given by dofvals
(in dof-order). Return outside_value
if point is out of bounds. Project the point into the domain if project_in==true
. For evaluation at many points, or for many dofvals, the function evaluate_function_from_dofvals_multiple
is more efficient.
CoherentStructures.evaluate_function_from_node_or_cellvals
— Functionevaluate_function_from_node_or_cellvals(ctx, vals, x_in; outside_value=0, project_in=false)
Like evaluate_function_from_dofvals
, but the coefficients from vals
are assumed to be in node order. This is more efficient than evaluate_function_from_dofvals
.
CoherentStructures.evaluate_function_from_node_or_cellvals_multiple
— Functionevaluate_function_from_node_or_cellvals_multiple(ctx, vals, xin; is_diag=false, kwargs...)
Like evaluate_function_from_dofvals_multiple
but uses node- (or cell- if piecewise constant interpolation) ordering for vals
, which makes it slightly more efficient. If vals is a diagonal matrix, set is_diag
to true
for much faster evaluation.
CoherentStructures.nodal_interpolation
— Functionnodal_interpolation(ctx,f)
Perform nodal interpolation of a function. Returns a vector of coefficients in dof order.
CoherentStructures.getH
— FunctiongetH(ctx)
Return the mesh width of a regular grid.
Plotting API
FEM
CoherentStructures.plot_u
— Functionplot_u(ctx, dof_vals, nx=100, ny=100, LL, UR; bdata=nothing, kwargs...)
Plot the function with coefficients (in dof order, possible boundary conditions in bdata
) given by dof_vals
on the grid ctx
. The domain to be plotted on is given by ctx.spatialBounds
. The function is evaluated on a regular nx
by ny
grid, the resulting plot is a heatmap. Keyword arguments are passed down to plot_u_eulerian
, which this function calls internally.
CoherentStructures.plot_u_eulerian
— Functionplot_u_eulerian(ctx, dof_vals, inverse_flow_map, LL, UR[, nx, ny];
euler_to_lagrange_points=nothing,
only_get_lagrange_points=false,
z=nothing,
postprocessor=nothing,
bdata=nothing,
throw_errors=false,
kwargs...)
Plot a heatmap of a function in Eulerian coordinates, i.e., the pushforward of $f$. This is given by $f \circ \Phi^{-1}$, $f$ is a function defined on the grid ctx
, represented by coefficients given by dof_vals
(with possible boundary conditions given in bdata
).
The argument inverse_flow_map
is $\Phi^{-1}$.
The resulting plot is on a regular nx x ny
grid with lower left corner LL
and upper right corner UR
. Points that fall outside of the domain represented by ctx
are plotted as NaN
, which results in transparency.
One can pass values to be plotted directly by providing them in an array in the argument z
. postprocessor
can modify the values being plotted, return_scalar_field
results in these values being returned. See the source code for further details. Additional keyword arguments are passed to Plots.heatmap
.
Inverse flow maps are computed in parallel if there are multiple workers.
CoherentStructures.eulerian_videos
— Functioneulerian_videos(ctx, us, inverse_flow_map_t, t0, tf, nx, ny, nt, LL,UR, num_videos=1;
extra_kwargs_fun=nothing, ...)
Create num_videos::Int
videos in eulerian coordinates, i.e., where the time $t$ is varied, plot $f_i \circ \Phi_t^0$ for $f_1, \dots$.
Arguments
us(i,t)
is a vector of dofs to be plotted at timet
for thei
th video.inverse_flow_map_t(t,x)
is $\Phi_t^0(x)$.t0, tf
are initial and final time.LL
andUR
are the coordinates of the domain's corners.nx
,ny
, andnt
determine the number of points in each direction.extra_kwargs_fun(i,t)
can be used to provide additional keyword arguments to Plots.heatmap().
Additional kwargs are passed on to plot_eulerian_video
.
As much as possible is done in parallel.
Returns a Vector of iterables result
. Call Plots.animate(result[i])
to get an animation.
CoherentStructures.eulerian_video
— Functioneulerian_video(ctx, u, inverse_flow_map_t, t0, tf, nx, ny, nt, LL, UR; extra_kwargs_fun=nothing, ...)
Like eulerian_videos
, but u(t)
is a vector of dofs, and extra_kwargs_fun(t)
gives extra keyword arguments. Returns only one result, on which Plots.animate()
can be applied for an animation.
Other plotting utilities
CoherentStructures.plot_ftle
— Functionplot_ftle(odefun, p, tspan, LL, UR, nx, ny;
δ=1e-9, tolerance=1e-4, solver=OrdinaryDiffEq.Tsit5(),
existing_plot=nothing, flip_y=false, check_inbounds=always_true, pass_on_errors=false)
Make a heatmap of a FTLE field using finite differences. If existing_plot
is given a value, plot using heatmap!
on top of it. If flip_y
is true, then flip the y-coordinate (needed sometimes due to a bug in Plots). Points where check_inbounds(x[1], x[2], p) == false
are set to NaN
, i.e., plotted transparently. Unless pass_on_errors
is set to true
, errors from calculating FTLE values are caught and ignored.
Defaults
const default_quadrature_order=5
const default_solver = OrdinaryDiffEq.Tsit5()