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}
endwhenever 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}
endSo, 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,0The size of octants at a specific level can be computed by a simple operation
julia> Ferrite.AMR._compute_size(#=b=#3,#=l=#0)
8This 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,4So, 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-----------xThe 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-----------xyou 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]:1The 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,0Octant operation
There are multiple useful functions to compute information about an octant e.g. parent, childs, etc.
Ferrite.AMR.isancestor — Functionisancestor(o1,o2,b) -> BoolIs o2 an ancestor of o1
Ferrite.AMR.morton — FunctionFrom 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/
Ferrite.AMR.children — Functionchildren(octant::OctantBWG{dim,N,T}, b::Integer) -> NTuple{M,OctantBWG}Computes all childern of octant
Ferrite.AMR.vertices — Functionvertices(octant::OctantBWG{dim}, b::Integer)Computes all vertices of a given octant. Each vertex is encoded within the octree coordinates i.e. by integers.
Ferrite.AMR.edges — Functionedges(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.
Ferrite.AMR.faces — Functionfaces(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.
Ferrite.AMR.transform_pointBWG — Functiontransform_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.
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_neighbor — Functioncorner_neighbor(octant::OctantBWG, c::Integer, b::Integer)Computes the corner neighbor octant which is only connected by the corner c to octant
Ferrite.AMR.edge_neighbor — Functionedge_neighbor(octant::OctantBWG, e::Integer, b::Integer)Computes the edge neighbor octant which is only connected by the edge e to octant.
Ferrite.AMR.facet_neighbor — Functionfacet_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-------xConsider 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).
Ferrite.AMR.possibleneighbors — Functionpossibleneighbors(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.
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.
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_corner — Functiontransform_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.
Ferrite.AMR.transform_edge — Functiontransform_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.
Ferrite.AMR.transform_facet — Functiontransform_facet(forest::ForestBWG, k', f', o::OctantBWG) -> OctantBWG
transform_facet(forest::ForestBWG, f'::FacetIndex, o::OctantBWG) -> OctantBWGInteroctree 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-------xConsider 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)
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_remote — Functiontransform_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.
Ferrite.AMR.transform_edge_remote — Functiontransform_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)
Ferrite.AMR.transform_facet_remote — Functiontransform_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-------xConsider 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)
despite being never used in the code base so far.