diff --git a/NEWS.md b/NEWS.md index 9abe35938..2001113d2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `reindex_free_dof_ids(space, algorithm)` to reorder the free DOFs of a `FESpace` for bandwidth/profile/fill-in reduction. Supported algorithms: `:rcm` (Reverse Cuthill-McKee), `:sloan` (wavefront minimisation), and `:coordinates` (sort by spatial coordinates, for Lagrangian spaces). An externally computed permutation vector can also be applied directly via `reindex_free_dof_ids(space, free_dof_ids)`. Since PR[#1299](https://github.com/gridap/Gridap.jl/pull/1299). + ### Changed - Minor generalisation of `return_value` for `LinearCombinationMap`. Since PR[#1298](https://github.com/gridap/Gridap.jl/pull/1298). diff --git a/Project.toml b/Project.toml index ae58e2510..78256e950 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Gridap" uuid = "56d4f2e9-7ea1-5844-9cf6-b9c51ca7ce8e" -authors = ["Santiago Badia ", "Francesc Verdugo ", "Alberto F. Martin "] version = "0.20.6" +authors = ["Santiago Badia ", "Francesc Verdugo ", "Alberto F. Martin "] [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" @@ -40,6 +40,7 @@ TikzPictures = "37f6aa50-8035-52d0-81c2-5a1d08754b2d" TikzPicturesExt = "TikzPictures" [compat] +AMD = "0.5" AbstractTrees = "0.3.3, 0.4" Aqua = "0.8" AutoHashEquals = "2.2.0" @@ -73,8 +74,9 @@ WriteVTK = "1.21.1" julia = "1.10" [extras] +AMD = "14f7f29c-3bd6-536c-9a0b-7339e30b5a3e" Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Test"] +test = ["AMD", "Aqua", "Test"] diff --git a/src/Arrays/Arrays.jl b/src/Arrays/Arrays.jl index 79686794c..1cf40b76e 100644 --- a/src/Arrays/Arrays.jl +++ b/src/Arrays/Arrays.jl @@ -111,6 +111,7 @@ export scatter_table_values export gather_posneg_table_values export gather_posneg_table_values! export scatter_posneg_table_values +export inverse_table export IdentityVector diff --git a/src/Arrays/Tables.jl b/src/Arrays/Tables.jl index dd6a43d7f..67dfaa6e1 100644 --- a/src/Arrays/Tables.jl +++ b/src/Arrays/Tables.jl @@ -419,7 +419,9 @@ function inverse_table( o = one(P) ptrs = zeros(P,nb+1) @inbounds for b in a_to_lb_to_b_data - ptrs[b+1] += o + if b > 0 + ptrs[b+1] += o + end end length_to_ptrs!(ptrs) @@ -430,7 +432,7 @@ function inverse_table( e = a_to_lb_to_b_ptrs[a+1] - o @inbounds for p in s:e b = a_to_lb_to_b_data[p] - if b != UNSET + if b > 0 data[ptrs[b]] = a ptrs[b] += o end @@ -1034,3 +1036,71 @@ end function Base.copy(a::Table) Table(copy(a.data),copy(a.ptrs)) end + +""" + compute_adjacency(t::Table[, nfree]) -> Table + +Build the symmetric node adjacency graph from a hyper-edge table `t` +(e.g. a cell-to-DOF table). Two nodes are adjacent iff they appear +in the same row. Entries ≤ 0 are treated as inactive (e.g. Dirichlet +DOF IDs encoded as negative values) and skipped. + +`nfree` defaults to the maximum positive entry in `t`. + +Returns `adj` where `adj[i]` lists the neighbours of node `i` in sorted +order. Node degrees are available cheaply as `diff(adj.ptrs)`. +""" +function compute_adjacency( + t::Table{T}, nfree::Int = Int(maximum(t.data; init=zero(T))) +) where T + d2c = inverse_table(t, nfree) + + # Upper-bound pointer array: degree(i) ≤ Σ |row(c)| for cells c containing i + ub_ptrs = Vector{Int}(undef, nfree + 1) + ub_ptrs[1] = 1 + for i in 1:nfree + s = 0 + for k in datarange(d2c, i) + s += length(datarange(t, d2c.data[k])) + end + ub_ptrs[i+1] = ub_ptrs[i] + s + end + + # Collect positive, non-self neighbours (may contain duplicates) + ub_data = Vector{Int32}(undef, ub_ptrs[nfree+1] - 1) + fill_end = copy(ub_ptrs) + for i in 1:nfree + for k in datarange(d2c, i) + c = d2c.data[k] + for k2 in datarange(t, c) + j = t.data[k2] + if j > 0 && j != i + ub_data[fill_end[i]] = j + fill_end[i] += 1 + end + end + end + end + + # Sort each row slice, then count and copy unique neighbours + ptrs = Vector{Int}(undef, nfree + 1) + ptrs[1] = 1 + for i in 1:nfree + sort!(view(ub_data, ub_ptrs[i]:fill_end[i]-1)) + n = 0; prev = zero(Int32) + for k in ub_ptrs[i]:fill_end[i]-1 + v = ub_data[k]; v != prev && (n += 1; prev = v) + end + ptrs[i+1] = ptrs[i] + n + end + + data = Vector{Int32}(undef, ptrs[nfree+1] - 1) + for i in 1:nfree + p = ptrs[i]; prev = zero(Int32) + for k in ub_ptrs[i]:fill_end[i]-1 + v = ub_data[k]; v != prev && (data[p] = v; p += 1; prev = v) + end + end + + return Table(data, ptrs) +end diff --git a/src/FESpaces/FESpaceReindexing.jl b/src/FESpaces/FESpaceReindexing.jl new file mode 100644 index 000000000..2f4b327f5 --- /dev/null +++ b/src/FESpaces/FESpaceReindexing.jl @@ -0,0 +1,343 @@ +# =================================================================== +# Abstract interface +# =================================================================== + +""" + reindex_free_and_dirichlet_dof_ids(space::FESpace, free_dof_ids, dir_dof_ids) + +Return a new `FESpace` obtained by reindexing the free and Dirichlet DOF IDs. +`free_dof_ids[old_free] = new_free` and `dir_dof_ids` uses the same convention +(negative values preserve the Dirichlet sign encoding of `cell_dof_ids`). +Concrete implementations exist for `UnconstrainedFESpace` and `PolytopalFESpace`. +""" +function reindex_free_and_dirichlet_dof_ids(space::FESpace,free_dof_ids,dir_dof_ids) + @abstractmethod +end + +""" + reindex_free_dof_ids(space::FESpace, free_dof_ids) + +Apply a given permutation of the free DOFs, leaving Dirichlet DOF IDs unchanged. +`free_dof_ids[old_free] = new_free`. +""" +function reindex_free_dof_ids(space::FESpace, free_dof_ids) + reindex_free_and_dirichlet_dof_ids(space, free_dof_ids, -1:-1:-num_dirichlet_dofs(space)) +end + +# TODO: deprecate on next major release +function renumber_free_and_dirichlet_dof_ids(space::FESpace,args...) + reindex_free_and_dirichlet_dof_ids(space,args...) +end + +# =================================================================== +# George-Liu pseudo-peripheral node finder (shared by RCM and Sloan). +# +# Starts from `seed` and iterates BFS from the minimum-degree node in +# the last level set until the eccentricity stops increasing. +# Returns the pseudo-peripheral node (the previous BFS root). +# =================================================================== +function _george_liu_peripheral!(bfs!::F, queue, level, degrees, seed) where {F} + n, last = bfs!(seed) + ecc = level[queue[n]] + current = seed + while true + best, best_deg = 0, typemax(Int) + for qi in last:n + v = queue[qi] + degrees[v] < best_deg && (best_deg = degrees[v]; best = v) + end + n2, last2 = bfs!(best) + ecc2 = level[queue[n2]] + ecc2 <= ecc && return current + ecc = ecc2; n = n2; last = last2; current = best + end +end + +# =================================================================== +# Reverse Cuthill-McKee permutation. +# +# Uses George-Liu for pseudo-peripheral node selection. Sorts each +# adjacency row by ascending degree on a private copy so CM visits +# lower-degree neighbours first without mutating the caller's adj. +# +# State encoding: 0 = unvisited, cg = visited in generation cg, -1 = done. +# Hot-loop condition state[u] % UInt < cg % UInt covers all three cases. +# =================================================================== +function _rcm_perm(adj::Table) + nfree = length(adj) + degrees = diff(adj.ptrs) + + sorted_data = copy(adj.data) + adj = Table(sorted_data, adj.ptrs) + for i in 1:nfree + sort!(view(sorted_data, datarange(adj, i)), by=j -> degrees[j]) + end + + queue = Vector{Int}(undef, nfree) + level = Vector{Int}(undef, nfree) + state = zeros(Int, nfree) + gen = Ref(0) + + function bfs!(start) + cg = (gen[] += 1) + state[start] = cg; level[start] = 0 + queue[1] = start; qhead = qtail = 1 + max_level = 0 + while qhead <= qtail + v = queue[qhead]; qhead += 1; lv = level[v] + for k in datarange(adj, v) + u = adj.data[k] + if state[u] % UInt < cg % UInt + state[u] = cg; level[u] = lv + 1 + lv + 1 > max_level && (max_level = lv + 1) + qtail += 1; queue[qtail] = u + end + end + end + last = qtail + while last > 1 && level[queue[last-1]] == max_level; last -= 1; end + return qtail, last + end + + perm = Vector{Int}(undef, nfree) + idx = 1 + + while idx <= nfree + seed, seed_deg = 0, typemax(Int) + for i in 1:nfree + state[i] == 0 && degrees[i] < seed_deg && (seed_deg = degrees[i]; seed = i) + end + peripheral = _george_liu_peripheral!(bfs!, queue, level, degrees, seed) + n, _ = bfs!(peripheral) + for qi in 1:n + v = queue[qi]; state[v] = -1; perm[idx] = v; idx += 1 + end + end + + reverse!(perm) + iperm = Vector{Int}(undef, nfree) + for i in 1:nfree; iperm[perm[i]] = i; end + return iperm +end + +# =================================================================== +# Sloan's algorithm. +# +# Minimises the active front (wavefront) rather than bandwidth. +# Uses a priority queue with f(i) = eff_w1·dist_E(i) + eff_w2·c(i) +# [maximise], where dist_E(i) = BFS distance from the end node E and +# c(i) = count of PREACTIVE+ACTIVE neighbours. +# +# Weights are auto-scaled per component: +# eff_w1 = w1 * max_degree, eff_w2 = w2 * max_dist_E +# so both terms span comparable ranges regardless of mesh order. +# +# Status: INACTIVE=0, PREACTIVE=1, ACTIVE=2, NUMBERED=3. +# =================================================================== +function _sloan_perm(adj::Table; w1::Int=1, w2::Int=2) + nfree = length(adj) + degrees = diff(adj.ptrs) + + queue = Vector{Int}(undef, nfree) + level = Vector{Int}(undef, nfree) + state = zeros(Int, nfree) + gen = Ref(0) + + function bfs!(start) + cg = (gen[] += 1) + state[start] = cg; level[start] = 0 + queue[1] = start; qhead = qtail = 1 + max_level = 0 + while qhead <= qtail + v = queue[qhead]; qhead += 1; lv = level[v] + for k in datarange(adj, v) + u = adj.data[k] + if state[u] % UInt < cg % UInt + state[u] = cg; level[u] = lv + 1 + lv + 1 > max_level && (max_level = lv + 1) + qtail += 1; queue[qtail] = u + end + end + end + last = qtail + while last > 1 && level[queue[last-1]] == max_level; last -= 1; end + return qtail, last + end + + dist_E = Vector{Int}(undef, nfree) + f = fill(typemin(Int), nfree) + c = zeros(Int, nfree) + status = zeros(Int8, nfree) + + heap = Tuple{Int,Int}[] + + function pq_push!(node, pr) + f[node] = pr + push!(heap, (pr, node)) + i = length(heap) + @inbounds while i > 1 + p = i >> 1; heap[p][1] >= heap[i][1] && break + heap[p], heap[i] = heap[i], heap[p]; i = p + end + end + + function pq_siftdown!() + i = 1; n = length(heap) + @inbounds while true + c_idx = 2i + c_idx > n && break + c_idx + 1 <= n && heap[c_idx+1][1] > heap[c_idx][1] && (c_idx += 1) + heap[c_idx][1] <= heap[i][1] && break + heap[c_idx], heap[i] = heap[i], heap[c_idx]; i = c_idx + end + end + + function pq_pop!() + @inbounds while !isempty(heap) + pr, node = heap[1] + if length(heap) == 1; pop!(heap) + else heap[1] = pop!(heap); pq_siftdown!() + end + f[node] == pr && return node + end + return 0 + end + + perm = Vector{Int}(undef, nfree) + idx = 1 + + while idx <= nfree + seed, seed_deg = 0, typemax(Int) + for i in 1:nfree + status[i] == 0 && degrees[i] < seed_deg && (seed_deg = degrees[i]; seed = i) + end + + E = _george_liu_peripheral!(bfs!, queue, level, degrees, seed) + n_comp, last_e = bfs!(E) + + S, S_deg = 0, typemax(Int) + max_dist = 0; max_deg = 0 + for qi in 1:n_comp + v = queue[qi] + dist_E[v] = level[v] + level[v] > max_dist && (max_dist = level[v]) + degrees[v] > max_deg && (max_deg = degrees[v]) + qi >= last_e && degrees[v] < S_deg && (S_deg = degrees[v]; S = v) + end + eff_w1 = w1 * max(max_deg, 1) + eff_w2 = w2 * max(max_dist, 1) + + status[S] = 1; c[S] = 0; pq_push!(S, eff_w1 * dist_E[S]) + + comp_done = 0 + while comp_done < n_comp + v = pq_pop!(); v == 0 && break + + if status[v] == 1 + status[v] = 2 + for k in datarange(adj, v) + u = adj.data[k] + if status[u] == 0 + status[u] = 1 + c_u = 0 + for k2 in datarange(adj, u) + w = adj.data[k2] + if status[w] == 1 || status[w] == 2 + c_u += 1 + c[w] += 1 + pq_push!(w, eff_w1 * dist_E[w] + eff_w2 * c[w]) + end + end + c[u] = c_u + pq_push!(u, eff_w1 * dist_E[u] + eff_w2 * c_u) + end + end + pq_push!(v, eff_w1 * dist_E[v] + eff_w2 * c[v]) + + elseif status[v] == 2 + status[v] = 3; f[v] = typemin(Int) + perm[idx] = v; idx += 1; comp_done += 1 + end + end + + for qi in 1:n_comp; state[queue[qi]] = -1; end + end + + iperm = Vector{Int}(undef, nfree) + for i in 1:nfree; iperm[perm[i]] = i; end + return iperm +end + +# =================================================================== +# Coordinate-based permutation (Lagrangian spaces). +# Sorts free DOFs by their spatial coordinates. +# by: key function applied to each VectorValue before comparison. +# Defaults to Tuple, giving lexicographic (x,y,z) ordering. +# lt: less-than predicate used by sortperm (default isless). +# =================================================================== +function _coordinates_perm(space::FESpace; by=Tuple, lt=isless) + coords = get_free_dof_coordinates(space) + invperm(sortperm(coords; by=by, lt=lt)) +end + +# =================================================================== +# Main public API +# =================================================================== + +""" + compute_dof_permutation(space::FESpace, algorithm::Symbol=:rcm; adj, by, lt, kwargs...) + +Compute a free-DOF permutation `iperm` where `iperm[old_dof] = new_dof`. + +`algorithm` selects the reordering strategy: +- `:rcm` — Reverse Cuthill-McKee with George-Liu pseudo-peripheral node + selection. Best for bandwidth and profile reduction on structured meshes. +- `:sloan` — Sloan's algorithm. Minimises the active wavefront; may + outperform `:rcm` on profile for irregular or 3D meshes. + Accepts keyword arguments `w1::Int=1` and `w2::Int=2` to tune the + distance/connectivity weight balance. +- `:coordinates` — Sort free DOFs by their spatial coordinates (Lagrangian + spaces only). Keyword `by` maps each `VectorValue` to a sortable key + (default `Tuple`, giving lexicographic x-y-z ordering); `lt` is the + less-than predicate (default `isless`). Example: `by=x->x[1]` sorts by + x-coordinate only. + +For `:rcm` and `:sloan`, the optional keyword argument `adj` is the DOF +adjacency `Table` returned by `compute_adjacency`. It defaults to the graph +built from the cell-DOF connectivity of `space`. Pass a pre-built table to +avoid redundant work when calling multiple algorithms on the same space. +Passing `adj` when `algorithm == :coordinates` has no effect. +""" +function compute_dof_permutation(space::FESpace, algorithm::Symbol=:rcm; adj=nothing, by=Tuple, lt=isless, kwargs...) + if algorithm == :coordinates + return _coordinates_perm(space; by=by, lt=lt) + end + if isnothing(adj) + adj = compute_adjacency(get_cell_dof_ids(space), num_free_dofs(space)) + end + @check length(adj) == num_free_dofs(space) + algorithm == :rcm && return _rcm_perm(adj) + algorithm == :sloan && return _sloan_perm(adj; kwargs...) + error("Unknown algorithm :$algorithm. Supported: :rcm, :sloan, :coordinates") +end + +""" + reindex_free_dof_ids(space::FESpace, algorithm::Symbol=:rcm; kwargs...) + +Return a new `FESpace` with free DOF IDs reordered using `algorithm`. +See [`compute_dof_permutation`](@ref) for supported algorithms and keyword arguments. +Dirichlet DOF IDs are left unchanged. + +# Examples +```julia +model = CartesianDiscreteModel((0,1,0,1), (8,8)) +V = FESpace(model, ReferenceFE(lagrangian, Float64, 2); dirichlet_tags="boundary") + +V_rcm = reindex_free_dof_ids(V, :rcm) +V_coords = reindex_free_dof_ids(V, :coordinates) +V_xonly = reindex_free_dof_ids(V, :coordinates; by=x->x[1]) +``` +""" +function reindex_free_dof_ids(space::FESpace, algorithm::Symbol=:rcm; kwargs...) + reindex_free_dof_ids(space, compute_dof_permutation(space, algorithm; kwargs...)) +end diff --git a/src/FESpaces/FESpaces.jl b/src/FESpaces/FESpaces.jl index 7c44abb06..b2ab8eb03 100644 --- a/src/FESpaces/FESpaces.jl +++ b/src/FESpaces/FESpaces.jl @@ -22,7 +22,7 @@ using Gridap.CellData using Gridap.TensorValues using Gridap.Polynomials -using Gridap.Arrays: Reindex, ConfigMap, DualizeMap, AutoDiffMap, lazy_map +using Gridap.Arrays: Reindex, ConfigMap, DualizeMap, AutoDiffMap, lazy_map, compute_adjacency using Gridap.Fields: ArrayBlock, BlockMap @@ -224,6 +224,9 @@ export LocalOperator export high_order_grid +export reindex_free_and_dirichlet_dof_ids +export reindex_free_dof_ids + include("FESpaceInterface.jl") include("SingleFieldFESpaces.jl") @@ -280,6 +283,8 @@ include("PatchFESpaces.jl") include("HighOrderGrids.jl") +include("FESpaceReindexing.jl") + """ deprecated !!! warning diff --git a/src/FESpaces/PatchFESpaces.jl b/src/FESpaces/PatchFESpaces.jl index 864036e55..0967dd707 100644 --- a/src/FESpaces/PatchFESpaces.jl +++ b/src/FESpaces/PatchFESpaces.jl @@ -6,7 +6,7 @@ function FESpaceWithoutBCs(space::SingleFieldFESpace) ndir = num_dirichlet_dofs(space) free_ids = Int32(ndir+1):Int32(nfree+ndir) dir_ids = Int32(ndir):Int32(-1):Int32(1) # Reverse order - return renumber_free_and_dirichlet_dof_ids(space,free_ids,dir_ids) + return reindex_free_and_dirichlet_dof_ids(space,free_ids,dir_ids) end FESpaceWithoutBCs(space::ConstantFESpace) = space diff --git a/src/FESpaces/PolytopalFESpaces.jl b/src/FESpaces/PolytopalFESpaces.jl index be37c81c1..49f88d2b4 100644 --- a/src/FESpaces/PolytopalFESpaces.jl +++ b/src/FESpaces/PolytopalFESpaces.jl @@ -391,7 +391,7 @@ end ################## -function FESpaces.renumber_free_and_dirichlet_dof_ids( +function FESpaces.reindex_free_and_dirichlet_dof_ids( space::FESpaces.PolytopalFESpace,free_dof_ids,dir_dof_ids ) @assert num_free_dofs(space) == length(free_dof_ids) diff --git a/src/FESpaces/UnconstrainedFESpaces.jl b/src/FESpaces/UnconstrainedFESpaces.jl index 8de431c38..216dba33f 100644 --- a/src/FESpaces/UnconstrainedFESpaces.jl +++ b/src/FESpaces/UnconstrainedFESpaces.jl @@ -139,7 +139,7 @@ function _free_and_dirichlet_values_fill!( end -function renumber_free_and_dirichlet_dof_ids( +function reindex_free_and_dirichlet_dof_ids( space::UnconstrainedFESpace,free_dof_ids,dir_dof_ids ) @assert num_free_dofs(space) == length(free_dof_ids) @@ -168,4 +168,4 @@ function renumber_free_and_dirichlet_dof_ids( space.ntags, space.metadata ) -end +end \ No newline at end of file diff --git a/src/Geometry/Geometry.jl b/src/Geometry/Geometry.jl index 23fad4600..0aeff06a7 100644 --- a/src/Geometry/Geometry.jl +++ b/src/Geometry/Geometry.jl @@ -93,6 +93,7 @@ export compute_face_vertices export compute_isboundary_face export get_cell_permutations export compute_cell_permutations +export compute_graph export test_grid_topology export get_cell_faces export get_isboundary_face diff --git a/test/FESpacesTests/FESpaceReindexingTests.jl b/test/FESpacesTests/FESpaceReindexingTests.jl new file mode 100644 index 000000000..41e0c2039 --- /dev/null +++ b/test/FESpacesTests/FESpaceReindexingTests.jl @@ -0,0 +1,51 @@ +module FESpaceReindexingTests + +using Test +using SparseArrays +using Gridap.Arrays +using Gridap.Geometry +using Gridap.ReferenceFEs +using Gridap.FESpaces +using Gridap.CellData + +import Gridap.Arrays: compute_adjacency # internal, accessed explicitly for adj kwarg test +import Gridap.FESpaces: compute_dof_permutation # internal, accessed explicitly + +model = CartesianDiscreteModel((0,1,0,1),(4,4)) +reffe = ReferenceFE(lagrangian, Float64, 2) +V = FESpace(model, reffe; dirichlet_tags="boundary") +nf = num_free_dofs(V) +ndir = num_dirichlet_dofs(V) + +# reindex_free_dof_ids (algorithm dispatch) preserves DOF counts +for alg in (:rcm, :sloan, :coordinates) + Vr = reindex_free_dof_ids(V, alg) + @test num_free_dofs(Vr) == nf + @test num_dirichlet_dofs(Vr) == ndir +end + +# :coordinates with custom by (sort by x-coordinate only) +Vx = reindex_free_dof_ids(V, :coordinates; by=x->x[1]) +@test num_free_dofs(Vx) == nf + +# reindex_free_dof_ids (direct permutation) preserves DOF counts +adj = compute_adjacency(get_cell_dof_ids(V), nf) +Vd = reindex_free_dof_ids(V, compute_dof_permutation(V, :rcm; adj=adj)) +@test num_free_dofs(Vd) == nf +@test num_dirichlet_dofs(Vd) == ndir + +# Pre-built adj table can be passed explicitly +Vp = reindex_free_dof_ids(V, :rcm; adj=adj) +@test num_free_dofs(Vp) == nf + +# RCM reduces bandwidth (end-to-end correctness check) +Ω = Triangulation(model) +dΩ = Measure(Ω, 5) +a(u, v) = ∫(∇(v) ⋅ ∇(u))dΩ +A = assemble_matrix(a, V, V) +Vr = reindex_free_dof_ids(V, :rcm) +Ar = assemble_matrix(a, Vr, Vr) +bw(M) = maximum(abs(i-j) for (i,j,_) in zip(findnz(M)...)) +@test bw(Ar) < bw(A) + +end # module diff --git a/test/FESpacesTests/runtests_2.jl b/test/FESpacesTests/runtests_2.jl index eefab398b..714ea244b 100644 --- a/test/FESpacesTests/runtests_2.jl +++ b/test/FESpacesTests/runtests_2.jl @@ -34,4 +34,6 @@ using Test @testset "HighOrderGrids" begin include("HighOrderGridsTests.jl") end +@testset "FESpaceReindexing" begin include("FESpaceReindexingTests.jl") end + end # module