Releases: SymbolicML/DynamicExpressions.jl
v2.4.0
DynamicExpressions v2.4.0
Merged pull requests:
- feat: get
parse_expressionsto work with aliases (#139) (@MilesCranmer)
v2.3.0
DynamicExpressions v2.3.0
Merged pull requests:
- Parse expressions without interpolation (#137) (@MilesCranmer)
v2.2.1
v2.2.0
DynamicExpressions v2.2.0
Merged pull requests:
- feat: option to skip fused kernels (#128) (@MilesCranmer)
v2.1.0
DynamicExpressions v2.1.0
Merged pull requests:
- fix:
default_node_typeshould not prescribe degree (#133) (@MilesCranmer) - test: fix issue where test eval fails due to incomplete eval (#134) (@MilesCranmer)
v2.0.1
DynamicExpressions v2.0.1
Merged pull requests:
- Permit n-argument operators (#127) (@MilesCranmer)
- fix: recursive type inference issue (#132) (@MilesCranmer)
v2.0.0
DynamicExpressions.jl v2.0 introduces support for n-arity operators (nodes with arbitrary numbers of children), which required some breaking changes to implement. This guide will help you migrate your code from v1.x to v2.0.
Summary
- Types
Node{T}is nowNode{T,D}whereDis the maximum degreeAbstractExpressionNode{T}is nowAbstractExpressionNode{T,D}AbstractNodeis nowAbstractNode{D}- Before,
Node{T}had fieldsl::Node{T}andr::Node{T}. Now, the type isNode{T,D}, and it has the fieldchildren::NTuple{D,Nullable{Node{T,D}}}.
- Accessors
- You can now access children by index with
get_child(tree, i)tree.lcan now be written asget_child(tree, 1)tree.rcan now be written asget_child(tree, 2)- note: you can access multiple children with
get_children(tree, Val(degree))
- You can now set children by index with
set_child!(tree, child, i)tree.l = childshould now be written asset_child!(tree, child, 1)tree.r = childshould now be written asset_child!(tree, child, 2)- note: you can set multiple children with
set_children!(tree, children)
- You can now access children by index with
- Constructors
Node{T}(; op=1, l=x)should now be written asNode{T}(; op=1, children=(x,))Node{T}(; op=1, l=x, r=y)should now be written asNode{T}(; op=1, children=(x, y))- You may now use
Node{T,D}(; op=1, children=(x,))to specify degree other than the default of 2.
- OperatorEnum
OperatorEnum(andGenericOperatorEnum) now uses a singleopsfield: this is a tuple of tuples, indexed by arity.operators.unaopsis now written asoperators.ops[1], andoperators.binopsis now written asoperators.ops[2].
OperatorEnum(binary_operators=(+, -, *), unary_operators=(sin, cos))can now be written asOperatorEnum(2 => (+, -, *), 1 => (sin, cos))- This API permits higher-arity operators:
OperatorEnum(1 => (sin, cos), 2 => (+, -, *), 3 => (fma, max)).
- This API permits higher-arity operators:
Breaking Changes
The main breaking change is that Node{T} is now Node{T,D} where D is the maximum degree of any possible node in the tree. node.degree is still the same as before, and is such that node.degree <= D.
Similarly, AbstractExpressionNode{T} is now AbstractExpressionNode{T,D}, and AbstractNode is now AbstractNode{D}.
Before, Node{T} had fields l::Node{T} and r::Node{T}. Now, it has a single combined field children::NTuple{D,Nullable{Node{T,D}}}. This is a tuple of wrapped node objects, which should be accessed with get_child(tree, i) and set with set_child!(tree, child, i). However, the old getters and setters will still function for binary trees (.l and .r).
You may now use Node{T,D}(; op=1, children=(x,)) to specify degree other than the default of 2. However, the default Node{T}(; op=1, children=(x,)) is still available and will result in type Node{T,2}.
Necessary Changes to Your Code
The main breaking change that requires some modifications is patterns that explicitly match tree.degree in conditional logic. The tree.degree == 0 branch can be left alone, but higher arity nodes should be generalized.
For code like this:
# This pattern ONLY works for binary trees (degree ≤ 2)
if tree.degree == 0
# leaf node
elseif tree.degree == 1
# unary operator
else # tree.degree == 2 # <-- This violates the assumption in 2.0
# binary operator
endYou have two options for upgrading
-
Constrain your type signatures: Use
::AbstractExpressionNode{T,2}to only accept binary trees, and refuse higher-arity nodesfunction my_function(tree::AbstractExpressionNode{T,2}) where T if tree.degree == 0 # leaf elseif tree.degree == 1 # unary else # tree.degree == 2, guaranteed # binary end end
-
Rewrite your code to be more generic. (Note that for recursive algorithms, you can often do things with a
tree_mapreduce, which already handles the general case.)# 2: Handle arbitrary arity function my_function(tree::AbstractExpressionNode{T}) where T if tree.degree == 0 # leaf else # higher arity deg = tree.degree for i in 1:deg child = get_child(tree, i) # process child... end end end
However, normally what is done internally for max efficiency for the general approach is to use patterns like:
@generated function my_function(tree::AbstractExpressionNode{T,D}) where {T,D} quote deg = tree.degree deg == 0 && process_leaf(tree) Base.Cartesian.@nif( $deg, i -> i == deg, i -> let children = get_children(tree, Val(i)) # Now, `children` is a type-stable tuple of children end, ) end end
Note that the
@generatedis needed to passDto the Cartesian macro.
Property Access (Non-Breaking)
Note: .l and .r property access still work and will continue to be supported on types with D == 2. However, the generic accessors are more flexible, so upgrading to them is recommended.
# old_child = tree.l
old_child = get_child(tree, 1)
# tree.r = new_child
set_child!(tree, new_child, 2)This lets you write code that prescribes arbitrary arity.
Node Construction (Non-Breaking)
For binary trees, you can still use the syntax:
x = Node{Float64}(; feature=1)
tree = Node{Float64}(; op=1, children=(x,))For higher-arity trees, you may pass D to specify the maximum degree in the tree:
x = Node{Float64,3}(; feature=1)
y = Node{Float64,3}(; feature=2)
z = Node{Float64,3}(; feature=3)
tree = Node{Float64,3}(; op=1, children=(x, y, z))OperatorEnum redesign (Non-Breaking)
OperatorEnum (and GenericOperatorEnum) now uses a single ops field: this is a tuple of tuples, indexed by arity. operators.unaops is now written as operators.ops[1], and operators.binops is now written as operators.ops[2].
However, the properties are aliased, so the old syntax will still work.
Along with this, there is a new API for constructing OperatorEnums:
# operators = OperatorEnum(binary_operators=(+, -, *), unary_operators=(sin, cos)) # old
operators = OperatorEnum(2 => (+, -, *), 1 => (sin, cos))This API permits higher-arity operators:
operators = OperatorEnum(1 => (sin, cos), 2 => (+, -, *), 3 => (fma, max))(Note that the order you pass the pairs is not important.)
Full Changelog: v1.10.3...v2.0.0
v1.10.3
DynamicExpressions v1.10.3
Merged pull requests:
- fix: mutation error for Zygote (#130) (@MilesCranmer)
- fix: move non_differentiable to special module for JET masking (#131) (@MilesCranmer)
v1.10.2
DynamicExpressions v1.10.2
Merged pull requests:
- CompatHelper: bump compat for Zygote in [weakdeps] to 0.7, (keep existing compat) (#119) (@github-actions[bot])
v1.10.1
DynamicExpressions v1.10.1
Merged pull requests:
- fix: avoid returning view for generic operators (#126) (@MilesCranmer)