StructuredExpression example

StructuredExpressions allow you to specify a predefined structure for an expression that exists outside of the regular AbstractExpressionNode objects which store expressions as trees.

Let's look at an example:

using DynamicExpressions, Random

First, we will create some normal Expression objects.

operators = OperatorEnum(; unary_operators=(cos, exp), binary_operators=(+, -, *, /))
variable_names = ["x", "y"]
x = Expression(Node{Float64}(; feature=1); operators, variable_names)
y = Expression(Node{Float64}(; feature=2); operators, variable_names)

typeof(x)
Expression{Float64, Node{Float64}, @NamedTuple{operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(cos), typeof(exp)}}, variable_names::Vector{String}}}

Any AbstractExpression, such as this Expression object, can be composed together using standard Julia math operations. For example, let's some complex expressions from these:

f = x * x - cos(2.5f0 * y + -0.5f0)
g = exp(2.0 - y * y)

f, g
((x * x) - cos((2.5 * y) + -0.5), exp(2.0 - (y * y)))

We can then create a StructuredExpression from these two expressions. This is a composite AbstractExpression object that composes multiple expressions during evaluation.

ex = StructuredExpression((; f, g), nt -> nt.f + nt.g; operators, variable_names)
ex
((x * x) - cos((2.5 * y) + -0.5)) + exp(2.0 - (y * y))

Note that this is displayed as a single tree, with the + operator used to combine them. Despite this, the expression is not actually stored with the + operator in an AbstractExpressionNode.

By default, using get_tree will evaluate the result of nt.f + nt.g. This let's us use things like the regular operations available to AbstractExpressionNodes:

length(get_tree(ex))
17

Next, let's try to evaluate this on some random data:

rng = Random.MersenneTwister(0)
X = randn(rng, Float64, 2, 5)
X
2×5 Matrix{Float64}:
 0.679107  -0.353007  0.586617   0.0649475  -0.51421
 0.828413  -0.134854  0.297336  -0.109017    1.57433

Followed by the evaluation. Since we have stored the operators directly in the expression object, we do not need to pass the operators explicitly. Evaluation of an AbstractExpression is set up to forward through get_tree, so this will work automatically.

ex(X)
5-element Vector{Float64}:
 4.181451927652151
 6.710916888797271
 6.137425035174551
 6.589838930129643
 1.841139832369141

Which we can verify against the individual expressions:

f(X) + g(X)
5-element Vector{Float64}:
 4.181451927652151
 6.710916888797271
 6.137425035174551
 6.589838930129643
 1.841139832369141

This page was generated using Literate.jl.