Skip to content

Modularize assembly internals and add SparseMatrixCSR extension #864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7edd47a
Some refactoring and trying to get the extension working.
termi-official Dec 23, 2023
7c8e7b1
Almost. Imhomogeneities are broken.
termi-official Dec 23, 2023
a29dbb0
revert faulty refactoring.
termi-official Dec 23, 2023
d8589ee
Refactor SpMSpV kernels and add tests
termi-official Dec 27, 2023
26c0f58
Revert heat example.
termi-official Dec 27, 2023
056f5c3
Devdocs.
termi-official Dec 27, 2023
f021d01
Symmetric assembler.
termi-official Dec 27, 2023
46f6428
Derp
termi-official Dec 27, 2023
4a47a3f
Revert constraints test battery.
termi-official Dec 28, 2023
d227ef7
Add sparsity pattern generator for CSR.
termi-official Dec 28, 2023
0c3efc8
Derp
termi-official Dec 28, 2023
a2160e0
Convenience function for sparsity pattern.
termi-official Dec 28, 2023
8102774
Test coverage for sparsity pattern, assembly and Dirichlet elimination.
termi-official Dec 28, 2023
cfabf75
Devdocs for add_inhomogeneities
termi-official Dec 28, 2023
0328f24
Add Knut's suggestion.
termi-official Dec 28, 2023
d20389a
Merge master
termi-official Jul 2, 2024
ed365f1
Trim ws
termi-official Jul 2, 2024
29888f6
Trim ws again
termi-official Jul 2, 2024
60bf45f
Update to new API
termi-official Jul 2, 2024
0b5a861
[skip ci] Comments.
termi-official Jul 2, 2024
d653475
Merge master
termi-official Sep 30, 2024
896fee1
Try to fix docs CI
termi-official Sep 30, 2024
3976701
Add matrix alloc to ext
termi-official Sep 30, 2024
43b755f
Docs
termi-official Sep 30, 2024
debdf9c
Merge master.
termi-official Nov 5, 2024
d7f2cc5
Runic
termi-official Nov 5, 2024
512eaac
Docs oopsie
termi-official Nov 5, 2024
cdc828e
Make CodeCov happy.
termi-official Nov 5, 2024
6f80dfd
More happy.
termi-official Nov 5, 2024
0f42594
Derp
termi-official Nov 5, 2024
4b45329
Merge master
termi-official Mar 26, 2025
e2ded19
Merge master
termi-official May 6, 2025
a18fdff
Thanks to Knut for pointing this out.
termi-official May 6, 2025
6a472b4
Export
termi-official May 6, 2025
f1bb29f
Make Aqua happy
termi-official May 6, 2025
a503c00
Add condense to devdocs
termi-official May 6, 2025
3ed968f
Resolve Knuts comments and add his suggestions
termi-official May 8, 2025
832f586
Format
termi-official May 8, 2025
47fc786
Pass ch into inhomogeneity fun.
termi-official May 9, 2025
17c5068
Revert
termi-official May 9, 2025
4793c2f
Apply suggestions from code review
termi-official May 12, 2025
d95b5c6
Update docs/src/devdocs/assembly.md
termi-official May 12, 2025
5d2e82c
Revert
termi-official May 12, 2025
af13a8f
Missing import
termi-official May 12, 2025
9f4ab68
[skip ci] Add changelog entries
termi-official May 13, 2025
0cce299
Apply suggestions from code review
termi-official May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ 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"
EnumX = "1"
Metis = "1.3"
SparseMatricesCSR = "0.6"
NearestNeighbors = "0.4"
Preferences = "1"
Reexport = "1"
Expand All @@ -46,8 +49,9 @@ NBInclude = "0db19996-df87-5ea3-a455-e3a50d440464"
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
SparseMatricesCSR = "a0a7dd2c-ebf4-11e9-1f05-cf50bc540ca1"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"

[targets]
test = ["BlockArrays", "Downloads", "FerriteGmsh", "ForwardDiff", "Gmsh", "IterativeSolvers", "JET", "Metis", "NBInclude", "ProgressMeter", "Random", "SHA", "Test", "TimerOutputs", "Logging"]
test = ["BlockArrays", "Downloads", "FerriteGmsh", "ForwardDiff", "Gmsh", "IterativeSolvers", "JET", "Metis", "NBInclude", "ProgressMeter", "Random", "SHA", "SparseMatricesCSR", "Test", "TimerOutputs", "Logging"]
33 changes: 33 additions & 0 deletions docs/src/devdocs/assembler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# [Assembler](@id devdocs-assembler)

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 assembler [`AssemblerSparsityPattern`](@ref) should be able to handle most cases in practice. To support a custom format users have to dispatch the following functions:

```@docs
Ferrite._assemble_inner!
Ferrite.zero_out_rows!
Ferrite.zero_out_columns!
```

and the `AbstractSparseMatrix` interface for their custom matrix type.

## 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.

```@docs
Ferrite.matrix_handle
Ferrite.vector_handle
start_assemble!
finish_assemble!
assemble!
```

For local elimination support the following functions might also need custom dispatches

```@docs
Ferrite._condense_local!
```

## Custom Matrix Type Sparsity Pattern

TODO `create_sparsity_pattern`
2 changes: 1 addition & 1 deletion docs/src/devdocs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ developing the library.

```@contents
Depth = 1
Pages = ["reference_cells.md", "interpolations.md", "elements.md", "FEValues.md", "dofhandler.md", "performance.md"]
Pages = ["reference_cells.md", "interpolations.md", "elements.md", "FEValues.md", "dofhandler.md", "assembler.md", "performance.md"]
```
80 changes: 80 additions & 0 deletions ext/FerriteSparseMatrixCSR.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
module FerriteSparseMatrixCSR

using Ferrite, SparseArrays, SparseMatricesCSR
import Base: @propagate_inbounds

@propagate_inbounds function Ferrite._assemble_inner!(K::SparseMatrixCSR, Ke::AbstractMatrix, dofs::AbstractVector, sorteddofs::AbstractVector, permutation::AbstractVector, sym::Bool)
current_row = 1
ld = length(dofs)
@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) || _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]])
_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
end

function Ferrite.zero_out_columns!(K::SparseMatrixCSR, ch::ConstraintHandler)
colval = K.colval
nzval = K.nzval
@inbounds for i in eachindex(colval, nzval)
if haskey(ch.dofmapping, colval[i])
nzval[i] = 0
end
end
end

function Ferrite.create_sparsity_pattern(::Type{<:SparseMatrixCSR}, dh, ch; kwargs...)
# create SparseMatrixCSC
K = create_sparsity_pattern(dh, ch; kwargs...)
# transform to SparseMatrixCSR
return SparseMatrixCSR(transpose(sparse(transpose(K))))
end

function Ferrite.create_sparsity_pattern(::Type{<:SparseMatrixCSR}, dh; kwargs...)
# create SparseMatrixCSC
K = create_sparsity_pattern(dh; kwargs...)
# transform to SparseMatrixCSR
return SparseMatrixCSR(transpose(sparse(transpose(K))))
end

end
101 changes: 59 additions & 42 deletions src/Dofs/ConstraintHandler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,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.
Expand Down Expand Up @@ -616,11 +616,11 @@ 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)
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)
apply!(K, f, ch, true)
end

Expand All @@ -629,7 +629,7 @@ end
const APPLY_TRANSPOSE = ApplyStrategy.Transpose
const APPLY_INPLACE = ApplyStrategy.Inplace

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;
strategy::ApplyStrategy.T=ApplyStrategy.Transpose)
@assert isclosed(ch)
sym = isa(KK, Symmetric)
Expand All @@ -641,43 +641,14 @@ function apply!(KK::Union{SparseMatrixCSC,Symmetric}, f::AbstractVector, ch::Con
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)
Expand All @@ -690,6 +661,45 @@ function apply!(KK::Union{SparseMatrixCSC,Symmetric}, f::AbstractVector, ch::Con
end
end

# Generic version to compute "f -= K*inhomogeneities"
function add_inhomogeneities!(K, f::AbstractVector, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this only modifies f.

Suggested change
function add_inhomogeneities!(K, f::AbstractVector, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping)
function add_inhomogeneities!(f::AbstractVector, K::AbstractMatrix, inhomogeneities::AbstractVector, prescribed_dofs::AbstractVector{<:Integer}, dofmapping)

I would prefer the interface
add_inhomogeneities!(f::AbstractVector, ch::ConstraintHandler, K::AbstractMatrix) for extra clarity (that only f is modified), and easier extensibility (following the change for zero_out_columns! and zero_out_rows!).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. That makes sense from an interface point of view.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, any reason not passing the constraint handler instead of inhomogeneities, prescribed_dofs, and dofmapping? (Esp. since dofmapping isn't always used).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No specific reason. The internals are not consistent here. I changed it to take the ch for now to simplify forward compatibility.

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
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)
Expand All @@ -698,6 +708,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

Expand Down Expand Up @@ -808,19 +825,19 @@ function create_constraint_matrix(ch::ConstraintHandler{dh,T}) where {dh,T}
end

# 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
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
Expand Down
4 changes: 2 additions & 2 deletions src/arrayutils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading