Basic Types
The major building block is called an algorithm. An algorithm is defined with variables, parameters, functions, oracles, and update equations. All the expressions in Linnaeus are defined symbolically using SymPy.
Algorithms
Algorithm is defined using keyword Algorithm
along with the name of the algorithm.
The following code defines an Algorithm
as new_algo
named my new algorithm
.
new_algo = Algorithm("my new algorithm")
Expressions such as variables, parameters, functions, oracles, and update equations are all attributes to an algorithm object. An algorithm must be defined before other expressions are defined and added to it.
Variables
Variables are algorithm states. They are declared and added to an algorithm
using syntax add_var
along with the names of the variables.
In the following example, we add variables x1
and x2
to new_algo
.
x1, x2 = new_algo.add_var("x1", "x2")
Each algorithm state (each variable) must be updated at each iteration. The
updated variables can be accessed by syntax update
.
Here we access the updated x1
with x1u
and updated x2
with x2u
in the following code.
x1u, x2u = new_algo.update(x1, x2)
Parameters
Parameters of an algorithm can be declared as scalar (commutative) or vector or matrix (noncommutative).
Parameters are declared and added to an algorithm using syntax add_parameter
along with the names of parameters.
To add a vector or matrix, the argument commutative
should be set as False
.
The default setting of argument commutative
is True
.
There is no need to specialize the dimensions of vectors or matrices, since they are symbolic.
The following code shows how to add scalar t
and matrix L
to new_algo
.
t = new_algo.add_parameter("t")
L = new_algo.add_parameter("L", commutative = False)
Warning for noncommutative parameters
Due to limited support for noncommutative symbol calculation in SymPy, Linnaeus may not correctly parse extremely complex algorithms including noncommutative symbols. It is also possible that Linnaeus may not get the parameter mapping for noncommutative symbols (matrices or vectors) when detecting equivalence for such algorithms. In such cases, we recommand users to declare all the parameters as commutative symbols!
Oracles
Linnaeus provides two approaches to declare and add oracles to an algorithm.
Black-box approach
The black-box approach is to define oracles as black boxes.
When parsing the algorithm, the system treats each oracle as a distinct entity unrelated to any other oracle.
An oracle declared using syntax add_oracle
uses the black-box approach
For example, we declare oracles gradient of f
and proximal operator of g
with the following code.
gradf = new_algo.add_oracle("gradf")
proxg = new_algo.add_oracle("proxg")
Functional approach
The functional approach is to define oracles in terms of the (sub)gradient of a function.
When parsing an algorithm, all the oracles will be decomposed into (sub)gradients.
This approach is important to allow detection algorithm conjugation.
This example shows the details.
With the functional approach, functions must be defined and added to the algorithm
using syntax add_function
before defining oracles.
The following example shows to define function f
and add it to new_algo
f = new_algo.add_function("f")
Linnaeus provides four types of oracles that can be decomposed into (sub)gradients,
- Gradient with syntax
grad
- Proximal operator with syntax
prox
- Projection with syntax
proj
- Argmin operator with syntax
argmin
import linnaeus as lin
# gradient of f with respect to x1
lin.grad(f)(x1)
# proximal operator of f with respect to x2 and parameter t
lin.prox(f,t)(x2)
# projection x1 onto set C
lin.proj(C)(x1)
# argmin of f(x) + g(x)
lin.argmin(x, f(x) + g(x))
To use the projection oracle, a set must be declared in advance with syntax add_set
.
Update equations
Update equations define the iterative procedures of an algorithm. All the update equations have common
structure that an updated variable equals an expression of variables, parameters, and oracles.
An update equation is defined with syntax add_update
.
new_algo.add_update(x1, x2 - gradf(x2))
new_algo.add_update(x2, x1 - proxg(x1))
The update equations in the above example are interpreted as
Or interpreted as equalities,
By default setting, variables which have already been updated will be substituted with their
updated versions. Thus, x1
in the second update equation is considered as the updated x1
, as the x1^+
in
the second equality interpretation.
For some cases, we can change the default setting by syntax set_auto
, such as
new_algo.set_auto(False)
new_algo.add_update(x1, x2 - gradf(x2))
new_algo.add_update(x2, x1 - proxg(x1))
Under this setting, we have a different interpretation of the update equations,
Warning!
To avoid overlap and misleading in expressions, it is highly recommanded that each algorithm should have its own variables, parameters, etc, and those expressions should only be used within this algorithm!
For example,
algo1 = Algorithm("my first algorithm")
x1, x2 = algo1.add_var("x1", "x2")
t, h = algo1.add_parameter("t", "h")
algo1.add_update(x1, x2 - t*x2)
algo1.add_update(x2, x1 - h*x2)
algo2 = Algorithm("my second algorithm")
x1, x2 = algo2.add_var("x1", "x2")
t, h = algo2.add_parameter("t", "h")
algo2.add_update(x1, x2 - h*x1)
algo2.add_update(x2, x1 - t*x2)
Variables x1
and x2
, parameters t
and h
are declared within the first algorithm algo1
. They should only be used within algo1
, such as the update equations of it.
For the second algorithm algo2
, if we still want to use variables x1
, x2
, and parameters t
, h
, they should be declared again within algo2
.