diff --git a/Project.toml b/Project.toml index e3c5eb3fbd..202f168b74 100644 --- a/Project.toml +++ b/Project.toml @@ -18,16 +18,19 @@ WriteVTK = "64499a7a-5c06-52f2-abe2-ccb03c286192" [weakdeps] BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" Metis = "2679e427-3c69-5b7f-982b-ece356f1e94b" +SparseMatricesCSR = "a0a7dd2c-ebf4-11e9-1f05-cf50bc540ca1" [extensions] FerriteBlockArrays = "BlockArrays" FerriteMetis = "Metis" +FerriteSparseMatrixCSR = "SparseMatricesCSR" [compat] BlockArrays = "0.16, 1" EnumX = "1" ForwardDiff = "0.10" Metis = "1.3" +SparseMatricesCSR = "0.6" NearestNeighbors = "0.4" OrderedCollections = "1" Preferences = "1" @@ -51,9 +54,10 @@ OhMyThreads = "67456a42-1dca-4109-a031-0a68de7e3ad5" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +SparseMatricesCSR = "a0a7dd2c-ebf4-11e9-1f05-cf50bc540ca1" TaskLocalValues = "ed4db957-447d-4319-bfb6-7fa9ae7ecf34" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" [targets] -test = ["BlockArrays", "Downloads", "FerriteGmsh", "ForwardDiff", "Gmsh", "IterativeSolvers", "Metis", "NBInclude", "OhMyThreads", "ProgressMeter", "Random", "SHA", "TaskLocalValues", "Test", "TimerOutputs", "Logging"] +test = ["BlockArrays", "Downloads", "FerriteGmsh", "ForwardDiff", "Gmsh", "IterativeSolvers", "Metis", "NBInclude", "OhMyThreads", "ProgressMeter", "Random", "SHA", "SparseMatricesCSR", "TaskLocalValues", "Test", "TimerOutputs", "Logging"] diff --git a/docs/src/devdocs/assembly.md b/docs/src/devdocs/assembly.md index a4660994a1..b7459dcde5 100644 --- a/docs/src/devdocs/assembly.md +++ b/docs/src/devdocs/assembly.md @@ -1,10 +1,41 @@ # [Assembly](@id devdocs-assembly) +The assembler handles the insertion of the element matrices and element vectors into the system matrix and right hand side. While the CSC and CSR formats are the most common sparse matrix formats in practice, users might want to have optimized custom matrix formats for their specific use-case. The default assemblers [`Ferrite.CSCAssembler`](@ref) and [`Ferrite.CSRAssembler`](@ref) should be able to handle most cases in practice. To support a custom format users have to dispatch the following functions on their new assembler and matrix type: + +```@docs +Ferrite.allocate_matrix +Ferrite.zero_out_rows! +Ferrite.zero_out_columns! +``` + +and the `AbstractSparseMatrix` interface for their custom matrix type. Optional dispatches to speed up operations might be + +```@docs +Ferrite.add_inhomogeneities! +``` + +## Custom Assembler + +In case the default assembler is insufficient, users can implement a custom assemblers. For this, they can create a custom type and dispatch the following functions. + +``` +Ferrite.start_assemble! +Ferrite.finish_assemble! +Ferrite.assemble! +``` + +For local elimination support the following functions might also need custom dispatches + +```@docs +Ferrite._condense_local! +``` + ## Type definitions ```@docs Ferrite.COOAssembler Ferrite.CSCAssembler +Ferrite.CSRAssembler Ferrite.SymmetricCSCAssembler ``` diff --git a/ext/FerriteSparseMatrixCSR.jl b/ext/FerriteSparseMatrixCSR.jl new file mode 100644 index 0000000000..31911b2d6e --- /dev/null +++ b/ext/FerriteSparseMatrixCSR.jl @@ -0,0 +1,110 @@ +module FerriteSparseMatrixCSR + +using Ferrite, SparseArrays, SparseMatricesCSR +import Ferrite: AbstractSparsityPattern, CSRAssembler +import Base: @propagate_inbounds + +#FIXME https://github.com/JuliaSparse/SparseArrays.jl/pull/546 +function Ferrite.start_assemble(K::SparseMatrixCSR{<:Any, T}, f::Vector = T[]; fillzero::Bool = true, maxcelldofs_hint::Int = 0) where {T} + fillzero && (Ferrite.fillzero!(K); Ferrite.fillzero!(f)) + return CSRAssembler(K, f, zeros(Int, maxcelldofs_hint), zeros(Int, maxcelldofs_hint)) +end + +@propagate_inbounds function Ferrite._assemble_inner!(K::SparseMatrixCSR, Ke::AbstractMatrix, dofs::AbstractVector, sorteddofs::AbstractVector, permutation::AbstractVector, sym::Bool) + current_row = 1 + ld = length(dofs) + return @inbounds for Krow in sorteddofs + maxlookups = sym ? current_row : ld + Kerow = permutation[current_row] + ci = 1 # col index pointer for the local matrix + Ci = 1 # col index pointer for the global matrix + nzr = nzrange(K, Krow) + while Ci <= length(nzr) && ci <= maxlookups + C = nzr[Ci] + Kcol = K.colval[C] + Kecol = permutation[ci] + val = Ke[Kerow, Kecol] + if Kcol == dofs[Kecol] + # Match: add the value (if non-zero) and advance the pointers + if !iszero(val) + K.nzval[C] += val + end + ci += 1 + Ci += 1 + elseif Kcol < dofs[Kecol] + # No match yet: advance the global matrix row pointer + Ci += 1 + else # Kcol > dofs[Kecol] + # No match: no entry exist in the global matrix for this row. This is + # allowed as long as the value which would have been inserted is zero. + iszero(val) || Ferrite._missing_sparsity_pattern_error(Krow, Kcol) + # Advance the local matrix row pointer + ci += 1 + end + end + # Make sure that remaining entries in this column of the local matrix are all zero + for i in ci:maxlookups + if !iszero(Ke[Kerow, permutation[i]]) + Ferrite._missing_sparsity_pattern_error(Krow, sorteddofs[i]) + end + end + current_row += 1 + end +end + +function Ferrite.zero_out_rows!(K::SparseMatrixCSR, ch::ConstraintHandler) # can be removed in 0.7 with #24711 merged + @debug @assert issorted(ch.prescribed_dofs) + for row in ch.prescribed_dofs + r = nzrange(K, row) + K.nzval[r] .= 0.0 + end + return +end + +function Ferrite.zero_out_columns!(K::SparseMatrixCSR, ch::ConstraintHandler) + colval = K.colval + nzval = K.nzval + return @inbounds for i in eachindex(colval, nzval) + if haskey(ch.dofmapping, colval[i]) + nzval[i] = 0 + end + end +end + +function Ferrite.allocate_matrix(::Type{SparseMatrixCSR}, sp::AbstractSparsityPattern) + return Ferrite.allocate_matrix(SparseMatrixCSR{1, Float64, Int64}, sp) +end + +function Ferrite.allocate_matrix(::Type{SparseMatrixCSR{1, Tv, Ti}}, sp::AbstractSparsityPattern) where {Tv, Ti} + return _allocate_matrix(SparseMatrixCSR{1, Tv, Ti}, sp, false) +end + +function _allocate_matrix(::Type{SparseMatrixCSR{1, Tv, Ti}}, sp::AbstractSparsityPattern, sym::Bool) where {Tv, Ti} + # 1. Setup rowptr + rowptr = zeros(Ti, Ferrite.getnrows(sp) + 1) + rowptr[1] = 1 + for (row, colidxs) in enumerate(Ferrite.eachrow(sp)) + for col in colidxs + sym && row > col && continue + rowptr[row + 1] += 1 + end + end + cumsum!(rowptr, rowptr) + nnz = rowptr[end] - 1 + # 2. Allocate colval and nzval now that nnz is known + colval = Vector{Ti}(undef, nnz) + nzval = zeros(Tv, nnz) + # 3. Populate colval. + k = 1 + for (row, colidxs) in zip(1:Ferrite.getnrows(sp), Ferrite.eachrow(sp)) # pairs(eachrow(sp)) + for col in colidxs + sym && row > col && continue + colval[k] = col + k += 1 + end + end + S = SparseMatrixCSR{1}(Ferrite.getnrows(sp), Ferrite.getncols(sp), rowptr, colval, nzval) + return S +end + +end diff --git a/src/Dofs/ConstraintHandler.jl b/src/Dofs/ConstraintHandler.jl index a48cb3940f..6702c6b388 100644 --- a/src/Dofs/ConstraintHandler.jl +++ b/src/Dofs/ConstraintHandler.jl @@ -506,7 +506,7 @@ end """ - apply!(K::SparseMatrixCSC, rhs::AbstractVector, ch::ConstraintHandler) + apply!(K::AbstractSparseMatrix, rhs::AbstractVector, ch::ConstraintHandler) Adjust the matrix `K` and right hand side `rhs` to account for the Dirichlet boundary conditions specified in `ch` such that `K \\ rhs` gives the expected solution. @@ -598,15 +598,15 @@ function _apply_v(v::AbstractVector, ch::ConstraintHandler, apply_zero::Bool) return v end -function apply!(K::Union{SparseMatrixCSC, Symmetric}, ch::ConstraintHandler) +function apply!(K::Union{AbstractSparseMatrix, Symmetric}, ch::ConstraintHandler) return apply!(K, eltype(K)[], ch, true) end -function apply_zero!(K::Union{SparseMatrixCSC, Symmetric}, f::AbstractVector, ch::ConstraintHandler) +function apply_zero!(K::Union{AbstractSparseMatrix, Symmetric}, f::AbstractVector, ch::ConstraintHandler) return apply!(K, f, ch, true) end -function apply!(KK::Union{SparseMatrixCSC, Symmetric}, f::AbstractVector, ch::ConstraintHandler, applyzero::Bool = false) +function apply!(KK::Union{AbstractSparseMatrix, Symmetric{<:Any, <:AbstractSparseMatrix}}, f::AbstractVector, ch::ConstraintHandler, applyzero::Bool = false) @assert isclosed(ch) sym = isa(KK, Symmetric) K = sym ? KK.data : KK @@ -617,43 +617,14 @@ function apply!(KK::Union{SparseMatrixCSC, Symmetric}, f::AbstractVector, ch::Co m = meandiag(K) # Use the mean of the diagonal here to not ruin things for iterative solver # Add inhomogeneities to f: (f - K * ch.inhomogeneities) - if !applyzero - @inbounds for i in 1:length(ch.inhomogeneities) - d = ch.prescribed_dofs[i] - v = ch.inhomogeneities[i] - if v != 0 - for j in nzrange(K, d) - r = K.rowval[j] - sym && r > d && break # don't look below diagonal - f[r] -= v * K.nzval[j] - end - end - end - if sym - # In the symmetric case, for a constrained dof `d`, we handle the contribution - # from `K[1:d, d]` in the loop above, but we are still missing the contribution - # from `K[(d+1):size(K,1), d]`. These values are not stored, but since the - # matrix is symmetric we can instead use `K[d, (d+1):size(K,1)]`. Looping over - # rows is slow, so loop over all columns again, and check if the row is a - # constrained row. - @inbounds for col in 1:size(K, 2) - for ri in nzrange(K, col) - row = K.rowval[ri] - row >= col && break - if (i = get(ch.dofmapping, row, 0); i != 0) - f[col] -= ch.inhomogeneities[i] * K.nzval[ri] - end - end - end - end - end + !applyzero && add_inhomogeneities!(KK, f, ch.inhomogeneities, ch.prescribed_dofs, ch.dofmapping) - # Condense K (C' * K * C) and f (C' * f) + # Condense K := (C' * K * C) and f := (C' * f) _condense!(K, f, ch.dofcoefficients, ch.dofmapping, sym) # Remove constrained dofs from the matrix - zero_out_columns!(K, ch.prescribed_dofs) - zero_out_rows!(K, ch.dofmapping) + zero_out_columns!(K, ch) + zero_out_rows!(K, ch) # Add meandiag to constraint dofs @inbounds for i in 1:length(ch.inhomogeneities) @@ -667,6 +638,50 @@ function apply!(KK::Union{SparseMatrixCSC, Symmetric}, f::AbstractVector, ch::Co return end +""" + add_inhomogeneities!(K, f::AbstractVector, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping::Dict{Int,Int}) + +Compute "f -= K*inhomogeneities". +By default this is a generic version via SpMSpV kernel. +""" +function add_inhomogeneities!(K, f::AbstractVector, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping) + return f .-= K * sparsevec(prescribed_dofs, inhomogeneities, size(K, 2)) +end + +# Optimized version for SparseMatrixCSC +add_inhomogeneities!(K::SparseMatrixCSC, f::AbstractVector, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping) = add_inhomogeneities_csc!(K, f, inhomogeneities, prescribed_dofs, dofmapping, false) +add_inhomogeneities!(K::Symmetric{<:Any, <:SparseMatrixCSC}, f::AbstractVector, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping) = add_inhomogeneities_csc!(K.data, f, inhomogeneities, prescribed_dofs, dofmapping, true) +function add_inhomogeneities_csc!(K::SparseMatrixCSC, f::AbstractVector, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping, sym::Bool) + @inbounds for i in 1:length(inhomogeneities) + d = prescribed_dofs[i] + v = inhomogeneities[i] + if v != 0 + for j in nzrange(K, d) + r = K.rowval[j] + sym && r > d && break # don't look below diagonal + f[r] -= v * K.nzval[j] + end + end + end + return if sym + # In the symmetric case, for a constrained dof `d`, we handle the contribution + # from `K[1:d, d]` in the loop above, but we are still missing the contribution + # from `K[(d+1):size(K,1), d]`. These values are not stored, but since the + # matrix is symmetric we can instead use `K[d, (d+1):size(K,1)]`. Looping over + # rows is slow, so loop over all columns again, and check if the row is a + # constrained row. + @inbounds for col in 1:size(K, 2) + for ri in nzrange(K, col) + row = K.rowval[ri] + row >= col && break + if (i = get(dofmapping, row, 0); i != 0) + f[col] -= inhomogeneities[i] * K.nzval[ri] + end + end + end + end +end + # Fetch dof coefficients for a dof prescribed by an affine constraint. Return nothing if the # dof is not prescribed, or prescribed by DBC. @inline function coefficients_for_dof(dofmapping, dofcoeffs, dof) @@ -675,6 +690,13 @@ end return dofcoeffs[idx] end + +function _condense!(K::AbstractSparseMatrix, f::AbstractVector, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, dofmapping::Dict{Int, Int}, sym::Bool = false) where {T} + # Return early if there are no non-trivial affine constraints + any(i -> !(i === nothing || isempty(i)), dofcoefficients) || return + error("condensation of ::$(typeof(K)) matrix not supported") +end + # Condenses K and f: C'*K*C, C'*f, in-place assuming the sparsity pattern is correct function _condense!(K::SparseMatrixCSC, f::AbstractVector, dofcoefficients::Vector{Union{Nothing, DofCoefficients{T}}}, dofmapping::Dict{Int, Int}, sym::Bool = false) where {T} @@ -785,22 +807,33 @@ function create_constraint_matrix(ch::ConstraintHandler{dh, T}) where {dh, T} return C, g end +""" + zero_out_columns!(K::AbstractMatrix, ch::ConstraintHandler) +Set the values of all columns associated with constrained dofs to zero. +""" +zero_out_columns! + +""" + zero_out_rows!(K::AbstractMatrix, ch::ConstraintHandler) +Set the values of all rows associated with constrained dofs to zero. +""" +zero_out_rows! # columns need to be stored entries, this is not checked -function zero_out_columns!(K, dofs::Vector{Int}) # can be removed in 0.7 with #24711 merged - @debug @assert issorted(dofs) - for col in dofs +function zero_out_columns!(K::SparseArrays.AbstractSparseMatrixCSC, ch::ConstraintHandler) # can be removed in 0.7 with #24711 merged + @debug @assert issorted(ch.prescribed_dofs) + for col in ch.prescribed_dofs r = nzrange(K, col) K.nzval[r] .= 0.0 end return end -function zero_out_rows!(K, dofmapping) +function zero_out_rows!(K::SparseArrays.AbstractSparseMatrixCSC, ch::ConstraintHandler) rowval = K.rowval nzval = K.nzval @inbounds for i in eachindex(rowval, nzval) - if haskey(dofmapping, rowval[i]) + if haskey(ch.dofmapping, rowval[i]) nzval[i] = 0 end end @@ -1673,9 +1706,16 @@ function _apply_local!( return end -# Condensation of affine constraints on element level. If possible this function only -# modifies the local arrays. @noinline missing_global() = error("can not condense constraint without the global matrix and vector") + +""" + _condense_local!(local_matrix::AbstractMatrix, local_vector::AbstractVector, + global_matrix#=::SparseMatrixCSC=#, global_vector#=::Vector=#, + global_dofs::AbstractVector, dofmapping::Dict, dofcoefficients::Vector) + +Condensation of affine constraints on element level. If possible this function only +modifies the local arrays. +""" function _condense_local!( local_matrix::AbstractMatrix, local_vector::AbstractVector, global_matrix #=::SparseMatrixCSC=#, global_vector #=::Vector=#, diff --git a/src/Ferrite.jl b/src/Ferrite.jl index 7b358463be..3fe45f199d 100644 --- a/src/Ferrite.jl +++ b/src/Ferrite.jl @@ -14,7 +14,8 @@ using NearestNeighbors: using OrderedCollections: OrderedSet using SparseArrays: - SparseArrays, SparseMatrixCSC, nonzeros, nzrange, rowvals, AbstractSparseMatrixCSC + SparseArrays, SparseMatrixCSC, nonzeros, nzrange, rowvals, + AbstractSparseMatrix, AbstractSparseMatrixCSC, sparsevec using StaticArrays: StaticArrays, MArray, MMatrix, SArray, SMatrix, SVector using WriteVTK: diff --git a/src/arrayutils.jl b/src/arrayutils.jl index f5b1bf868c..0537298ccf 100644 --- a/src/arrayutils.jl +++ b/src/arrayutils.jl @@ -77,11 +77,11 @@ function addindex!(A::SparseMatrixCSC{Tv}, v::Tv, i::Int, j::Int) where {Tv} end end -function fillzero!(A::SparseMatrixCSC{T}) where {T} +function fillzero!(A::AbstractSparseMatrix{T}) where {T} fill!(nonzeros(A), zero(T)) return A end -function fillzero!(A::Symmetric{T, <:SparseMatrixCSC}) where {T} +function fillzero!(A::Symmetric{T, <:AbstractSparseMatrix}) where {T} fillzero!(A.data) return A end diff --git a/src/assembler.jl b/src/assembler.jl index 078f5fcd34..816c0d038c 100644 --- a/src/assembler.jl +++ b/src/assembler.jl @@ -1,5 +1,6 @@ abstract type AbstractAssembler end abstract type AbstractCSCAssembler <: AbstractAssembler end +abstract type AbstractCSRAssembler <: AbstractAssembler end """ struct COOAssembler{Tv, Ti} @@ -179,6 +180,16 @@ struct CSCAssembler{Tv, Ti, MT <: AbstractSparseMatrixCSC{Tv, Ti}} <: AbstractCS sorteddofs::Vector{Int} end +""" +Assembler for sparse matrix with CSR storage type. +""" +struct CSRAssembler{Tv, Ti, MT <: AbstractSparseMatrix{Tv, Ti}} <: AbstractCSRAssembler #AbstractSparseMatrixCSR does not exist + K::MT + f::Vector{Tv} + permutation::Vector{Int} + sorteddofs::Vector{Int} +end + """ Assembler for symmetric sparse matrix with CSC storage type. """ @@ -189,7 +200,7 @@ struct SymmetricCSCAssembler{Tv, Ti, MT <: Symmetric{Tv, <:AbstractSparseMatrixC sorteddofs::Vector{Int} end -function Base.show(io::IO, ::MIME"text/plain", a::Union{CSCAssembler, SymmetricCSCAssembler}) +function Base.show(io::IO, ::MIME"text/plain", a::Union{CSCAssembler, CSRAssembler, SymmetricCSCAssembler}) print(io, typeof(a), " for assembling into:\n - ") summary(io, a.K) f = a.f @@ -200,9 +211,9 @@ function Base.show(io::IO, ::MIME"text/plain", a::Union{CSCAssembler, SymmetricC return end -matrix_handle(a::AbstractCSCAssembler) = a.K +matrix_handle(a::Union{AbstractCSCAssembler, AbstractCSRAssembler}) = a.K matrix_handle(a::SymmetricCSCAssembler) = a.K.data -vector_handle(a::AbstractCSCAssembler) = a.f +vector_handle(a::Union{AbstractCSCAssembler, AbstractCSRAssembler}) = a.f """ start_assemble(K::AbstractSparseMatrixCSC; fillzero::Bool=true) -> CSCAssembler @@ -221,6 +232,8 @@ necessary for efficient matrix assembly. To assemble the contribution from an el The keyword argument `fillzero` can be set to `false` if `K` and `f` should not be zeroed out, but instead keep their current values. + +Depending on the loaded extensions more assembly formats become available through this interface. """ start_assemble(K::Union{AbstractSparseMatrixCSC, Symmetric{<:Any, <:AbstractSparseMatrixCSC}}, f::Vector; fillzero::Bool) @@ -233,7 +246,7 @@ function start_assemble(K::Symmetric{T, <:SparseMatrixCSC}, f::Vector = T[]; fil return SymmetricCSCAssembler(K, f, zeros(Int, maxcelldofs_hint), zeros(Int, maxcelldofs_hint)) end -function finish_assemble(a::Union{CSCAssembler, SymmetricCSCAssembler}) +function finish_assemble(a::Union{CSCAssembler, CSRAssembler, SymmetricCSCAssembler}) return a.K, a.f end @@ -270,7 +283,7 @@ Sorts the dofs into a separate buffer and returns it together with a permutation return sorteddofs, permutation end -@propagate_inbounds function _assemble!(A::AbstractCSCAssembler, dofs::AbstractVector{<:Integer}, Ke::AbstractMatrix, fe::Union{AbstractVector, Nothing}, sym::Bool) +@propagate_inbounds function _assemble!(A::Union{AbstractCSCAssembler, AbstractCSRAssembler}, dofs::AbstractVector{<:Integer}, Ke::AbstractMatrix, fe::Union{AbstractVector, Nothing}, sym::Bool) ld = length(dofs) @boundscheck checkbounds(Ke, keys(dofs), keys(dofs)) if fe !== nothing @@ -281,15 +294,20 @@ end K = matrix_handle(A) @boundscheck checkbounds(K, dofs, dofs) - Krows = rowvals(K) - Kvals = nonzeros(K) # We assume that the input dofs are not sorted, because the cells need the dofs in # a specific order, which might not be the sorted order. Hence we sort them. # Note that we are not allowed to mutate `dofs` in the process. sorteddofs, permutation = _sortdofs_for_assembly!(A.permutation, A.sorteddofs, dofs) + return _assemble_inner!(K, Ke, dofs, sorteddofs, permutation, sym) +end + +@propagate_inbounds function _assemble_inner!(K::SparseMatrixCSC, Ke::AbstractMatrix, dofs::AbstractVector, sorteddofs::AbstractVector, permutation::AbstractVector, sym::Bool) current_col = 1 + Krows = rowvals(K) + Kvals = nonzeros(K) + ld = length(dofs) @inbounds for Kcol in sorteddofs maxlookups = sym ? current_col : ld Kecol = permutation[current_col] diff --git a/test/runtests.jl b/test/runtests.jl index 57cd82d8a7..8818e44d35 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -39,6 +39,7 @@ include("test_apply_analytical.jl") include("PoolAllocator.jl") include("test_deprecations.jl") include("blockarrays.jl") +include("test_assembler_extensions.jl") include("test_examples.jl") @test all(x -> isdefined(Ferrite, x), names(Ferrite)) # Test that all exported symbols are defined diff --git a/test/test_assembler_extensions.jl b/test/test_assembler_extensions.jl new file mode 100644 index 0000000000..3d234a8446 --- /dev/null +++ b/test/test_assembler_extensions.jl @@ -0,0 +1,109 @@ +import SparseMatricesCSR: SparseMatrixCSR, sparsecsr +using SparseArrays, LinearAlgebra + +@testset "SparseMatricesCSR extension" begin + + @testset "apply!(::SparseMatrixCSR,...)" begin + # Specifically this test that values below the diagonal of K2::Symmetric aren't touched + # and that the missing values are instead taken from above the diagonal. + grid = generate_grid(Line, (2,)) + dh = DofHandler(grid) + add!(dh, :u, Lagrange{RefLine, 1}()) + close!(dh) + ch = ConstraintHandler(dh) + add!(ch, Dirichlet(:u, getfacetset(grid, "left"), x -> 1)) + close!(ch) + K0 = sparse(rand(3, 3)) + K0 = K0' * K0 + K1 = SparseMatrixCSR(transpose(copy(K0))) + K2 = Symmetric(SparseMatrixCSR(transpose(copy(K0)))) + @test K0 == K1 + @test K1 == K2 + sol = [1.0, 2.0, 3.0] + f0 = K0 * sol + f1 = K1 * sol + f2 = K2 * sol + apply!(K0, f0, ch) + apply!(K1, f1, ch) + apply!(K2, f2, ch) + @test K0 == K1 + @test K1 == K2 + @test f0 == f1 + @test f1 == f2 + # Error for affine constraints + ch = ConstraintHandler(dh) + add!(ch, AffineConstraint(1, [3 => 1.0], 1.0)) + close!(ch) + @test_throws ErrorException("condensation of ::SparseMatrixCSR{1, Float64, Int64} matrix not supported") apply!(K2, f2, ch) + end + + @testset "assembly integration" begin + # Setup simple problem + grid = generate_grid(Line, (2,)) + dh = DofHandler(grid) + add!(dh, :u, Lagrange{RefLine, 1}()) + close!(dh) + + # Check if the matrix is correctly allocated + K = allocate_matrix(SparseMatrixCSR, dh) + I = [1, 1, 2, 2, 2, 3, 3] + J = [1, 2, 1, 2, 3, 2, 3] + V = zeros(7) + @test K == sparsecsr(I, J, V) + f = zeros(3) + + # Check that incuding the ch doesnot mess up the pattern + ch = ConstraintHandler(dh) + add!(ch, Dirichlet(:u, getfacetset(grid, "left"), (x, t) -> 1)) + close!(ch) + @test K == allocate_matrix(SparseMatrixCSR, dh, ch) + + # Check if assembly works + assembler = start_assemble(K, f) + ke = [-1.0 1.0; 2.0 -1.0] + fe = [1.0, 2.0] + assemble!(assembler, [1, 2], ke, fe) + assemble!(assembler, [3, 2], ke, fe) + I = [1, 1, 2, 2, 2, 3, 3] + J = [1, 2, 1, 2, 3, 2, 3] + V = [-1.0, 1.0, 2.0, -2.0, 2.0, 1.0, -1.0] + finish_assemble(assembler) + @test K ≈ sparsecsr(I, J, V) + @test f ≈ [1.0, 4.0, 1.0] + + # Check if constraint handler integration works + apply!(K, f, ch) + I = [1, 1, 2, 2, 2, 3, 3] + J = [1, 2, 1, 2, 3, 2, 3] + V = [4 / 3, 0.0, 0.0, -2.0, 2.0, 1.0, -1.0] + @test K ≈ sparsecsr(I, J, V) + @test f ≈ [4 / 3, 2.0, 1.0] + + # Check if coupling works + grid = generate_grid(Quadrilateral, (2, 2)) + ip = Lagrange{RefQuadrilateral, 1}() + dh = DofHandler(grid) + add!(dh, :u, ip) + add!(dh, :v, ip) + close!(dh) + + Ke_zeros = zeros(ndofs_per_cell(dh, 1), ndofs_per_cell(dh, 1)) + Ke_rand = rand(ndofs_per_cell(dh, 1), ndofs_per_cell(dh, 1)) + dofs = celldofs(dh, 1) + + for c1 in [true, false], c2 in [true, false], c3 in [true, false], c4 in [true, false] + coupling = [c1; c2;; c3; c4] + K = allocate_matrix(SparseMatrixCSR, dh; coupling) + a = start_assemble(K) + assemble!(a, dofs, Ke_zeros) + finish_assemble(a) + if all(coupling) + assemble!(a, dofs, Ke_rand) + @test Ke_rand ≈ K[dofs, dofs] + else + @test_throws ErrorException assemble!(a, dofs, Ke_rand) + end + end + end + +end