Symbolic Arrays

Symbolic Arrays vs Arrays of Symbolic Expressions

Symbolics.jl contains two forms for handling symbolic arrays:

  1. Arrays of symbolic expressions: these are Julia arrays with Symbolics.jl objects in them.
  2. Symbolic Arrays: these are symbolic (O(1)) representations of arrays.

Arrays of symbolic expressions are simply Symbolics.jl objects put into Julia arrays. For example:

using Symbolics
@variables x y
u = [x,y]

\[ \begin{equation} \left[ \begin{array}{c} x \\ y \\ \end{array} \right] \end{equation} \]

is a vector of two symbolic variables. As shorthand,

u2 = Symbolics.variables(:x, 1:3, 3:6)

\[ \begin{equation} \left[ \begin{array}{cccc} x_{1}ˏ_3 & x_{1}ˏ_4 & x_{1}ˏ_5 & x_{1}ˏ_6 \\ x_{2}ˏ_3 & x_{2}ˏ_4 & x_{2}ˏ_5 & x_{2}ˏ_6 \\ x_{3}ˏ_3 & x_{3}ˏ_4 & x_{3}ˏ_5 & x_{3}ˏ_6 \\ \end{array} \right] \end{equation} \]

creates a Julia matrix of symbolic variables. Indexing u or u2 gives symbolic values which act as a normal scalar symbolic value. This form these uses Julia's array functionality and performs symbolic operations on the scalar values.

On the otherhand, Julia's symbolic array form is an O(1) representation of the whole array.

@variables A[1:5, 1:3]
1-element Vector{Symbolics.Arr{Num, 2}}:
 A[1:5,1:3]

When using this form, A[1,1] is not a symbolic variable but a symbolic expression for indexing the variable A. This representation holds linear algebra expressions in a non-expanded form. For example:

@variables B[1:3, 1:3]
A * B

\[ \begin{equation} A B \end{equation} \]

in comparison to:

a = Symbolics.variables(:a, 1:5, 1:3)
b = Symbolics.variables(:b, 1:3, 1:3)
a * b

\[ \begin{equation} \left[ \begin{array}{ccc} a_{1}ˏ_1 b_{1}ˏ_1 + a_{1}ˏ_2 b_{2}ˏ_1 + a_{1}ˏ_3 b_{3}ˏ_1 & a_{1}ˏ_1 b_{1}ˏ_2 + a_{1}ˏ_2 b_{2}ˏ_2 + a_{1}ˏ_3 b_{3}ˏ_2 & a_{1}ˏ_1 b_{1}ˏ_3 + a_{1}ˏ_2 b_{2}ˏ_3 + a_{1}ˏ_3 b_{3}ˏ_3 \\ a_{2}ˏ_1 b_{1}ˏ_1 + a_{2}ˏ_2 b_{2}ˏ_1 + a_{2}ˏ_3 b_{3}ˏ_1 & a_{2}ˏ_1 b_{1}ˏ_2 + a_{2}ˏ_2 b_{2}ˏ_2 + a_{2}ˏ_3 b_{3}ˏ_2 & a_{2}ˏ_1 b_{1}ˏ_3 + a_{2}ˏ_2 b_{2}ˏ_3 + a_{2}ˏ_3 b_{3}ˏ_3 \\ a_{3}ˏ_1 b_{1}ˏ_1 + a_{3}ˏ_2 b_{2}ˏ_1 + a_{3}ˏ_3 b_{3}ˏ_1 & a_{3}ˏ_1 b_{1}ˏ_2 + a_{3}ˏ_2 b_{2}ˏ_2 + a_{3}ˏ_3 b_{3}ˏ_2 & a_{3}ˏ_1 b_{1}ˏ_3 + a_{3}ˏ_2 b_{2}ˏ_3 + a_{3}ˏ_3 b_{3}ˏ_3 \\ a_{4}ˏ_1 b_{1}ˏ_1 + a_{4}ˏ_2 b_{2}ˏ_1 + a_{4}ˏ_3 b_{3}ˏ_1 & a_{4}ˏ_1 b_{1}ˏ_2 + a_{4}ˏ_2 b_{2}ˏ_2 + a_{4}ˏ_3 b_{3}ˏ_2 & a_{4}ˏ_1 b_{1}ˏ_3 + a_{4}ˏ_2 b_{2}ˏ_3 + a_{4}ˏ_3 b_{3}ˏ_3 \\ a_{5}ˏ_1 b_{1}ˏ_1 + a_{5}ˏ_2 b_{2}ˏ_1 + a_{5}ˏ_3 b_{3}ˏ_1 & a_{5}ˏ_1 b_{1}ˏ_2 + a_{5}ˏ_2 b_{2}ˏ_2 + a_{5}ˏ_3 b_{3}ˏ_2 & a_{5}ˏ_1 b_{1}ˏ_3 + a_{5}ˏ_2 b_{2}ˏ_3 + a_{5}ˏ_3 b_{3}ˏ_3 \\ \end{array} \right] \end{equation} \]

This makes the symbolic array form much more efficient, but requires that the expressions uses things with registered symbolic array functions which currently has much lower coverage. Also, there are many fallbacks for which arrays of symbolics which makes this approach more accessible but with larger expressions.

We recommend defaulting to arrays of symbolics unless you need the expression symplifications of the symbolic array approach.

Using Symbolic Arrays

Symbolic array-valued expressions (symbolic arrays) are supported by Symbolics. Symbolic array expressions propagate useful metadata that depends on input arrays: array dimension, element type and shape.

You can create a symbolic array variable with the following syntax:

using Symbolics
@variables A[1:5, 1:3] b[1:3]
2-element Vector{Symbolics.Arr{Num}}:
 A[1:5,1:3]
 b[1:3]

Here, A is a symbolic matrix of size (5, 3) and b is a symbolic vector of length 3.

size(A)
(5, 3)
size(b)
(3,)
ndims(A)
2
ndims(b)
1
eltype(A)
Real
eltype(b)
Real

Array operations

Operations on symbolic arrays return symbolic array expressions:

c = A * b

\[ \begin{equation} A b \end{equation} \]

size(c)
(5,)
eltype(c)
Real

Adjoints, matrix-matrix, and matrix-vector multiplications are supported. Dot product returns a scalar-valued expression:

b'b

\[ \begin{equation} adjoint(b) * b_1 \end{equation} \]

size(b'b)
()

Outer product returns a matrix:

b * b'

\[ \begin{equation} b \mathrm{adjoint}\left( b \right) \end{equation} \]

size(b*b')
(3, 3)

Broadcast, map and reduce

A .* b'

\[ \begin{equation} \mathrm{broadcast}\left( *, A, \mathrm{adjoint}\left( b \right) \right) \end{equation} \]

map(asin, (A*b))

\[ \begin{equation} \mathrm{map}\left( \arcsin, A b \right) \end{equation} \]

#sum(A) #latexify not working
typeof(sum(A))
Num
typeof(sum(A, dims=2))
Symbolics.Arr{Num, 2}

Indexing and delayed computation

Indexing array expressions is fairly flexible in Symbolics. Let's go through all the possible ways to index arrays.

Scalar indexing and scalarization

AAt = A*A'

\[ \begin{equation} A \mathrm{adjoint}\left( A \right) \end{equation} \]

AAt[2,3]

\[ \begin{equation} A * adjoint(A)_{2}ˏ_3 \end{equation} \]

Here we indexed for the element (2,3), but we got back a symbolic indexing expression. You may want to force the element to be computed in terms of the elements of A. This can be done, using the scalarize function.

Symbolics.scalarize(AAt[2,3])

\[ \begin{equation} A_{2}ˏ_1 A_{3}ˏ_1 + A_{2}ˏ_2 A_{3}ˏ_2 + A_{2}ˏ_3 A_{3}ˏ_3 \end{equation} \]

@syms i::Int j::Int
Symbolics.scalarize(AAt[i,j])

In general, any scalar expression which is derived from array expressions can be scalarized.

#sum(A[:,1]) + sum(A[2,:])#latexify not working
Symbolics.scalarize(sum(A[:,1]) + sum(A[2,:]))