Skip to content

Commit d905c8a

Browse files
committed
Add Schur Complement Preconditioner
1 parent 7743a4b commit d905c8a

5 files changed

Lines changed: 134 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [2.1.0]
4+
5+
- Add `SchurComplementPreconditioner` and `SchurComplementPreconBuilder`
6+
37
## [2.0.0] - 2026-01-06
48

59
- Remove solver + precon API which is not based on precs or directly overloading `\`.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ExtendableSparse"
22
uuid = "95c220a8-a1cf-11e9-0c77-dbfce5f500b3"
3-
version = "2.0.1"
3+
version = "2.1.0"
44
authors = ["Juergen Fuhrmann <juergen.fuhrmann@wias-berlin.de>", "Daniel Runge"]
55

66
[deps]

src/ExtendableSparse.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module ExtendableSparse
77

88
using DocStringExtensions: DocStringExtensions, SIGNATURES, TYPEDEF, TYPEDFIELDS, TYPEDSIGNATURES
99
using ILUZero: ILUZero
10-
using LinearAlgebra: LinearAlgebra, Diagonal, Hermitian, Symmetric, Tridiagonal, convert, mul!, ldiv!
10+
using LinearAlgebra: LinearAlgebra, Diagonal, Hermitian, Symmetric, Tridiagonal, convert, mul!, ldiv!, lu
1111
using SparseArrays: SparseArrays, AbstractSparseMatrix, AbstractSparseMatrixCSC, SparseMatrixCSC
1212
using SparseArrays: dropzeros!, findnz, nzrange, sparse, spzeros, rowvals, getcolptr, nonzeros, nnz
1313
using Sparspak: sparspaklu
@@ -42,7 +42,7 @@ export eliminate_dirichlet, eliminate_dirichlet!, mark_dirichlet
4242

4343

4444
include("preconbuilders.jl")
45-
export LinearSolvePreconBuilder, BlockPreconBuilder, JacobiPreconBuilder
45+
export LinearSolvePreconBuilder, BlockPreconBuilder, JacobiPreconBuilder, SchurComplementPreconBuilder
4646
@public ILUZeroPreconBuilder, ILUTPreconBuilder
4747

4848

src/preconbuilders.jl

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,112 @@ LinearAlgebra.ldiv!(fact::JacobiPreconditioner, v) = ldiv!(fact.factorization, v
294294

295295
allow_views(::JacobiPreconditioner) = true
296296
allow_views(::Type{JacobiPreconditioner}) = true
297+
298+
299+
"""
300+
SchurComplementPreconditioner{AT, ST}
301+
302+
Preconditioner for saddle-point problems of the form:
303+
```
304+
[ A B ]
305+
[ Bᵀ 0 ]
306+
```
307+
308+
# Fields
309+
- `partitions`: Vector of two ranges defining matrix partitioning
310+
- `A_fac::AT`: Factorization of the A-block (top-left)
311+
- `S_fac::ST`: Factorization of the Schur complement (approximation of -BᵀA⁻¹B)
312+
"""
313+
struct SchurComplementPreconditioner{AT, ST}
314+
partitions
315+
A_fac::AT
316+
S_fac::ST
317+
end
318+
319+
320+
"""
321+
SchurComplementPreconBuilder(dofs_first_block, A_factorization, S_factorization = lu)
322+
323+
Factory function creating preconditioners for saddle-point problems.
324+
325+
Creates a preconditioner of the form
326+
```
327+
[ ≈ A 0 ]
328+
[ 0 ≈ -α BᵀA⁻¹B ]
329+
```
330+
331+
where α is 1 by default (can be adjusted by `S_factor`).
332+
333+
In the Schur factor, we use `Diagonal(A)` for efficiency.
334+
335+
# Arguments
336+
- `dofs_first_block`: Number of degrees of freedom in the first block (size of A matrix)
337+
- `A_factorization`: Factorization method for the A-block (e.g., `ilu0, Diagonal, lu`)
338+
- `S_factorization`: Factorization method for the Schur complement (defaults to full `lu`)
339+
- `S_factor`: An additional scalar factor for the Schur complement block. Can be used to obtain positive definite preconditioners
340+
341+
Note: All factorizations need to be able for operate on vector views, as
342+
343+
Returns a function `prec(M, p)` that creates a `SchurComplementPreconditioner`.
344+
"""
345+
function SchurComplementPreconBuilder(dofs_first_block, A_factorization, S_factorization = lu; verbosity = 0, S_factor = 1.0)
346+
347+
# this is the resulting preconditioner
348+
function prec(M)
349+
350+
# We have
351+
# M = [ A B ] → n1 dofs
352+
# [ Bᵀ 0 ] → n2 dofs
353+
354+
n1 = dofs_first_block
355+
n2 = size(M, 1) - n1
356+
357+
# I see no other way than creating this expensive copy
358+
A = M[1:n1, 1:n1]
359+
B = M[1:n1, (n1 + 1):end]
360+
361+
# first factorization
362+
A_fac = A_factorization(A)
363+
364+
verbosity > 0 && @info "SchurComplementPreconBuilder: A ($n1×$n1) is factorized"
365+
366+
# compute the Schur Matrix
367+
# S ≈ -α BᵀA⁻¹B
368+
# we use the diagonal of A: this is _very_ performant and creates a sparse result
369+
S = -S_factor * B' * (Diagonal(A) \ B)
370+
371+
verbosity > 0 && @info "SchurComplementPreconBuilder: S ($n2×$n2) is computed"
372+
373+
# factorize S
374+
S_fac = S_factorization(S)
375+
376+
verbosity > 0 && @info "SchurComplementPreconBuilder: S ($n2×$n2) is factorized"
377+
378+
return SchurComplementPreconditioner([1:n1, (n1 + 1):(n1 + n2)], A_fac, S_fac)
379+
end
380+
381+
return prec
382+
end
383+
384+
385+
function LinearAlgebra.ldiv!(u, p::SchurComplementPreconditioner, v)
386+
387+
(part1, part2) = p.partitions
388+
# do it in parallel
389+
t1 = Threads.@spawn @views ldiv!(u[part1], p.A_fac, v[part1])
390+
t2 = Threads.@spawn @views ldiv!(u[part2], p.S_fac, v[part2])
391+
fetch.([t1, t2])
392+
393+
return u
394+
end
395+
396+
function LinearAlgebra.ldiv!(p::SchurComplementPreconditioner, v)
397+
398+
(part1, part2) = p.partitions
399+
# do it in parallel
400+
t1 = Threads.@spawn @views ldiv!(p.A_fac, v[part1])
401+
t2 = Threads.@spawn @views ldiv!(p.S_fac, v[part2])
402+
fetch.([t1, t2])
403+
404+
return v
405+
end

test/test_block.jl

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
module test_block
2-
using Test
2+
using AMGCLWrap
33
using ExtendableSparse
4-
using ExtendableSparse: BlockPreconditioner, jacobi
4+
using ExtendableSparse: BlockPreconditioner, jacobi, SchurComplementPreconBuilder
55
using ILUZero, AlgebraicMultigrid
66
using IterativeSolvers
77
using LinearAlgebra
8+
using SparseArrays
89
using Sparspak
9-
using AMGCLWrap
10+
using Test
1011

1112
ExtendableSparse.allow_views(::typeof(ilu0)) = true
1213

@@ -29,6 +30,20 @@ function main(; n = 100)
2930
sol = cg(A, b, Pl = BlockPreconditioner(A; partitioning, factorization = sparspaklu))
3031
@test sol sol0
3132

33+
# Schur complement: create a saddle point system
34+
let
35+
m = n ÷ 10
36+
B = I[1:(n^2), 1:(m^2)]
37+
M = [ A B; B' spzeros(m^2, m^2)]
38+
39+
sol1 = ones(n^2 + m^2)
40+
c = M * sol1
41+
42+
# S_factor = -1.0 to make the precon SPD
43+
sol = cg(M, c, Pl = SchurComplementPreconBuilder(n^2, lu, S_factor = -1.0)(M))
44+
@test sol sol1
45+
end
46+
3247
return
3348
end
3449

0 commit comments

Comments
 (0)