Skip to content

Commit 98843dd

Browse files
Add EmbeddedMap (#174)
Co-authored-by: Jeff Fessler <[email protected]>
1 parent df9ccd3 commit 98843dd

File tree

6 files changed

+186
-7
lines changed

6 files changed

+186
-7
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.6.2"
3+
version = "3.7.0"
44

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

docs/src/history.md

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

3+
## What's new in v3.7
4+
5+
* A new map type called `EmbeddedMap` is introduced. It is a wrapper of a "small" `LinearMap`
6+
(or a suitably converted `AbstractVecOrMat`) embedded into a "larger" zero map. Hence,
7+
the "small" map acts only on a subset of the coordinates and maps to another subset of
8+
the coordinates of the "large" map. The large map `L` can therefore be imagined as the
9+
composition of a sampling/projection map `P`, of the small map `A`, and of an embedding
10+
map `E`: `L = E ⋅ A ⋅ P`. It is implemented, however, by acting on a view of the vector
11+
`x` and storing the result into a view of the result vector `y`. Such maps can be
12+
constructed by the new methods:
13+
* `LinearMap(A::MapOrVecOrMat, dims::Dims{2}, index::NTuple{2, AbstractVector{Int}})`,
14+
where `dims` is the dimensions of the "large" map and index is a tuple of the `x`- and
15+
`y`-indices that interact with `A`, respectively;
16+
* `LinearMap(A::MapOrVecOrMat, dims::Dims{2}; offset::Dims{2})`, where the keyword
17+
argument `offset` determines the dimension of a virtual upper-left zero block, to which
18+
`A` gets (virtually) diagonally appended.
19+
320
## What's new in v3.6
421

522
* Support for Julia versions below v1.6 has been dropped.

src/LinearMaps.jl

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ include("functionmap.jl") # using a function as linear map
266266
include("blockmap.jl") # block linear maps
267267
include("kronecker.jl") # Kronecker product of linear maps
268268
include("fillmap.jl") # linear maps representing constantly filled matrices
269+
include("embeddedmap.jl") # embedded linear maps
269270
include("conversion.jl") # conversion of linear maps to matrices
270271
include("show.jl") # show methods for LinearMap objects
271272

@@ -274,12 +275,19 @@ include("show.jl") # show methods for LinearMap objects
274275
LinearMap(A::AbstractVecOrMat; kwargs...)::WrappedMap
275276
LinearMap(J::UniformScaling, M::Int)::UniformScalingMap
276277
LinearMap{T=Float64}(f, [fc,], M::Int, N::Int = M; kwargs...)::FunctionMap
278+
LinearMap(A::MapOrVecOrMat, dims::Dims{2}, index::NTuple{2, AbstractVector{Int}})::EmbeddedMap
279+
LinearMap(A::MapOrVecOrMat, dims::Dims{2}; offset::Dims{2})::EmbeddedMap
277280
278-
Construct a linear map object, either from an existing `LinearMap` or `AbstractVecOrMat` `A`,
279-
with the purpose of redefining its properties via the keyword arguments `kwargs`;
280-
a `UniformScaling` object `J` with specified (square) dimension `M`; or
281-
from a function or callable object `f`. In the latter case, one also needs to specify
282-
the size of the equivalent matrix representation `(M, N)`, i.e., for functions `f` acting
281+
Construct a linear map object, either
282+
283+
1. from an existing `LinearMap` or `AbstractVecOrMat` `A`, with the purpose of redefining
284+
its properties via the keyword arguments `kwargs`;
285+
2. a `UniformScaling` object `J` with specified (square) dimension `M`;
286+
3. from a function or callable object `f`;
287+
4. from an existing `LinearMap` or `AbstractVecOrMat` `A`, embedded in a larger zero map.
288+
289+
In the case of item 3, one also needs to specify the size of the equivalent matrix
290+
representation `(M, N)`, i.e., for functions `f` acting
283291
on length `N` vectors and producing length `M` vectors (with default value `N=M`).
284292
Preferably, also the `eltype` `T` of the corresponding matrix representation needs to be
285293
specified, i.e., whether the action of `f` on a vector will be similar to, e.g., multiplying
@@ -305,6 +313,13 @@ For the function-based constructor, there is one more keyword argument:
305313
or as a normal matrix multiplication that is called as `y=f(x)` (in case of `false`).
306314
The default value is guessed by looking at the number of arguments of the first
307315
occurrence of `f` in the method table.
316+
317+
For the `EmbeddedMap` constructors, `dims` specifies the total dimensions of the map. The
318+
`index` argument specifies two collections of indices `inds1` and `inds2`, such that for
319+
the big zero map `L` (thought of as a matrix), one has `L[inds1,inds2] == A`. In other
320+
words, `inds1` specifies the output indices, `inds2` specifies the input indices.
321+
Alternatively, `A` may be shifted by `offset`, such that (thinking in terms of matrices
322+
again) `L[offset[1] .+ axes(A, 1), offset[2] .+ axes(A, 2)] == A`.
308323
"""
309324
LinearMap(A::MapOrVecOrMat; kwargs...) = WrappedMap(A; kwargs...)
310325
LinearMap(J::UniformScaling, M::Int) = UniformScalingMap(J.λ, M)
@@ -313,7 +328,12 @@ LinearMap(f, M::Int, N::Int; kwargs...) = LinearMap{Float64}(f, M, N; kwargs...)
313328
LinearMap(f, fc, M::Int; kwargs...) = LinearMap{Float64}(f, fc, M; kwargs...)
314329
LinearMap(f, fc, M::Int, N::Int; kwargs...) = LinearMap{Float64}(f, fc, M, N; kwargs...)
315330

316-
LinearMap{T}(A::MapOrMatrix; kwargs...) where {T} = WrappedMap{T}(A; kwargs...)
331+
LinearMap(A::MapOrVecOrMat, dims::Dims{2}, index::NTuple{2, AbstractVector{Int}}) =
332+
EmbeddedMap(convert(LinearMap, A), dims, index[1], index[2])
333+
LinearMap(A::MapOrVecOrMat, dims::Dims{2}; offset::Dims{2}) =
334+
EmbeddedMap(convert(LinearMap, A), dims; offset=offset)
335+
336+
LinearMap{T}(A::MapOrVecOrMat; kwargs...) where {T} = WrappedMap{T}(A; kwargs...)
317337
LinearMap{T}(f, args...; kwargs...) where {T} = FunctionMap{T}(f, args...; kwargs...)
318338

319339
end # module

src/embeddedmap.jl

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
struct EmbeddedMap{T, As <: LinearMap, Rs <: AbstractVector{Int},
2+
Cs <: AbstractVector{Int}} <: LinearMap{T}
3+
lmap::As
4+
dims::Dims{2}
5+
rows::Rs # typically i1:i2 with 1 <= i1 <= i2 <= size(map,1)
6+
cols::Cs # typically j1:j2 with 1 <= j1 <= j2 <= size(map,2)
7+
8+
function EmbeddedMap{T}(map::As, dims::Dims{2}, rows::Rs, cols::Cs) where {T,
9+
As <: LinearMap, Rs <: AbstractVector{Int}, Cs <: AbstractVector{Int}}
10+
check_index(rows, size(map, 1), dims[1])
11+
check_index(cols, size(map, 2), dims[2])
12+
return new{T,As,Rs,Cs}(map, dims, rows, cols)
13+
end
14+
end
15+
16+
EmbeddedMap(map::LinearMap{T}, dims::Dims{2}; offset::Dims{2}) where {T} =
17+
EmbeddedMap{T}(map, dims, offset[1] .+ (1:size(map, 1)), offset[2] .+ (1:size(map, 2)))
18+
EmbeddedMap(map::LinearMap, dims::Dims{2}, rows::AbstractVector{Int}, cols::AbstractVector{Int}) =
19+
EmbeddedMap{eltype(map)}(map, dims, rows, cols)
20+
21+
@static if VERSION >= v"1.8-"
22+
Base.reverse(A::LinearMap; dims=:) = _reverse(A, dims)
23+
function _reverse(A, dims::Integer)
24+
if dims == 1
25+
return EmbeddedMap(A, size(A), reverse(axes(A, 1)), axes(A, 2))
26+
elseif dims == 2
27+
return EmbeddedMap(A, size(A), axes(A, 1), reverse(axes(A, 2)))
28+
else
29+
throw(ArgumentError("invalid dims argument to reverse, should be 1 or 2, got $dims"))
30+
end
31+
end
32+
_reverse(A, ::Colon) = EmbeddedMap(A, size(A), map(reverse, axes(A))...)
33+
_reverse(A, dims::NTuple{1,Integer}) = _reverse(A, first(dims))
34+
function _reverse(A, dims::NTuple{M,Integer}) where {M}
35+
dimrev = ntuple(k -> k in dims, 2)
36+
if 2 < M || M != sum(dimrev)
37+
throw(ArgumentError("invalid dimensions $dims in reverse!"))
38+
end
39+
ax = ntuple(k -> dimrev[k] ? reverse(axes(A, k)) : axes(A, k), 2)
40+
return EmbeddedMap(A, size(A), ax...)
41+
end
42+
end
43+
44+
function check_index(index::AbstractVector{Int}, dimA::Int, dimB::Int)
45+
length(index) != dimA && throw(ArgumentError("invalid length of index vector"))
46+
minimum(index) <= 0 && throw(ArgumentError("minimal index is below 1"))
47+
maximum(index) > dimB && throw(ArgumentError(
48+
"maximal index $(maximum(index)) exceeds dimension $dimB"
49+
))
50+
# _isvalidstep(index) || throw(ArgumentError("non-monotone index set"))
51+
nothing
52+
end
53+
54+
# _isvalidstep(index::AbstractRange) = step(index) > 0
55+
# _isvalidstep(index::AbstractVector) = all(diff(index) .> 0)
56+
57+
Base.size(A::EmbeddedMap) = A.dims
58+
59+
# sufficient but not necessary conditions
60+
LinearAlgebra.issymmetric(A::EmbeddedMap) =
61+
issymmetric(A.lmap) && (A.dims[1] == A.dims[2]) && (A.rows == A.cols)
62+
LinearAlgebra.ishermitian(A::EmbeddedMap) =
63+
ishermitian(A.lmap) && (A.dims[1] == A.dims[2]) && (A.rows == A.cols)
64+
65+
Base.:(==)(A::EmbeddedMap, B::EmbeddedMap) =
66+
(eltype(A) == eltype(B)) && (A.lmap == B.lmap) &&
67+
(A.dims == B.dims) && (A.rows == B.rows) && (A.cols == B.cols)
68+
69+
LinearAlgebra.adjoint(A::EmbeddedMap) = EmbeddedMap(adjoint(A.lmap), reverse(A.dims), A.cols, A.rows)
70+
LinearAlgebra.transpose(A::EmbeddedMap) = EmbeddedMap(transpose(A.lmap), reverse(A.dims), A.cols, A.rows)
71+
72+
for (In, Out) in ((AbstractVector, AbstractVecOrMat), (AbstractMatrix, AbstractMatrix))
73+
@eval function _unsafe_mul!(y::$Out, A::EmbeddedMap, x::$In)
74+
fill!(y, zero(eltype(y)))
75+
_unsafe_mul!(selectdim(y, 1, A.rows), A.lmap, selectdim(x, 1, A.cols))
76+
return y
77+
end
78+
@eval function _unsafe_mul!(y::$Out, A::EmbeddedMap, x::$In, alpha::Number, beta::Number)
79+
LinearAlgebra._rmul_or_fill!(y, beta)
80+
_unsafe_mul!(selectdim(y, 1, A.rows), A.lmap, selectdim(x, 1, A.cols), alpha, !iszero(beta))
81+
return y
82+
end
83+
end

test/embeddedmap.jl

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Test, LinearMaps, LinearAlgebra, SparseArrays
2+
3+
@testset "embeddedmap" begin
4+
m = 6; n = 5
5+
M = 10(1:m) .+ (1:n)'; L = LinearMap(M)
6+
offset = (3,4)
7+
8+
BM = [zeros(offset...) zeros(offset[1], size(M,2));
9+
zeros(size(M,1), offset[2]) M]
10+
BL = @inferred LinearMap(L, size(BM); offset=offset)
11+
s1, s2 = size(BM)
12+
@test (@inferred Matrix(BL)) == BM
13+
@test (@inferred Matrix(BL')) == BM'
14+
@test (@inferred Matrix(transpose(BL))) == transpose(BM)
15+
16+
@test_throws UndefKeywordError LinearMap(M, (10, 10))
17+
@test_throws ArgumentError LinearMap(M, (m, n), (0:m, 1:n))
18+
@test_throws ArgumentError LinearMap(M, (m, n), (0:m-1, 1:n))
19+
@test_throws ArgumentError LinearMap(M, (m, n), (1:m, 1:n+1))
20+
@test_throws ArgumentError LinearMap(M, (m, n), (1:m, 2:n+1))
21+
@test_throws ArgumentError LinearMap(M, (m, n), offset=(3,3))
22+
# @test_throws ArgumentError LinearMap(M, (m, n), (m:-1:1, 1:n))
23+
# @test_throws ArgumentError LinearMap(M, (m, n), (collect(m:-1:1), 1:n))
24+
@test size(@inferred LinearMap(M, (2m, 2n), (1:2:2m, 1:2:2n))) == (2m, 2n)
25+
@test @inferred !ishermitian(BL)
26+
@test @inferred !issymmetric(BL)
27+
@test @inferred LinearMap(L, size(BM), (offset[1] .+ (1:m), offset[2] .+ (1:n))) == BL
28+
Wc = @inferred LinearMap([2 im; -im 0]; ishermitian=true)
29+
Bc = @inferred LinearMap(Wc, (4,4); offset=(2,2))
30+
@test (@inferred ishermitian(Bc))
31+
32+
x = randn(s2); X = rand(s2, 3)
33+
y = BM * x; Y = zeros(s1, 3)
34+
35+
@test @inferred BL * x BM * x
36+
@test @inferred BL' * y BM' * y
37+
38+
for α in (true, false, rand()),
39+
β in (true, false, rand()),
40+
t in (identity, adjoint, transpose)
41+
42+
@test t(BL) * x mul!(copy(y), t(BL), x) t(BM) * x
43+
@test Matrix(t(BL) * X) mul!(copy(Y), t(BL), X) t(BM) * X
44+
y = randn(s1); Y = randn(s1, 3)
45+
@test (@inferred mul!(copy(y), t(BL), x, α, β)) mul!(copy(y), t(BM), x, α, β)
46+
@test (@inferred mul!(copy(Y), t(BL), X, α, β)) mul!(copy(Y), t(BM), X, α, β)
47+
end
48+
49+
if VERSION >= v"1.8"
50+
M = rand(3,4)
51+
L = LinearMap(M)
52+
@test Matrix(reverse(L)) == reverse(M)
53+
for dims in (1, 2, (1,), (2,), (1, 2), (2, 1), :)
54+
@test Matrix(reverse(L, dims=dims)) == reverse(M, dims=dims)
55+
end
56+
end
57+
end

test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ include("left.jl")
3333
include("fillmap.jl")
3434

3535
include("nontradaxes.jl")
36+
37+
include("embeddedmap.jl")

0 commit comments

Comments
 (0)