Methods for mappings
LazyAlgebra
provides a number of mappings and linear operators. To create new primitive mapping types (not by combining existing mappings) and benefit from the LazyAlgebra
infrastruture, you have to:
Create a new type derived from
Mapping
or one of its abstract sub-types such asLinearMapping
.Implement at least two methods
apply!
andvcreate
specialized for the new mapping type. Applying the mapping is done by the former method. The latter method is called to create a new output variable suitable to store the result of applying the mapping (or one of its variants) to some input variable.Optionally specialize method
identical
for two arguments of the new mapping type.
The vcreate
method
The signature of the vcreate
method to be implemented by specific mapping types is:
vcreate(::Type{P}, A::Ta, x::Tx, scratch::Bool) -> y
where A
is the mapping, x
its argument and P
is one of Direct
, Adjoint
, Inverse
and/or InverseAdjoint
(or equivalently AdjointInverse
) and indicates how A
is to be applied:
Direct
to applyA
tox
, e.g. to computeA⋅x
;Adjoint
to apply the adjoint ofA
tox
, e.g. to computeA'⋅x
;Inverse
to apply the inverse ofA
tox
, e.g. to computeA\x
;InverseAdjoint
orAdjointInverse
to apply the inverse ofA'
tox
, e.g. to computeA'\x
.
The result returned by vcreate
is a new output variables suitable to store the result of applying the mapping A
(or one of its variants as indicated by P
) to the input variables x
.
The scratch
argument is a boolean to let the caller indicate whether the input variable x
may be re-used to store the result. If scratch
is true
and if that make sense, the value returned by vcreate
may be x
. Calling vcreate
with scratch=true
can be used to limit the allocation of resources when possible. Having scratch=true
is only indicative and a specific implementation of vcreate
may legitimately always assume scratch=false
and return a new variable whatever the value of this argument (e.g. because applying the considered mapping in-place is not possible or because the considered mapping is not an endomorphism). Of course, the opposite behavior (i.e., assuming that scratch=true
while the method was called with scratch=false
) is forbidden.
The result returned by vcreate
should be of predictible type to ensure type-stability. Checking the validity (e.g. the size) of argument x
in vcreate
may be skipped because this argument will be eventually checked by the apply!
method.
The apply!
method
The signature of the apply!
method to be implemented by specific mapping types is:
apply!(α::Number, ::Type{P}, A::Ta, x::Tx, scratch::Bool, β::Number, y::Ty) -> y
This method shall overwrites the contents of output variables y
with the result of α*P(A)⋅x + β*y
where P
is one of Direct
, Adjoint
, Inverse
and/or InverseAdjoint
(or equivalently AdjointInverse
) and shall return y
. The convention is that the prior contents of y
is not used at all if β = 0
so the contents of y
does not need to be initialized in that case.
Not all operations P
must be implemented, only the supported ones. For iterative resolution of (inverse) problems, it is generally needed to implement at least the Direct
and Adjoint
operations for linear operators. However nonlinear mappings are not supposed to implement the Adjoint
and derived operations.
Argument scratch
is a boolean to let the caller indicate whether the contents of the input variable x
may be overwritten during the operations. If scratch=false
, the apply!
method shall not modify the contents of x
.
The identical
method
The method identical(A,B)
yields whether A
and B
are the same mappings in the sense that their effects will always be the same. This method is used to perform some simplifications and optimizations and may have to be specialized for specific mapping types. The default implementation is to return A === B
.
The returned result may be true although A
and B
are not necessarily the same object. In the below example, if A
and B
are two sparse matrices whose coefficients and indices are stored in the same vectors (as can be tested with the ===
operator) this method should return true
because the two operators will behave identically (any changes in the coefficients or indices of A
will be reflected in B
). If any of the vectors storing the coefficients or the indices are not the same objects, then identical(A,B)
must return false
even though the stored values may be the same because it is possible, later, to change one operator without affecting identically the other.
Example
The following example implements a simple sparse linear operator which is able to operate on multi-dimensional arrays (the so-called variables):
# Use LazyAlgebra framework and import methods that need to be extended.
using LazyAlgebra
import LazyAlgebra: vcreate, apply!, input_size, output_size
struct SparseOperator{T<:AbstractFloat,M,N} <: LinearMapping
outdims::NTuple{M,Int}
inpdims::NTuple{N,Int}
A::Vector{T}
I::Vector{Int}
J::Vector{Int}
end
input_size(S::SparseOperator) = S.inpdims
output_size(S::SparseOperator) = S.outdims
function vcreate(::Type{Direct}, S::SparseOperator{Ts,M,N},
x::DenseArray{Tx,N},
scratch::Bool) where {Ts<:Real,Tx<:Real,M,N}
@assert size(x) == input_size(S)
Ty = promote_type(Ts, Tx)
return Array{Ty}(undef, output_size(S))
end
function vcreate(::Type{Adjoint}, S::SparseOperator{Ts,M,N},
x::DenseArray{Tx,M},
scratch::Bool) where {Ts<:Real,Tx<:Real,M,N}
@assert size(x) == output_size(S)
Ty = promote_type(Ts, Tx)
return Array{Ty}(undef, input_size(S))
end
function apply!(α::Real,
::Type{Direct},
S::SparseOperator{Ts,M,N},
x::DenseArray{Tx,N},
scratch::Bool,
β::Real,
y::DenseArray{Ty,M}) where {Ts<:Real,Tx<:Real,Ty<:Real,M,N}
@assert size(x) == input_size(S)
@assert size(y) == output_size(S)
β == 1 || vscale!(y, β)
if α != 0
A, I, J = S.A, S.I, S.J
alpha = convert(promote_type(Ts,Tx,Ty), α)
@assert length(I) == length(J) == length(A)
for k in 1:length(A)
i, j = I[k], J[k]
y[i] += alpha*A[k]*x[j]
end
end
return y
end
function apply!(α::Real,
::Type{Adjoint},
S::SparseOperator{Ts,M,N},
x::DenseArray{Tx,M},
scratch::Bool,
β::Real,
y::DenseArray{Ty,N}) where {Ts<:Real,Tx<:Real,Ty<:Real,M,N}
@assert size(x) == output_size(S)
@assert size(y) == input_size(S)
β == 1 || vscale!(y, β)
if α != 0
A, I, J = S.A, S.I, S.J
alpha = convert(promote_type(Ts,Tx,Ty), α)
@assert length(I) == length(J) == length(A)
for k in 1:length(A)
i, j = I[k], J[k]
y[j] += alpha*A[k]*x[i]
end
end
return y
end
identical(A::T, B::T) where {T<:SparseOperator} =
(A.outdims == B.outdims && A.inpdims == B.inpdims &&
A.A === B.A && A.I === B.I && A.J === B.J)
Remarks:
In our example, arrays are restricted to be dense so that linear indexing is efficient. For the sake of clarity, the above code is intended to be correct although there are many possible optimizations.
If
α = 0
there is nothing to do except scaley
byβ
.The call to
vscale!(β, y)
is to properly initializey
. Remember the convention that the contents ofy
is not used at all ifβ = 0
soy
does not need to be properly initialized in that case, it will simply be zero-filled by the call tovscale!
. The statementsβ == 1 || vscale!(y, β)
are equivalent to:
if β != 1 vscale!(y, β) end
which may be simplified to just calling
vscale!
unconditionally:vscale!(y, β)
as
vscale!(y, β)
does nothing ifβ = 1
.@inbounds
could be used for the loops but this would require checking that all indices are whithin the bounds. In this example, onlyk
is guaranteed to be valid,i
andj
have to be checked.