Evaluation

Given an expression tree specified with a Node type, you may evaluate the expression over an array of data with the following command:

DynamicExpressions.EvaluateEquationModule.eval_tree_arrayMethod
eval_tree_array(tree::Node, cX::AbstractMatrix{T}, operators::OperatorEnum; turbo::Bool=false)

Evaluate a binary tree (equation) over a given input data matrix. The operators contain all of the operators used. This function fuses doublets and triplets of operations for lower memory usage.

This function can be represented by the following pseudocode:

function eval(current_node)
    if current_node is leaf
        return current_node.value
    elif current_node is degree 1
        return current_node.operator(eval(current_node.left_child))
    else
        return current_node.operator(eval(current_node.left_child), eval(current_node.right_child))

The bulk of the code is for optimizations and pre-emptive NaN/Inf checks, which speed up evaluation significantly.

Arguments

  • tree::Node: The root node of the tree to evaluate.
  • cX::AbstractMatrix{T}: The input data to evaluate the tree on.
  • operators::OperatorEnum: The operators used in the tree.
  • turbo::Bool: Use LoopVectorization.@turbo for faster evaluation.

Returns

  • (output, complete)::Tuple{AbstractVector{T}, Bool}: the result, which is a 1D array, as well as if the evaluation completed successfully (true/false). A false complete means an infinity or nan was encountered, and a large loss should be assigned to the equation.
source

Assuming you are only using a single OperatorEnum, you can also use the following shorthand by using the expression as a function:

    (tree::Node)(X::AbstractMatrix{T}, operators::OperatorEnum; turbo::Bool=false)

Evaluate a binary tree (equation) over a given data matrix. The
operators contain all of the operators used in the tree.

# Arguments
- `X::AbstractMatrix{T}`: The input data to evaluate the tree on.
- `operators::OperatorEnum`: The operators used in the tree.
- `turbo::Bool`: Use `LoopVectorization.@turbo` for faster evaluation.

# Returns
- `output::AbstractVector{T}`: the result, which is a 1D array.
    Any NaN, Inf, or other failure during the evaluation will result in the entire
    output array being set to NaN.

For example,

using DynamicExpressions

operators = OperatorEnum(; binary_operators=[+, -, *], unary_operators=[cos])
tree = Node(; feature=1) * cos(Node(; feature=2) - 3.2)

tree([1 2 3; 4 5 6.], operators)
3-element Vector{Float64}:
  0.6967067093471655
 -0.4544041893861738
 -2.8266670220059744

This is possible because when you call OperatorEnum, it automatically re-defines (::Node)(X) to call the evaluation operation with the given operators loaded. It also re-definesprint,show, and the various operators, to work with theNode` type.

Warning

The Node type does not know about which OperatorEnum you used to create it. Thus, if you define an expression with one OperatorEnum, and then try to evaluate it or print it with a different OperatorEnum, you will get undefined behavior!

You can also work with arbitrary types, by defining a GenericOperatorEnum instead. The notation is the same for eval_tree_array, though it will return nothing when it can't find a method, and not do any NaN checks:

DynamicExpressions.EvaluateEquationModule.eval_tree_arrayMethod
eval_tree_array(tree::Node, cX::AbstractMatrix, operators::GenericOperatorEnum; throw_errors::Bool=true)

Evaluate a generic binary tree (equation) over a given input data, whatever that input data may be. The operators enum contains all of the operators used. Unlike eval_tree_array with the normal OperatorEnum, the array cX is sliced only along the first dimension. i.e., if cX is a vector, then the output of a feature node will be a scalar. If cX is a 3D tensor, then the output of a feature node will be a 2D tensor. Note also that tree.feature will index along the first axis of cX.

However, there is no requirement about input and output types in general. You may set up your tree such that some operator nodes work on tensors, while other operator nodes work on scalars. eval_tree_array will simply return nothing if a given operator is not defined for the given input type.

This function can be represented by the following pseudocode:

function eval(current_node)
    if current_node is leaf
        return current_node.value
    elif current_node is degree 1
        return current_node.operator(eval(current_node.left_child))
    else
        return current_node.operator(eval(current_node.left_child), eval(current_node.right_child))

Arguments

  • tree::Node: The root node of the tree to evaluate.
  • cX::AbstractArray: The input data to evaluate the tree on.
  • operators::GenericOperatorEnum: The operators used in the tree.
  • throw_errors::Bool=true: Whether to throw errors if they occur during evaluation. Otherwise, MethodErrors will be caught before they happen and evaluation will return nothing, rather than throwing an error. This is useful in cases where you are unsure if a particular tree is valid or not, and would prefer to work with nothing as an output.

Returns

  • (output, complete)::Tuple{Any, Bool}: the result, as well as if the evaluation completed successfully (true/false). If evaluation failed, nothing will be returned for the first argument. A false complete means an operator was called on input types that it was not defined for.
source

Likewise for the shorthand notation:

    (tree::Node)(X::AbstractMatrix, operators::GenericOperatorEnum; throw_errors::Bool=true)

# Arguments
- `X::AbstractArray`: The input data to evaluate the tree on.
- `operators::GenericOperatorEnum`: The operators used in the tree.
- `throw_errors::Bool=true`: Whether to throw errors
    if they occur during evaluation. Otherwise,
    MethodErrors will be caught before they happen and 
    evaluation will return `nothing`,
    rather than throwing an error. This is useful in cases
    where you are unsure if a particular tree is valid or not,
    and would prefer to work with `nothing` as an output.

# Returns
- `output`: the result of the evaluation.
    If evaluation failed, `nothing` will be returned for the first argument.
    A `false` complete means an operator was called on input types
    that it was not defined for. You can change this behavior by
    setting `throw_errors=false`.

Derivatives

DynamicExpressions.jl can efficiently compute first-order derivatives of expressions with respect to variables or constants. This is done using either eval_diff_tree_array, to compute derivative with respect to a single variable, or with eval_grad_tree_array, to compute the gradient with respect all variables (or, all constants). Both use forward-mode automatic, but use Zygote.jl to compute derivatives of each operator, so this is very efficient.

DynamicExpressions.EvaluateEquationDerivativeModule.eval_diff_tree_arrayMethod
eval_diff_tree_array(tree::Node{T}, cX::AbstractMatrix{T}, operators::OperatorEnum, direction::Int; turbo::Bool=false)

Compute the forward derivative of an expression, using a similar structure and optimization to evaltreearray. direction is the index of a particular variable in the expression. e.g., direction=1 would indicate derivative with respect to x1.

Arguments

  • tree::Node: The expression tree to evaluate.
  • cX::AbstractMatrix{T}: The data matrix, with each column being a data point.
  • operators::OperatorEnum: The operators used to create the tree. Note that operators.enable_autodiff must be true. This is needed to create the derivative operations.
  • direction::Int: The index of the variable to take the derivative with respect to.
  • turbo::Bool: Use LoopVectorization.@turbo for faster evaluation.

Returns

  • (evaluation, derivative, complete)::Tuple{AbstractVector{T}, AbstractVector{T}, Bool}: the normal evaluation, the derivative, and whether the evaluation completed as normal (or encountered a nan or inf).
source
DynamicExpressions.EvaluateEquationDerivativeModule.eval_grad_tree_arrayMethod
eval_grad_tree_array(tree::Node{T}, cX::AbstractMatrix{T}, operators::OperatorEnum; variable::Bool=false, turbo::Bool=false)

Compute the forward-mode derivative of an expression, using a similar structure and optimization to evaltreearray. variable specifies whether we should take derivatives with respect to features (i.e., cX), or with respect to every constant in the expression.

Arguments

  • tree::Node{T}: The expression tree to evaluate.
  • cX::AbstractMatrix{T}: The data matrix, with each column being a data point.
  • operators::OperatorEnum: The operators used to create the tree. Note that operators.enable_autodiff must be true. This is needed to create the derivative operations.
  • variable::Bool: Whether to take derivatives with respect to features (i.e., cX - with variable=true), or with respect to every constant in the expression (variable=false).
  • turbo::Bool: Use LoopVectorization.@turbo for faster evaluation.

Returns

  • (evaluation, gradient, complete)::Tuple{AbstractVector{T}, AbstractMatrix{T}, Bool}: the normal evaluation, the gradient, and whether the evaluation completed as normal (or encountered a nan or inf).
source

You can compute gradients this with shorthand notation as well (which by default computes gradients with respect to input matrix, rather than constants).

    (tree::Node{T})'(X::AbstractMatrix{T}, operators::OperatorEnum; turbo::Bool=false, variable::Bool=true)

Compute the forward-mode derivative of an expression, using a similar
structure and optimization to eval_tree_array. `variable` specifies whether
we should take derivatives with respect to features (i.e., X), or with respect
to every constant in the expression.

# Arguments
- `X::AbstractMatrix{T}`: The data matrix, with each column being a data point.
- `operators::OperatorEnum`: The operators used to create the `tree`. Note that `operators.enable_autodiff`
    must be `true`. This is needed to create the derivative operations.
- `variable::Bool`: Whether to take derivatives with respect to features (i.e., `X` - with `variable=true`),
    or with respect to every constant in the expression (`variable=false`).
- `turbo::Bool`: Use `LoopVectorization.@turbo` for faster evaluation.

# Returns

- `(evaluation, gradient, complete)::Tuple{AbstractVector{T}, AbstractMatrix{T}, Bool}`: the normal evaluation,
    the gradient, and whether the evaluation completed as normal (or encountered a nan or inf).

Alternatively, you can compute higher-order derivatives by using ForwardDiff on the function differentiable_eval_tree_array, although this will be slower.

Printing

You can also print a tree as follows:

DynamicExpressions.EquationModule.string_treeMethod
string_tree(tree::Node, operators::AbstractOperatorEnum[; bracketed, variable_names, f_variable, f_constant])

Convert an equation to a string.

Arguments

  • tree: the tree to convert to a string
  • operators: the operators used to define the tree

Keyword Arguments

  • bracketed: (optional) whether to put brackets around the outside.
  • f_variable: (optional) function to convert a variable to a string, of the form (feature::Int, variable_names).
  • f_constant: (optional) function to convert a constant to a string, of the form (val, bracketed::Bool)
  • variable_names::Union{Array{String, 1}, Nothing}=nothing: (optional) what variables to print for each feature.
source

When you define an OperatorEnum, the standard show and print methods will be overwritten to use string_tree.