Conversation
…nd corner_neighbor need tests
…hanges. should be correct imho
…x corresponds to child_id
…corner and edge neighbor
|
Found 2 more issues in 3D with Maxi. Last commit has test coverage for them.
|
|
Failing analytical example: |
| end | ||
|
|
||
| function Ferrite.add!(ch::ConstraintHandler{<:DofHandler{<:Any,<:NonConformingGrid}}, cc::ConformityConstraint) | ||
| @assert length(ch.dh.field_names) == 1 "Multiple fields not supported yet." |
There was a problem hiding this comment.
For a dofhandler with two scalar fields, the present function can apply the conformity constraint for hanging nodes. So I think that the assert can be replaced with a warning.
| @assert length(ch.dh.field_names) == 1 "Multiple fields not supported yet." | |
| @assert length(ch.dh.field_names) == 1 "Multiple fields not supported yet." |
Four bugs in the inter-octree branch of hangingnodes(): - Use pface_i directly for facet_neighborhood lookup instead of looping over rootfaces with ri - Check any rootface contains the parent face, not just one - Pass pface_i (not ri) to rotation_permutation for correct 3D face rotation - Compare vertex index c' against ci (integer) not c (coordinate tuple), and use parent edges instead of interoctree neighbor edges for edge hanging node detection Updates test expectations from 58 to 59 conformity entries for both the combined and combined+rotated test cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Manufactured solution: Gaussian ring exp(-((|x|-0.5)/ε)²) with ε=0.02, naturally zero at boundary so homogeneous Dirichlet BCs are correct - ZZ error estimator: L2-project raw flux to smooth nodal field, compare recovered vs raw flux per cell - Dörfler marking: mark smallest cell set accounting for θ=0.5 of total error, replacing the previous absolute threshold - CG solver (IterativeSolvers.jl) instead of direct solve for scalability - Add Literate.jl comments explaining the tutorial - Enable initial uniform refinement in elasticity adaptivity example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…and update adaptive heat for doc build
|
3D seems to work now, need an additional real life test for rotated elements or maybe adjust the example here: https://ferrite-fem.github.io/Ferrite.jl/previews/PR780/tutorials/heat_adaptivity/ and rotate a few elements before the adaptive loop begins |
| u, dh, ch, cv = solve(transfered_grid) | ||
|
|
||
| ## Step 1: Compute the raw FE flux σ_h = ∇u_h at each quadrature point | ||
| σ_gp = Vector{Vector{Vec{3, Float64}}}() |
There was a problem hiding this comment.
Can we use a flat vector here? This is not really performant.
There was a problem hiding this comment.
TODO dump into Ferrite.asset_url before merging
There was a problem hiding this comment.
Is this intended to be dumped in addition to the geo file?
| dh.vertexdicts[fi] = zeros(Int, nnodes) | ||
| dh.edgedicts[fi] = Dict{Tuple{Int, Int}, Int}() | ||
| dh.facedicts[fi] = Dict{NTuple{3, Int}, Int}() |
| # Maps from entity to dofs | ||
| # `vertexdict` keeps track of the visited vertices. The first dof added to vertex v is | ||
| # stored in vertexdict[v]. | ||
| vertexdicts::Vector{Vector{Int}} | ||
| # `edgedict` keeps track of the visited edges, this will only be used for a 3D problem. | ||
| # An edge is uniquely determined by two global vertices, with global direction going | ||
| # from low to high vertex number. | ||
| edgedicts::Vector{Dict{Tuple{Int, Int}, Int}} | ||
| # `facedict` keeps track of the visited faces. We only need to store the first dof we | ||
| # add to the face since currently more dofs per face isn't supported. In | ||
| # 2D a face (i.e. a line) is uniquely determined by 2 vertices, and in 3D a face (i.e. a | ||
| # surface) is uniquely determined by 3 vertices. | ||
| facedicts::Vector{Dict{NTuple{3, Int}, Int}} |
There was a problem hiding this comment.
Should we add a second dof handler type to not strain problems not using AMR?
There was a problem hiding this comment.
So new struct with the same fields and then use the default dispatches und AbstractDofHandler and later if intervention is needed dispatch on the new type?
There was a problem hiding this comment.
What about having an additional field collecting all of these in a special struct? Then that internal struct can be documented in devdocs with what is now described in the code comments, which can help explaining the dof-distribution better as well?
That field can be nothing or the content can be empty if not used for the regular cases?
There was a problem hiding this comment.
But how would we proceed with that structure? It could be needed in any code that handles the DofHandler. It's difficult for me to imagine an interface where this struct is handed over whenever it's needed, so that it can be reused for other cases. Don't you agree?
Ideally, the ForestBWG would provide its own iterators for volumes, facets, edges and nodes, which would then be used optimally for the tailored DofHandler.
There was a problem hiding this comment.
I was more thinking that for the dof-distribution, we could refactor the dicts by introducing
struct EntityDicts
vertices::Vector{Vector{Int}}
edges::Vector{Dict{NTuple{2, Int}, Int}}
faces::Vector{Dict{NTuple{3, Int}, Int}}
endwhich potentially makes it easier to understand the dof-distribution. For this PR, it would help by reducing the extra fields in the dofhandler, by only adding the field entitydicts::EntityDicts, changing the DofHandler.jl#L364-L395 code to
numfields = length(dh.field_names)
+ resize!(dh.entitydicts.vertices, numfields)
+ resize!(dh.entitydicts.edges, numfields)
+ resize!(dh.entitydicts.faces, numfields)
- # NOTE: Maybe it makes sense to store *Index in the dicts instead.
-
- # `vertexdict` keeps track of the visited vertices. The first dof added to vertex v is
- # stored in vertexdict[v].
- # TODO: No need to allocate this vector for fields that don't have vertex dofs
- vertexdicts = [zeros(Int, getnnodes(get_grid(dh))) for _ in 1:numfields]
-
- # `edgedict` keeps track of the visited edges.
- # An edge is uniquely determined by two global vertices, with global direction going
- # from low to high vertex node number, see sortedge
- edgedicts = [Dict{NTuple{2, Int}, Int}() for _ in 1:numfields]
-
- # `facedict` keeps track of the visited faces. We only need to store the first dof -we
- # add to the face since currently more dofs per face isn't supported.
- # A face is uniquely determined by 3 vertex nodes, see sortface
- facedicts = [Dict{NTuple{3, Int}, Int}() for _ in 1:numfields]
# Set initial values
nextdof = 1 # next free dof to distribute
@debug println("\n\nCreating dofs\n")
for (sdhi, sdh) in pairs(dh.subdofhandlers)
nextdof = _close_subdofhandler!(
dh,
sdh,
sdhi, # TODO: Store in the SubDofHandler?
nextdof,
+ dh.entitydicts,
- vertexdicts,
- edgedicts,
- facedicts,There was a problem hiding this comment.
But if more things should be different, perhaps just having a
struct ForestBWGDofHandler{DH <: DofHandler, ...} <: AbstractDofHandler
basedh::DH
...
endis easier and then just forwarding all methods that don't change to basedh?
| return Grid(cells, nodes, facetsets = facetsets) | ||
| end | ||
|
|
||
| function generate_simple_disc_grid(::Type{Quadrilateral}, n; radius = 1.0) |
There was a problem hiding this comment.
Is this used somewhere in this PR?
| end | ||
| M = _assemble_L2_matrix(proj.dh, proj.ch, proj.qrs_lhs) | ||
| if proj.ch !== nothing | ||
| apply!(M.data, proj.ch) |
There was a problem hiding this comment.
Can't we apply! directly on the Symmetric matrix?
| function _assemble_L2_matrix(dh::DofHandler, ch::ConstraintHandler, qrs_lhs::Vector{<:QuadratureRule}) | ||
| M = Symmetric(allocate_matrix(dh, ch)) |
There was a problem hiding this comment.
Should work for both cases?
| function _assemble_L2_matrix(dh::DofHandler, ch::ConstraintHandler, qrs_lhs::Vector{<:QuadratureRule}) | |
| M = Symmetric(allocate_matrix(dh, ch)) | |
| function _assemble_L2_matrix(dh::DofHandler, ch::Union{ConstraintHandler, Nothing}, qrs_lhs::Vector{<:QuadratureRule}) | |
| M = Symmetric(allocate_matrix(dh, ch)) |
Or alternatively allocate_matrix outside and make mutating to avoid having to pass the constraint handler to this function?
| for (i, col) in enumerate(eachcol(f)) | ||
| apply!(col, ch) | ||
| u = proj.M_cholesky \ col | ||
| apply!(u, ch) | ||
| projected_vals[:, i] = u | ||
| end |
There was a problem hiding this comment.
Maybe microoptimization, but we could avoid allocating twice by (probably requires using LinearAlgebra: ldiv!)
| for (i, col) in enumerate(eachcol(f)) | |
| apply!(col, ch) | |
| u = proj.M_cholesky \ col | |
| apply!(u, ch) | |
| projected_vals[:, i] = u | |
| end | |
| for (f_i, u_i) in zip(eachcol(f), eachcol(projected_vals)) | |
| apply!(f_i, ch) | |
| ldiv!(u_i, proj.M_cholesky, f_i) | |
| apply!(u_i, ch) | |
| end |
KnutAM
left a comment
There was a problem hiding this comment.
Cool to see this PR alive again 🚀
|
I think a roadmap would be nice before merging, there are multiple things that can be adressed
What do you think should be addressed beside the review points you guys made already? |
I also thought already about whether the adaptive dof handler should have another field for the conformity constraints. This would also pave the way for p-adaptivity. |
AMR Performance Analysis and Optimization PlanBenchmark Results SummaryTested on Julia 1.11.4, 2026-02-11. Uniformly refined grids with 25% additional adaptive refinement. Time Breakdown (Largest Cases)2D (16x16 base, 3 levels, 28672 cells):
3D (8x8x8 base, 2 levels, 90112 cells):
creategrid Phase Breakdown (3D, 11264 cells)
balancetree Internal Breakdown (3D, 512 leaves/tree)
Identified Bottlenecks (Priority Order)1.
|
| Component | Status | Location | Notes |
|---|---|---|---|
OctantBWG ordering (Alg 2.1) |
Done | BWG.jl:107 | Morton-based isless |
split_array (Alg 3.3) |
Done | BWG.jl:367 | Binary search partitioning of leaf arrays |
search (Alg 3.1) |
Partial | BWG.jl:390 | Framework exists but match callback is a stub (has println debug, always returns false) |
find_range_boundaries (Alg 4.2) |
Done | BWG.jl:261 | Recursive boundary identification, but no tests |
boundaryset (Fig 4.1) |
Done | BWG.jl:214-253 | 2D and 3D boundary tables |
isrelevant (Alg 5.1) |
Stub | BWG.jl:296 | Always returns true (serial-only placeholder) |
ancestor_id (Alg 3.2) |
Done | BWG.jl:1289 | Generalized child_id for arbitrary levels |
descendants |
Done | BWG.jl:1317 | First/last descendant computation |
| Inter-tree transforms | Done | BWG.jl:1508-1734 | transform_facet, transform_edge, transform_corner |
| Topology iterator | Not started | -- | The core iter_volume/iter_face/iter_edge/iter_corner recursive descent |
| LNodes | Not started | -- | Node numbering via iterator callbacks |
Why the iterator approach is the right path forward
The current implementation has a fundamental architectural problem: it uses flat iteration over all leaves for every operation (node assignment, inter-octree merging, hanging node detection, facetset reconstruction). This leads to:
- O(n) linear scans where O(log n) descent would suffice (the
findfirstcalls inhangingnodes) - Redundant work: the same topological relationships are rediscovered independently in each phase of
creategrid - Complex inter-octree logic duplicated across
creategridPhase 2,hangingnodes,balanceforest!, andreconstruct_facetsets-- each has its own face/edge/corner neighbor iteration logic - No reuse of the sorted structure: leaves are sorted in Morton order, but this is barely exploited (only in
split_array, which is implemented but unused in the hot paths)
The IBWG2015 topology iterator would solve all of these simultaneously:
| Current Problem | Iterator Solution |
|---|---|
hangingnodes O(n^2) scans |
Hanging status detected for free during face/edge descent |
creategrid 5 separate phases |
Single pass: callbacks assign nodes + detect hanging + build cells |
| Duplicated inter-octree transform logic | Inter-tree face/edge/corner descent handles transforms once |
balanceforest! repeated full rebalance |
Could use iterator for targeted neighbor checking |
reconstruct_facetsets separate pass |
Face callback naturally identifies boundary faces |
findfirst on unsorted leaf arrays |
split_array gives O(log n) access to any sub-range |
Argument: Iterator vs. quick fixes
There are two paths forward:
Path A: Quick fixes (Phases 1-3 from the optimization plan above)
- Replace
findfirstwithsearchsortedfirstin ~10 locations - Add
Setfor parent checks inbalancetree - Effort: ~1-2 days. Speedup: ~20-50x on the worst cases.
- Limitation: Still has the architectural problem of multiple passes, duplicated logic, and O(n log n) instead of O(n) for the overall pipeline.
Path B: Implement the IBWG2015 topology iterator
- Build
forest_iterate(forest, volume_cb, face_cb, edge_cb, corner_cb) - Rewrite
creategridas a set of callbacks (LNodes pattern) - Effort: ~2-4 weeks. Speedup: Asymptotically optimal O(n), plus cleaner architecture.
- Benefit: The iterator becomes the single traversal primitive for everything -- grid creation, hanging node detection, error estimation, solution transfer, load balancing, visualization. Every future feature benefits.
Recommendation: Do both, in order. Apply the quick fixes (Path A) first to get immediate relief -- the searchsortedfirst changes are mechanical and safe. Then implement the iterator (Path B) as the long-term solution. The quick fixes will also serve as a correctness reference: you can verify the iterator produces identical results.
Detailed Plan for the Topology Iterator Implementation
Step 1: Core forest_iterate skeleton
Implement the recursive descent framework:
struct ForestIterateCallbacks{V,F,E,C}
volume::V # (info::VolumeInfo) -> nothing
face::F # (info::FaceInfo) -> nothing
edge::E # (info::EdgeInfo) -> nothing (3D only)
corner::C # (info::CornerInfo) -> nothing
end
struct VolumeInfo{dim,N,T}
treeid::Int
quadrant::OctantBWG{dim,N,T}
leafid::Int # index into flattened leaf array
end
struct FaceSide{dim,N,T}
treeid::Int
face_local::Int
is_hanging::Bool
# if !is_hanging: single octant
# if is_hanging: 2^(dim-1) smaller octants on the hanging side
quadrants::Union{
Tuple{OctantBWG{dim,N,T}}, # full (non-hanging)
NTuple{M,OctantBWG{dim,N,T}} where M # hanging (2 in 2D, 4 in 3D)
}
leafids::Vector{Int}
end
struct FaceInfo{dim,N,T}
tree_boundary::Bool
orientation::Int
sides::Tuple{FaceSide{dim,N,T}, FaceSide{dim,N,T}}
end
# Similarly for EdgeInfo (3D) and CornerInfo
function forest_iterate(forest::ForestBWG{dim}, callbacks) where {dim}
# Phase 1: Volume + intra-tree interfaces
leaf_offset = 0
for (k, tree) in enumerate(forest.cells)
_iter_volume(root(dim), tree.leaves, 1, length(tree.leaves),
k, tree.b, leaf_offset, callbacks)
leaf_offset += length(tree.leaves)
end
# Phase 2: Inter-tree faces
facet_neighborhood = get_facet_facet_neighborhood(forest)
for k in 1:length(forest.cells)
for f in 1:(2*dim)
# ... setup face iteration between trees k and k'
# ... call _iter_face with leaves from both trees
end
end
# Phase 3: Inter-tree edges (3D only)
# Phase 4: Inter-tree corners
endStep 2: Recursive volume descent with split_array
function _iter_volume(octant, leaves, lo, hi, treeid, b, leaf_offset, callbacks)
n = hi - lo + 1
if n == 0
return
end
if n == 1 && leaves[lo] == octant
# Leaf reached -- call volume callback
callbacks.volume(VolumeInfo(treeid, octant, leaf_offset + lo))
return
end
# Split leaves among children using split_array
children_ranges = _split_ranges(leaves, lo, hi, octant, b)
child_octants = children(octant, b)
for (ci, (clo, chi)) in enumerate(children_ranges)
_iter_volume(child_octants[ci], leaves, clo, chi, treeid, b, leaf_offset, callbacks)
end
# Intra-octant face interfaces between children
for (fi, (ci, cj)) in enumerate(CHILD_FACE_PAIRS[dim])
_iter_face(child_octants[ci], child_octants[cj],
leaves, children_ranges[ci], children_ranges[cj],
treeid, treeid, b, b, fi, false, 0, callbacks)
end
# Intra-octant edges (3D) and corners between children
# ...
endStep 3: Face descent with hanging detection
function _iter_face(oct_L, oct_R, leaves, (lo_L, hi_L), (lo_R, hi_R),
tree_L, tree_R, b_L, b_R, face_idx, tree_boundary, orientation,
callbacks)
n_L = hi_L - lo_L + 1
n_R = hi_R - lo_R + 1
is_leaf_L = (n_L == 1 && leaves_L[lo_L] == oct_L) # left side is a single leaf
is_leaf_R = (n_R == 1 && leaves_R[lo_R] == oct_R) # right side is a single leaf
if is_leaf_L && is_leaf_R
# Conforming face -- both sides are leaves at same level
callbacks.face(FaceInfo(tree_boundary, orientation,
(FaceSide(tree_L, ..., false, ...), FaceSide(tree_R, ..., false, ...))))
return
end
if is_leaf_L && !is_leaf_R
# HANGING face: left is coarser, right has 2^(dim-1) children on the face
# The children on the right that touch the face are exactly the hanging nodes!
callbacks.face(FaceInfo(tree_boundary, orientation,
(FaceSide(tree_L, ..., false, ...), FaceSide(tree_R, ..., true, ...))))
return
end
# ... symmetric case for is_leaf_R ...
# Neither side is a leaf -- descend both sides
# Split and recurse into matching child pairs
endStep 4: LNodes as iterator callbacks
mutable struct LNodesState{dim}
next_nodeid::Int
cell_nodes::Vector{NTuple{N,Int}} where N # node ids per cell
hanging_nodes::Dict{Int, Vector{Int}} # constrained -> constrainers
node_coordinates::Vector{Vec{dim,Float64}}
end
function lnodes_volume_callback(info::VolumeInfo, state::LNodesState)
# Assign interior node ids for this leaf
# (For linear elements, vertices are assigned here)
end
function lnodes_face_callback(info::FaceInfo, state::LNodesState)
# If conforming: merge shared face nodes between the two sides
# If hanging: record hanging node constraints
# The hanging nodes on the finer side are constrained by
# the face nodes on the coarser side
end
function lnodes_edge_callback(info::EdgeInfo, state::LNodesState)
# Same pattern for edges
end
function lnodes_corner_callback(info::CornerInfo, state::LNodesState)
# Same pattern for corners
endStep 5: Rewrite creategrid using the iterator
function creategrid(forest::ForestBWG{dim}) where {dim}
state = LNodesState(...)
forest_iterate(forest, ForestIterateCallbacks(
(info) -> lnodes_volume_callback(info, state),
(info) -> lnodes_face_callback(info, state),
(info) -> lnodes_edge_callback(info, state),
(info) -> lnodes_corner_callback(info, state),
))
# Convert state into NonConformingGrid
return NonConformingGrid(state.cells, state.nodes, ...,
conformity_info = state.hanging_nodes)
endSummary of implementation status and what to build
ALREADY IMPLEMENTED (building blocks):
[x] OctantBWG with Morton ordering
[x] split_array (Algorithm 3.3) -- efficient O(log n) leaf partitioning
[x] search framework (Algorithm 3.1) -- but match callback is a stub
[x] find_range_boundaries (Algorithm 4.2) -- untested
[x] boundaryset (Fig 4.1) -- determines which faces/edges/corners an octant touches
[x] ancestor_id (Algorithm 3.2)
[x] descendants, children, parent, siblings
[x] Inter-tree transforms (transform_facet, transform_edge, transform_corner)
[x] Orientation computation (compute_face_orientation, compute_edge_orientation)
TO BE IMPLEMENTED:
[ ] forest_iterate -- top-level orchestration (intra-tree + inter-tree)
[ ] _iter_volume -- recursive volume descent using split_array
[ ] _iter_face -- recursive face descent with hanging detection
[ ] _iter_edge -- recursive edge descent (3D)
[ ] _iter_corner -- corner callback dispatch
[ ] Info structs (VolumeInfo, FaceInfo, EdgeInfo, CornerInfo)
[ ] CHILD_FACE_PAIRS / CHILD_EDGE_PAIRS tables (which children share a face/edge)
[ ] LNodes callbacks (volume, face, edge, corner)
[ ] Rewritten creategrid using the iterator
[ ] Tests comparing old creategrid output with new iterator-based output
Quick-Fix Optimization Plan (Do First)
These are mechanical fixes that give immediate speedup while the iterator is being developed.
Phase 1: Fix hangingnodes() -- Expected 10-50x speedup on creategrid
Strategy: Replace findfirst(x -> x == candidate, tree.leaves) with O(log n) binary search.
Since tree.leaves is maintained in Morton order (sorted), we can use searchsortedfirst:
# BEFORE (O(n)):
neighbor_candidate_idx = findfirst(x -> x == neighbor_candidate, tree.leaves)
# AFTER (O(log n)):
idx = searchsortedfirst(tree.leaves, neighbor_candidate)
neighbor_candidate_idx = (idx <= length(tree.leaves) && tree.leaves[idx] == neighbor_candidate) ? idx : nothingPhase 2: Fix balance_face/corner/edge -- Expected 5-10x speedup on balanceforest
Strategy: Replace in tree.leaves linear scans with binary search.
function leaf_exists(tree, octant)
idx = searchsortedfirst(tree.leaves, octant)
return idx <= length(tree.leaves) && tree.leaves[idx] == octant
endPhase 3: Fix balancetree() parent check -- Expected 2-5x speedup on balancetree
Strategy: Replace O(n^2) p not in parent.(T, b) with a Set:
parent_set = Set{typeof(x)}()
for x in Q
p = parent(x, tree.b)
if p not in parent_set
push!(T_set, x)
push!(parent_set, p)
end
endPhase 4: Dirty tree tracking in balanceforest!()
Track which trees were modified and only re-balance those.
Expected Impact
| Optimization | Target | Expected Speedup |
|---|---|---|
| Quick fix: Binary search in hangingnodes | creategrid 3D | 10-50x |
| Quick fix: Binary search in balance_* | balanceforest | 5-10x |
| Quick fix: Set-based parent check | balancetree | 2-5x |
| Quick fix: Dirty tree tracking | balanceforest | 2-3x |
| Iterator (long-term) | Everything | Asymptotically optimal O(n) |
Quick fixes estimated overall speedup for 3D: 20-100x (dominated by hangingnodes fix).
Iterator: replaces the entire multi-pass architecture with a single O(n) traversal, eliminates all linear scans, and provides a reusable primitive for future features (error estimation, solution transfer, load balancing, parallel ghost layer, etc.).
How to Run the Benchmark
julia --project --startup-file=no -e '
using Dates
include("benchmark/benchmarks-amr.jl")
run_all_benchmarks()
'For quick profiling of a specific component:
using Ferrite, Ferrite.AMR
include("benchmark/benchmarks-amr.jl")
forest = create_refined_forest(Val(3), 4, 2; max_level=8)
profile_creategrid(forest)
profile_balancetree(forest)References
- [IBWG2015] Isaac, Burstedde, Wilcox, Ghattas. "Recursive Algorithms for Distributed Forests of Octrees." SIAM J. Sci. Comput. 37(5), C497-C531, 2015.
- [BWG2011] Burstedde, Wilcox, Ghattas. "p4est: Scalable Algorithms for Parallel Adaptive Mesh Refinement on Forests of Octrees." SIAM J. Sci. Comput. 33(3), 2011.
- [SSB2008] Sundar, Sampath, Biros. "Bottom-up construction and 2:1 balance refinement of linear octrees in parallel." 2008.
Very cool and detailed analysis! 🤖
Regarding the roadmap, to me it seems like a key point is having the interface for mesh refinement and test the p4est-refinement. Personally, considering the magnitude of this PR, I think the key focus should be adding sufficient tests to check for correctness including all the corner-cases that I understand exists? And before spending too much time on optimizing the performance (as long as it is usable for testing purposes and that refining doesn't take more than say 20x the linear solve), I think it could make sense to merge it and document it as experimental, to allow gaining some experience if the interface works well? |
| - `conformity_info::CIT`: a container for conformity information | ||
| - `boundary_matrix::SparseMatrixCSC{Bool,Int}`: optional, only needed by `onboundary` to check if a cell is on the boundary, see, e.g. Helmholtz example | ||
| """ | ||
| mutable struct NonConformingGrid{dim,C<:Ferrite.AbstractCell,T<:Real,CIT} <: Ferrite.AbstractGrid{dim} |
There was a problem hiding this comment.
As I understand this, compared to the current Grid, this allows
- Storing the
conformity_info - Dispatching when adding
ConformityConstraint()to give error if aGrid
But if we want this to work more general (e.g. with different interpolations on different parts of the domain), shouldn't the conformity information belong to the dofhandler instead? And then have a field in the grid for denoting hanging_nodes with the required information to create the conformity_information when adding fields to the dofhandler? Having just hanging_nodes as an additional field seems quite clear to me and avoids having an extra grid datastructure, and then we can have the general case supported in the DofHandler even when the grid is conforming but the added interpolations don't.
Just some cases in my head as reference when thinking
a) Tie constraint between 3 bilinear (possible with `NonConformingGrid`)
o ---- o ---- o
| | |
| o ---- o
| | |
o ---- o ---- o
b) Tie constraint between 2 different order elements (not possible with `NonConformingGrid`)
o ---- o -- o -- o
| | |
| o o o
| | |
o ---- o -- o -- o
c) Tie constraints between 3 different order elements (case 1)
o ---- o -- o -- o
| | |
o ---- o o o
| | |
o ---- o -- o -- o
# Actually invalid - would be nonconforming even if the center (on shared edge) nodes are tied
d) Tie constraints between 3 different order elements (case 2) (possible with `NonConformingGrid`?)
o -- o -- o ------------- o
| | |
o o o |
| | |
o -- o -- o |
| | |
o o o |
| | |
o -- o -- o ------------- o
|
And another comment: To make this PR a bit more manageable, maybe the ability to deal with nonconformity can be extracted out and done first? Seems like that would be a usable components on its own. The same if there are other parts that are useful on their own, that will make reviewing easier down the line IMO. |
Claude Code is quite helpful for these code monkey tasks. 🤠 I personally want to get the iterators working before merging because they are crucial for the performance. You visit by construction entities once (by utilizing 2:1 balance information and other things that are guaranteed) and get on the fly hanging nodes (geometric ones). I already understood a big part of the coarse idea but I need @termi-official s help probably to understand the last bits and pieces to write the first sketch of it. As written in the claude report a large junk of the base function is already implemented and works as expected. I also think that @termi-official should comment a bit on code design regarding nonconformity information. From my gut feeling I agree that it should be part of the Dofhandler (or probably the AdaptiveDofHandler) |
to be discussed at FerriteCon 23, register now: https://ferrite-fem.github.io/FerriteCon/
TODO
OctantBWGmorton index encoded octant in 2 and 3DOctreeBWGlinear tree with homogeneousOctantBWGleavesForestBWGforest of homogeneousOctreeBWGtransform_facetransform_cornertransform_edge[X-PR] p4est edge operations and consistent corner operation #902Ferrite.Gridfor unstructured meshesdocs/srcs/literate-tutorials/adaptivity.jldocs/src/literate-tutorials/adaptivity.jlwith (almost?maybe?) working but slow ZZ error estimatordocs/src/literate-tutorials/heat-adaptivity.jldocs/src/literate-tutorials/adaptivity.jlto a proper tutorialFollowup PRs