`Symbolics.value` behavior change

A preface to this question. I use Symbolics.jl purely for study purposes and this is not an urgent question that impacts my work. Nor am I in anyway an expert in either mathematics or Julia.

So a few weeks ago as parts of my studies I implemented a Symbolics function to compute the area of an n-gon and used Symbolics.value to extract the numeric value after doing a substitution:

using Symbolics
@variables N, b
area_formula = (1/4) * N * b^2 * cot(π/N)
area_test_simplified = simplify(substitute(area_formula, Dict(N=>3600, b=>1)))
area_numerical_value = Symbolics.value(area_test_simplified)
println("Area: ", round(area_numerical_value, digits = 6))

As noted as this code has nothing to do with production, I constantly update the repository as well as my version of Julia. So I am pretty sure that when I last ran this code and it worked it was with version 1.11.x. Recently I wanted to make use of a similar formula and tried the same approach and it failed. And when I reran this code it also failed with the following error:

RROR: MethodError: no method matching round(::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymReal}, ::RoundingMode{:Nearest}; digits::Int64)
The function `round` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  round(::BigFloat, ::RoundingMode{:Nearest}) got unsupported keyword argument "digits"
   @ Base mpfr.jl:1123
  round(::Missing, ::RoundingMode; sigdigits, digits, base)
   @ Base missing.jl:144
  round(::Type{T}, ::Any, ::RoundingMode) where T>:Missing got unsupported keyword argument "digits"
   @ Base missing.jl:148
  ...

Stacktrace:
 [1] round(x::SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymReal}; kws::@Kwargs{digits::Int64})
   @ Base ./rounding.jl:472
 [2] top-level scope
   @ REPL[8]:1

Looking at the results it became obvious why it failed. The variable value is 900.0cot(0.0008726646259971648) i.e. the cotangent isn’t being calculated. I read the documentation carefully and it is not clear to me what changed since I upgraded to Julia 1.12.x and the latest version of Symbolics.

I did find a workaround for my purposes by using build_function and evaluating the function (which is fine, since I want to iterate on this function over multiple values). However, I am curious why

  1. the substitute no longer executes the cotangent and
  2. How I can make that happen so that I can extract the final numeric value

Digging deeper into SymbolicsUtil.jl documentation I found the answer. I needed to add the flag fold=Val(true) to the substitute And PS I think the simplify is unnecessary:

area_test_simplified = substitute(area_formula, Dict(N=>3600, b=>1); fold=Val(true))
area_numerical_value = Symbolics.value(area_test_simplified)
println("Area: ", round(area_numerical_value, digits = 6))
Area: 1.031323769436e6

The only thing still not clear to me is whether this flag is a recent requirement, and if not, why did it work in the past.

There was a recent breaking release, SymbolicUtils v4 and Symbolics v7, which changed the internal representation and this was one of the changes that reflects from that. All operations are type-stable, and thus they return a symbolic value. There is a symbolic Const type in the sumtype now that holds it. Folding is thus not a default because it’s cannot be made type-stable, so the standard simplification does not do numerical evaluation without the user explicitly asking for it. And I kind of think that makes sense because symbolic manipulation and running an interpreter for numerical calculations are kind of two different things, so it was kind of weird that we always merged those together (and then paid an extremely high startup time price because every simplify call was deeply not type-stable).