Function Registration and Tracing
Direct Tracing
Because Symbolics expressions respect Julia semantics, one way to generate symbolic expressions is to simply place Symbolics variables as inputs into existing Julia code. For example, the following uses the standard Julia function for the Lorenz equations to generate the symbolic expression for the Lorenz equations:
using Symbolics
function lorenz(du,u,p,t)
du[1] = 10.0(u[2]-u[1])
du[2] = u[1]*(28.0-u[3]) - u[2]
du[3] = u[1]*u[2] - (8/3)*u[3]
end
@variables t p[1:3] u(t)[1:3]
du = Array{Any}(undef, 3)
lorenz(du,u,p,t)
du
3-element Vector{Any}:
10.0(-(u(t))[1] + (u(t))[2])
-(u(t))[2] + (u(t))[1]*(28.0 - (u(t))[3])
-2.6666666666666665(u(t))[3] + (u(t))[1]*(u(t))[2]
Or similarly:
@variables t x(t) y(t) z(t) dx(t) dy(t) dz(t) σ ρ β
du = [dx,dy,dz]
u = [x,y,z]
p = [σ,ρ,β]
lorenz(du,u,p,t)
du
\[ \begin{equation} \left[ \begin{array}{c} 10 \left( - x\left( t \right) + y\left( t \right) \right) \\ - y\left( t \right) + x\left( t \right) \left( 28 - z\left( t \right) \right) \\ - 2.6667 z\left( t \right) + x\left( t \right) y\left( t \right) \\ \end{array} \right] \end{equation} \]
Registering Functions
The Symbolics graph only allows registered Julia functions within its type. All other functions are automatically traced down to registered functions. By default, Symbolics.jl pre-registers the common functions utilized in SymbolicUtils.jl and pre-defines their derivatives. However, the user can utilize the @register_symbolic
macro to add their function to allowed functions of the computation graph.
Additionally, @register_array_symbolic
can be used to define array functions. For size propagation it's required that a computation of how the sizes are computed is also supplied.
Defining Derivatives of Registered Functions
In order for symbolic differentiation to work, an overload of Symbolics.derivative
is required. The syntax is derivative(typeof(f), args::NTuple{i,Any}, ::Val{j})
where i
is the number of arguments to the function and j
is which argument is being differentiated. So for example:
function derivative(::typeof(min), args::NTuple{2,Any}, ::Val{1})
x, y = args
IfElse.ifelse(x < y, one(x), zero(x))
end
is the partial derivative of the Julia min(x,y)
function with respect to x
.
Downstream symbolic derivative functionality only work if every partial derivative that is required in the derivative expression is defined. Note that you only need to define the partial derivatives which are actually computed.
Registration API
Symbolics.@register_symbolic
— Macro@register_symbolic(expr, define_promotion = true, Ts = [Real])
Overload appropriate methods so that Symbolics can stop tracing into the registered function. If define_promotion
is true, then a promotion method in the form of
SymbolicUtils.promote_symtype(::typeof(f_registered), args...) = Real # or the annotated return type
is defined for the register function. Note that when defining multiple register overloads for one function, all the rest of the registers must set define_promotion
to false
except for the first one, to avoid method overwriting.
Examples
@register_symbolic foo(x, y)
@register_symbolic foo(x, y::Bool) false # do not overload a duplicate promotion rule
@register_symbolic goo(x, y::Int) # `y` is not overloaded to take symbolic objects
@register_symbolic hoo(x, y)::Int # `hoo` returns `Int`
See @register_array_symbolic
to register functions which return arrays.
Symbolics.@register_array_symbolic
— Macro@register_array_symbolic(expr)
Example:
# Let's say vandermonde takes an n-vector and returns an n x n matrix
@register_array_symbolic vandermonde(x::AbstractVector) begin
size=(length(x), length(x))
eltype=eltype(x) # optional, will default to the promoted eltypes of x
end
You can also register calls on callable structs:
@register_array_symbolic (c::Conv)(x::AbstractMatrix) begin
size=size(x) .- size(c.kernel) .+ 1
eltype=promote_type(eltype(x), eltype(c))
end