Understanding display of equations and computation with Symbolics.jl

I got from the documentation that you can use Symbolics.jl and Latexify.jl together to display your equations.

However, what can be evaluated gets evaluated right away so in that sense not the same as sympy or Sympy.jl [1][2].

For instance,

using Symbolics
using Latexify

@variables r
v = 4 / 3 * π * r ^ 3
D = Differential(r)
a = expand_derivatives(D(v))
latexify(a)

returns

L"\begin{equation}
12.566 r^{2}
\end{equation}
"

A workaround (launching a new instance of Julia):

using Symbolics
using Latexify

@variables r, π
v = 4 // 3 * π * r ^ 3
D = Differential(r)
a = expand_derivatives(D(v))
latexify(a)

assuming that π has not already been associated with Base.pi through a prior calculation (hence requiring the launch of a new Julia instance; otherwise maybe enclose in a let...end block).

This returns

L"\begin{equation}
4 r^{2} \pi
\end{equation}
"

To use this for computation,

a_expr = (x) -> substitute(a, Dict(π => Symbolics.pi, r => x))
a_expr(2)

returns 50.26548245743669. In this case, care must be taken that the argument to the anonymous function cannot be r as it is now considered a symbol in this namespace (which I found out by accident - that r would remain a symbol within the scope of the anonymous function was expected behavior).

Alternatively, I can also convert this to a function. If I understand correctly, π will evaluate to Symbolics.pi (which is the same as Base.pi) even without my substitution.

a_fn = build_function(a, r, expression=Val{false})
a_fn(2)

returns the same value as above.

Am I understanding the use cases correctly - or is there a more direct way to use the same notation for defining symbolic expressions for displaying and calculations?

Is there a better way to contain the scope of symbolic variables? Should they always be used within a let...end block?

I am not entirely sure what the questions are here? I haven’t answered because I’m trying to parse what you’re saying and a lot of it doesn’t make sense, so I’m just going to post what I can answer and see if that helps.

If you need them to stay scoped, then yes do that.

@variables x, y creates lines of code like x = Symbolics.variable(...). I’m not sure how else it could work. It has to create a Julia variable. There’s nothing different about them from a normal Julia variable. It just has dispatches for mathematical operations and display overrides for Latex.

That’s just Julia’s standard behavior with shadowing base variables. I just wouldn’t shadow variables exported from Base.

1 Like

Thanks @ChrisRackauckas,

I just wanted to check if my understanding was correct so thanks for confirming - that’s exactly what I was looking for.

I guess the one surprising behavior that remains is that the behavior of the function differs whether r or something else (like x) is used as the name of the argument of the anonymous function.

a_expr = let 
    @variables r, π
    v = 4 // 3 * π * r ^ 3
    D = Differential(r)
    a = expand_derivatives(D(v))
    r -> substitute(a, Dict(π => Symbolics.pi, r => r))
end

The type for r as a symbolic variable appears to carry into the argument of the anonymous function. I thought when the function creates its own scope, that r would be allowed to take on any type within that scope but it’s not the case.