Symbolics substitute - what has changed?

What has changed with Symbolics substitute? In the past, I could compose two substitutions, but that doesn’t seem to work any more.

Concretely, I consider:

Asn = substitute(As, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals) 


julia> Dict(name_s .=> s_ss)
Dict{SymbolicUtils.BasicSymbolic{Real}, Float64} with 5 entries:
  md_e(t) => 2.00009
  V(t)    => 2.40023
  m(t)    => 2.40023
  md_i(t) => 2.0
  h(t)    => 0.480045


julia> default_vals = ModelingToolkit.defaults(tank)
Dict{Any, Any} with 5 entries:
  h_ς  => 3
  K    => 5
  m(t) => 1.5A*ρ
  A    => 5
  ρ    => 1

It still works to do:

substitute(As, Dict(name_s .=> s_ss))


substitute(As, default_vals)

individually, but I cannot compose the substitutions – I get:

julia> substitute(As, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals)
1×1 Matrix{SymbolicUtils.BasicSymbolic{Real}}:
 (-K) / (2A*h_ς*sqrt(0.48004505680494347 / h_ς)*ρ)

thus the second substitution is not carried out. Similarly:

julia> substitute(As, default_vals) |> x -> substitute(x, Dict(name_s .=> s_ss))
1×1 Matrix{SymbolicUtils.BasicSymbolic{Real}}:
 -1 / (6sqrt((1//3)*h(t)))

and, again, the second substitution is not carried out.

HOWEVER, if I merge the dictionaries, and only carry out one substitute, things seems to work:

julia> substitute(As, merge(default_vals, Dict(name_s .=> s_ss)))
1×1 Matrix{Float64}:

Question 1: What has changed wrt. substitute?

In the past, I also needed to pipe the result through an unwrap function. That doesn’t seem to be the case any more… In other words, in the past, I needed to do:

Asn = substitute(As, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals)  |> x -> Symbolics.unwrap.(x)

to get the proper matrix, while now, both

substitute(As, merge(default_vals, Dict(name_s .=> s_ss)))


substitute(As, merge(default_vals, Dict(name_s .=> s_ss))) |> x -> Symbolics.unwrap.(x)

seems to give the same result.

Question 2: Has the unwrap function been made redundant?

My understanding is nothing has changed here in a long time. Do you have a version where it worked differently? I would suspect something about the pipe syntax there.

Hm. OK, will check. I see from the documentation of piping that it says that piping in combination with anonymous functions requires the anonymous functions to be wrapped in parenthesis. I don’t recall that this was a requirement in the past.

It is not possible changes in the piping operation that causes the change in behavior – so I assume it is either in Symbolics or in ModelingToolkit. The following code used to work some months ago:

mats, tank_ = ModelingToolkit.linearize_symbolic(tank, [md_i], [h])

leading to:


julia> typeof(mats.A)
Matrix{Num} (alias for Array{Num, 2})

OBSERVE that here, the A matrix mats.A is produced by MTK. If I now substitute initial value unknowns + parameters:

substitute(mats.A, Dict(name_s .=> s_ss)) |> (x -> substitute(x, default_vals)) |> (x -> Symbolics.unwrap.(x))

this used to produce:

1×1 Matrix{Float64}:

But now it produces:

1×1 Matrix{SymbolicUtils.BasicSymbolic{Real}}:
 (-K) / (2A*h_ς*NaNMath.sqrt(2.4002252840247174 / (A*h_ς*ρ))*ρ)

Even if I merge the dictionaries, and only substitute one time:

julia> substitute(mats.A, merge(Dict(name_s .=> s_ss), default_vals))
1×1 Matrix{SymbolicUtils.BasicSymbolic{Real}}:
 -1 / (6NaNMath.sqrt(0.1A*ρ))

On the other hand, if I produce the A matrix using my own symbolic linearization function (which only works for index 1 problems), I get the same A matrix (my linearization function has simplified the expression by utilizing that m(t) = rho*A*h(t)):

julia> typeof(As)
Matrix{Num} (alias for Array{Num, 2})

Next, for my linearization, the following used to work (but doesn’t work any more):

julia> substitute(As, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals) |> x -> Symbolics.unwrap.(x)
substitute(As, Dict(name_s .=> s_ss)) |> x -> substitute(x, default_vals) |> x -> Symbolics.unwrap.(x)

HOWEVER, the following works for my linearized model:

julia> substitute(As, merge(Dict(name_s .=> s_ss), default_vals))
1×1 Matrix{Float64}:


  1. To me, it looks like something has changed, either in Symbolics or the type of objects produced by ModelingToolkit, or as a consequence of other changes in Julia: it doesn’t work to substitute numeric values for symbolic values in a sequence of substitutions (substitute(substitute(As, ...), ...) produces the same result as when using piping).
  2. The linearized symbolic matrices produced by my own linearization function seem to correctly substitute numeric values for symbols when I merge dictionaries for unknowns and parameters, and only substitute one time.
  3. The linearized symbolic matrices produced by MTK seem to be the same as from my own linearization function, but substituting merged dictionaries for some weird reason does not work for the symbolic matrix produced by MTK.

For MTK, this is not a big problem, I guess, because it is possible to use numeric linearization (a different function than symbolic linearization) and get the correct results.

It is still puzzling that substitution of numeric values into the symbolic MTK matrix doesn’t seem to work for my case.

It looks like the element type of the returned matrices are no longer Num, does it work if you first do Num.(A)?

1 Like

Yes, then it works. A small caveat… I have streamlined the code slightly:

op_par = ModelingToolkit.defaults(tank)  # tank is the model
name_s = unknowns(tank)  # I use "_s" because I wrote the code when unknowns were still known as "states". 
op_unk = Dict(name_s .=> s_ss)   # s_ss is the steady state/unknowns
op_point = merge(op_par, op_unk)

The following works:

julia> substitute(Num.(substitute(mats.A, op_unk)), op_par)
1×1 Matrix{Float64}:

What is still weird, is that the following does not work:

julia> substitute(mats.A, op_point)
1×1 Matrix{SymbolicUtils.BasicSymbolic{Real}}:
 -1 / (6NaNMath.sqrt(0.1A*ρ))

– here, if I use the Num mapping on this result, it is converted into a Num matrix, but the values for A and rho are still not inserted. Which is contrary to when I did the similar thing to the matrix generated from my own symbolic linearization function.

In Summary: something has changed in that substitution does not return a Num matrix any more. I don’t know whether that change lies in Symbolics or elsewhere, and whether it is intentional or not.

I have another problem with numeric linearization, but I’ll open another thread for that problem.

I meant to call Num on A before calling substitute.

Ah. OK. But:

So it seems to me that the matrix is of type Num when returned from the linearize function, and that substitute converts it into another type.

I tried to apply Num to the matrix prior to first substitute, but that had no effect.