API
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:
DynamicExpressions.OperatorEnumModule.OperatorEnum
— TypeOperatorEnum
Defines an enum over operators, along with their derivatives.
Fields
binops
: A tuple of binary operators. Scalar input type.unaops
: A tuple of unary operators. Scalar input type.
Construct this operator specification as follows:
DynamicExpressions.OperatorEnumModule.OperatorEnum
— MethodOperatorEnum(; 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.GenericOperatorEnum
— MethodGenericOperatorEnum(; 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_operators
— Macro@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.Node
— TypeNode{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. Ifdegree==0
, andconstant==true
, this is the value of the constant. It has a type specified by the overall type of theNode
(e.g.,Float64
).feature::UInt16
: Index of the feature to use in the case of a feature node. Only used ifdegree==0
andconstant==false
. Only defined ifdegree == 0 && constant == false
.op::UInt8
: Ifdegree==1
, this is the index of the operator inoperators.unaops
. Ifdegree==2
, this is the index of the operator inoperators.binops
. In other words, this is an enum of the operators, and is dependent on the specificOperatorEnum
object. Only defined ifdegree >= 1
l::Node{T}
: Left child of the node. Only defined ifdegree >= 1
. Same type as the parent node.r::Node{T}
: Right child of the node. Only defined ifdegree == 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.convert
— Methodconvert(::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!
— Functionset_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_node
— Functioncopy_node(tree::AbstractExpressionNode; break_sharing::Val{BS}=Val(false)) where {BS}
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.GraphNode
— TypeGraphNode{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.AbstractExpressionNode
— TypeAbstractExpressionNode{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. Ifdegree==0
, andconstant==true
, this is the value of the constant. It has a type specified by the overall type of theNode
(e.g.,Float64
).feature::UInt16
: Index of the feature to use in the case of a feature node. Only used ifdegree==0
andconstant==false
. Only defined ifdegree == 0 && constant == false
.op::UInt8
: Ifdegree==1
, this is the index of the operator inoperators.unaops
. Ifdegree==2
, this is the index of the operator inoperators.binops
. In other words, this is an enum of the operators, and is dependent on the specificOperatorEnum
object. Only defined ifdegree >= 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, as well as constructorof
and with_type_parameters
.
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
andbranch_copy
leaf_convert
andbranch_convert
leaf_equal
andbranch_equal
leaf_hash
andbranch_hash
preserve_sharing
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.AbstractNode
— TypeAbstractNode
Abstract type for binary trees. Must have the following fields:
degree::Integer
: Degree of the node. Either 0, 1, or 2. If 1, thenl
needs to be defined as the left child. If 2, thenr
also needs to be defined as the right child.l::AbstractNode
: Left child of the current node. Should only be defined ifdegree >= 1
; otherwise, leave it undefined (see the the constructors ofNode{T}
for an example). Don't usenothing
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 ifdegree == 2
.
Expressions
A higher-level user-facing type is the Expression
:
DynamicExpressions.ExpressionModule.Expression
— TypeExpression{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.AbstractExpression
— TypeAbstractExpression{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
:
DynamicExpressions.ParametricExpressionModule.ParametricExpression
— TypeParametricExpression{T,N<:ParametricNode{T},D<:NamedTuple} <: AbstractExpression{T,N}
(Experimental) An expression to store parameters for a tree
DynamicExpressions.ParametricExpressionModule.ParametricNode
— TypeA type of expression node that also stores a parameter index
Another example is the StructuredExpression
type, for defining rigid predefined operations in an expression tree:
DynamicExpressions.StructuredExpressionModule.StructuredExpression
— TypeStructuredExpression{T,F,N,E,TS,D} <: AbstractStructuredExpression{T,F,N,E,D} <: AbstractExpression{T,N}
This expression type allows you to combine multiple expressions together in a predefined way.
Parameters
T
: The numeric value type of the expressions.F
: The type of the structure function, which combines each expression into a single expression.N
: The type of the nodes inside expressions.E
: The type of the expressions.TS
: The type of the named tuple containing those inner expressions.D
: The type of the metadata, another named tuple.
Usage
For example, we can create two expressions, f
, and g
, and then combine them together in a new expression, f_plus_g
, using a constructor function that simply adds them together:
kws = (;
binary_operators=[+, -, *, /],
unary_operators=[-, cos, exp],
variable_names=["x", "y"],
)
f = parse_expression(:(x * x - cos(2.5f0 * y + -0.5f0)); kws...)
g = parse_expression(:(exp(-(y * y))); kws...)
f_plus_g = StructuredExpression((; f, g); structure=nt -> nt.f + nt.g)
Now, when evaluating f_plus_g
, this expression type will return the result of adding together the results of f
and g
.
You can dispatch on a particular structured expression with the second type parameter, F
, which is the function defined above:
my_factory(nt) = nt.f + nt.g
Base.show(io::IO, e::StructuredExpression{T,typeof(my_factory)}) where {T} = ...
which will create a new method particular to this expression type defined on that function.
You may use operators directly on AbstractExpression
objects to create a new object containing the combined expression tree, so long as those objects have identical operators in their metadata.
You can extract and set contents and metadata with a few utility functions, including:
DynamicExpressions.ExpressionModule.get_contents
— Functionget_contents(ex::AbstractExpression)
Get the contents of the expression, which might be a plain AbstractExpressionNode
, or some combination of them, or other data. This should include everything other than that returned by get_metadata
.
DynamicExpressions.ExpressionModule.with_contents
— Functionwith_contents(ex::AbstractExpression, tree::AbstractExpressionNode)
with_contents(ex::AbstractExpression, tree::AbstractExpression)
Create a new expression based on ex
but with a different tree
DynamicExpressions.ExpressionModule.get_metadata
— Functionget_metadata(ex::AbstractExpression)
Get the metadata of the expression, which might be a plain NamedTuple
, or some combination of them, or other data. This should include everything other than that returned by get_contents
.
DynamicExpressions.ExpressionModule.with_metadata
— Functionwith_metadata(ex::AbstractExpression, metadata)
with_metadata(ex::AbstractExpression; metadata...)
Create a new expression based on ex
but with a different metadata
.
DynamicExpressions.ExpressionModule.get_tree
— Functionget_tree(ex::AbstractExpression)
A method that extracts the expression tree from AbstractExpression
and should return an AbstractExpressionNode
.
To declare a new operator for expressions, you may use:
DynamicExpressions.ExpressionAlgebraModule.@declare_expression_operator
— Macro@declare_expression_operator(op, arity)
Declare an operator function for AbstractExpression
types.
This macro generates a method for the given operator op
that works with AbstractExpression
arguments. The arity
parameter specifies whether the operator is unary (1) or binary (2).
Arguments
op
: The operator to be declared (e.g.,Base.sin
,Base.:+
).arity
: The number of arguments the operator takes (1 for unary, 2 for binary).
Interfaces
The interfaces for AbstractExpression
and AbstractExpressionNode
are tested using Interfaces.jl. You can see the interfaces with:
DynamicExpressions.InterfacesModule.ExpressionInterface
— Type 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_constant_nodes, :count_depth, :index_constant_nodes, :has_operators, :has_constants, :get_scalar_constants, :set_scalar_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.
Extended help
Mandatory keys:
get_contents
: extracts the runtime contents of an expressionget_metadata
: extracts the runtime metadata of an expressionget_tree
: extracts the expression tree fromAbstractExpression
get_operators
: returns the operators used in the expression (or passoperators
explicitly to override)get_variable_names
: returns the variable names used in the expression (or passvariable_names
explicitly to override)copy
: returns a copy of the expressionwith_contents
: returns the expression with different treewith_metadata
: returns the expression with different metadata
Optional keys:
count_nodes
: counts the number of nodes in the expression treecount_constant_nodes
: counts the number of constant nodes in the expression treecount_depth
: calculates the depth of the expression treeindex_constant_nodes
: indexes constants in the expression treehas_operators
: checks if the expression has operatorshas_constants
: checks if the expression has constantsget_scalar_constants
: gets constants from the expression tree, returning a tuple of: (1) a flat vector of the constants, and (2) an reference object that can be used byset_scalar_constants!
to efficiently set them backset_scalar_constants!
: sets constants in the expression tree, given: (1) a flat vector of constants, (2) the expression, and (3) the reference object produced byget_scalar_constants
string_tree
: returns a string representation of the expression treedefault_node_type
: returns the default node type for the expressionconstructorof
: gets the constructor function for a typetree_mapreduce
: applies a function across the tree
DynamicExpressions.InterfacesModule.NodeInterface
— Type 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_convert, :leaf_hash, :leaf_equal, :branch_copy, :branch_convert, :branch_hash, :branch_equal, :count_depth, :is_node_constant, :count_constant_nodes, :filter_map, :has_constants, :get_scalar_constants, :set_scalar_constants!, :index_constant_nodes, :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.
Extended help
Mandatory keys:
create_node
: creates a new instance of the node typecopy
: returns a copy of the treehash
: returns the hash of the treeany
: checks if any element of the tree satisfies a conditionequality
: checks equality of the tree with itself and its copypreserve_sharing
: checks if the node type preserves sharingconstructorof
: gets the constructor function for a node typeeltype
: gets the element type of the nodewith_type_parameters
: applies type parameters to the node typedefault_allocator
: gets the default allocator for the node typeset_node!
: sets the node's valuecount_nodes
: counts the number of nodes in the treetree_mapreduce
: applies a function across the tree
Optional keys:
leaf_copy
: copies a leaf nodeleaf_convert
: converts a leaf nodeleaf_hash
: computes the hash of a leaf nodeleaf_equal
: checks equality of two leaf nodesbranch_copy
: copies a branch nodebranch_convert
: converts a branch nodebranch_hash
: computes the hash of a branch nodebranch_equal
: checks equality of two branch nodescount_depth
: calculates the depth of the treeis_node_constant
: checks if the node is a constantcount_constant_nodes
: counts the number of constant nodesfilter_map
: applies a filter and map function to the treehas_constants
: checks if the tree has constantsget_scalar_constants
: gets constants from the tree, returning a tuple of: (1) a flat vector of the constants, and (2) a reference object that can be used byset_scalar_constants!
to efficiently set them backset_scalar_constants!
: sets constants in the tree, given: (1) a flat vector of constants, (2) the tree, and (3) the reference object produced byget_scalar_constants
index_constant_nodes
: indexes constants in the treehas_operators
: checks if the tree has operators
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.