Lazy algebra framework
LazyAlgebra is a Julia package to generalize the notion of matrices and vectors used in linear algebra.
Many numerical methods (e.g. in numerical optimization or digital signal processing) involve essentially linear operations on the considered variables. LazyAlgebra provides a framework to implement these kind of numerical methods independently of the specific type of the variables. This is exploited in OptimPackNextGen package, an attempt to provide most optimization algorithms of OptimPack in pure Julia.
LazyAlgebra also provides a flexible and extensible framework for creating complex mappings and linear mappings to operate on the variables.
A few concepts are central to LazyAlgebra:
- vectors represent the variables of interest and can be anything providing a few methods are implemented for their specific type;
- mappings are any functions between such vectors;
- linear mappings (a.k.a. linear operators) behave linearly with respect to their arguments.
There are several reasons to have special methods for basic vector operations rather than relying on Julia linear algebra methods. First, the notion of vector is different, in Julia a mono-dimensional array is a vector while, here any object with embedded values can be assumed to be a vector providing a subset of methods are specialized for this type of object. For instance, LazyAlgebra provides such methods specialized for real-valued and complex-valued (with real components) arrays of any dimensionality. Second, the meaning of the methods may have to be different. For instance, only real-valued functions can be minimized (or maximized) and for this task, complex-valued variables can just be considered as real-valued variables (each complex value being equivalent to a pair of reals).
Mappings
LazyAlgebra features:
- flexible and extensible framework for creating complex mappings;
- lazy evaluation of the mappings;
- lazy assumptions when combining mappings;
- efficient memory allocation by avoiding temporaries.
General mappings
A Mapping
can be any function between two variables spaces. Using Householder-like notation (that is upper case Latin letters denote mappings, lower case Latin letters denote variables, and Greek letters denote scalars), then:
A(x)
,A*x
orA⋅x
yields the result of applying the mappingA
tox
;A\x
yields the result of applying the inverse ofA
tox
;
Simple constructions are allowed for any kind of mappings and can be used to create new instances of mappings which behave correctly. For instance:
B = α*A
(whereα
is a number) is a mapping which behaves asA
timesα
; that isB(x)
yields the same result asα*(A(x))
.C = A + B + ...
is a mapping which behaves as the sum of the mappingsA
,B
, ...; that isC(x)
yields the same result asA(x) + B(x) + ...
.C = A*B
,C = A∘B
orC = A⋅B
is a mapping which behaves as the composition of the mappingsA
andB
; that isC⋅x
yields the same result asA(B(x))
. As for the sum of mappings, there may be an arbitrary number of mappings in a composition; for example, ifD = A*B*C
thenD(x)
yields the same result asA(B(C(x)))
.C = A\B
is a mapping such thatC(x)
yields the same result asinv(A)(B(x))
.C = A/B
is a mapping such thatC(x)
yields the same result asA(inv(B)(x))
.
These constructions can be combined to build up more complex mappings. For example:
D = A*(B + 3C)
is a mapping such thatD⋅x
yields the same result asA(B(x) + 3*C(x))
.
Linear mappings
A LinearMapping
can be any linear mapping between two spaces. This abstract subtype of Mapping
is introduced to extend the notion of matrices and vectors. Assuming the type of A
inherits from LinearMapping
, then:
for linear mappings
A
andB
,A⋅B
is the same asA∘B
orA*B
which yields the composition ofA
andB
whose effect is to applyB
and thenA
;A'⋅x
andA'*x
yields the result of applying the adjoint of the mappingA
tox
;A'\x
yields the result of applying the adjoint of the inverse of mappingA
tox
.B = A'
is a mapping such thatB⋅x
yields the same result asA'⋅x
.
Beware that, due to the priority of operators in Julia, A*B(x)
is the same as A(B(x))
not (A*B)(x)
.
Automatic simplifications
An important feature of LazyAlgebra framework for mappings is that a number of simplifications are automatically made at contruction time. For instance, assuming A
is a mapping:
B = A'
C = B'
yields C
which is just a reference to A
. In other words, adjoint(adjoint(A)) -> A
holds. Likely
D = inv(A)
E = inv(D)
yields E
which is another reference to A
. In other words, inv(inv(A)) -> A
holds assuming by default that A
is invertible. This follows the principles of laziness. It is however, possible to prevent this by extending the Base.inv
method so as to throw an exception when applied to the specific type of A
:
Base.inv(::SomeNonInvertibleMapping) = error("non-invertible mapping")
where SomeNonInvertibleMapping <: Mapping
is the type of A
.
Other example of simplifications:
B = 3A
C = 7B'
where mappings B
and C
are such that B*x -> 3*(A*x)
and C*x -> 21*(A*x)
for any vector x
. That is C*x
is evaluated as 21*(A*x)
not as 7*(3*(A*x))
thanks to simplifications occurring while the mapping C
is constructed.
Using the ->
to denote in the right-hand side the actual construction made by LazyAlgebra for the expression in the left-hand side and assuming A
, B
and C
are linear mappings, the following simplications will occur:
(A + C + B + 3C)' -> A' + B' + 4C'
(A*B*3C)' -> 3C'*B'*A'
inv(A*B*3C) -> 3\inv(C)*inv(B)*inv(A)
However, if M
is a non-linear mapping, then:
inv(A*B*3M) -> inv(M)*(3\inv(B))*inv(A)
which can be compared to inv(A*B*3C)
when all operands are linear mappings.
Due to the associative rules applied by Julia, parentheses are needed around constructions like 3*C
if it has to be interpreted as 3C
in all contexes. Otherwise, A*B*(3*C)
is equivalent to A*B*3C
while A*B*3*C
is interpreted as ((A*B)*3)*C
; that is, compose A
and B
, apply A*B
to 3
and right multiply the result by C
.
Creating new mappings
LazyAlgebra provides a number of simple mappings. Creating new primitive mapping types (not by combining existing mappings as explained above) which benefit from the LazyAlgebra framework is as simple as declaring a new mapping subtype of Mapping
(or one of its abstract subtypes) and extending two methods vcreate
and apply!
specialized for the new mapping type. For mode details, see here.