Skip to content

Commit 16a6ef1

Browse files
committed
Add InverseMap for lazy inverse of a linear map.
1 parent 99bbc0e commit 16a6ef1

File tree

6 files changed

+121
-0
lines changed

6 files changed

+121
-0
lines changed

docs/src/types.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ LinearMaps.FillMap
9696

9797
Type for representing linear maps that are embedded in larger zero maps.
9898

99+
100+
### `InverseMap`
101+
102+
Type for lazy inverse of another linear map.
103+
104+
```@docs
105+
LinearMaps.InverseMap
106+
```
107+
99108
## Methods
100109

101110
### Multiplication methods

src/LinearMaps.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module LinearMaps
33
export LinearMap
44
export , kronsum,
55
export FillMap
6+
export InverseMap
67

78
using LinearAlgebra
89
import LinearAlgebra: mul!
@@ -344,6 +345,7 @@ include("embeddedmap.jl") # embedded linear maps
344345
include("conversion.jl") # conversion of linear maps to matrices
345346
include("show.jl") # show methods for LinearMap objects
346347
include("getindex.jl") # getindex functionality
348+
include("inversemap.jl")
347349

348350
"""
349351
LinearMap(A::LinearMap; kwargs...)::WrappedMap

src/inversemap.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
struct InverseMap{T, F, S} <: LinearMap{T}
2+
A::F
3+
ldiv!::S
4+
end
5+
6+
"""
7+
InverseMap(A; solver = ldiv!)
8+
9+
Lazy inverse of `A` such that `InverseMap(A) * x` is the same as `A \\ x`. Letting an
10+
`InverseMap` act on a vector thus requires solving a linear system.
11+
12+
A solver function can be passed with the `solver` keyword argument. The solver should
13+
be of the form `f(y, A, x)` where `A` is the wrapped map, `x` the right hand side, and
14+
`y` a preallocated output vector in which the result should be stored. The default solver
15+
is `LinearAlgebra.ldiv!`.
16+
17+
Note that `A` must be compatible with the solver function. `A` can, for example`, be a
18+
factorization of a matrix, or another another `LinearMap` (in combination with an
19+
iterative solver such as conjugate gradient).
20+
21+
# Examples
22+
```julia
23+
julia> using LinearMaps, LinearAlgebra
24+
25+
julia> A = rand(2, 2); b = rand(2);
26+
27+
julia> InverseMap(lu(A)) * b
28+
2-element Vector{Float64}:
29+
1.0531895201271027
30+
-0.4718540250893251
31+
32+
julia> A \\ b
33+
2-element Vector{Float64}:
34+
1.0531895201271027
35+
-0.4718540250893251
36+
```
37+
"""
38+
function InverseMap(A::F; solver::S=LinearAlgebra.ldiv!) where {F, S}
39+
T = eltype(A)
40+
InverseMap{T,F,S}(A, solver)
41+
end
42+
43+
Base.size(imap::InverseMap) = size(imap.A)
44+
45+
function LinearMaps._unsafe_mul!(y::AbstractVecOrMat, imap::InverseMap, x::AbstractVector)
46+
imap.ldiv!(y, imap.A, x)
47+
return y
48+
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
33
BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e"
44
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
5+
IterativeSolvers = "42fd0dbc-a981-5370-80f2-aaf504508153"
56
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
67
Quaternions = "94ee1d12-ae83-5a48-8b1c-48b8ff168ae0"
78
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

test/inversemap.jl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using Test, LinearMaps, LinearAlgebra, SparseArrays, IterativeSolvers
2+
3+
@testset "inversemap" begin
4+
# Dense test data
5+
A = rand(10, 10) + 5I; A = A'A
6+
B = rand(10, 10)
7+
b = rand(10)
8+
# LU
9+
@test A \ b InverseMap(lu(A)) * b
10+
# Cholesky
11+
@test A \ b InverseMap(cholesky(A)) * b
12+
# Specify solver
13+
@test A \ b InverseMap(A; solver=IterativeSolvers.cg!) * b
14+
# Composition with other maps
15+
@test A \ B * b InverseMap(lu(A)) * B * b
16+
# Composition: make sure solvers called with vector B * b and not matrix B
17+
my_ldiv! = (y, A, x) -> begin
18+
@test x isa AbstractVector
19+
@test x B * b
20+
return ldiv!(y, lu(A), x)
21+
end
22+
@test A \ B * b InverseMap(A; solver=my_ldiv!) * B * b
23+
24+
# Sparse test data
25+
A = sprand(10, 10, 0.2) + 5I; A = A'A
26+
B = sprand(10, 10, 0.5)
27+
@test A \ b InverseMap(lu(A)) * b
28+
# Cholesky (CHOLMOD doesn't support inplace ldiv!)
29+
my_ldiv! = (y, A, x) -> copy!(y, A \ x)
30+
@test A \ b InverseMap(cholesky(A); solver=my_ldiv!) * b
31+
# Specify solver
32+
@test A \ b InverseMap(A; solver=IterativeSolvers.cg!) * b
33+
# Composition with other maps
34+
@test A \ (B * b) InverseMap(lu(A)) * B * b
35+
# Composition: make sure solver is called with vector B * b and not matrix B
36+
my_ldiv! = (y, A, x) -> begin
37+
@test x isa AbstractVector
38+
@test x B * b
39+
return ldiv!(y, lu(A), x)
40+
end
41+
@test A \ (B * b) InverseMap(A; solver=my_ldiv!) * B * b
42+
43+
44+
# Example from https://www.dealii.org/current/doxygen/deal.II/step_20.html#SolvingusingtheSchurcomplement
45+
M = sparse(2.0I, 10, 10) + sprand(10, 10, 0.1); M = M'M
46+
iM = InverseMap(M; solver=IterativeSolvers.cg!)
47+
B = sparse(5.0I, 10, 5)
48+
F = rand(10)
49+
G = rand(5)
50+
51+
# Solve using Schur complement
52+
G′ = B' * iM * F - G
53+
S = B' * iM * B
54+
P = IterativeSolvers.cg(S, G′)
55+
U = IterativeSolvers.cg(M, F - B * P)
56+
57+
# Solve using standard method and compare
58+
@test [M B; B' 0I] \ [F; G] [U; P]
59+
end

test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ include("nontradaxes.jl")
3737
include("embeddedmap.jl")
3838

3939
include("getindex.jl")
40+
41+
include("inversemap.jl")

0 commit comments

Comments
 (0)