Skip to content

Commit 7a5f372

Browse files
Add ScaledMap (#94)
Co-authored-by: Daniel Karrasch <[email protected]>
1 parent 39fd1d9 commit 7a5f372

13 files changed

+288
-36
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 = "2.6.1"
3+
version = "2.7.0"
44

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

src/LinearMaps.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ end
1616
abstract type LinearMap{T} end
1717

1818
const MapOrMatrix{T} = Union{LinearMap{T},AbstractMatrix{T}}
19+
const RealOrComplex = Union{Real,Complex}
1920

2021
Base.eltype(::LinearMap{T}) where {T} = T
2122

@@ -117,6 +118,7 @@ include("wrappedmap.jl") # wrap a matrix of linear map in a new type, thereby al
117118
include("uniformscalingmap.jl") # the uniform scaling map, to be able to make linear combinations of LinearMap objects and multiples of I
118119
include("transpose.jl") # transposing linear maps
119120
include("linearcombination.jl") # defining linear combinations of linear maps
121+
include("scaledmap.jl") # multiply by a (real or complex) scalar
120122
include("composition.jl") # composition of linear maps
121123
include("functionmap.jl") # using a function as linear map
122124
include("blockmap.jl") # block linear maps
@@ -132,10 +134,10 @@ Construct a linear map object, either from an existing `LinearMap` or `AbstractM
132134
with the purpose of redefining its properties via the keyword arguments `kwargs`;
133135
a `UniformScaling` object `J` with specified (square) dimension `M`; or
134136
from a function or callable object `f`. In the latter case, one also needs to specify
135-
the size of the equivalent matrix representation `(M, N)`, i.e. for functions `f` acting
137+
the size of the equivalent matrix representation `(M, N)`, i.e., for functions `f` acting
136138
on length `N` vectors and producing length `M` vectors (with default value `N=M`). Preferably,
137139
also the `eltype` `T` of the corresponding matrix representation needs to be specified, i.e.
138-
whether the action of `f` on a vector will be similar to e.g. multiplying by numbers of type `T`.
140+
whether the action of `f` on a vector will be similar to, e.g., multiplying by numbers of type `T`.
139141
If not specified, the devault value `T=Float64` will be assumed. Optionally, a corresponding
140142
function `fc` can be specified that implements the transpose/adjoint of `f`.
141143
@@ -150,7 +152,7 @@ For functions `f`, there is one more keyword argument
150152
* ismutating::Bool : flags whether the function acts as a mutating matrix multiplication
151153
`f(y,x)` where the result vector `y` is the first argument (in case of `true`),
152154
or as a normal matrix multiplication that is called as `y=f(x)` (in case of `false`).
153-
The default value is guessed by looking at the number of arguments of the first occurence
155+
The default value is guessed by looking at the number of arguments of the first occurrence
154156
of `f` in the method table.
155157
"""
156158
LinearMap(A::MapOrMatrix; kwargs...) = WrappedMap(A; kwargs...)

src/composition.jl

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,28 @@ Base.size(A::CompositeMap) = (size(A.maps[end], 1), size(A.maps[1], 2))
2121
Base.isreal(A::CompositeMap) = all(isreal, A.maps) # sufficient but not necessary
2222

2323
# the following rules are sufficient but not necessary
24-
const RealOrComplex = Union{Real,Complex}
2524
for (f, _f, g) in ((:issymmetric, :_issymmetric, :transpose),
2625
(:ishermitian, :_ishermitian, :adjoint),
2726
(:isposdef, :_isposdef, :adjoint))
2827
@eval begin
2928
LinearAlgebra.$f(A::CompositeMap) = $_f(A.maps)
3029
$_f(maps::Tuple{}) = true
3130
$_f(maps::Tuple{<:LinearMap}) = $f(maps[1])
32-
function $_f(maps::Tuple{Vararg{<:LinearMap}}) # length(maps) >= 2
33-
if maps[1] isa UniformScalingMap{<:RealOrComplex}
34-
return $f(maps[1]) && $_f(Base.tail(maps))
35-
elseif maps[end] isa UniformScalingMap{<:RealOrComplex}
36-
return $f(maps[end]) && $_f(Base.front(maps))
37-
else
38-
return maps[end] == $g(maps[1]) && $_f(Base.front(Base.tail(maps)))
39-
end
40-
end
31+
$_f(maps::Tuple{Vararg{<:LinearMap}}) = maps[end] == $g(maps[1]) && $_f(Base.front(Base.tail(maps)))
32+
# since the introduction of ScaledMap, the following cases cannot occur
33+
# function $_f(maps::Tuple{Vararg{<:LinearMap}}) # length(maps) >= 2
34+
# if maps[1] isa UniformScalingMap{<:RealOrComplex}
35+
# return $f(maps[1]) && $_f(Base.tail(maps))
36+
# elseif maps[end] isa UniformScalingMap{<:RealOrComplex}
37+
# return $f(maps[end]) && $_f(Base.front(maps))
38+
# else
39+
# return maps[end] == $g(maps[1]) && $_f(Base.front(Base.tail(maps)))
40+
# end
41+
# end
4142
end
4243
end
4344

44-
# scalar multiplication and division
45+
# scalar multiplication and division (non-commutative case)
4546
function Base.:(*)(α::Number, A::LinearMap)
4647
T = promote_type(typeof(α), eltype(A))
4748
return CompositeMap{T}(tuple(A, UniformScalingMap(α, size(A, 1))))
@@ -55,6 +56,11 @@ function Base.:(*)(α::Number, A::CompositeMap)
5556
return CompositeMap{T}(tuple(A.maps..., UniformScalingMap(α, size(A, 1))))
5657
end
5758
end
59+
# needed for disambiguation
60+
function Base.:(*)(α::RealOrComplex, A::CompositeMap)
61+
T = Base.promote_op(*, typeof(α), eltype(A))
62+
return ScaledMap{T}(α, A)
63+
end
5864
function Base.:(*)(A::LinearMap, α::Number)
5965
T = promote_type(typeof(α), eltype(A))
6066
return CompositeMap{T}(tuple(UniformScalingMap(α, size(A, 2)), A))
@@ -68,13 +74,18 @@ function Base.:(*)(A::CompositeMap, α::Number)
6874
return CompositeMap{T}(tuple(UniformScalingMap(α, size(A, 2)), A.maps...))
6975
end
7076
end
77+
# needed for disambiguation
78+
function Base.:(*)(A::CompositeMap, α::RealOrComplex)
79+
T = Base.promote_op(*, typeof(α), eltype(A))
80+
return ScaledMap{T}(α, A)
81+
end
82+
7183
Base.:(\)(α::Number, A::LinearMap) = inv(α) * A
7284
Base.:(/)(A::LinearMap, α::Number) = A * inv(α)
73-
Base.:(-)(A::LinearMap) = -1 * A
7485

7586
# composition of linear maps
7687
"""
77-
A::LinearMap * B::LinearMap
88+
*(A::LinearMap, B::LinearMap)
7889
7990
Construct a `CompositeMap <: LinearMap`, a (lazy) representation of the product
8091
of the two operators. Products of `LinearMap`/`CompositeMap` objects and
@@ -109,8 +120,6 @@ function Base.:(*)(A₁::CompositeMap, A₂::CompositeMap)
109120
T = promote_type(eltype(A₁), eltype(A₂))
110121
return CompositeMap{T}(tuple(A₂.maps..., A₁.maps...))
111122
end
112-
Base.:(*)(A₁::LinearMap, A₂::UniformScaling) = A₁ * A₂.λ
113-
Base.:(*)(A₁::UniformScaling, A₂::LinearMap) = A₁.λ * A₂
114123

115124
# special transposition behavior
116125
LinearAlgebra.transpose(A::CompositeMap{T}) where {T} = CompositeMap{T}(map(transpose, reverse(A.maps)))

src/conversion.jl

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ Base.convert(::Type{SparseMatrixCSC}, A::LinearMap) = sparse(A)
4545

4646
# special cases
4747

48+
# ScaledMap
49+
Base.Matrix(A::ScaledMap{<:Any,<:Any,<:MatrixMap}) = convert(Matrix, A.λ*A.lmap.lmap)
50+
SparseArrays.sparse(A::ScaledMap{<:Any,<:Any,<:MatrixMap}) = convert(SparseMatrixCSC, A.λ*A.lmap.lmap)
51+
4852
# UniformScalingMap
4953
Base.Matrix(J::UniformScalingMap) = Matrix(J.λ*I, size(J))
5054
Base.convert(::Type{AbstractMatrix}, J::UniformScalingMap) = Diagonal(fill(J.λ, size(J, 1)))
@@ -77,14 +81,22 @@ for (TT, T) in ((Type{Matrix}, Matrix), (Type{SparseMatrixCSC}, SparseMatrixCSC)
7781
B, A = AB.maps
7882
return convert($T, A.lmap*B.lmap)
7983
end
80-
@eval function Base.convert(::$TT, λA::CompositeMap{<:Any,<:Tuple{MatrixMap,UniformScalingMap}})
81-
A, J = λA.maps
82-
return convert($T, J.λ*A.lmap)
83-
end
84-
@eval function Base.convert(::$TT, Aλ::CompositeMap{<:Any,<:Tuple{UniformScalingMap,MatrixMap}})
85-
J, A =.maps
86-
return convert($T, A.lmap*J.λ)
87-
end
84+
end
85+
function Base.Matrix(λA::CompositeMap{<:Any,<:Tuple{MatrixMap,UniformScalingMap}})
86+
A, J = λA.maps
87+
return convert(Matrix, J.λ*A.lmap)
88+
end
89+
function SparseArrays.sparse(λA::CompositeMap{<:Any,<:Tuple{MatrixMap,UniformScalingMap}})
90+
A, J = λA.maps
91+
return convert(SparseMatrixCSC, J.λ*A.lmap)
92+
end
93+
function Base.Matrix(Aλ::CompositeMap{<:Any,<:Tuple{UniformScalingMap,MatrixMap}})
94+
J, A =.maps
95+
return convert(Matrix, A.lmap*J.λ)
96+
end
97+
function SparseArrays.sparse(Aλ::CompositeMap{<:Any,<:Tuple{UniformScalingMap,MatrixMap}})
98+
J, A =.maps
99+
return convert(SparseMatrixCSC, A.lmap*J.λ)
88100
end
89101

90102
# BlockMap & BlockDiagonalMap

src/linearcombination.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ LinearAlgebra.isposdef(A::LinearCombination) = all(isposdef, A.maps) # sufficien
2323

2424
# adding linear maps
2525
"""
26-
A::LinearMap + B::LinearMap
26+
+(A::LinearMap, B::LinearMap)
2727
2828
Construct a `LinearCombination <: LinearMap`, a (lazy) representation of the sum
2929
of the two operators. Sums of `LinearMap`/`LinearCombination` objects and

src/scaledmap.jl

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
struct ScaledMap{T, S<:RealOrComplex, A<:LinearMap} <: LinearMap{T}
2+
λ::S
3+
lmap::A
4+
function ScaledMap{T,S,A}::S, lmap::A) where {T, S <: RealOrComplex, A <: LinearMap}
5+
Base.promote_op(*, S, eltype(lmap)) == T || throw(InexactError())
6+
new{T,S,A}(λ, lmap)
7+
end
8+
end
9+
10+
# constructor
11+
ScaledMap{T}::S, lmap::A) where {T,S<:RealOrComplex,A<:LinearMap} =
12+
ScaledMap{Base.promote_op(*, S, eltype(lmap)),S,A}(λ, lmap)
13+
14+
# show
15+
function Base.show(io::IO, A::ScaledMap{T}) where {T}
16+
println(io, "LinearMaps.ScaledMap{$T}, scale = $(A.λ)")
17+
show(io, A.lmap)
18+
end
19+
20+
# basic methods
21+
Base.size(A::ScaledMap) = size(A.lmap)
22+
Base.isreal(A::ScaledMap) = isreal(A.λ) && isreal(A.lmap)
23+
LinearAlgebra.issymmetric(A::ScaledMap) = issymmetric(A.lmap)
24+
LinearAlgebra.ishermitian(A::ScaledMap) = ishermitian(A.lmap)
25+
LinearAlgebra.isposdef(A::ScaledMap) = isposdef(A.λ) && isposdef(A.lmap)
26+
27+
Base.transpose(A::ScaledMap) = A.λ * transpose(A.lmap)
28+
Base.adjoint(A::ScaledMap) = conj(A.λ) * adjoint(A.lmap)
29+
30+
# comparison (sufficient, not necessary)
31+
Base.:(==)(A::ScaledMap, B::ScaledMap) =
32+
(eltype(A) == eltype(B) && A.lmap == B.lmap) && A.λ == B.λ
33+
34+
# scalar multiplication and division
35+
function Base.:(*)(α::RealOrComplex, A::LinearMap)
36+
T = Base.promote_op(*, typeof(α), eltype(A))
37+
return ScaledMap{T}(α, A)
38+
end
39+
function Base.:(*)(A::LinearMap, α::RealOrComplex)
40+
T = Base.promote_op(*, typeof(α), eltype(A))
41+
return ScaledMap{T}(α, A)
42+
end
43+
44+
Base.:(*)(α::Number, A::ScaledMap) =* A.λ) * A.lmap
45+
Base.:(*)(A::ScaledMap, α::Number) = A.lmap * (A.λ * α)
46+
# needed for disambiguation
47+
Base.:(*)(α::RealOrComplex, A::ScaledMap) =* A.λ) * A.lmap
48+
Base.:(*)(A::ScaledMap, α::RealOrComplex) = (A.λ * α) * A.lmap
49+
Base.:(-)(A::LinearMap) = -1 * A
50+
51+
# composition (not essential, but might save multiple scaling operations)
52+
Base.:(*)(A::ScaledMap, B::ScaledMap) = (A.λ * B.λ) * (A.lmap * B.lmap)
53+
Base.:(*)(A::ScaledMap, B::LinearMap) = A.λ * (A.lmap * B)
54+
Base.:(*)(A::LinearMap, B::ScaledMap) = (A * B.lmap) * B.λ
55+
56+
# multiplication with vectors
57+
function A_mul_B!(y::AbstractVector, A::ScaledMap, x::AbstractVector)
58+
# no size checking, will be done by map
59+
mul!(y, A.lmap, x, A.λ, false)
60+
end
61+
62+
function LinearAlgebra.mul!(y::AbstractVector, A::ScaledMap, x::AbstractVector, α::Number, β::Number)
63+
# no size checking, will be done by map
64+
mul!(y, A.lmap, x, A.λ * α, β)
65+
end
66+
67+
At_mul_B!(y::AbstractVector, A::ScaledMap, x::AbstractVector) = A_mul_B!(y, transpose(A), x)
68+
Ac_mul_B!(y::AbstractVector, A::ScaledMap, x::AbstractVector) = A_mul_B!(y, adjoint(A), x)

src/uniformscalingmap.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@ LinearAlgebra.transpose(A::UniformScalingMap) = A
2828
LinearAlgebra.adjoint(A::UniformScalingMap) = UniformScalingMap(conj(A.λ), size(A))
2929

3030
# multiplication with scalar
31+
Base.:(*)(A::UniformScaling, B::LinearMap) = A.λ * B
32+
Base.:(*)(A::LinearMap, B::UniformScaling) = A * B.λ
3133
Base.:(*)(α::Number, J::UniformScalingMap) = UniformScalingMap* J.λ, size(J))
3234
Base.:(*)(J::UniformScalingMap, α::Number) = UniformScalingMap(J.λ * α, size(J))
35+
# needed for disambiguation
36+
Base.:(*)(α::RealOrComplex, J::UniformScalingMap) = UniformScalingMap* J.λ, size(J))
37+
Base.:(*)(J::UniformScalingMap, α::RealOrComplex) = UniformScalingMap(J.λ * α, size(J))
3338

3439
# multiplication with vector
3540
Base.:(*)(A::UniformScalingMap, x::AbstractVector) =

test/composition.jl

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
using Test, LinearMaps, LinearAlgebra
22

33
@testset "composition" begin
4-
F = @inferred LinearMap(cumsum, y -> reverse(cumsum(reverse(x))), 10; ismutating=false)
5-
FC = @inferred LinearMap{ComplexF64}(cumsum, y -> reverse(cumsum(reverse(x))), 10; ismutating=false)
4+
F = @inferred LinearMap(cumsum, reverse cumsum reverse, 10; ismutating=false)
5+
FC = @inferred LinearMap{ComplexF64}(cumsum, reverse cumsum reverse, 10; ismutating=false)
66
FCM = LinearMaps.CompositeMap{ComplexF64}((FC,))
7+
L = LowerTriangular(ones(10,10))
78
A = 2 * rand(ComplexF64, (10, 10)) .- 1
89
B = rand(size(A)...)
10+
H = LinearMap(Hermitian(A'A))
11+
S = LinearMap(Symmetric(real(A)'real(A)))
912
M = @inferred 1 * LinearMap(A)
1013
N = @inferred LinearMap(B)
1114
v = rand(ComplexF64, 10)
@@ -14,7 +17,11 @@ using Test, LinearMaps, LinearAlgebra
1417
@test @inferred (F * A) * v == @inferred F * (A * v)
1518
@test @inferred (A * F) * v == @inferred A * (F * v)
1619
@test @inferred A * (F * F) * v == @inferred A * (F * (F * v))
17-
@test @inferred (F * F) * (F * F) * v == @inferred F * (F * (F * (F * v)))
20+
F2 = F*F
21+
FC2 = FC*FC
22+
F4 = FC2 * F2
23+
@test length(F4.maps) == 4
24+
@test @inferred F4 * v == @inferred F * (F * (F * (F * v)))
1825
@test @inferred Matrix(M * transpose(M)) A * transpose(A)
1926
@test @inferred !isposdef(M * transpose(M))
2027
@test @inferred isposdef(M * M')
@@ -23,9 +30,12 @@ using Test, LinearMaps, LinearAlgebra
2330
@test @inferred !issymmetric(M' * M)
2431
@test @inferred ishermitian(M' * M)
2532
@test @inferred issymmetric(F'F)
33+
@test @inferred issymmetric(F'*S*F)
2634
@test @inferred ishermitian(F'F)
35+
@test @inferred ishermitian(F'*H*F)
2736
@test @inferred !issymmetric(FC'FC)
2837
@test @inferred ishermitian(FC'FC)
38+
@test @inferred ishermitian(FC'*H*FC)
2939
@test @inferred isposdef(transpose(F) * F * 3)
3040
@test @inferred isposdef(transpose(F) * 3 * F)
3141
@test @inferred !isposdef(-5*transpose(F) * F)

test/conversion.jl

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Test, LinearMaps, LinearAlgebra, SparseArrays
1+
using Test, LinearMaps, LinearAlgebra, SparseArrays, Quaternions
22

33
@testset "conversion" begin
44
A = 2 * rand(ComplexF64, (20, 10)) .- 1
@@ -31,6 +31,26 @@ using Test, LinearMaps, LinearAlgebra, SparseArrays
3131
JM = convert(AbstractMatrix, J)
3232
@test JM == Diagonal(fill(α, 10))
3333

34+
# ScaledMap
35+
q = rand(ComplexF64)
36+
A = rand(ComplexF64, 3, 3)
37+
@test convert(Matrix, q*LinearMap(A)) q*A
38+
qAs = convert(SparseMatrixCSC, q*LinearMap(A))
39+
@test qAs q*A
40+
@test qAs isa SparseMatrixCSC
41+
42+
# CompositeMap of MatrixMap and UniformScalingMap
43+
q = Quaternion(rand(4)...)
44+
A = Quaternion.(rand(3,3), rand(3,3), rand(3,3), rand(3,3))
45+
for mat in (Matrix, SparseMatrixCSC)
46+
Aqmat = convert(mat, LinearMap(A)*q)
47+
@test Aqmat A*q
48+
@test Aqmat isa mat
49+
qAmat = convert(mat, q*LinearMap(A))
50+
@test qAmat q*A
51+
@test qAmat isa mat
52+
end
53+
3454
# sparse matrix generation/conversion
3555
@test sparse(M) == sparse(Array(M))
3656
@test convert(SparseMatrixCSC, M) == sparse(Array(M))

test/functionmap.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Test, LinearMaps, LinearAlgebra
1+
using Test, LinearMaps, LinearAlgebra, BenchmarkTools
22

33
@testset "function maps" begin
44
N = 100
@@ -58,7 +58,7 @@ using Test, LinearMaps, LinearAlgebra
5858
@test_throws ErrorException adjoint(CS!) * v
5959
@test_throws ErrorException mul!(similar(v), CS!', v)
6060
@test_throws ErrorException mul!(similar(v), transpose(CS!), v)
61-
CS! = LinearMap{ComplexF64}(cumsum!, (y, x) -> (copyto!(y, x); reverse!(y); cumsum!(y, y)), 10; ismutating=true)
61+
CS! = LinearMap{ComplexF64}(cumsum!, (y, x) -> (copyto!(y, x); reverse!(y); cumsum!(y, y); reverse!(y)), 10; ismutating=true)
6262
@inferred adjoint(CS!)
6363
@test @inferred LinearMaps.ismutating(CS!)
6464
@test @inferred CS! * v == cv
@@ -67,6 +67,11 @@ using Test, LinearMaps, LinearAlgebra
6767
@test @inferred CS' * v == reverse!(cumsum(reverse(v)))
6868
@test @inferred mul!(similar(v), transpose(CS), v) == reverse!(cumsum(reverse(v)))
6969
@test @inferred mul!(similar(v), adjoint(CS), v) == reverse!(cumsum(reverse(v)))
70+
u = similar(v)
71+
b = @benchmarkable mul!($u, $(3*CS!), $v)
72+
@test run(b, samples=3).allocs == 0
73+
b = @benchmarkable mul!($u, $(3*CS!'), $v)
74+
@test run(b, samples=3).allocs == 0
7075

7176
# Test fallback methods:
7277
L = @inferred LinearMap(x -> x, x -> x, 10)

0 commit comments

Comments
 (0)