Skip to content

Commit 341b22d

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

File tree

9 files changed

+225
-1
lines changed

9 files changed

+225
-1
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "LinearMaps"
22
uuid = "7a12625a-238d-50fd-b39a-03d52299707e"
3-
version = "3.7.0"
3+
version = "3.8.0"
44

55
[deps]
66
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

docs/src/history.md

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

3+
## What's new in v3.8
4+
5+
* A new map called [`InverseMap`](@ref) is introduced. Letting an `InverseMap` act on a
6+
vector is equivalent to solving the linear system, i.e. `InverseMap(A) * b` is the same as
7+
`A \ b`. The default solver is `ldiv!`, but can be specified with the `solver` keyword
8+
argument to the constructor (see the docstring for details). Note that `A` must be
9+
compatible with the solver: `A` can, for example, be a factorization, or another
10+
`LinearMap` in combination with an iterative solver.
11+
312
## What's new in v3.7
413

514
* `mul!(M::AbstractMatrix, A::LinearMap, s::Number, a, b)` methods are provided, mimicking

docs/src/index.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,51 @@ KrylovKit.eigsolve(Δ, size(Δ, 1), 3, :LR)
111111

112112
leveraging the fact that objects of type `L <: LinearMap` are callable.
113113

114+
115+
The `InverseMap` type can be used to lazily represent the inverse of an operator.
116+
When this map acts on a vector the linear system is solved. This can be used to solve a
117+
system of the form ``Sx = (C A^{-1} B) x = b`` without explicitly computing ``A^{-1}``
118+
(see for example solving a linear system using the
119+
[Schur complement](https://en.wikipedia.org/wiki/Schur_complement#Application_to_solving_linear_equations)).
120+
121+
```julia
122+
using LinearMaps, IterativeSolvers
123+
124+
A = [2.0 1.5 0.0
125+
1.5 3.0 0.0
126+
0.0 0.0 4.0]
127+
B = [2.0 0.0
128+
0.0 1.0
129+
0.0 0.0]
130+
C = B'
131+
b = [2.0, 3.0]
132+
133+
# Use IterativeSolvers.cg! to solve the system with 0 as the initial guess
134+
linsolve = (x, A, b) -> IterativeSolvers.cg!(fill!(x, 0), A, b)
135+
136+
# Construct the linear map S
137+
S = C * InverseMap(A; solver=linsolve) * B
138+
139+
# Solve the system
140+
IterativeSolvers.cg(S, b)
141+
```
142+
143+
In every CG iteration `S` acts on the vector `b`. Since `S` is a composed linear map,
144+
`S * b` is roughly equivalent to
145+
146+
```julia
147+
# Apply first linear map B to b
148+
tmp1 = B * b
149+
# Apply second linear map: solve linear system with vector tmp1 as RHS
150+
tmp2 = A \ tmp1
151+
# Apply third linear map C to tmp2
152+
result = C * tmp2
153+
```
154+
155+
i.e. inside the CG solver for solving `Sx = b` we use CG to solve another inner linear
156+
system.
157+
158+
114159
## Philosophy
115160

116161
Several iterative linear algebra methods such as linear solvers or eigensolvers

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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 `LinearMap` (in combination with an iterative solver
19+
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+
Base.transpose(imap::InverseMap) = InverseMap(transpose(imap.A); solver=imap.ldiv!)
45+
Base.adjoint(imap::InverseMap) = InverseMap(adjoint(imap.A); solver=imap.ldiv!)
46+
47+
LinearAlgebra.issymmetric(imap::InverseMap) = issymmetric(imap.A)
48+
LinearAlgebra.ishermitian(imap::InverseMap) = ishermitian(imap.A)
49+
LinearAlgebra.isposdef(imap::InverseMap) = isposdef(imap.A)
50+
51+
function LinearMaps._unsafe_mul!(y::AbstractVecOrMat, imap::InverseMap, x::AbstractVecOrMat)
52+
imap.ldiv!(y, imap.A, x)
53+
return y
54+
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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using Test, LinearMaps, LinearAlgebra, SparseArrays, IterativeSolvers
2+
3+
@testset "inversemap" begin
4+
# First argument to cg! doubles as the initial guess so we make sure it is 0 instead
5+
# of potential garbage since we don't control the allocation of the output vector.
6+
cgz! = (x, A, b) -> IterativeSolvers.cg!(fill!(x), A, b)
7+
# Dense test data
8+
A = rand(10, 10) + 5I; A = A'A
9+
B = rand(10, 10)
10+
b = rand(10)
11+
# LU
12+
@test A \ b InverseMap(lu(A)) * b
13+
# Cholesky
14+
@test A \ b InverseMap(cholesky(A)) * b
15+
# Specify solver
16+
@test A \ b InverseMap(A; solver=cgz!) * b atol=1e-8
17+
# Composition with other maps
18+
@test A \ B * b InverseMap(lu(A)) * B * b
19+
# Composition: make sure solvers called with vector B * b and not matrix B
20+
my_ldiv! = (y, A, x) -> begin
21+
@test x isa AbstractVector
22+
@test x B * b
23+
return ldiv!(y, lu(A), x)
24+
end
25+
@test A \ B * b InverseMap(A; solver=my_ldiv!) * B * b
26+
27+
# Sparse test data
28+
A = sprand(10, 10, 0.2) + 5I; A = A'A
29+
B = sprand(10, 10, 0.5)
30+
@test A \ b InverseMap(lu(A)) * b
31+
# Cholesky (CHOLMOD doesn't support inplace ldiv!)
32+
my_ldiv! = (y, A, x) -> copy!(y, A \ x)
33+
@test A \ b InverseMap(cholesky(A); solver=my_ldiv!) * b
34+
# Specify solver
35+
@test A \ b InverseMap(A; solver=cgz!) * b atol=1e-8
36+
# Composition with other maps
37+
@test A \ (B * b) InverseMap(lu(A)) * B * b
38+
# Composition: make sure solver is called with vector B * b and not matrix B
39+
my_ldiv! = (y, A, x) -> begin
40+
@test x isa AbstractVector
41+
@test x B * b
42+
return ldiv!(y, lu(A), x)
43+
end
44+
@test A \ (B * b) InverseMap(A; solver=my_ldiv!) * B * b
45+
46+
# Interface testing: note that not all combinations of factorization and
47+
# is(symmetric|hermitian|posdef) and transpose/adjoint are supported, so some tests are
48+
# run with just wrapping the matrix and specifying a solver that factorizes the matrix.
49+
## Real symmetric (and Hermitian)
50+
A = Float64[3 2; 2 5]; x = rand(2)
51+
iA = InverseMap(A; solver=(y, A, x)->ldiv!(y, cholesky(A), x))
52+
@test ishermitian(A) == ishermitian(iA) == true
53+
@test issymmetric(A) == issymmetric(iA) == true
54+
@test transpose(A) \ x transpose(iA) * x
55+
iA = InverseMap(cholesky(A))
56+
@test isposdef(A) == isposdef(iA) == true
57+
@test adjoint(A) \ x adjoint(iA) * x
58+
## Real non-symmetric
59+
A = Float64[3 2; -2 5]; x = rand(2)
60+
iA = InverseMap(A; solver=(y, A, x)->ldiv!(y, lu(A), x))
61+
@test ishermitian(A) == ishermitian(iA) == false
62+
@test issymmetric(A) == issymmetric(iA) == false
63+
@test isposdef(A) == isposdef(iA) == false
64+
iA = InverseMap(lu(A))
65+
@test transpose(A) \ x transpose(iA) * x
66+
@test adjoint(A) \ x adjoint(iA) * x
67+
## Complex Hermitian
68+
A = ComplexF64[3 2im; -2im 5]; x = rand(ComplexF64, 2)
69+
iA = InverseMap(A; solver=(y, A, x)->ldiv!(y, cholesky(A), x))
70+
@test ishermitian(A) == ishermitian(iA) == true
71+
@test issymmetric(A) == issymmetric(iA) == false
72+
@test transpose(A) \ x transpose(iA) * x
73+
iA = InverseMap(cholesky(A))
74+
@test isposdef(A) == isposdef(iA) == true
75+
@test adjoint(A) \ x adjoint(iA) * x
76+
## Complex non-Hermitian
77+
A = ComplexF64[3 2im; 3im 5]; x = rand(ComplexF64, 2)
78+
iA = InverseMap(A; solver=(y, A, x)->ldiv!(y, lu(A), x))
79+
@test ishermitian(A) == ishermitian(iA) == false
80+
@test issymmetric(A) == issymmetric(iA) == false
81+
@test isposdef(A) == isposdef(iA) == false
82+
iA = InverseMap(lu(A))
83+
@test transpose(A) \ x transpose(iA) * x
84+
@test adjoint(A) \ x adjoint(iA) * x
85+
86+
# Example from https://www.dealii.org/current/doxygen/deal.II/step_20.html#SolvingusingtheSchurcomplement
87+
M = sparse(2.0I, 10, 10) + sprand(10, 10, 0.1); M = M'M
88+
@show M.colptr
89+
@show M.rowval
90+
@show M.nzval
91+
iM = InverseMap(M; solver=cgz!)
92+
B = sparse(5.0I, 10, 5)
93+
F = rand(10)
94+
G = rand(5)
95+
## Solve using Schur complement
96+
G′ = B' * iM * F - G
97+
S = B' * iM * B
98+
P = IterativeSolvers.cg(S, G′)
99+
U = IterativeSolvers.cg(M, F - B * P)
100+
## Solve using standard method and compare
101+
@test [M B; B' 0I] \ [F; G] [U; P] atol=1e-8
102+
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)