Skip to content

Commit 3031bc0

Browse files
authored
Add documentation on how to define custom maps (#111)
1 parent bc21d92 commit 3031bc0

File tree

4 files changed

+223
-5
lines changed

4 files changed

+223
-5
lines changed

docs/Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
[deps]
2+
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
23
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
34
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
45
LinearMaps = "7a12625a-238d-50fd-b39a-03d52299707e"
6+
Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306"
57

68
[compat]
9+
BenchmarkTools = "0.4, 0.5"
710
Documenter = "0.25"
11+
Literate = "2"

docs/make.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
using Documenter
2+
using Literate
23
using LinearMaps
34
using LinearAlgebra
45
using SparseArrays
56

7+
Literate.markdown(joinpath(@__DIR__, "src", "custom.jl"), joinpath(@__DIR__, "src/generated"))
8+
69
makedocs(
7-
sitename = "LinearMaps",
10+
sitename = "LinearMaps.jl",
811
format = Documenter.HTML(),
912
modules = [LinearMaps],
1013
pages = Any[
1114
"Home" => "index.md",
1215
"Version history" => "history.md",
1316
"Types and methods" => "types.md",
14-
"Custom maps" => "custom.md",
17+
"Custom maps" => "generated/custom.md",
1518
"Related packages" => "related.md"
1619
]
1720
)

docs/src/custom.jl

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# # Defining custom `LinearMap` types
2+
3+
# In this section, we want to demonstrate on a simple, actually built-in, linear map type
4+
# how to define custom `LinearMap` subtypes. First of all, `LinearMap{T}` is an extendable
5+
# abstract type, where `T` denotes the `eltype`.
6+
7+
# ## Basics
8+
9+
# As an example, we want to define a map type whose objects correspond to lazy analogues
10+
# of `fill`ed matrices. Naturally, we need to store the filled value `λ` and the `size`
11+
# of the linear map.
12+
13+
using LinearMaps, LinearAlgebra
14+
15+
struct MyFillMap{T} <: LinearMaps.LinearMap{T}
16+
λ::T
17+
size::Dims{2}
18+
function MyFillMap::T, dims::Dims{2}) where {T}
19+
all((0), dims) || throw(ArgumentError("dims of MyFillMap must be non-negative"))
20+
promote_type(T, typeof(λ)) == T || throw(InexactError())
21+
return new{T}(λ, dims)
22+
end
23+
end
24+
25+
# By default, for any `A::MyFillMap{T}`, `eltype(A)` returns `T`. Upon application to a
26+
# vector `x` and/or interaction with other `LinearMap` objects, we need to check consistent
27+
# sizes.
28+
29+
Base.size(A::MyFillMap) = A.size
30+
31+
# By a couple of defaults provided for all subtypes of `LinearMap`, we only need to define
32+
# a `LinearAlgebra.mul!` method to have minimal, operational type.
33+
34+
function LinearAlgebra.mul!(y::AbstractVecOrMat, A::MyFillMap, x::AbstractVector)
35+
LinearMaps.check_dim_mul(y, A, x)
36+
return fill!(y, iszero(A.λ) ? zero(eltype(y)) : A.λ*sum(x))
37+
end
38+
39+
# Again, due to generic fallbacks the following now "just work":
40+
41+
# * out-of-place multiplication `A*x`,
42+
# * in-place multiplication with vectors `mul!(y, A, x)`,
43+
# * in-place multiply-and-add with vectors `mul!(y, A, x, α, β)`,
44+
# * in-place multiplication and multiply-and-add with matrices `mul!(Y, A, X, α, β)`,
45+
# * conversion to a (sparse) matrix `Matrix(A)` and `sparse(A)`.
46+
47+
A = MyFillMap(5.0, (3, 3)); x = ones(3); sum(x)
48+
49+
#-
50+
51+
A * x
52+
53+
#-
54+
55+
mul!(zeros(3), A, x)
56+
57+
#-
58+
59+
mul!(ones(3), A, x, 2, 2)
60+
61+
#-
62+
63+
mul!(ones(3,3), A, reshape(collect(1:9), 3, 3), 2, 2)
64+
65+
# ## Multiply-and-add and the `MulStyle` trait
66+
67+
# While the above function calls work out of the box due to generic fallbacks, the latter
68+
# may be suboptimally implemented for your custom map type. Let's see some benchmarks.
69+
70+
using BenchmarkTools
71+
72+
@benchmark mul!($(zeros(3)), $A, $x)
73+
74+
#-
75+
76+
@benchmark mul!($(zeros(3)), $A, $x, $(rand()), $(rand()))
77+
78+
# The second benchmark indicates the allocation of an intermediate vector `z`
79+
# which stores the result of `A*x` before it gets scaled and added to (the scaled)
80+
# `y = zeros(3)`. For that reason, it is beneficial to provide a custom "5-arg `mul!`"
81+
# if you can avoid the allocation of an intermediate vector. To indicate that there
82+
# exists an allocation-free implementation, you should set the `MulStyle` trait,
83+
# whose default is `ThreeArg()`.
84+
85+
LinearMaps.MulStyle(A::MyFillMap) = FiveArg()
86+
87+
function LinearAlgebra.mul!(
88+
y::AbstractVecOrMat,
89+
A::MyFillMap,
90+
x::AbstractVector,
91+
α::Number,
92+
β::Number
93+
)
94+
if iszero(α)
95+
!isone(β) && rmul!(y, β)
96+
return y
97+
else
98+
temp = A.λ * sum(x) * α
99+
if iszero(β)
100+
y .= temp
101+
elseif isone(β)
102+
y .+= temp
103+
else
104+
y .= y .* β .+ temp
105+
end
106+
end
107+
return y
108+
end
109+
110+
# With this function at hand, let's redo the benchmark.
111+
112+
@benchmark mul!($(zeros(3)), $A, $x, $(rand()), $(rand()))
113+
114+
# There you go, the allocation is gone and the computation time is significantly reduced.
115+
116+
# ## Adjoints and transposes
117+
118+
# Generically, taking the transpose (or the adjoint) of a (real, resp.) map wraps the
119+
# linear map by a `TransposeMap`, taking the adjoint of a complex map wraps it by an
120+
# `AdjointMap`.
121+
122+
typeof(A')
123+
124+
# Not surprisingly, without further definitions, multiplying `A'` by `x` yields an error.
125+
126+
try A'x catch e println(e) end
127+
128+
# If the operator is symmetric or Hermitian, the transpose and the adjoint, respectively,
129+
# of the linear map `A` is given by `A` itself. So let's define corresponding checks.
130+
131+
LinearAlgebra.issymmetric(A::MyFillMap) = A.size[1] == A.size[2]
132+
LinearAlgebra.ishermitian(A::MyFillMap) = isreal(A.λ) && A.size[1] == A.size[2]
133+
LinearAlgebra.isposdef(A::MyFillMap) = (size(A, 1) == size(A, 2) == 1 && isposdef(A.λ))
134+
Base.:(==)(A::MyFillMap, B::MyFillMap) = A.λ == B.λ && A.size == B.size
135+
136+
# These are used, for instance, in checking symmetry or positive definiteness of
137+
# higher-order `LinearMap`s, like products or linear combinations of linear maps, or signal
138+
# to iterative eigenproblem solvers that real eigenvalues are to be computed.
139+
# Without these definitions, the first three functions would return `false` (by default),
140+
# and the last one would fall back to `===`.
141+
142+
# With this at hand, we note that `A` above is symmetric, and we can compute
143+
144+
transpose(A)*x
145+
146+
# This, however, does not work for nonsquare maps
147+
148+
try MyFillMap(5.0, (3, 4))' * ones(3) catch e println(e) end
149+
150+
# which require explicit adjoint/transpose handling, for which there exist two *distinct* paths.
151+
152+
# ### Path 1: Generic, non-invariant `LinearMap` subtypes
153+
154+
# The first option is to write `LinearAlgebra.mul!` methods for the corresponding wrapped
155+
# map types; for instance,
156+
157+
function LinearAlgebra.mul!(
158+
y::AbstractVecOrMat,
159+
transA::LinearMaps.TransposeMap{<:Any,<:MyFillMap},
160+
x::AbstractVector
161+
)
162+
LinearMaps.check_dim_mul(y, transA, x)
163+
λ = transA.lmap.λ
164+
return fill!(y, iszero(λ) ? zero(eltype(y)) : transpose(λ)*sum(x))
165+
end
166+
167+
# If you have set the `MulStyle` trait to `FiveArg()`, you should provide a corresponding
168+
# 5-arg `mul!` method for `LinearMaps.TransposeMap{<:Any,<:MyFillMap}` and
169+
# `LinearMaps.AdjointMap{<:Any,<:MyFillMap}`.
170+
171+
# ### Path 2: Invariant `LinearMap` subtypes
172+
173+
# The seconnd option is when your class of linear maps that are modelled by your custom
174+
# `LinearMap` subtype are invariant under taking adjoints and transposes.
175+
176+
LinearAlgebra.adjoint(A::MyFillMap) = MyFillMap(adjoint(A.λ), reverse(A.size))
177+
LinearAlgebra.transpose(A::MyFillMap) = MyFillMap(transpose(A.λ), reverse(A.size))
178+
179+
# With such invariant definitions, i.e., the adjoint/transpose of a `MyFillMap` is again
180+
# a `MyFillMap`, no further method definitions are required, and the entire functionality
181+
# listed above just works for adjoints/transposes of your custom map type.
182+
183+
mul!(ones(3), A', x, 2, 2)
184+
185+
#-
186+
187+
MyFillMap(5.0, (3, 4))' * ones(3)
188+
189+
# Now that we have defined the action of adjoints/transposes, the
190+
# following right action on vectors is automatically defined:
191+
192+
ones(3)' * MyFillMap(5.0, (3, 4))
193+
194+
# and `transpose(x) * A` correspondingly, as well as in-place multiplication
195+
196+
mul!(similar(x)', x', A)
197+
198+
# and `mul!(transpose(y), transpose(x), A)`.
199+
200+
# ## Application to matrices
201+
202+
# By default, applying a `LinearMap` `A` to a matrix `X` via `A*X` does
203+
# *not* aplly `A` to each column of `X` viewed as a vector, but interprets
204+
# `X` as a linear map, wraps it as such and returns `(A*X)::CompositeMap`.
205+
# Calling the in-place multiplication function `mul!(Y, A, X)` for matrices,
206+
# however, does compute the columnwise action of `A` on `X` and stores the
207+
# result in `Y`. In case there is a more efficient implementation for the
208+
# matrix application, you can provide `mul!` methods with signature
209+
# `mul!(Y::AbstractMatrix, A::MyFillMap, X::AbstractMatrix)`, and, depending
210+
# on the chosen path to handle adjoints/transposes, corresponding methods
211+
# for wrapped maps of type `AdjointMap` or `TransposeMap`, plus potentially
212+
# corresponding 5-arg `mul!` methods. This may seem like a lot of methods to
213+
# be implemented, but note that adding such methods is only necessary/recommended
214+
# for performance.

docs/src/custom.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)