Adaptive Mesh Refinement (AMR)

P4est

Ferrite's P4est implementation is based on these papers:

where almost everything is implemented in a serial way from the first paper. Only certain specific algorithms of the second paper are implemented and there is a lot of open work to include the iterators of the second paper. Look into the issues of Ferrite.jl and search for the AMR tag.

Important Concepts

One of the most important concepts, which everything is based on, are space filling curves (SFC). In particular, Z-order (also named Morton order, Morton space-filling curves) are used in p4est. The basic idea is that each Octant (in 3D) or quadrant (in 2D) can be encoded by 2 quantities

  • the level l
  • the lower left (front) coordinates xyz

Based on them a unique identifier, the morton index, can be computed. The mapping from (l, xyz) -> mortonidx(l,xyz) is bijective, meaning we can flip the approach and can construct each octant/quadrant solely by the mortonidx and a given level l.

The current implementation of an octant looks currently like this:

struct OctantBWG{dim, N, T} <: AbstractCell{RefHypercube{dim}}
    #Refinement level
    l::T
    #x,y,z \in {0,...,2^b} where (0 ≤ l ≤ b)}
    xyz::NTuple{dim,T}
end

whenever coordinates are considered we follow the z order logic, meaning x before y before z. Note that the acronym BWG stands for the initials of the surname of the authors of the p4est paper. The coordinates of an octant are described in the octree coordinate system which goes from $[0,2^b]^{dim}$. The parameter $b$ describes the maximum level of refinement and is set a priori. Another important aspect of the octree coordinate system is, that it is a discrete integer coordinate system. The size of an octant at the lowest possible level b is always 1, sometimes these octants are called atoms.

The octree is implemented as:

struct OctreeBWG{dim,N,T} <: AbstractAdaptiveCell{RefHypercube{dim}}
    leaves::Vector{OctantBWG{dim,N,T}}
    #maximum refinement level
    b::T
    nodes::NTuple{N,Int}
end

So, only the leaves of the tree are stored and not any intermediate refinement level. The field b is the maximum refinement level and is crucial. This parameter determines the size of the octree coordinate system. The octree coordinate system is the coordinate system in which the coordinates xyz of any octant::OctantBWG are described.

Examples

Let's say the maximum octree level is $b=3$, then the coordinate system is in 2D $[0,2^3]^2 = [0, 8]^2$. So, our root is on level 0 of size 8 and has the lower left coordinates (0,0)

# different constructors available, first one OctantBWG(dim,level,mortonid,maximumlevel)
# other possibility by giving directly level and a tuple of coordinates OctantBWG(level,(x,y))
julia> dim = 2; level = 0; maximumlevel=3
julia> oct = OctantBWG(dim,level,1,maximumlevel)
OctantBWG{2,4,4}
   l = 0
   xy = 0,0

The size of octants at a specific level can be computed by a simple operation

julia> Ferrite.AMR._compute_size(#=b=#3,#=l=#0)
8

This computation is based on the relation $\text{size}=2^{b-l}$. Now, to fully understand the octree coordinate system we go a level down, i.e. we cut the space in $x$ and $y$ in half. This means, that the octants are now of size $2^{3-1}=4$. Construct all level 1 octants based on mortonid:

# note the arguments are dim,level,mortonid,maximumlevel
julia> dim = 2; level = 1; maximumlevel = 3
julia> oct = Ferrite.AMR.OctantBWG(dim, level, 1, maximumlevel)
OctantBWG{2,4,4}
   l = 1
   xy = 0,0

julia> oct = Ferrite.AMR.OctantBWG(dim, level, 2, maximumlevel)
OctantBWG{2,4,4}
   l = 1
   xy = 4,0

julia> oct = Ferrite.AMR.OctantBWG(dim, level, 3, maximumlevel)
OctantBWG{2,4,4}
   l = 1
   xy = 0,4

julia> oct = Ferrite.AMR.OctantBWG(dim, level, 4, maximumlevel)
OctantBWG{2,4,4}
   l = 1
   xy = 4,4

So, the morton index is on one specific level just a x before y before z "cell" or "element" identifier

x-----------x-----------x
|           |           |
|           |           |
|     3     |     4     |
|           |           |
|           |           |
x-----------x-----------x
|           |           |
|           |           |
|     1     |     2     |
|           |           |
|           |           |
x-----------x-----------x

The operation to compute octants/quadrants is cheap, since it is just bitshifting. An important aspect of the morton index is that it's only consecutive on one level in this specific implementation. Note that other implementation exists that incorporate the level integer within the morton identifier and by that have a unique identifier across levels. If you have a tree like this below:

x-----------x-----------x
|           |           |
|           |           |
|     9     |    10     |
|           |           |
|           |           |
x-----x--x--x-----------x
|     |6 |7 |           |
|  3  x--x--x           |
|     |4 |5 |           |
x-----x--x--x     8     |
|     |     |           |
|  1  |  2  |           |
x-----x-----x-----------x

you would maybe think this is the morton index, but strictly speaking it is not. What we see above is just the leafindex, i.e. the index where you find this leaf in the leaves array of OctreeBWG. Let's try to construct the lower right based on the morton index on level 1

julia> o = Ferrite.OctantBWG(2,1,8,3)
ERROR: AssertionError: m ≤ (one(T) + one(T)) ^ (dim * l) # 8 > 4
Stacktrace:
 [1] OctantBWG(dim::Int64, l::Int32, m::Int32, b::Int32)
   @ Ferrite ~/repos/Ferrite.jl/src/Adaptivity/AdaptiveCells.jl:23
 [2] OctantBWG(dim::Int64, l::Int64, m::Int64, b::Int64)
   @ Ferrite ~/repos/Ferrite.jl/src/Adaptivity/AdaptiveCells.jl:43
 [3] top-level scope
   @ REPL[93]:1

The assertion expresses that it is not possible to construct a morton index 8 octant, since the upper bound of the morton index is 4 on level 1. The morton index of the lower right cell is 2 on level 1.

julia> o = Ferrite.AMR.OctantBWG(2,1,2,3)
OctantBWG{2,4,4}
   l = 1
   xy = 4,0

Octant operation

There are multiple useful functions to compute information about an octant e.g. parent, childs, etc.

Ferrite.AMR.mortonFunction

From Burstedde et al. [17];

The octant coordinates are stored as integers of a fixed number b of bits, where the highest (leftmost) bit represents the first vertical level of the octree (counting the root as level zero), the second highest bit the second level of the octree, and so on.

A morton index can thus be constructed by interleaving the integer bits (2D): $m(\text{Oct}) := (y_b,x_b,y_{b-1},x_{b-1},...y_0,x_0)_2$ further we assume the following

Due to the two-complement representation of integers in practically all current hardware, where the highest digit denotes the negated appropriate power of two, bitwise operations as used, for example, in Algorithm 1 yield the correct result even for negative coordinates.

also from Burstedde et al. [17]

TODO: use LUT method from https://www.forceflow.be/2013/10/07/morton-encodingdecoding-through-bit-interleaving-implementations/

source
Ferrite.AMR.childrenFunction
children(octant::OctantBWG{dim,N,T}, b::Integer) -> NTuple{M,OctantBWG}

Computes all childern of octant

source
Ferrite.AMR.verticesFunction
vertices(octant::OctantBWG{dim}, b::Integer)

Computes all vertices of a given octant. Each vertex is encoded within the octree coordinates i.e. by integers.

source
Ferrite.AMR.edgesFunction
edges(octant::OctantBWG{dim}, b::Integer)

Computes all edges of a given octant. Each edge is encoded within the octree coordinates i.e. by integers. Further, each edge consists of two three-dimensional integer coordinates.

source
Ferrite.AMR.facesFunction
faces(octant::OctantBWG{dim}, b::Integer)

Computes all faces of a given octant. Each face is encoded within the octree coordinates i.e. by integers. Further, each face consists of either two two-dimensional integer coordinates or four three-dimensional integer coordinates.

source
Ferrite.AMR.transform_pointBWGFunction
transform_pointBWG(forest, vertices) -> Vector{Vec{dim}}
transform_pointBWG(forest::ForestBWG{dim}, k::Integer, vertex::NTuple{dim,T}) where {dim,T} -> Vec{dim}

Transformation of a octree coordinate system point vertex (or a collection vertices) to the corresponding physical coordinate system.

source

Intraoctree operation

Intraoctree operation stay within one octree and compute octants that are attached in some way to a pivot octant o. These operations are useful to collect unique entities within a single octree or to compute possible neighbors of o. Burstedde et al. [17] Algorithm 5, 6, and 7 describe the following intraoctree operations:

Ferrite.AMR.corner_neighborFunction
corner_neighbor(octant::OctantBWG, c::Integer, b::Integer)

Computes the corner neighbor octant which is only connected by the corner c to octant

source
Ferrite.AMR.edge_neighborFunction
edge_neighbor(octant::OctantBWG, e::Integer, b::Integer)

Computes the edge neighbor octant which is only connected by the edge e to octant.

source
Ferrite.AMR.facet_neighborFunction
facet_neighbor(octant::OctantBWG{dim,N,T}, f::T, b::T=_maxlevel[2]) -> OctantBWG{dim,N,T}

Intraoctree face neighbor for a given faceindex f (in p4est, i.e. z order convention) and specified maximum refinement level b. Implements Algorithm 5 of Burstedde et al. [17].

x-------x-------x
|       |       |
|   3   |   4   |
|       |       |
x-------x-------x
|       |       |
o   1   *   2   |
|       |       |
x-------x-------x

Consider octant 1 at xyz=(0,0), a maximum refinement level of 1 and faceindex 2 (marked as *). Then, the computed face neighbor will be octant 2 with xyz=(1,0). Note that the function is not sensitive in terms of leaving the octree boundaries. For the above example, a query for face index 1 (marked as o) will return an octant outside of the octree with xyz=(-1,0).

source
Ferrite.AMR.possibleneighborsFunction
possibleneighbors(o::OctantBWG{2},l,b;insidetree=true)

Returns a list of possible neighbors, where the first four are corner neighbors that are exclusively connected via a corner. The other four possible neighbors are face neighbors.

source
possibleneighbors(o::OctantBWG{3},l,b;insidetree=true)

Returns a list of possible neighbors, where the first eight are corner neighbors that are exclusively connected via a corner. After the first eight corner neighbors, the 6 possible face neighbors follow and after them, the edge neighbors.

source

Interoctree operation

Interoctree operation are in contrast to intraoctree operation by computing octant transformations across different octrees. Thereby, one needs to account for topological connections between the octrees as well as possible rotations of the octrees. Burstedde et al. [17] Algorithm 8, 10, and 12 explain the algorithms that are implemented in the following functions:

Ferrite.AMR.transform_cornerFunction
transform_corner(forest,k,c',oct,inside::Bool)
transform_corner(forest,v::VertexIndex,oct,inside::Bool)

Algorithm 12 but with flipped logic in Burstedde et al. [17] to transform corner into different octree coordinate system Implements flipped logic in the sense of pushing the Octant oct through vertex v and stays within octree coordinate system k.

source
Ferrite.AMR.transform_edgeFunction
transform_edge(forest,k,e,oct,inside::Bool)
transform_edge(forest,e::Edgeindex,oct,inside::Bool)

Algorithm 10 in Burstedde et al. [17] to transform cedge into different octree coordinate system but reversed logic. See transform_edge_remote with logic from paper. In this function we stick to the coordinate system of the pivot tree k and transform an octant through edge e into this k-th octree coordinate system.

source
Ferrite.AMR.transform_facetFunction
transform_facet(forest::ForestBWG, k', f', o::OctantBWG) -> OctantBWG
transform_facet(forest::ForestBWG, f'::FacetIndex, o::OctantBWG) -> OctantBWG

Interoctree coordinate transformation of an given octant o that lies outside of the pivot octree k, namely in neighbor octree k'. However, the coordinate of o is given in octree coordinates of k. Thus, this algorithm implements the transformation of the octree coordinates of o into the octree coordinates of k'. Useful in order to check whether or not a possible neighbor exists in a neighboring octree. Implements Algorithm 8 of Burstedde et al. [17].

x-------x-------x
|       |       |
|   3   |   4   |
|       |       |
x-------x-------x
|       |       |
|   1   *   2   |
|       |       |
x-------x-------x

Consider 4 octrees with a single leaf each and a maximum refinement level of 1 This function transforms octant 1 into the coordinate system of octant 2 by specifying k=1 and f=2. While from the perspective of octree coordinates k=2 octant 1 is at xyz=(-2,0), the returned and transformed octant is located at xyz=(0,0)

source

Note that we flipped the input and to expected output logic a bit to the proposed algorithms of the paper. However, the original proposed versions are implemented as well in:

Ferrite.AMR.transform_corner_remoteFunction
transform_corner_remote(forest,k,c',oct,inside::Bool)
transform_corner_remote(forest,v::VertexIndex,oct,inside::Bool)

Algorithm 12 in Burstedde et al. [17] to transform corner into different octree coordinate system. Follows exactly the version of the paper by taking oct and looking from the neighbor octree coordinate system (neighboring to k,v) at oct.

source
Ferrite.AMR.transform_edge_remoteFunction
transform_edge_remote(forest,k,e,oct,inside::Bool)
transform_edge_remote(forest,e::Edgeindex,oct,inside::Bool)

Algorithm 10 in Burstedde et al. [17] to transform edge into different octree coordinate system. This function looks at the octant from the octree coordinate system of the neighbor that can be found at (k,e)

source
Ferrite.AMR.transform_facet_remoteFunction
transform_facet_remote(forest::ForestBWG, k::T1, f::T1, o::OctantBWG{dim,N,T2}) -> OctantBWG{dim,N,T1,T2}
transform_facet_remote(forest::ForestBWG, f::FacetIndex, o::OctantBWG{dim,N,T2}) -> OctantBWG{dim,N,T2}

Interoctree coordinate transformation of an given octant o to the face-neighboring of octree k by virtually pushing os coordinate system through ks face f. Implements Algorithm 8 of Burstedde et al. [17].

x-------x-------x
|       |       |
|   3   |   4   |
|       |       |
x-------x-------x
|       |       |
|   1   *   2   |
|       |       |
x-------x-------x

Consider 4 octrees with a single leaf each and a maximum refinement level of 1 This function transforms octant 1 into the coordinate system of octant 2 by specifying k=2 and f=1. While in the own octree coordinate system octant 1 is at xyz=(0,0), the returned and transformed octant is located at xyz=(-2,0)

source

despite being never used in the code base so far.