Types

Operator Enum

All equations are represented as a tree of operators. Each node in this tree specifies its operator with an integer - which indexes an enum of operators. This enum is defined as follows:

Construct this operator specification as follows:

DynamicExpressions.OperatorEnumModule.OperatorEnumMethod
OperatorEnum(; binary_operators=[], unary_operators=[],
               define_helper_functions::Bool=true,
               empty_old_operators::Bool=true)

Construct an OperatorEnum object, defining the possible expressions. This will also redefine operators for AbstractExpressionNode types, as well as show, print, and (::AbstractExpressionNode)(X). It will automatically compute derivatives with Zygote.jl.

Arguments

  • binary_operators::Vector{Function}: A vector of functions, each of which is a binary operator.
  • unary_operators::Vector{Function}: A vector of functions, each of which is a unary operator.
  • define_helper_functions::Bool=true: Whether to define helper functions for creating and evaluating node types. Turn this off when doing precompilation. Note that these are not needed for the package to work; they are purely for convenience.
  • empty_old_operators::Bool=true: Whether to clear the old operators.

This is just for scalar operators. However, you can use the following for more general operators:

DynamicExpressions.OperatorEnumModule.GenericOperatorEnumMethod
GenericOperatorEnum(; binary_operators=[], unary_operators=[],
                      define_helper_functions::Bool=true, empty_old_operators::Bool=true)

Construct a GenericOperatorEnum object, defining possible expressions. Unlike OperatorEnum, this enum one will work arbitrary operators and data types. This will also redefine operators for AbstractExpressionNode types, as well as show, print, and (::AbstractExpressionNode)(X).

Arguments

  • binary_operators::Vector{Function}: A vector of functions, each of which is a binary operator.
  • unary_operators::Vector{Function}: A vector of functions, each of which is a unary operator.
  • define_helper_functions::Bool=true: Whether to define helper functions for creating and evaluating node types. Turn this off when doing precompilation. Note that these are not needed for the package to work; they are purely for convenience.
  • empty_old_operators::Bool=true: Whether to clear the old operators.

By default, these operators will define helper functions for constructing trees, so that you can write Node(;feature=1) + Node(;feature=2) instead of Node(1, Node(;feature=1), Node(;feature=2)) (assuming + is the first operator). You can turn this off with define_helper_functions=false.

For other operators not found in Base, including user-defined functions, you may use the @extend_operators macro:

DynamicExpressions.OperatorEnumConstructionModule.@extend_operatorsMacro
@extend_operators operators [kws...]

Extends all operators defined in this operator enum to work on the Node type. While by default this is already done for operators defined in Base when you create an enum and pass define_helper_functions=true, this does not apply to the user-defined operators. Thus, to do so, you must apply this macro to the operator enum in the same module you have the operators defined.

This will extend the operators you have passed to work with Node types, so that it is easier to construct expression trees.

Note that you are free to use the Node constructors directly. This is a more robust approach, and should be used when creating libraries which use DynamicExpressions.jl.

Nodes

Equations are specified as binary trees with the Node type, defined as follows:

DynamicExpressions.NodeModule.NodeType
Node{T} <: AbstractExpressionNode{T}

Node defines a symbolic expression stored in a binary tree. A single Node instance is one "node" of this tree, and has references to its children. By tracing through the children nodes, you can evaluate or print a given expression.

Fields

  • degree::UInt8: Degree of the node. 0 for constants, 1 for unary operators, 2 for binary operators.
  • constant::Bool: Whether the node is a constant.
  • val::T: Value of the node. If degree==0, and constant==true, this is the value of the constant. It has a type specified by the overall type of the Node (e.g., Float64).
  • feature::UInt16: Index of the feature to use in the case of a feature node. Only used if degree==0 and constant==false. Only defined if degree == 0 && constant == false.
  • op::UInt8: If degree==1, this is the index of the operator in operators.unaops. If degree==2, this is the index of the operator in operators.binops. In other words, this is an enum of the operators, and is dependent on the specific OperatorEnum object. Only defined if degree >= 1
  • l::Node{T}: Left child of the node. Only defined if degree >= 1. Same type as the parent node.
  • r::Node{T}: Right child of the node. Only defined if degree == 2. Same type as the parent node. This is to be passed as the right argument to the binary operator.

Constructors

Node([T]; val=nothing, feature=nothing, op=nothing, l=nothing, r=nothing, children=nothing, allocator=default_allocator)
Node{T}(; val=nothing, feature=nothing, op=nothing, l=nothing, r=nothing, children=nothing, allocator=default_allocator)

Create a new node in an expression tree. If T is not specified in either the type or the first argument, it will be inferred from the value of val passed or l and/or r. If it cannot be inferred from these, it will default to Float32.

The children keyword can be used instead of l and r and should be a tuple of children. This is to permit the use of splatting in constructors.

You may also construct nodes via the convenience operators generated by creating an OperatorEnum.

You may also choose to specify a default memory allocator for the node other than simply Node{T}() in the allocator keyword argument.

When you create an Options object, the operators passed are also re-defined for Node types. This allows you use, e.g., t=Node(; feature=1) * 3f0 to create a tree, so long as * was specified as a binary operator.

When using these node constructors, types will automatically be promoted. You can convert the type of a node using convert:

Base.convertMethod
convert(::Type{<:AbstractExpressionNode{T1}}, n::AbstractExpressionNode{T2}) where {T1,T2}

Convert a AbstractExpressionNode{T2} to a AbstractExpressionNode{T1}. This will recursively convert all children nodes to AbstractExpressionNode{T1}, using convert(T1, tree.val) at constant nodes.

Arguments

  • ::Type{AbstractExpressionNode{T1}}: Type to convert to.
  • tree::AbstractExpressionNode{T2}: AbstractExpressionNode to convert.

You can set a tree (in-place) with set_node!:

DynamicExpressions.NodeModule.set_node!Function
set_node!(tree::AbstractExpressionNode{T}, new_tree::AbstractExpressionNode{T}) where {T}

Set every field of tree equal to the corresponding field of new_tree.

You can create a copy of a node with copy_node:

DynamicExpressions.NodeModule.copy_nodeFunction
copy_node(tree::AbstractExpressionNode; break_sharing::Val=Val(false))

Copy a node, recursively copying all children nodes. This is more efficient than the built-in copy.

If break_sharing is set to Val(true), sharing in a tree will be ignored.

Graph Nodes

You can describe an equation as a graph rather than a tree by using the GraphNode type:

DynamicExpressions.NodeModule.GraphNodeType
GraphNode{T} <: AbstractExpressionNode{T}

Exactly the same as Node{T}, but with the assumption that some nodes will be shared. All copies of this graph-like structure will be performed with this assumption, to preserve structure of the graph.

Examples

julia> operators = OperatorEnum(;
           binary_operators=[+, -, *], unary_operators=[cos, sin]
       );

julia> x = GraphNode(feature=1)
x1

julia> y = sin(x) + x
sin(x1) + {x1}

julia> cos(y) * y
cos(sin(x1) + {x1}) * {(sin(x1) + {x1})}

Note how the {} indicates a node is shared, and this is the same node as seen earlier in the string.

This has the same constructors as Node{T}. Shared nodes are created simply by using the same node in multiple places when constructing or setting properties.

This makes it so you can have multiple parents for a given node, and share parts of an expression. For example:

julia> operators = OperatorEnum(;
           binary_operators=[+, -, *], unary_operators=[cos, sin, exp]
       );

julia> x1, x2 = GraphNode(feature=1), GraphNode(feature=2)
(x1, x2)

julia> y = sin(x1) + 1.5
sin(x1) + 1.5

julia> z = exp(y) + y
exp(sin(x1) + 1.5) + {(sin(x1) + 1.5)}

Here, the curly braces {} indicate that the node is shared by another (or more) parent node.

This means that we only need to change it once to have changes propagate across the expression:

julia> y.r.val *= 0.9
1.35

julia> z
exp(sin(x1) + 1.35) + {(sin(x1) + 1.35)}

This also means there are fewer nodes to describe an expression:

julia> length(z)
6

julia> length(convert(Node, z))
10

where we have converted the GraphNode to a Node type, which breaks shared connections into separate nodes.

Abstract Types

Both the Node and GraphNode types are subtypes of the abstract type:

DynamicExpressions.NodeModule.AbstractExpressionNodeType
AbstractExpressionNode{T} <: AbstractNode

Abstract type for nodes that represent an expression. Along with the fields required for AbstractNode, this additionally must have fields for:

  • constant::Bool: Whether the node is a constant.
  • val::T: Value of the node. If degree==0, and constant==true, this is the value of the constant. It has a type specified by the overall type of the Node (e.g., Float64).
  • feature::UInt16: Index of the feature to use in the case of a feature node. Only used if degree==0 and constant==false. Only defined if degree == 0 && constant == false.
  • op::UInt8: If degree==1, this is the index of the operator in operators.unaops. If degree==2, this is the index of the operator in operators.binops. In other words, this is an enum of the operators, and is dependent on the specific OperatorEnum object. Only defined if degree >= 1

Interface

See NodeInterface for a full description of the interface implementation, as well as tests to verify correctness.

You must define CustomNode{_T} where {_T} = new{_T}() for each custom node type.

In addition, you may choose to define the following functions, to override the defaults behavior, in particular if you wish to add additional fields to your type.

  • leaf_copy and branch_copy
  • leaf_equal and branch_equal
  • leaf_hash and branch_hash
  • preserve_sharing

You likely do not need to, but you could choose to override the following:

  • constructorof
  • with_type_parameters

which can be used to create additional expression-like types. The supertype of this abstract type is the AbstractNode type, which is more generic but does not have all of the same methods:

DynamicExpressions.NodeModule.AbstractNodeType
AbstractNode

Abstract type for binary trees. Must have the following fields:

  • degree::Integer: Degree of the node. Either 0, 1, or 2. If 1, then l needs to be defined as the left child. If 2, then r also needs to be defined as the right child.
  • l::AbstractNode: Left child of the current node. Should only be defined if degree >= 1; otherwise, leave it undefined (see the the constructors of Node{T} for an example). Don't use nothing to represent an undefined value as it will incur a large performance penalty.
  • r::AbstractNode: Right child of the current node. Should only be defined if degree == 2.

Expressions

A higher-level user-facing type is the Expression:

DynamicExpressions.ExpressionModule.ExpressionType
Expression{T, N, D} <: AbstractExpression{T, N}

(Experimental) Defines a high-level, user-facing, expression type that encapsulates an expression tree (like Node) along with associated metadata for evaluation and rendering.

Fields

  • tree::N: The root node of the raw expression tree.
  • metadata::Metadata{D}: A named tuple of settings for the expression, such as the operators and variable names.

Constructors

  • Expression(tree::AbstractExpressionNode, metadata::NamedTuple): Construct from the fields
  • @parse_expression(expr, operators=operators, variable_names=variable_names, node_type=Node): Parse a Julia expression with a given context and create an Expression object.

Usage

This type is intended for end-users to interact with and manipulate expressions at a high level, abstracting away the complexities of the underlying expression tree operations.

This is a subtype of AbstractExpression.

DynamicExpressions.ExpressionModule.AbstractExpressionType
AbstractExpression{T,N}

(Experimental) Abstract type for user-facing expression types, which contain both the raw expression tree operating on a value type of T, as well as associated metadata to evaluate and render the expression.

See ExpressionInterface for a full description of the interface implementation, as well as tests to verify correctness.

If you wish to use @parse_expression, you can also customize the parsing behavior with

  • parse_leaf

which can be used for defining custom types, such as the ParametricExpression:

Interfaces

The interfaces for AbstractExpression and AbstractExpressionNode are tested using Interfaces.jl. You can see the interfaces with:

DynamicExpressions.InterfacesModule.ExpressionInterfaceType
    ExpressionInterface

An Interfaces.jl Interface with mandatory components (:get_contents, :get_metadata, :get_tree, :get_operators, :get_variable_names, :copy, :with_contents, :with_metadata) and optional components (:count_nodes, :count_constants, :count_depth, :index_constants, :has_operators, :has_constants, :get_constants, :set_constants!, :string_tree, :default_node_type, :constructorof, :tree_mapreduce).

Defines the interface of AbstractExpression for user-facing expression types, which can store operators, extra parameters, functional forms, variable names, etc.

DynamicExpressions.InterfacesModule.NodeInterfaceType
    NodeInterface

An Interfaces.jl Interface with mandatory components (:create_node, :copy, :hash, :any, :equality, :preserve_sharing, :constructorof, :eltype, :with_type_parameters, :default_allocator, :set_node!, :count_nodes, :tree_mapreduce) and optional components (:leaf_copy, :leaf_hash, :leaf_equal, :branch_copy, :branch_hash, :branch_equal, :count_depth, :is_node_constant, :count_constants, :filter_map, :has_constants, :get_constants, :set_constants!, :index_constants, :has_operators).

Defines the interface for AbstractExpressionNode which can include various operations such as copying, hashing, and checking equality, as well as tree-specific operations like map-reduce and node manipulation.

You can declare a new type as implementing these with, e.g.,

using DynamicExpressions: ExpressionInterface, all_ei_methods_except
using Interface: @implements, Arguments, Interface

# Add all optional methods:
valid_optional_methods = all_ei_methods_except(())

@implements ExpressionInterface{valid_optional_methods} MyCustomExpression [Arguments()]

You can then test the interface is implemented correctly using, for example,

@test Interface.test(ExpressionInterface, MyCustomExpression, [ex::MyCustomExpression])

Note that this may not flag all potential issues, so be sure to still read the details about what methods can be implemented and customized.