Skip to content

Commit

Permalink
Merge pull request #1063 from gridap/aposteriori-estimators
Browse files Browse the repository at this point in the history
Aposteriori estimators
  • Loading branch information
JordiManyer authored Dec 9, 2024
2 parents 2c5e688 + bcfba31 commit 3b87738
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 31 deletions.
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Added AMR-related methods `mark` and `estimate` to `Adaptivity` module. Implemented Dorfler marking strategy. Since PR[#1063](https://github.com/gridap/Gridap.jl/pull/1063).

## [0.18.8] - 2024-12-2

### Added
Expand Down
12 changes: 12 additions & 0 deletions docs/src/Adaptivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ The API is given by the following methods:
MacroReferenceFE
```

## Adaptive Mesh Refinement

One of the main uses of mesh refinement is Adaptive Mesh Refinement, where the mesh is refined only in regions of interest.

The typical AMR workflow is the so-called `solve-estimate-mark-refine` loop. Since estimators will generally be problem-dependent, we only aim to provide some generic tools that can be combined by the user:

```@docs
DorflerMarking
mark
estimate
```

## Notes for users

Most of the tools provided by this module are showcased in the tests of the module itself, as well as the following tutorial (coming soon).
Expand Down
188 changes: 188 additions & 0 deletions src/Adaptivity/AdaptiveMeshRefinement.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@

# Marking strategies

"""
struct DorflerMarking
θ :: Float64
ν :: Float64
strategy :: Symbol
end
DorflerMarking(θ::Float64; ν::Float64 = 0.5, strategy::Symbol = :quickmark)
Implements the Dorfler marking strategy. Given a vector `η` of real positive numbers,
the marking strategy find a subset of indices `I` such that
sum(η[I]) > θ * sum(η)
where `0 < θ < 1` is a threshold parameter.
For more details, see the following reference:
"Dörfler marking with minimal cardinality is a linear complexity problem", Pfeiler et al. (2020)
The marking algorithm is controlled by the `strategy` parameter, which can take
the following values:
- `:sort`: Optimal cardinality, O(N log N) complexity. See Algorithm 2 in the reference.
- `:binsort`: Quasi-optimal cardinality, O(N) complexity. See Algorithm 7 in the reference.
- `:quickmark`: Optimal cardinality, O(N) complexity. See Algorithm 10 in the reference.
# Arguments
- `θ::Float64`: The threshold parameter. Between 0 and 1.
- `ν::Float64`: Extra parameter for `:binsort`. Default is 0.5.
- `strategy::Symbol`: The marking strategy. Default is `:quickmark`.
# Usage
```julia
η = abs.(randn(1000))
m = DorflerMarking(0.5)
I = mark(m,η)
```
"""
struct DorflerMarking
θ :: Float64
ν :: Float64
strategy :: Symbol
function DorflerMarking(
θ::Float64;
ν::Float64 = 0.5,
strategy::Symbol = :quickmark
)
@assert 0 < θ < 1
@assert strategy (:sort,:binsort,:quickmark) "Strategy not recognized. Available values are (:sort)"
new(θ,ν,strategy)
end
end

"""
mark(m::DorflerMarking, η::Vector{<:Real}) -> Vector{Int}
Given a vector `η` of real positive numbers, returns a subset of indices `I` such that
satisfying the Dorfler marking condition.
"""
mark(m::DorflerMarking, η::Vector{<:Real}) = mark(Val(m.strategy), m, η)

function mark(::Val{:sort}, m::DorflerMarking, η::Vector{<:Real})
target = m.θ * sum(η)
perm = sortperm(η, rev=true, alg=QuickSort)
s = zero(eltype(η))
k = 0
while s < target
k += 1
s += η[perm[k]]
end
return perm[1:k]
end

function mark(::Val{:binsort}, m::DorflerMarking, η::Vector{<:Real})
target = m.θ * sum(η)
M = maximum(η)
N = length(η)

# Find minimal K such that
# νᴷ⁺¹ M ≤ (1 - θ) / (θ * N) * target
K = 0
a = M*m.ν
b = (1 - m.θ) / (m.θ * N * M) * target
while a > b
K += 1
a *= m.ν
end

# Sort into bins Bk such that ηi ∈ Bk if
# νᴷ⁺¹ M ≤ ηi < νᴷ M
bins = zeros(Int,N)
sums = zeros(Float64,K+1)
lbs = zeros(Float64,K+1)
lbs[1] = M * m.ν
for i in 2:K
lbs[i] = lbs[i-1] * m.ν
end
lbs[end] = 0.0

for (i,ηi) in enumerate(η)
k = 1
while ηi < lbs[k]
k += 1
end
bins[i] = k
sums[k] += ηi
end

# Find minimal set of bins that gets over target
k = 0
s = 0.0
while s < target
k += 1
s += sums[k]
end

return findall(i -> i <= k, bins)
end

function mark(::Val{:quickmark}, m::DorflerMarking, η::Vector{<:Real})
function quickmark!(η, perm, l, u, target) :: Int
m = (u - l) ÷ 2
sort!(view(perm,l:u), by=i->η[i], rev=true, alg=PartialQuickSort(m))

p = l + m
t = η[perm[p]]
σ = sum(η[perm[l:p-1]])

>= target) && return quickmark!(η, perm, l, p, target)
+ t >= target) && return p
return quickmark!(η, perm, p + 1, u, target - σ - t)
end

N = length(η)
l, u = 1, N
perm = collect(1:N)
target = m.θ * sum(η)
m = quickmark!(η, perm, l, u, target)
return perm[1:m]
end

# Estimators

"""
estimate(f::Function, uh::Function) -> Vector{Float64}
Given a functional `f` and a function `uh`, such that `f(uh)` produces a
scalar-valued `DomainContribution`, collects the estimator values for
each cell in the background model.
"""
function estimate(f::Function, uh)
collect_estimator(f(uh))
end

function collect_estimator(c::DomainContribution)
trians = get_domains(c)
bgmodel = get_background_model(first(trians))
msg = "Estimator not implemented for mixed background models"
@notimplementedif !all([bgmodel == get_background_model(trian) for trian in trians]) msg

Dc = num_cell_dims(bgmodel)
η = zeros(Float64,num_cells(bgmodel))
for trian in trians
glue = get_glue(trian,Val(Dc))
collect_estimator!(η,glue,get_contribution(c,trian))
end

return η
end

function collect_estimator!(η, glue::FaceToFaceGlue, c)
cache = array_cache(c)
for (face,bgcell) in enumerate(glue.tface_to_mface)
η[bgcell] += getindex!(cache,c,face)
end
end

function collect_estimator!(η, glue::SkeletonPair, c)
collect_estimator!(η,glue.plus,c)
collect_estimator!(η,glue.minus,c)
end
3 changes: 3 additions & 0 deletions src/Adaptivity/Adaptivity.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export AdaptedTriangulation
export Triangulation, is_change_possible, best_target, get_adapted_model
export change_domain, move_contributions

export DorflerMarking, mark, estimate

include("RefinementRules.jl")
include("FineToCoarseFields.jl")
include("OldToNewFields.jl")
Expand All @@ -51,5 +53,6 @@ include("MacroFEs.jl")
include("CompositeQuadratures.jl")
include("EdgeBasedRefinement.jl")
include("SimplexifyRefinement.jl")
include("AdaptiveMeshRefinement.jl")

end # module
69 changes: 38 additions & 31 deletions src/Adaptivity/EdgeBasedRefinement.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ function refine_edge_based_topology(
c2n_map_new = get_refined_cell_to_vertex_map(topo,rrules,faces_list)
polys_new, cell_type_new = _get_cell_polytopes(rrules)
orientation = NonOriented()

return UnstructuredGridTopology(coords_new,c2n_map_new,cell_type_new,polys_new,orientation)
end

Expand Down Expand Up @@ -109,17 +108,28 @@ The new vertices are ordered by parent dimension and face id (in that order).
function get_new_coordinates_from_faces(p::Union{Polytope{D},GridTopology{D}},faces_list::Tuple) where {D}
@check length(faces_list) == D+1

nN_new = sum(x->length(x),faces_list)
nN_new = sum(length,faces_list)
coords_old = get_vertex_coordinates(p)
coords_new = Vector{eltype(coords_old)}(undef,nN_new)

n = 1
for (d,dfaces) in enumerate(faces_list)
if length(dfaces) > 0
nf = length(dfaces)
d2n_map = get_faces(p,d-1,0)
coords_new[n:n+nf-1] .= map(f -> sum(coords_old[d2n_map[f]])/length(d2n_map[f]), dfaces)
n += nf
# Nodes
if !isempty(faces_list[1])
for node in faces_list[1]
coords_new[n] = coords_old[node]
n += 1
end
end
# Faces (d > 0)
for (d,dfaces) in enumerate(faces_list[2:end])
if !isempty(dfaces)
d2n_map = get_faces(p,d,0)
cache = array_cache(d2n_map)
for face in dfaces
face_nodes = getindex!(cache,d2n_map,face)
coords_new[n] = sum(coords_old[face_nodes])/length(face_nodes)
n += 1
end
end
end

Expand Down Expand Up @@ -255,12 +265,13 @@ function setup_edge_based_rrules(method::NVBRefinement, topo::UnstructuredGridTo
setup_edge_based_rrules(method, topo, collect(1:num_faces(topo,Dc)))
end

function setup_edge_based_rrules(method::NVBRefinement, topo::UnstructuredGridTopology{Dc},cells_to_refine::AbstractArray{<:Integer}) where Dc
function setup_edge_based_rrules(
method::NVBRefinement, topo::UnstructuredGridTopology{Dc}, cells_to_refine::AbstractArray{<:Integer}
) where Dc
nE = num_faces(topo,1)
c2e_map = get_faces(topo,Dc,1)
c2e_map_cache = array_cache(c2e_map)
e2c_map = get_faces(topo,1,Dc)
polys = topo.polytopes
c2e_map = get_faces(topo,Dc,1)
e2c_map = get_faces(topo,1,Dc)
polys = topo.polytopes
cell_types = topo.cell_type
cell_color = copy(cell_types) # WHITE
# Hardcoded for TRI
Expand All @@ -274,43 +285,39 @@ function setup_edge_based_rrules(method::NVBRefinement, topo::UnstructuredGridTo
c_to_longest_edge_lid = method.cell_to_longest_edge_lid
is_refined = falses(nE)
# Loop over cells and mark edges to refine i.e. is_refined
# The reason to not loop directly on c is that we need to change c within
# a given iteration of the for loop
for i in 1:length(cells_to_refine)
c = cells_to_refine[i]
e_longest = c_to_longest_edge_gid[c]
for i in eachindex(cells_to_refine)
# Has to terminate because an edge is marked each iteration or we skip an
# iteration due to a boundary cell
while !is_refined[e_longest]
is_refined[e_longest] = true
c_nbor_lid = findfirst(c′ -> c′ != c, e2c_map[e_longest])
if isnothing(c_nbor_lid) # We've reach the boundary
c = cells_to_refine[i]
e = c_to_longest_edge_gid[c]
while !is_refined[e]
is_refined[e] = true
e_cells = view(e2c_map,e)
if length(e_cells) == 1 # We've reach the boundary
continue
else
# Get the longest edge of the neighbor
c_nbor_gid = e2c_map[e_longest][c_nbor_lid]
e_longest = c_to_longest_edge_gid[c_nbor_gid]
# Set the current cell gid to that of the neighbor
c = c_nbor_gid
# Propagate to neighboring cell
c = ifelse(e_cells[1] == c, e_cells[2], e_cells[1])
e = c_to_longest_edge_gid[c]
end
end
end
# Loop over cells and refine based on marked edges
for c in 1:length(c2e_map)
c_edges = getindex!(c2e_map_cache, c2e_map, c)
c_edges = view(c2e_map,c)
refined_edge_lids = findall(is_refined[c_edges])
# GREEN refinement because only one edge should be bisected
if length(refined_edge_lids) == 1
# GREEN refinement because only one edge should be bisected
ref_edge = refined_edge_lids[1]
cell_color[c] = GREEN + Int8(ref_edge-1)
# BLUE refinement: two bisected
elseif length(refined_edge_lids) == 2
# BLUE refinement: two bisected
long_ref_edge_lid = c_to_longest_edge_lid[c]
short_ref_edge_lid = setdiff(refined_edge_lids, long_ref_edge_lid)[1]
blue_idx = BLUE_dict[(long_ref_edge_lid, short_ref_edge_lid)]
cell_color[c] = BLUE + Int8(blue_idx - 1)
# DOUBLE BLUE refinement: three bisected edges (somewhat rare)
elseif length(refined_edge_lids) == 3
# DOUBLE BLUE refinement: three bisected edges (somewhat rare)
long_ref_edge_lid = c_to_longest_edge_lid[c]
cell_color[c] = BLUE_DOUBLE + Int(long_ref_edge_lid - 1)
end
Expand Down
Loading

0 comments on commit 3b87738

Please sign in to comment.