JuliaSymbolics / Symbolics.jl

Symbolic programming for the next generation of numerical software
https://docs.sciml.ai/Symbolics/stable/
Other
1.35k stars 151 forks source link

Comparing Tupels of Num's is different using == and isequal #492

Open AnnaBuettner opened 2 years ago

AnnaBuettner commented 2 years ago

I have been comparing two Tuple{Num} and realized that using == and isequal shows different behaviors. The results of == in this example also baffle me:

using Symbolics
@variables x y

(x,) == (y,) #true
isequal((x,), (y,)) #false

┆Issue is synchronized with this Trello card by Unito

albheim commented 2 years ago

It comes down to

julia> x == y
x == y

julia> isequal(x, y)
false

where the == does not check for equality, but rather create a new symbolic number. This interacts a bit odd with other comparisons sometimes, here it comes form this code in base for tuples

isequal(t1::Tuple, t2::Tuple) = length(t1) == length(t2) && _isequal(t1, t2)
_isequal(::Tuple{}, ::Tuple{}) = true
function _isequal(t1::Tuple{Any,Vararg{Any}}, t2::Tuple{Any,Vararg{Any}})
    return isequal(t1[1], t2[1]) && _isequal(tail(t1), tail(t2))
end
function _isequal(t1::Any32, t2::Any32)
    for i = 1:length(t1)
        if !isequal(t1[i], t2[i])
            return false
        end
    end
    return true
end

where the line eq = t1[1] == t2[1] is then a symbolic variable which is neither false or missing for the if cases so the last case is run and we return the value of the tail of the tuples, which for empty tuples is always true.

I'm not sure I like the behaviour of == for symbolic numbers, we had problems with the when wanting to support both symbolics and floats and comparing with isequal can be problematic for floats since

julia> -0.0 == 0.0
true

julia> isequal(-0.0, 0.0)
false

which makes it seem like one has to make special cases to be able to support both.

jlbosse commented 1 year ago

Just chiming in here that exactly the same issues just bit me to and make it hard to work generic code that works for both Floats and Nums. Is there a good reason that ==(::Num, ::Num) returns a Num and not a Bool?

aravindh-krishnamoorthy commented 1 year ago

Just chiming in here that exactly the same issues just bit me to and make it hard to work generic code that works for both Floats and Nums. Is there a good reason that ==(::Num, ::Num) returns a Num and not a Bool?

I agree that some work is needed to ensure that code works for both floats and Num's. But I'm confused about your recommendation that ==(::Num, ::Num) should return bool. Assume @variables x y, now should x==y be true or false?

jlbosse commented 1 year ago

In SymPy == test for exact structural equality. So (x == x) == true, (x == y) == false but also ((x+y)**2 == x**2 + 2*x*y + y**2) == false. Of course, it would be nice if the latter was also true but since expression equality testing can be shown to be impossible, in general, that is asking to much. So I think SymPy's beheaviour is probably the most sensible.

aravindh-krishnamoorthy commented 1 year ago

@jlbosse Thank you for the clarification. I do not know SymPy. I guess a detailed look at how SymPy handles the following is needed.

Consider the Julia function f(x) = x == 2 which takes an x and returns a bool. Now, consider using Symbolics; @variables z and w = f(z). Now, w surely can't evaluate to false since substitute(w, Dict(z => 2)) must evaluate to true and substitute(w, Dict(z => 3)) must be false.

So, how does SymPy handle this, if it already assigns w to false? In fact, even x == x can be false for x = NaN.

(At this point, I've to mention, that I'm also starting to learn the Symbolics library, so I'm not an expert.)

jlbosse commented 1 year ago

SymPy simply w to False and then there is no way to do any substitions in w. It seems to me that the questions is whether one thinks of a Num as a placeholder for a later to be specified value, in which case x == y could be true or false depending on what one puts for x and y, or if one sess them as formal Symbols, in which case x != y because they are different Symbols.

I am not sure which behavior / interpretation is the more useful one. But saying x != y instead of typeof(x==y) == Num would enable at least some control flow when working with Nums.

aravindh-krishnamoorthy commented 1 year ago

@jlbosse Hallo, we've been running into several of these issues where an Boolean check (inequality, equality, ternary operator, short-circuit operators) kinda kills the whole execution, as it cannot be determined without knowing the values of the variables. One of the methods I've been thinking about is similar to auto differentiation (AD), i.e., provide a monadic type with an "example value" along with a symbolic value. All comparisons are then based on the function's effect on the example value, whereas, the symbolic values are also computed and propagated. Not the best way (as the execution paths will depend on the example value), but at least this way, we do not run into these comparison errors. In a pure symbolic computation, we have to either abstract the function away (see my comment in #888) or carry it forward using unevaluated ifelse, both of which seem to have problems.

However, saying x and y are unequal because they have different variable names seems a bit short-sighted to me, i.e., it might get things going now but will lead to a larger roadblock later.

ChrisRackauckas commented 1 year ago

This thread seems to be going down an ill-informed path. Please read this blog post about quasi-static algorithms:

https://www.stochasticlifestyle.com/useful-algorithms-that-are-not-optimized-by-jax-pytorch-or-tensorflow/

Things that are not quasi-static are not symbolically tracable in a way that is generally correct. Yes, you can do it in a non-correct way, which is equivalent to linearization around a single point, but Symbolics.jl wants to build correct traces. Value-dependent logic is in a fundamentally different space than the static compute graphs that symbolics represents.

aravindh-krishnamoorthy commented 1 year ago

@ChrisRackauckas Thank you for the opinion and the link to the blog post. For about a month, I was trying out Symbolics.jl for my work. Unfortunately, it hasn't worked out so far, partly because of the reasons of this issue and #888. I ended up stubbing (@register) a large number of Julia functions, which rendered the framework useless. I understand from your post above that the focus of Symbolics.jl is to represent static compute graphs, which limits it to "quasi-static" code (as per your blog and post above).

Unfortunately, this is too restrictive for my use. In my case, tracing with an example value (linearisation around a single point in the above post) is still sufficient and useful. In fact, if instead of an example value, if the simplification can be done using a set of rules (like Mathematica's assumptions), this will be much more interesting.

However, I do realise that Symbolics.jl is a part of SciML group and focuses on ML applications (differentiability?). I will come back after a while and take a look again. Great job and good luck!

ChrisRackauckas commented 1 year ago

that the focus of Symbolics.jl is to represent static compute graphs, which limits it to "quasi-static" code (as per your blog and post above).

All symbolic libraries do that. It's a limitation of symbolic mathematics as an IR. It has nothing to do with opinion, you just cannot put a non-static program into a static language (symbolic mathematics).

Unfortunately, this is too restrictive for my use. In my case, tracing with an example value (linearisation around a single point in the above post) is still sufficient and useful. In fact, if instead of an example value, if the simplification can be done using a set of rules (like Mathematica's assumptions), this will be much more interesting.

You can probably do that without any work just by putting a Num in a Dual number. That has nothing to do with the symbolic library and the workings of a symbolic library wouldn't prevent that.