Skip to content

Commit 8201996

Browse files
authored
add Kronecker products for LinearMaps (#61)
* add Kronecker products for LinearMaps * announce feature in README * add promotion of matrices, tests * add comments, use mixed-product trick * export UniformScalingMaps, add scalar mul, tests * simplify scalar multiplication of composites * adopt to new test suite * prepare for require_one_based_indexing * revert export of UnifformScalingMap, add LinearMap constructor instead * update docstring * add docstring to kron * add KroneckerSumMap, yet untested * add KroneckerPowerMap, yet untested * KroneckerMap with many maps, optimize mul, more tests * add dim-checker helper, resolve type instability, tests * add conversion rules * optimize MatrixMap and scaling multiplication, enable KroneckerSum tests * fix matrix map handling across different julia versions * minor fixes, improve coverage * improve coverage, minor fixes * improve coverage further * remove false type-instability comment, uniform scaling comparison * require one-based indexing, move kronecker power, simplify size * clean up Kronecker (sum) powers * make kron(sum)powers inferrable * remove obsolete specifiers * rename promote_to_lmaps -> convert_to_lmaps to avoid conflict with BlockMaps * move conversion and check_dim to Main, pepper with at-propagate_inbounds * make at-inbounds work properly, add some conversion rules * simplify kronsum constructor * edits as per review, update docstrings * use memory requirement heuristic for branching
1 parent 95cc89c commit 8201996

12 files changed

+565
-92
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77

88
A Julia package for defining and working with linear maps, also known as linear transformations or linear operators acting on vectors. The only requirement for a LinearMap is that it can act on a vector (by multiplication) efficiently.
99

10+
## What's new in v2.6
11+
* New feature: "lazy" Kronecker product, Kronecker sums, and powers thereof
12+
for `LinearMap`s. `AbstractMatrix` objects are promoted to `LinearMap`s if
13+
one of the first 8 Kronecker factors is a `LinearMap` object.
14+
* Compatibility with the generic multiply-and-add interface (a.k.a. 5-arg
15+
`mul!`) introduced in julia v1.3
16+
1017
## What's new in v2.5.0
1118
* New feature: concatenation of `LinearMap`s objects with `UniformScaling`s,
1219
consistent with (h-, v-, and hc-)concatenation of matrices. Note, matrices

src/LinearMaps.jl

Lines changed: 32 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module LinearMaps
22

33
export LinearMap
4+
export , kronsum,
45

56
using LinearAlgebra
67
using SparseArrays
@@ -14,6 +15,8 @@ end
1415

1516
abstract type LinearMap{T} end
1617

18+
const MapOrMatrix{T} = Union{LinearMap{T},AbstractMatrix{T}}
19+
1720
Base.eltype(::LinearMap{T}) where {T} = T
1821

1922
Base.isreal(A::LinearMap) = eltype(A) <: Real
@@ -26,6 +29,26 @@ Base.ndims(::LinearMap) = 2
2629
Base.size(A::LinearMap, n) = (n==1 || n==2 ? size(A)[n] : error("LinearMap objects have only 2 dimensions"))
2730
Base.length(A::LinearMap) = size(A)[1] * size(A)[2]
2831

32+
# check dimension consistency for y = A*x and Y = A*X
33+
function check_dim_mul(y::AbstractVector, A::LinearMap, x::AbstractVector)
34+
m, n = size(A)
35+
(m == length(y) && n == length(x)) || throw(DimensionMismatch("mul!"))
36+
return nothing
37+
end
38+
function check_dim_mul(Y::AbstractMatrix, A::LinearMap, X::AbstractMatrix)
39+
m, n = size(A)
40+
(m == size(Y, 1) && n == size(X, 1) && size(Y, 2) == size(X, 2)) || throw(DimensionMismatch("mul!"))
41+
return nothing
42+
end
43+
44+
# conversion of AbstractMatrix to LinearMap
45+
convert_to_lmaps_(A::AbstractMatrix) = LinearMap(A)
46+
convert_to_lmaps_(A::LinearMap) = A
47+
convert_to_lmaps() = ()
48+
convert_to_lmaps(A) = (convert_to_lmaps_(A),)
49+
@inline convert_to_lmaps(A, B, Cs...) =
50+
(convert_to_lmaps_(A), convert_to_lmaps_(B), convert_to_lmaps(Cs...)...)
51+
2952
Base.:(*)(A::LinearMap, x::AbstractVector) = mul!(similar(x, promote_type(eltype(A), eltype(x)), size(A, 1)), A, x)
3053
function LinearAlgebra.mul!(y::AbstractVector, A::LinearMap, x::AbstractVector, α::Number=true, β::Number=false)
3154
length(y) == size(A, 1) || throw(DimensionMismatch("mul!"))
@@ -68,64 +91,24 @@ A_mul_B!(y::AbstractVector, A::AbstractMatrix, x::AbstractVector) = mul!(y, A,
6891
At_mul_B!(y::AbstractVector, A::AbstractMatrix, x::AbstractVector) = mul!(y, transpose(A), x)
6992
Ac_mul_B!(y::AbstractVector, A::AbstractMatrix, x::AbstractVector) = mul!(y, adjoint(A), x)
7093

71-
# Matrix: create matrix representation of LinearMap
72-
function Base.Matrix(A::LinearMap)
73-
M, N = size(A)
74-
T = eltype(A)
75-
mat = Matrix{T}(undef, (M, N))
76-
v = fill(zero(T), N)
77-
@inbounds for i = 1:N
78-
v[i] = one(T)
79-
mul!(view(mat, :, i), A, v)
80-
v[i] = zero(T)
81-
end
82-
return mat
83-
end
84-
85-
Base.Array(A::LinearMap) = Matrix(A)
86-
Base.convert(::Type{Matrix}, A::LinearMap) = Matrix(A)
87-
Base.convert(::Type{Array}, A::LinearMap) = Matrix(A)
88-
Base.convert(::Type{SparseMatrixCSC}, A::LinearMap) = sparse(A)
89-
90-
# sparse: create sparse matrix representation of LinearMap
91-
function SparseArrays.sparse(A::LinearMap{T}) where {T}
92-
M, N = size(A)
93-
rowind = Int[]
94-
nzval = T[]
95-
colptr = Vector{Int}(undef, N+1)
96-
v = fill(zero(T), N)
97-
Av = Vector{T}(undef, M)
98-
99-
for i = 1:N
100-
v[i] = one(T)
101-
mul!(Av, A, v)
102-
js = findall(!iszero, Av)
103-
colptr[i] = length(nzval) + 1
104-
if length(js) > 0
105-
append!(rowind, js)
106-
append!(nzval, Av[js])
107-
end
108-
v[i] = zero(T)
109-
end
110-
colptr[N+1] = length(nzval) + 1
111-
112-
return SparseMatrixCSC(M, N, colptr, rowind, nzval)
113-
end
114-
11594
include("wrappedmap.jl") # wrap a matrix of linear map in a new type, thereby allowing to alter its properties
11695
include("uniformscalingmap.jl") # the uniform scaling map, to be able to make linear combinations of LinearMap objects and multiples of I
11796
include("transpose.jl") # transposing linear maps
11897
include("linearcombination.jl") # defining linear combinations of linear maps
11998
include("composition.jl") # composition of linear maps
12099
include("functionmap.jl") # using a function as linear map
121100
include("blockmap.jl") # block linear maps
101+
include("kronecker.jl") # Kronecker product of linear maps
102+
include("conversion.jl") # conversion of linear maps to matrices
122103

123104
"""
124105
LinearMap(A; kwargs...)
106+
LinearMap(J, M::Int)
125107
LinearMap{T=Float64}(f, [fc,], M::Int, N::Int = M; kwargs...)
126108
127109
Construct a linear map object, either from an existing `LinearMap` or `AbstractMatrix` `A`,
128-
with the purpose of redefining its properties via the keyword arguments `kwargs`, or
110+
with the purpose of redefining its properties via the keyword arguments `kwargs`;
111+
a `UniformScaling` object `J` with specified (square) dimension `M`; or
129112
from a function or callable object `f`. In the latter case, one also needs to specify
130113
the size of the equivalent matrix representation `(M, N)`, i.e. for functions `f` acting
131114
on length `N` vectors and producing length `M` vectors (with default value `N=M`). Preferably,
@@ -141,20 +124,21 @@ The keyword arguments and their default values for functions `f` are
141124
For existing linear maps or matrices `A`, the default values will be taken by calling
142125
`issymmetric`, `ishermitian` and `isposdef` on the existing object `A`.
143126
144-
For functions `f`, there is one more keyword arguments
127+
For functions `f`, there is one more keyword argument
145128
* ismutating::Bool : flags whether the function acts as a mutating matrix multiplication
146129
`f(y,x)` where the result vector `y` is the first argument (in case of `true`),
147130
or as a normal matrix multiplication that is called as `y=f(x)` (in case of `false`).
148131
The default value is guessed by looking at the number of arguments of the first occurence
149132
of `f` in the method table.
150133
"""
151-
LinearMap(A::Union{AbstractMatrix, LinearMap}; kwargs...) = WrappedMap(A; kwargs...)
134+
LinearMap(A::MapOrMatrix; kwargs...) = WrappedMap(A; kwargs...)
135+
LinearMap(J::UniformScaling, M::Int) = UniformScalingMap(J.λ, M)
152136
LinearMap(f, M::Int; kwargs...) = LinearMap{Float64}(f, M; kwargs...)
153137
LinearMap(f, M::Int, N::Int; kwargs...) = LinearMap{Float64}(f, M, N; kwargs...)
154138
LinearMap(f, fc, M::Int; kwargs...) = LinearMap{Float64}(f, fc, M; kwargs...)
155139
LinearMap(f, fc, M::Int, N::Int; kwargs...) = LinearMap{Float64}(f, fc, M, N; kwargs...)
156140

157-
LinearMap{T}(A::Union{AbstractMatrix, LinearMap}; kwargs...) where {T} = WrappedMap{T}(A; kwargs...)
141+
LinearMap{T}(A::MapOrMatrix; kwargs...) where {T} = WrappedMap{T}(A; kwargs...)
158142
LinearMap{T}(f, args...; kwargs...) where {T} = FunctionMap{T}(f, args...; kwargs...)
159143

160144
end # module

src/composition.jl

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
# helper function
2+
check_dim_mul(A, B) = size(A, 2) == size(B, 1)
3+
14
struct CompositeMap{T, As<:Tuple{Vararg{LinearMap}}} <: LinearMap{T}
25
maps::As # stored in order of application to vector
36
function CompositeMap{T, As}(maps::As) where {T, As}
47
N = length(maps)
58
for n in 2:N
6-
size(maps[n], 2) == size(maps[n-1], 1) || throw(DimensionMismatch("CompositeMap"))
9+
check_dim_mul(maps[n], maps[n-1]) || throw(DimensionMismatch("CompositeMap"))
710
end
811
for n in 1:N
912
promote_type(T, eltype(maps[n])) == T || throw(InexactError())
@@ -47,7 +50,7 @@ function Base.:(*)(α::Number, A::CompositeMap)
4750
T = promote_type(typeof(α), eltype(A))
4851
Alast = last(A.maps)
4952
if Alast isa UniformScalingMap
50-
return CompositeMap{T}(tuple(Base.front(A.maps)..., UniformScalingMap(α * Alast.λ, size(Alast, 1))))
53+
return CompositeMap{T}(tuple(Base.front(A.maps)..., α * Alast))
5154
else
5255
return CompositeMap{T}(tuple(A.maps..., UniformScalingMap(α, size(A, 1))))
5356
end
@@ -60,7 +63,7 @@ function Base.:(*)(A::CompositeMap, α::Number)
6063
T = promote_type(typeof(α), eltype(A))
6164
Afirst = first(A.maps)
6265
if Afirst isa UniformScalingMap
63-
return CompositeMap{T}(tuple(UniformScalingMap(Afirst.λ * α, size(Afirst, 1)), Base.tail(A.maps)...))
66+
return CompositeMap{T}(tuple(Afirst * α, Base.tail(A.maps)...))
6467
else
6568
return CompositeMap{T}(tuple(UniformScalingMap(α, size(A, 2)), A.maps...))
6669
end
@@ -87,22 +90,22 @@ julia> LinearMap(ones(Int, 3, 3)) * CS * I * rand(3, 3);
8790
```
8891
"""
8992
function Base.:(*)(A₁::LinearMap, A₂::LinearMap)
90-
size(A₁, 2) == size(A₂, 1) || throw(DimensionMismatch("*"))
93+
check_dim_mul(A₁, A₂) || throw(DimensionMismatch("*"))
9194
T = promote_type(eltype(A₁), eltype(A₂))
9295
return CompositeMap{T}(tuple(A₂, A₁))
9396
end
9497
function Base.:(*)(A₁::LinearMap, A₂::CompositeMap)
95-
size(A₁, 2) == size(A₂, 1) || throw(DimensionMismatch("*"))
98+
check_dim_mul(A₁, A₂) || throw(DimensionMismatch("*"))
9699
T = promote_type(eltype(A₁), eltype(A₂))
97100
return CompositeMap{T}(tuple(A₂.maps..., A₁))
98101
end
99102
function Base.:(*)(A₁::CompositeMap, A₂::LinearMap)
100-
size(A₁, 2) == size(A₂, 1) || throw(DimensionMismatch("*"))
103+
check_dim_mul(A₁, A₂) || throw(DimensionMismatch("*"))
101104
T = promote_type(eltype(A₁), eltype(A₂))
102105
return CompositeMap{T}(tuple(A₂, A₁.maps...))
103106
end
104107
function Base.:(*)(A₁::CompositeMap, A₂::CompositeMap)
105-
size(A₁, 2) == size(A₂, 1) || throw(DimensionMismatch("*"))
108+
check_dim_mul(A₁, A₂) || throw(DimensionMismatch("*"))
106109
T = promote_type(eltype(A₁), eltype(A₂))
107110
return CompositeMap{T}(tuple(A₂.maps..., A₁.maps...))
108111
end

src/conversion.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Matrix: create matrix representation of LinearMap
2+
function Base.Matrix(A::LinearMap)
3+
M, N = size(A)
4+
T = eltype(A)
5+
mat = Matrix{T}(undef, (M, N))
6+
v = fill(zero(T), N)
7+
@inbounds for i = 1:N
8+
v[i] = one(T)
9+
mul!(view(mat, :, i), A, v)
10+
v[i] = zero(T)
11+
end
12+
return mat
13+
end
14+
Base.Array(A::LinearMap) = Matrix(A)
15+
Base.convert(::Type{Matrix}, A::LinearMap) = Matrix(A)
16+
Base.convert(::Type{Array}, A::LinearMap) = convert(Matrix, A)
17+
Base.convert(::Type{AbstractMatrix}, A::LinearMap) = convert(Matrix, A)
18+
Base.convert(::Type{AbstractArray}, A::LinearMap) = convert(AbstractMatrix, A)
19+
20+
# special cases
21+
Base.convert(::Type{AbstractMatrix}, A::WrappedMap) = convert(AbstractMatrix, A.lmap)
22+
Base.convert(::Type{Matrix}, A::WrappedMap) = convert(Matrix, A.lmap)
23+
function Base.convert(::Type{Matrix}, ΣA::LinearCombination{<:Any,<:Tuple{Vararg{MatrixMap}}})
24+
if length(ΣA.maps) <= 10
25+
return (+).(map(A->getfield(A, :lmap), ΣA.maps)...)
26+
else
27+
S = zero(first(ΣA.maps).lmap)
28+
for A in ΣA.maps
29+
S .+= A.lmap
30+
end
31+
return S
32+
end
33+
end
34+
function Base.convert(::Type{Matrix}, AB::CompositeMap{<:Any,<:Tuple{MatrixMap,MatrixMap}})
35+
B, A = AB.maps
36+
return A.lmap*B.lmap
37+
end
38+
function Base.convert(::Type{Matrix}, λA::CompositeMap{<:Any,<:Tuple{MatrixMap,UniformScalingMap}})
39+
A, J = λA.maps
40+
return J.λ*A.lmap
41+
end
42+
function Base.convert(::Type{Matrix}, Aλ::CompositeMap{<:Any,<:Tuple{UniformScalingMap,MatrixMap}})
43+
J, A =.maps
44+
return A.lmap*J.λ
45+
end
46+
47+
Base.Matrix(A::BlockMap) = hvcat(A.rows, convert.(AbstractMatrix, A.maps)...)
48+
49+
# sparse: create sparse matrix representation of LinearMap
50+
function SparseArrays.sparse(A::LinearMap{T}) where {T}
51+
M, N = size(A)
52+
rowind = Int[]
53+
nzval = T[]
54+
colptr = Vector{Int}(undef, N+1)
55+
v = fill(zero(T), N)
56+
Av = Vector{T}(undef, M)
57+
58+
for i = 1:N
59+
v[i] = one(T)
60+
mul!(Av, A, v)
61+
js = findall(!iszero, Av)
62+
colptr[i] = length(nzval) + 1
63+
if length(js) > 0
64+
append!(rowind, js)
65+
append!(nzval, Av[js])
66+
end
67+
v[i] = zero(T)
68+
end
69+
colptr[N+1] = length(nzval) + 1
70+
71+
return SparseMatrixCSC(M, N, colptr, rowind, nzval)
72+
end
73+
74+
Base.convert(::Type{SparseMatrixCSC}, A::LinearMap) = sparse(A)
75+
Base.convert(::Type{SparseMatrixCSC}, A::WrappedMap) = convert(SparseMatrixCSC, A.lmap)
76+
Base.convert(::Type{SparseMatrixCSC}, A::BlockMap) = hvcat(A.rows, convert.(SparseMatrixCSC, A.maps)...)

0 commit comments

Comments
 (0)