Symbolic framework
We can define scalar symbolic variables as follows
from gradgen import SX
x = SX.sym("x")
for vectors, use SXVector. For example, to create a vector of dimensions 2 do
from gradgen import SXVector
x = SXVector.sym("x", 2)
If you print x you will see
SXVector(elements=(SX.sym('x_0'), SX.sym('x_1')))
Details
Although SXVectors of length 1 behave like scalar, you can "cast"
an SXVector of length 1 as an SX using
x = SXVector.sym("x", 1)
x = x[0]
Lastly, note that x[a:b] returns an SXVector view.
n = 10
x = SXVector.sym("x", n)
x_slice = x[1:4] # <-- this is (x[1], x[2], x[3])
Symbolic expressions
Using scalar and vector symbols we can construct symbolic expressions. For example, to define the function we can do
x = SXVector.sym("x", 5)
u = SX.sym("u")
f = u * x / x.norm1()
Several operations are supported. Indicatively, we support the operators **;
to define the expression we can write
z = SX.sym("z")
f = 1 + 0.1 * z - 4 * z**3
The operator **, when applied to vectors (SXVector), operates element-wise.
All trigonometric (cos, sin, tan),
inverse trigonometric (acos, asin, atan) and hyperbolic operations
(cosh, sinh, tanh) and their inverses (acosh, asinh, atanh) are
supported.
For vectors, the following scalar-valued operations are available:
norm2(): Euclidean normnorm2sq(): squared Euclidean normnorm1(): norm-1norm_inf(): Infinity normnorm_p(p): -normnorm_p_to_p(p): -norm to the power
Constants
Scalar constants are first-class symbolic expressions. You can create them
explicitly with SX.const(...) or the top-level const(...) helper:
from gradgen import SX
c1 = SX.const(3)
Constants participate in the same symbolic expressions as variables, so they work naturally inside function definitions and code generation.
Vectors from iterables
If you already have an iterable of scalar-like values, use vector(...) to
coerce it into an SXVector:
from gradgen import SX, SXVector, vector
a = SX.sym("a")
b = SX.sym("b")
v = vector([a, b, 1.0])
This is convenient when building vectors from Python lists or tuples. If you want a flat packed vector made from existing vectors, unpack them first:
w = SXVector.sym("w", 5)
u = SXVector((a, b))
p = SXVector((*w, *u))
Matvec, bilinear, and quadratic functions
In optimal control applications we frequently encounter terms of the form
, where is a symmetric matrix. Such expressions can be
constructed with quadform as follows
import gradgen as gg
x = SXVector.sym("x", 2)
P = [[1, 4],
[4, 5]]
f = gg.quadform(P, x, is_symmetric=True)
If is_symmetric=True is used, then a simpler expression is generated,
however, an exception is raised if the provided matrix P is not truly
symmetric.
One limitation of the current framework is that needs to be a constant matrix. Quadratic forms with a symbolic matrix will be supported in a future version.
The lower-level helpers matvec(...) and bilinear_form(...) are also
available when you want to express a constant matrix-vector product or a
bilinear form directly:
import gradgen as gg
x = SXVector.sym("x", 2)
y = SXVector.sym("y", 2)
P = [[1, 4],
[2, 5]]
mx = gg.matvec(P, x)
b = gg.bilinear_form(x, P, y)
q = gg.quadform(P, x)
matvec(...) returns a vector, bilinear_form(...) returns a scalar, and
quadform(...) is the quadratic-form special case.
Likewise, we often need to compute dot products. This can be done with dot:
n = 3
x = SXVector.sym("x", n)
q = SXVector.sym("q", n)
f = x.dot(q)
To get the length of a vector, do
x = SXVector.sym("x", 10)
l = len(x) # l = 10
Equality of symbols
Symbols are uniquely identified by their name. For example, the following symbols are equal
x1 = SX.sym("x")
x2 = SX.sym("x")
assert x1 == x2
Because symbols compare by name, they are also suitable as dictionary keys or set members when you want name-based aliasing.
Concatenation
Two or more symbols can be packed into a vector using the constructor of SXVector. For example,
a = SX.sym("a")
b = SX.sym("b")
c = SX.sym("c")
x = SXVector((a, b, c))
assert x[0] == a
assert len(x) == 3
Vectors can also be concatenated as follows:
x = SXVector.sym("x", 3)
y = SXVector.sym("y", 5)
a = SX.sym("a")
z = SXVector((*x, *y, a))
assert len(z) == 9
assert z[1] == x[1]
assert z[3] == y[0]
assert z[8] == a
Note that we unpack *x and *y to pass them to the constructor.
If you omit the unpacking, the constructor stores nested vectors instead of flattening them. For packed state and parameter vectors, the unpacked form is usually what you want.
Expressions defined piecewise
Often we need to define expressions piecewise. As an example, take
This defines a function .
More generally, we can have an expression
where and are expressions and the condition is an inequality
(using the operators <, <=, >, or >=) involving symbols.
Such piecewise expressions can be defined using if_else(f1, f2, condition).
Here is an example for the above function
x = SX.sym("x")
p = SX.sym("p")
f = if_else(0, (x-p)**2, x < p)
The derivative of an expression defined piecewise is the piecewise function
Note that the function may not be differentiable everywhere. Nevertheless, gradgen will always compute a derivative, provided and are differentiable.
Details
Consisder the Heaviside function which is
x = SX.sym("x")
f = if_else(0, 1, x < 0)
We know that for all , while at the derivative is not defined. However, at gradgen uses the branch and computes the derivative; it is
df = Function("f", [x], [gradient(f, x)])
print(df(0)) # prints 0.0