Substitution and simplification of terms that do not appear explicitly, using `Symbolics.jl` (and `SymbolicUtils.jl`)

Hi everyone.
I am encountering a problem where I want to solve and simplify a specific set of equations. Even in a very simple use case I cannot get simplify(expr) to do simple substitutions if the elements to be substituted are themselves part of other expressions.

An example of the simple case is as follows. We consider the equation f(t) = ax(t) + by(t), and now let z(t) = x(t) + y(t). It is clear that f(t) = (a+b)z(t), but I cannot seem to get an expression like this when using simplify, substitute, or when providing a rule using SymbolicUtils.jl, so my guess is that I am doing something wrong or that I am missing something (perhaps trivial).

Here is the code

julia> using Symbolics
julia> @variables t, a, b, x(t), y(t)
julia> f = a*x + b*y
julia> z = x + y
a*x(t) + b*y(t)
julia> substitute(f, Dict([x+y => z]))
a*x(t) + b*y(t)
julia> simplify(substitute(f, Dict([x+y => z])))
a*x(t) + b*y(t)

In other words, I never get the expected simplification (a+b)z(t). Is there a way to enforce such simplifications? Of course, in my real problem my equation is more complex, but the same substitution is needed to simplify.

I have noticed that with SymbolicUtils.jl you can provide a ‘rule’, e.g.

julia> r = @rule ~x + ~y => ~z
julia> simplify(f; rewriter=r)
ERROR: Failed to apply rule ~x + ~y => ~z on expression a*x(t) + b*y(t)
caused by: KeyError: key :z not found

but this raises a KeyError, so I suspect I am using the @rule wrong. How does one use a rule for substitution? Or is this how rules should be used?

Any help or pointers would be greatly appreciated! Thanks!

That’s not quite the way to think about it. It’s just not even represented like how it’s shown. That’s how it’s printed, not how it’s stored. We should allow for some nicer ways of changing printing, but note that it’s not just an operation tree under the hood.

So what is the way to think about it? I admit that I know very little about symbolic programming in general, let alone how Symbolics.jl does it – but substitutions like these are very widespread and seem very logical to me.

You also mention ‘nice ways of printing’, but does this suggest that, under the hood, the equation is like f(x) = (a+b)z(t)? Do I just need to call a different printing function?

That’s worth an issue.

Perhaps I am missing something here, but isn’t (a+b)z(t) = (a+b)(x(t)+y(t)) = ax(t) + bx(t) + ay(t) + by(t) which is not your f.

Also you z has a value before you substitute. Here are some possibilities

julia> using Symbolics

julia> @variables t, a, b, x(t), y(t), z(t)
6-element Vector{Num}:
    t
    a
    b
 x(t)
 y(t)
 z(t)

julia> f = a*x + b*x + a*y + b*y
a*x(t) + a*y(t) + b*x(t) + b*y(t)

julia> substitute(f, Dict([x+y=>z]))
a*x(t) + a*y(t) + b*x(t) + b*y(t)

julia> substitute(simplify(f), Dict([x+y=>z]))
(a + b)*z(t)

julia> substitute(f, Dict([x=>z-y]))
a*(z(t) - y(t)) + b*(z(t) - y(t)) + a*y(t) + b*y(t)

julia> simplify(substitute(f, Dict([x=>z-y])))
(a + b)*z(t)
1 Like

You were indeed not missing something – I made a stupid and very critical error. I edited my question to represent my mistake. I also learned that I should make the variable z(t) and not define it as z = x+y beforehand. However there are still some simplifications that are not caught by my (naive) implementation.
(Let’s just hope I am correct this time…)

First, to replicate what you have done:

julia> using Symbolics

julia> @variables t, a, b, c, x(t), y(t), z(t)
7-element Vector{Num}:
    t
    a
    b
    c
 x(t)
 y(t)
 z(t)

julia> f = a*(x+y) + b*(x+y)
a*(x(t) + y(t)) + b*(x(t) + y(t))

julia> substitute(f, Dict([x+y=>z]))
a*z(t) + b*z(t)

julia> simplify(substitute(f, Dict([x+y=>z])))
(a + b)*z(t)

julia> substitute(simplify(f), Dict([x+y=>z]))
(a + b)*z(t)

But consider

julia> g = (a+b)*x + (a+c)*y
(a + b)*x(t) + (a + c)*y(t)

julia> h = g - b*x - c*y
(a + b)*x(t) + (a + c)*y(t) - b*x(t) - c*y(t)

julia> simplify(substitute(h, Dict([x+y=>z])))
(a + b)*x(t) + (a + c)*y(t) - b*x(t) - c*y(t)

julia> substitute(simplify(h), Dict([x+y=>z]))
(a + b)*x(t) + (a + c)*y(t) - b*x(t) - c*y(t)

julia> simplify(h)
(a + b)*x(t) + (a + c)*y(t) - b*x(t) - c*y(t)

Even though here it is obvious that the result should be

\begin{align} h(t) &= (a+b)x(t) + (a+c)y(t) - bx(t) - cy(t) \\ &= ax(t) + ay(t) + bx(t) + cy(t) - bx(t) - cy(t) \\ &= az(t) \end{align}

simplify comes with the expand keyword for cases like this!

julia> substitute(simplify(h), Dict([x+y=>z]))
(a + b)*x(t) + (a + c)*y(t) - b*x(t) - c*y(t)

julia> substitute(simplify(h, expand=true), Dict([x+y=>z]))
a*z(t)
1 Like