sympy / sympy

A computer algebra system written in pure Python
https://sympy.org/
Other
12.78k stars 4.39k forks source link

Zero check has an inconsistent behavior in different versions #26817

Open guangyey opened 2 months ago

guangyey commented 2 months ago

I found a difference between sympy 1.12 and 1.13.

# for 1.12
>>> import sympy
>>> a = sympy.Number(0.0)
>>> a == 0
True
# for 1.13
>>> import sympy
>>> a = sympy.Number(0.0)
>>> a == 0
False

But in different sympy versions, sympy.Number(0) always has the same behavior that equals 0.0.

>>> import sympy
>>> a = sympy.Number(0)
>>> a == 0.0
True # for different sympy versions

Is this a regression or BC breaking change? It confused me that sympy.Number(0.0) == 0 returns False for version 1.13.

oscarbenjamin commented 2 months ago

This is intentional. See gh-25614.

It has already been the case that expressions containing float vs rational compare unequal:

In [1]: sin(2*x) == sin(2.0*x)
Out[1]: False

However until SymPy 1.13 Float and Rational would compare equal but that is now changed:

In [2]: Float(1) == Integer(1)
Out[2]: False

This is because in SymPy == is for structural equality and is supposed to verify that two expressions are the same (not that they have the same value).

guangyey commented 2 months ago

Thanks for your elaboration.

oscarbenjamin commented 2 months ago

Let's keep this open. I expect that others will run into this problem as well. We can use this issue for people to report problems caused by it and solutions.

posita commented 1 month ago

UPDATE: Comment edited from original for clarity.

I think there are still inconsistencies here. With 1.13.1:

In [1]: from sympy import Integer, Float, Number, Rational

In [2]: from itertools import product

In [3]: types = (int, float, Integer, Float, Number, Rational)

In [4]: for v in 0, 0.0:
   ...:     for l_type, r_type in product((int, float, Integer, Float, Number, Rational), repeat=2):
   ...:         if l_type is r_type or l_type in (int, float) and r_type in (int, float):
   ...:             continue
   ...:         print(f"{l_type.__name__}({v}) == {r_type.__name__}({v}) -> {l_type(v) == r_type(v)}")
   ...:
int(0) == Integer(0) -> True
int(0) == Float(0) -> False
int(0) == Number(0) -> True
int(0) == Rational(0) -> True
float(0) == Integer(0) -> True
float(0) == Float(0) -> True
float(0) == Number(0) -> True
float(0) == Rational(0) -> True
Integer(0) == int(0) -> True
Integer(0) == float(0) -> True
Integer(0) == Float(0) -> True
Integer(0) == Number(0) -> True
Integer(0) == Rational(0) -> True
Float(0) == int(0) -> False
Float(0) == float(0) -> True
Float(0) == Integer(0) -> False
Float(0) == Number(0) -> False
Float(0) == Rational(0) -> False
Number(0) == int(0) -> True
Number(0) == float(0) -> True
Number(0) == Integer(0) -> True
Number(0) == Float(0) -> True
Number(0) == Rational(0) -> True
Rational(0) == int(0) -> True
Rational(0) == float(0) -> True
Rational(0) == Integer(0) -> True
Rational(0) == Float(0) -> True
Rational(0) == Number(0) -> True
int(0.0) == Integer(0.0) -> True
int(0.0) == Float(0.0) -> False
int(0.0) == Number(0.0) -> False
int(0.0) == Rational(0.0) -> True
float(0.0) == Integer(0.0) -> True
float(0.0) == Float(0.0) -> True
float(0.0) == Number(0.0) -> True
float(0.0) == Rational(0.0) -> True
Integer(0.0) == int(0.0) -> True
Integer(0.0) == float(0.0) -> True
Integer(0.0) == Float(0.0) -> True
Integer(0.0) == Number(0.0) -> True
Integer(0.0) == Rational(0.0) -> True
Float(0.0) == int(0.0) -> False
Float(0.0) == float(0.0) -> True
Float(0.0) == Integer(0.0) -> False
Float(0.0) == Number(0.0) -> True
Float(0.0) == Rational(0.0) -> False
Number(0.0) == int(0.0) -> False
Number(0.0) == float(0.0) -> True
Number(0.0) == Integer(0.0) -> False
Number(0.0) == Float(0.0) -> True
Number(0.0) == Rational(0.0) -> False
Rational(0.0) == int(0.0) -> True
Rational(0.0) == float(0.0) -> True
Rational(0.0) == Integer(0.0) -> True
Rational(0.0) == Float(0.0) -> True
Rational(0.0) == Number(0.0) -> True

If I'm understanding the intention correctly, those should all evaluate to False, no? Even so, the actual results are perplexing, e.g.:

Float(0) == int(0) -> False
Float(0) == float(0) -> True
...
Number(0) == int(0) -> True
Number(0) == float(0) -> True

Many comparisons aren't even symmetrical, e.g.:

Float(0.0) == Rational(0.0) -> False
...
Rational(0.0) == Float(0.0) -> True

If the intention is that equivalent values with disparate types are not considered "equal", I'm quite surprised there aren't already tests to make sure this intention is consistently applied?

posita commented 1 month ago

I think you'll have problems trying to recast the meaning of __eq__ to take types into account without also addressing the remainder of the numerical comparisons. Many results don't seem consistent:

In [5]: from operator import __ge__, __gt__, __le__, __lt__

In [6]: for op in __ge__, __gt__, __le__, __lt__:
   ...:     for v in 0, 0.0:
   ...:         for l_type, r_type in product((int, float, Integer, Float, Number, Rational), repeat=2):
   ...:             if l_type is r_type or l_type in (int, float) and r_type in (int, float):
   ...:                 continue
   ...:             print(f"{l_type.__name__}({v}) {op.__name__} {r_type.__name__}({v}) -> {op(l_type(v), r_type(v))}")
   ...:             print(f"{l_type.__name__}({v}) {op.__name__} {r_type.__name__}({v + 1}) -> {op(l_type(v), r_type(v + 1))}")
   ...:             print(f"{l_type.__name__}({v + 1}) {op.__name__} {r_type.__name__}({v}) -> {op(l_type(v + 1), r_type(v))}")
   ...:
int(0) ge Integer(0) -> True
int(0) ge Integer(1) -> False
int(1) ge Integer(0) -> True
int(0) ge Float(0) -> True
int(0) ge Float(1) -> False
int(1) ge Float(0) -> True
int(0) ge Number(0) -> True
int(0) ge Number(1) -> False
int(1) ge Number(0) -> True
int(0) ge Rational(0) -> True
int(0) ge Rational(1) -> False
int(1) ge Rational(0) -> True
float(0) ge Integer(0) -> True
float(0) ge Integer(1) -> False
float(1) ge Integer(0) -> True
float(0) ge Float(0) -> True
float(0) ge Float(1) -> False
float(1) ge Float(0) -> True
float(0) ge Number(0) -> True
float(0) ge Number(1) -> False
float(1) ge Number(0) -> True
float(0) ge Rational(0) -> True
float(0) ge Rational(1) -> False
float(1) ge Rational(0) -> True
Integer(0) ge int(0) -> True
Integer(0) ge int(1) -> False
Integer(1) ge int(0) -> True
Integer(0) ge float(0) -> True
Integer(0) ge float(1) -> False
Integer(1) ge float(0) -> True
Integer(0) ge Float(0) -> True
Integer(0) ge Float(1) -> False
Integer(1) ge Float(0) -> True
Integer(0) ge Number(0) -> True
Integer(0) ge Number(1) -> False
Integer(1) ge Number(0) -> True
Integer(0) ge Rational(0) -> True
Integer(0) ge Rational(1) -> False
Integer(1) ge Rational(0) -> True
Float(0) ge int(0) -> True
Float(0) ge int(1) -> False
Float(1) ge int(0) -> True
Float(0) ge float(0) -> True
Float(0) ge float(1) -> False
Float(1) ge float(0) -> True
Float(0) ge Integer(0) -> True
Float(0) ge Integer(1) -> False
Float(1) ge Integer(0) -> True
Float(0) ge Number(0) -> True
Float(0) ge Number(1) -> False
Float(1) ge Number(0) -> True
Float(0) ge Rational(0) -> True
Float(0) ge Rational(1) -> False
Float(1) ge Rational(0) -> True
Number(0) ge int(0) -> True
Number(0) ge int(1) -> False
Number(1) ge int(0) -> True
Number(0) ge float(0) -> True
Number(0) ge float(1) -> False
Number(1) ge float(0) -> True
Number(0) ge Integer(0) -> True
Number(0) ge Integer(1) -> False
Number(1) ge Integer(0) -> True
Number(0) ge Float(0) -> True
Number(0) ge Float(1) -> False
Number(1) ge Float(0) -> True
Number(0) ge Rational(0) -> True
Number(0) ge Rational(1) -> False
Number(1) ge Rational(0) -> True
Rational(0) ge int(0) -> True
Rational(0) ge int(1) -> False
Rational(1) ge int(0) -> True
Rational(0) ge float(0) -> True
Rational(0) ge float(1) -> False
Rational(1) ge float(0) -> True
Rational(0) ge Integer(0) -> True
Rational(0) ge Integer(1) -> False
Rational(1) ge Integer(0) -> True
Rational(0) ge Float(0) -> True
Rational(0) ge Float(1) -> False
Rational(1) ge Float(0) -> True
Rational(0) ge Number(0) -> True
Rational(0) ge Number(1) -> False
Rational(1) ge Number(0) -> True
int(0.0) ge Integer(0.0) -> True
int(0.0) ge Integer(1.0) -> False
int(1.0) ge Integer(0.0) -> True
int(0.0) ge Float(0.0) -> True
int(0.0) ge Float(1.0) -> False
int(1.0) ge Float(0.0) -> True
int(0.0) ge Number(0.0) -> True
int(0.0) ge Number(1.0) -> False
int(1.0) ge Number(0.0) -> True
int(0.0) ge Rational(0.0) -> True
int(0.0) ge Rational(1.0) -> False
int(1.0) ge Rational(0.0) -> True
float(0.0) ge Integer(0.0) -> True
float(0.0) ge Integer(1.0) -> False
float(1.0) ge Integer(0.0) -> True
float(0.0) ge Float(0.0) -> True
float(0.0) ge Float(1.0) -> False
float(1.0) ge Float(0.0) -> True
float(0.0) ge Number(0.0) -> True
float(0.0) ge Number(1.0) -> False
float(1.0) ge Number(0.0) -> True
float(0.0) ge Rational(0.0) -> True
float(0.0) ge Rational(1.0) -> False
float(1.0) ge Rational(0.0) -> True
Integer(0.0) ge int(0.0) -> True
Integer(0.0) ge int(1.0) -> False
Integer(1.0) ge int(0.0) -> True
Integer(0.0) ge float(0.0) -> True
Integer(0.0) ge float(1.0) -> False
Integer(1.0) ge float(0.0) -> True
Integer(0.0) ge Float(0.0) -> True
Integer(0.0) ge Float(1.0) -> False
Integer(1.0) ge Float(0.0) -> True
Integer(0.0) ge Number(0.0) -> True
Integer(0.0) ge Number(1.0) -> False
Integer(1.0) ge Number(0.0) -> True
Integer(0.0) ge Rational(0.0) -> True
Integer(0.0) ge Rational(1.0) -> False
Integer(1.0) ge Rational(0.0) -> True
Float(0.0) ge int(0.0) -> True
Float(0.0) ge int(1.0) -> False
Float(1.0) ge int(0.0) -> True
Float(0.0) ge float(0.0) -> True
Float(0.0) ge float(1.0) -> False
Float(1.0) ge float(0.0) -> True
Float(0.0) ge Integer(0.0) -> True
Float(0.0) ge Integer(1.0) -> False
Float(1.0) ge Integer(0.0) -> True
Float(0.0) ge Number(0.0) -> True
Float(0.0) ge Number(1.0) -> False
Float(1.0) ge Number(0.0) -> True
Float(0.0) ge Rational(0.0) -> True
Float(0.0) ge Rational(1.0) -> False
Float(1.0) ge Rational(0.0) -> True
Number(0.0) ge int(0.0) -> True
Number(0.0) ge int(1.0) -> False
Number(1.0) ge int(0.0) -> True
Number(0.0) ge float(0.0) -> True
Number(0.0) ge float(1.0) -> False
Number(1.0) ge float(0.0) -> True
Number(0.0) ge Integer(0.0) -> True
Number(0.0) ge Integer(1.0) -> False
Number(1.0) ge Integer(0.0) -> True
Number(0.0) ge Float(0.0) -> True
Number(0.0) ge Float(1.0) -> False
Number(1.0) ge Float(0.0) -> True
Number(0.0) ge Rational(0.0) -> True
Number(0.0) ge Rational(1.0) -> False
Number(1.0) ge Rational(0.0) -> True
Rational(0.0) ge int(0.0) -> True
Rational(0.0) ge int(1.0) -> False
Rational(1.0) ge int(0.0) -> True
Rational(0.0) ge float(0.0) -> True
Rational(0.0) ge float(1.0) -> False
Rational(1.0) ge float(0.0) -> True
Rational(0.0) ge Integer(0.0) -> True
Rational(0.0) ge Integer(1.0) -> False
Rational(1.0) ge Integer(0.0) -> True
Rational(0.0) ge Float(0.0) -> True
Rational(0.0) ge Float(1.0) -> False
Rational(1.0) ge Float(0.0) -> True
Rational(0.0) ge Number(0.0) -> True
Rational(0.0) ge Number(1.0) -> False
Rational(1.0) ge Number(0.0) -> True
int(0) gt Integer(0) -> False
int(0) gt Integer(1) -> False
int(1) gt Integer(0) -> True
int(0) gt Float(0) -> False
int(0) gt Float(1) -> False
int(1) gt Float(0) -> True
int(0) gt Number(0) -> False
int(0) gt Number(1) -> False
int(1) gt Number(0) -> True
int(0) gt Rational(0) -> False
int(0) gt Rational(1) -> False
int(1) gt Rational(0) -> True
float(0) gt Integer(0) -> False
float(0) gt Integer(1) -> False
float(1) gt Integer(0) -> True
float(0) gt Float(0) -> False
float(0) gt Float(1) -> False
float(1) gt Float(0) -> True
float(0) gt Number(0) -> False
float(0) gt Number(1) -> False
float(1) gt Number(0) -> True
float(0) gt Rational(0) -> False
float(0) gt Rational(1) -> False
float(1) gt Rational(0) -> True
Integer(0) gt int(0) -> False
Integer(0) gt int(1) -> False
Integer(1) gt int(0) -> True
Integer(0) gt float(0) -> False
Integer(0) gt float(1) -> False
Integer(1) gt float(0) -> True
Integer(0) gt Float(0) -> False
Integer(0) gt Float(1) -> False
Integer(1) gt Float(0) -> True
Integer(0) gt Number(0) -> False
Integer(0) gt Number(1) -> False
Integer(1) gt Number(0) -> True
Integer(0) gt Rational(0) -> False
Integer(0) gt Rational(1) -> False
Integer(1) gt Rational(0) -> True
Float(0) gt int(0) -> False
Float(0) gt int(1) -> False
Float(1) gt int(0) -> True
Float(0) gt float(0) -> False
Float(0) gt float(1) -> False
Float(1) gt float(0) -> True
Float(0) gt Integer(0) -> False
Float(0) gt Integer(1) -> False
Float(1) gt Integer(0) -> True
Float(0) gt Number(0) -> False
Float(0) gt Number(1) -> False
Float(1) gt Number(0) -> True
Float(0) gt Rational(0) -> False
Float(0) gt Rational(1) -> False
Float(1) gt Rational(0) -> True
Number(0) gt int(0) -> False
Number(0) gt int(1) -> False
Number(1) gt int(0) -> True
Number(0) gt float(0) -> False
Number(0) gt float(1) -> False
Number(1) gt float(0) -> True
Number(0) gt Integer(0) -> False
Number(0) gt Integer(1) -> False
Number(1) gt Integer(0) -> True
Number(0) gt Float(0) -> False
Number(0) gt Float(1) -> False
Number(1) gt Float(0) -> True
Number(0) gt Rational(0) -> False
Number(0) gt Rational(1) -> False
Number(1) gt Rational(0) -> True
Rational(0) gt int(0) -> False
Rational(0) gt int(1) -> False
Rational(1) gt int(0) -> True
Rational(0) gt float(0) -> False
Rational(0) gt float(1) -> False
Rational(1) gt float(0) -> True
Rational(0) gt Integer(0) -> False
Rational(0) gt Integer(1) -> False
Rational(1) gt Integer(0) -> True
Rational(0) gt Float(0) -> False
Rational(0) gt Float(1) -> False
Rational(1) gt Float(0) -> True
Rational(0) gt Number(0) -> False
Rational(0) gt Number(1) -> False
Rational(1) gt Number(0) -> True
int(0.0) gt Integer(0.0) -> False
int(0.0) gt Integer(1.0) -> False
int(1.0) gt Integer(0.0) -> True
int(0.0) gt Float(0.0) -> False
int(0.0) gt Float(1.0) -> False
int(1.0) gt Float(0.0) -> True
int(0.0) gt Number(0.0) -> False
int(0.0) gt Number(1.0) -> False
int(1.0) gt Number(0.0) -> True
int(0.0) gt Rational(0.0) -> False
int(0.0) gt Rational(1.0) -> False
int(1.0) gt Rational(0.0) -> True
float(0.0) gt Integer(0.0) -> False
float(0.0) gt Integer(1.0) -> False
float(1.0) gt Integer(0.0) -> True
float(0.0) gt Float(0.0) -> False
float(0.0) gt Float(1.0) -> False
float(1.0) gt Float(0.0) -> True
float(0.0) gt Number(0.0) -> False
float(0.0) gt Number(1.0) -> False
float(1.0) gt Number(0.0) -> True
float(0.0) gt Rational(0.0) -> False
float(0.0) gt Rational(1.0) -> False
float(1.0) gt Rational(0.0) -> True
Integer(0.0) gt int(0.0) -> False
Integer(0.0) gt int(1.0) -> False
Integer(1.0) gt int(0.0) -> True
Integer(0.0) gt float(0.0) -> False
Integer(0.0) gt float(1.0) -> False
Integer(1.0) gt float(0.0) -> True
Integer(0.0) gt Float(0.0) -> False
Integer(0.0) gt Float(1.0) -> False
Integer(1.0) gt Float(0.0) -> True
Integer(0.0) gt Number(0.0) -> False
Integer(0.0) gt Number(1.0) -> False
Integer(1.0) gt Number(0.0) -> True
Integer(0.0) gt Rational(0.0) -> False
Integer(0.0) gt Rational(1.0) -> False
Integer(1.0) gt Rational(0.0) -> True
Float(0.0) gt int(0.0) -> False
Float(0.0) gt int(1.0) -> False
Float(1.0) gt int(0.0) -> True
Float(0.0) gt float(0.0) -> False
Float(0.0) gt float(1.0) -> False
Float(1.0) gt float(0.0) -> True
Float(0.0) gt Integer(0.0) -> False
Float(0.0) gt Integer(1.0) -> False
Float(1.0) gt Integer(0.0) -> True
Float(0.0) gt Number(0.0) -> False
Float(0.0) gt Number(1.0) -> False
Float(1.0) gt Number(0.0) -> True
Float(0.0) gt Rational(0.0) -> False
Float(0.0) gt Rational(1.0) -> False
Float(1.0) gt Rational(0.0) -> True
Number(0.0) gt int(0.0) -> False
Number(0.0) gt int(1.0) -> False
Number(1.0) gt int(0.0) -> True
Number(0.0) gt float(0.0) -> False
Number(0.0) gt float(1.0) -> False
Number(1.0) gt float(0.0) -> True
Number(0.0) gt Integer(0.0) -> False
Number(0.0) gt Integer(1.0) -> False
Number(1.0) gt Integer(0.0) -> True
Number(0.0) gt Float(0.0) -> False
Number(0.0) gt Float(1.0) -> False
Number(1.0) gt Float(0.0) -> True
Number(0.0) gt Rational(0.0) -> False
Number(0.0) gt Rational(1.0) -> False
Number(1.0) gt Rational(0.0) -> True
Rational(0.0) gt int(0.0) -> False
Rational(0.0) gt int(1.0) -> False
Rational(1.0) gt int(0.0) -> True
Rational(0.0) gt float(0.0) -> False
Rational(0.0) gt float(1.0) -> False
Rational(1.0) gt float(0.0) -> True
Rational(0.0) gt Integer(0.0) -> False
Rational(0.0) gt Integer(1.0) -> False
Rational(1.0) gt Integer(0.0) -> True
Rational(0.0) gt Float(0.0) -> False
Rational(0.0) gt Float(1.0) -> False
Rational(1.0) gt Float(0.0) -> True
Rational(0.0) gt Number(0.0) -> False
Rational(0.0) gt Number(1.0) -> False
Rational(1.0) gt Number(0.0) -> True
int(0) le Integer(0) -> True
int(0) le Integer(1) -> True
int(1) le Integer(0) -> False
int(0) le Float(0) -> True
int(0) le Float(1) -> True
int(1) le Float(0) -> False
int(0) le Number(0) -> True
int(0) le Number(1) -> True
int(1) le Number(0) -> False
int(0) le Rational(0) -> True
int(0) le Rational(1) -> True
int(1) le Rational(0) -> False
float(0) le Integer(0) -> True
float(0) le Integer(1) -> True
float(1) le Integer(0) -> False
float(0) le Float(0) -> True
float(0) le Float(1) -> True
float(1) le Float(0) -> False
float(0) le Number(0) -> True
float(0) le Number(1) -> True
float(1) le Number(0) -> False
float(0) le Rational(0) -> True
float(0) le Rational(1) -> True
float(1) le Rational(0) -> False
Integer(0) le int(0) -> True
Integer(0) le int(1) -> True
Integer(1) le int(0) -> False
Integer(0) le float(0) -> True
Integer(0) le float(1) -> True
Integer(1) le float(0) -> False
Integer(0) le Float(0) -> True
Integer(0) le Float(1) -> True
Integer(1) le Float(0) -> False
Integer(0) le Number(0) -> True
Integer(0) le Number(1) -> True
Integer(1) le Number(0) -> False
Integer(0) le Rational(0) -> True
Integer(0) le Rational(1) -> True
Integer(1) le Rational(0) -> False
Float(0) le int(0) -> True
Float(0) le int(1) -> True
Float(1) le int(0) -> False
Float(0) le float(0) -> True
Float(0) le float(1) -> True
Float(1) le float(0) -> False
Float(0) le Integer(0) -> True
Float(0) le Integer(1) -> True
Float(1) le Integer(0) -> False
Float(0) le Number(0) -> True
Float(0) le Number(1) -> True
Float(1) le Number(0) -> False
Float(0) le Rational(0) -> True
Float(0) le Rational(1) -> True
Float(1) le Rational(0) -> False
Number(0) le int(0) -> True
Number(0) le int(1) -> True
Number(1) le int(0) -> False
Number(0) le float(0) -> True
Number(0) le float(1) -> True
Number(1) le float(0) -> False
Number(0) le Integer(0) -> True
Number(0) le Integer(1) -> True
Number(1) le Integer(0) -> False
Number(0) le Float(0) -> True
Number(0) le Float(1) -> True
Number(1) le Float(0) -> False
Number(0) le Rational(0) -> True
Number(0) le Rational(1) -> True
Number(1) le Rational(0) -> False
Rational(0) le int(0) -> True
Rational(0) le int(1) -> True
Rational(1) le int(0) -> False
Rational(0) le float(0) -> True
Rational(0) le float(1) -> True
Rational(1) le float(0) -> False
Rational(0) le Integer(0) -> True
Rational(0) le Integer(1) -> True
Rational(1) le Integer(0) -> False
Rational(0) le Float(0) -> True
Rational(0) le Float(1) -> True
Rational(1) le Float(0) -> False
Rational(0) le Number(0) -> True
Rational(0) le Number(1) -> True
Rational(1) le Number(0) -> False
int(0.0) le Integer(0.0) -> True
int(0.0) le Integer(1.0) -> True
int(1.0) le Integer(0.0) -> False
int(0.0) le Float(0.0) -> True
int(0.0) le Float(1.0) -> True
int(1.0) le Float(0.0) -> False
int(0.0) le Number(0.0) -> True
int(0.0) le Number(1.0) -> True
int(1.0) le Number(0.0) -> False
int(0.0) le Rational(0.0) -> True
int(0.0) le Rational(1.0) -> True
int(1.0) le Rational(0.0) -> False
float(0.0) le Integer(0.0) -> True
float(0.0) le Integer(1.0) -> True
float(1.0) le Integer(0.0) -> False
float(0.0) le Float(0.0) -> True
float(0.0) le Float(1.0) -> True
float(1.0) le Float(0.0) -> False
float(0.0) le Number(0.0) -> True
float(0.0) le Number(1.0) -> True
float(1.0) le Number(0.0) -> False
float(0.0) le Rational(0.0) -> True
float(0.0) le Rational(1.0) -> True
float(1.0) le Rational(0.0) -> False
Integer(0.0) le int(0.0) -> True
Integer(0.0) le int(1.0) -> True
Integer(1.0) le int(0.0) -> False
Integer(0.0) le float(0.0) -> True
Integer(0.0) le float(1.0) -> True
Integer(1.0) le float(0.0) -> False
Integer(0.0) le Float(0.0) -> True
Integer(0.0) le Float(1.0) -> True
Integer(1.0) le Float(0.0) -> False
Integer(0.0) le Number(0.0) -> True
Integer(0.0) le Number(1.0) -> True
Integer(1.0) le Number(0.0) -> False
Integer(0.0) le Rational(0.0) -> True
Integer(0.0) le Rational(1.0) -> True
Integer(1.0) le Rational(0.0) -> False
Float(0.0) le int(0.0) -> True
Float(0.0) le int(1.0) -> True
Float(1.0) le int(0.0) -> False
Float(0.0) le float(0.0) -> True
Float(0.0) le float(1.0) -> True
Float(1.0) le float(0.0) -> False
Float(0.0) le Integer(0.0) -> True
Float(0.0) le Integer(1.0) -> True
Float(1.0) le Integer(0.0) -> False
Float(0.0) le Number(0.0) -> True
Float(0.0) le Number(1.0) -> True
Float(1.0) le Number(0.0) -> False
Float(0.0) le Rational(0.0) -> True
Float(0.0) le Rational(1.0) -> True
Float(1.0) le Rational(0.0) -> False
Number(0.0) le int(0.0) -> True
Number(0.0) le int(1.0) -> True
Number(1.0) le int(0.0) -> False
Number(0.0) le float(0.0) -> True
Number(0.0) le float(1.0) -> True
Number(1.0) le float(0.0) -> False
Number(0.0) le Integer(0.0) -> True
Number(0.0) le Integer(1.0) -> True
Number(1.0) le Integer(0.0) -> False
Number(0.0) le Float(0.0) -> True
Number(0.0) le Float(1.0) -> True
Number(1.0) le Float(0.0) -> False
Number(0.0) le Rational(0.0) -> True
Number(0.0) le Rational(1.0) -> True
Number(1.0) le Rational(0.0) -> False
Rational(0.0) le int(0.0) -> True
Rational(0.0) le int(1.0) -> True
Rational(1.0) le int(0.0) -> False
Rational(0.0) le float(0.0) -> True
Rational(0.0) le float(1.0) -> True
Rational(1.0) le float(0.0) -> False
Rational(0.0) le Integer(0.0) -> True
Rational(0.0) le Integer(1.0) -> True
Rational(1.0) le Integer(0.0) -> False
Rational(0.0) le Float(0.0) -> True
Rational(0.0) le Float(1.0) -> True
Rational(1.0) le Float(0.0) -> False
Rational(0.0) le Number(0.0) -> True
Rational(0.0) le Number(1.0) -> True
Rational(1.0) le Number(0.0) -> False
int(0) lt Integer(0) -> False
int(0) lt Integer(1) -> True
int(1) lt Integer(0) -> False
int(0) lt Float(0) -> False
int(0) lt Float(1) -> True
int(1) lt Float(0) -> False
int(0) lt Number(0) -> False
int(0) lt Number(1) -> True
int(1) lt Number(0) -> False
int(0) lt Rational(0) -> False
int(0) lt Rational(1) -> True
int(1) lt Rational(0) -> False
float(0) lt Integer(0) -> False
float(0) lt Integer(1) -> True
float(1) lt Integer(0) -> False
float(0) lt Float(0) -> False
float(0) lt Float(1) -> True
float(1) lt Float(0) -> False
float(0) lt Number(0) -> False
float(0) lt Number(1) -> True
float(1) lt Number(0) -> False
float(0) lt Rational(0) -> False
float(0) lt Rational(1) -> True
float(1) lt Rational(0) -> False
Integer(0) lt int(0) -> False
Integer(0) lt int(1) -> True
Integer(1) lt int(0) -> False
Integer(0) lt float(0) -> False
Integer(0) lt float(1) -> True
Integer(1) lt float(0) -> False
Integer(0) lt Float(0) -> False
Integer(0) lt Float(1) -> True
Integer(1) lt Float(0) -> False
Integer(0) lt Number(0) -> False
Integer(0) lt Number(1) -> True
Integer(1) lt Number(0) -> False
Integer(0) lt Rational(0) -> False
Integer(0) lt Rational(1) -> True
Integer(1) lt Rational(0) -> False
Float(0) lt int(0) -> False
Float(0) lt int(1) -> True
Float(1) lt int(0) -> False
Float(0) lt float(0) -> False
Float(0) lt float(1) -> True
Float(1) lt float(0) -> False
Float(0) lt Integer(0) -> False
Float(0) lt Integer(1) -> True
Float(1) lt Integer(0) -> False
Float(0) lt Number(0) -> False
Float(0) lt Number(1) -> True
Float(1) lt Number(0) -> False
Float(0) lt Rational(0) -> False
Float(0) lt Rational(1) -> True
Float(1) lt Rational(0) -> False
Number(0) lt int(0) -> False
Number(0) lt int(1) -> True
Number(1) lt int(0) -> False
Number(0) lt float(0) -> False
Number(0) lt float(1) -> True
Number(1) lt float(0) -> False
Number(0) lt Integer(0) -> False
Number(0) lt Integer(1) -> True
Number(1) lt Integer(0) -> False
Number(0) lt Float(0) -> False
Number(0) lt Float(1) -> True
Number(1) lt Float(0) -> False
Number(0) lt Rational(0) -> False
Number(0) lt Rational(1) -> True
Number(1) lt Rational(0) -> False
Rational(0) lt int(0) -> False
Rational(0) lt int(1) -> True
Rational(1) lt int(0) -> False
Rational(0) lt float(0) -> False
Rational(0) lt float(1) -> True
Rational(1) lt float(0) -> False
Rational(0) lt Integer(0) -> False
Rational(0) lt Integer(1) -> True
Rational(1) lt Integer(0) -> False
Rational(0) lt Float(0) -> False
Rational(0) lt Float(1) -> True
Rational(1) lt Float(0) -> False
Rational(0) lt Number(0) -> False
Rational(0) lt Number(1) -> True
Rational(1) lt Number(0) -> False
int(0.0) lt Integer(0.0) -> False
int(0.0) lt Integer(1.0) -> True
int(1.0) lt Integer(0.0) -> False
int(0.0) lt Float(0.0) -> False
int(0.0) lt Float(1.0) -> True
int(1.0) lt Float(0.0) -> False
int(0.0) lt Number(0.0) -> False
int(0.0) lt Number(1.0) -> True
int(1.0) lt Number(0.0) -> False
int(0.0) lt Rational(0.0) -> False
int(0.0) lt Rational(1.0) -> True
int(1.0) lt Rational(0.0) -> False
float(0.0) lt Integer(0.0) -> False
float(0.0) lt Integer(1.0) -> True
float(1.0) lt Integer(0.0) -> False
float(0.0) lt Float(0.0) -> False
float(0.0) lt Float(1.0) -> True
float(1.0) lt Float(0.0) -> False
float(0.0) lt Number(0.0) -> False
float(0.0) lt Number(1.0) -> True
float(1.0) lt Number(0.0) -> False
float(0.0) lt Rational(0.0) -> False
float(0.0) lt Rational(1.0) -> True
float(1.0) lt Rational(0.0) -> False
Integer(0.0) lt int(0.0) -> False
Integer(0.0) lt int(1.0) -> True
Integer(1.0) lt int(0.0) -> False
Integer(0.0) lt float(0.0) -> False
Integer(0.0) lt float(1.0) -> True
Integer(1.0) lt float(0.0) -> False
Integer(0.0) lt Float(0.0) -> False
Integer(0.0) lt Float(1.0) -> True
Integer(1.0) lt Float(0.0) -> False
Integer(0.0) lt Number(0.0) -> False
Integer(0.0) lt Number(1.0) -> True
Integer(1.0) lt Number(0.0) -> False
Integer(0.0) lt Rational(0.0) -> False
Integer(0.0) lt Rational(1.0) -> True
Integer(1.0) lt Rational(0.0) -> False
Float(0.0) lt int(0.0) -> False
Float(0.0) lt int(1.0) -> True
Float(1.0) lt int(0.0) -> False
Float(0.0) lt float(0.0) -> False
Float(0.0) lt float(1.0) -> True
Float(1.0) lt float(0.0) -> False
Float(0.0) lt Integer(0.0) -> False
Float(0.0) lt Integer(1.0) -> True
Float(1.0) lt Integer(0.0) -> False
Float(0.0) lt Number(0.0) -> False
Float(0.0) lt Number(1.0) -> True
Float(1.0) lt Number(0.0) -> False
Float(0.0) lt Rational(0.0) -> False
Float(0.0) lt Rational(1.0) -> True
Float(1.0) lt Rational(0.0) -> False
Number(0.0) lt int(0.0) -> False
Number(0.0) lt int(1.0) -> True
Number(1.0) lt int(0.0) -> False
Number(0.0) lt float(0.0) -> False
Number(0.0) lt float(1.0) -> True
Number(1.0) lt float(0.0) -> False
Number(0.0) lt Integer(0.0) -> False
Number(0.0) lt Integer(1.0) -> True
Number(1.0) lt Integer(0.0) -> False
Number(0.0) lt Float(0.0) -> False
Number(0.0) lt Float(1.0) -> True
Number(1.0) lt Float(0.0) -> False
Number(0.0) lt Rational(0.0) -> False
Number(0.0) lt Rational(1.0) -> True
Number(1.0) lt Rational(0.0) -> False
Rational(0.0) lt int(0.0) -> False
Rational(0.0) lt int(1.0) -> True
Rational(1.0) lt int(0.0) -> False
Rational(0.0) lt float(0.0) -> False
Rational(0.0) lt float(1.0) -> True
Rational(1.0) lt float(0.0) -> False
Rational(0.0) lt Integer(0.0) -> False
Rational(0.0) lt Integer(1.0) -> True
Rational(1.0) lt Integer(0.0) -> False
Rational(0.0) lt Float(0.0) -> False
Rational(0.0) lt Float(1.0) -> True
Rational(1.0) lt Float(0.0) -> False
Rational(0.0) lt Number(0.0) -> False
Rational(0.0) lt Number(1.0) -> True
Rational(1.0) lt Number(0.0) -> False
oscarbenjamin commented 1 month ago

Many of the different types that you compare are actually the same types:


In [7]: type(Integer(0))
Out[7]: sympy.core.numbers.Zero

In [8]: type(Rational(0))
Out[8]: sympy.core.numbers.Zero

In [9]: type(Number(0))
Out[9]: sympy.core.numbers.Zero

In [10]: type(Integer(0.0))
Out[10]: sympy.core.numbers.Zero

In [11]: type(Rational(0.0))
Out[11]: sympy.core.numbers.Zero

In [12]: type(Number(0.0))
Out[12]: sympy.core.numbers.Float

In [13]: type(Float(0))
Out[13]: sympy.core.numbers.Float

There are only 4 distinct types in your examples: int(0), float(0), Integer(0), Float(0).

oscarbenjamin commented 1 month ago

Inequalities like > have a different meaning in SymPy compared to == which is for structural equality. The equality version of > is Eq(a,b) rather than a==b.

oscarbenjamin commented 1 month ago

If the intention is that equivalent values with disparate types are not considered "equal"

The intention is that approximate floating point values are not considered equal to exact integer/rational values.

More broadly the intention is that a == b is True only for structurally equal expressions (and False otherwise). Many of the different "types" that you show reduce to structurally equivalent expressions or even the identical same object in memory e.g. these are all precisely the same object (the singleton S.Zero):

In [5]: Number(0) is Integer(0) is Rational(0) is Integer(0.0)
Out[5]: True
oscarbenjamin commented 1 month ago

This is a bug (both should be False):

In [6]: Float(0.0) == Rational(0.0)
Out[6]: False

In [7]: Rational(0.0) == Float(0.0)
Out[7]: True
oscarbenjamin commented 1 month ago

This is also a bug (both should be False):

In [8]: 0.0 == Integer(0)
Out[8]: True

In [9]: Integer(0) == 0.0
Out[9]: True
oscarbenjamin commented 1 month ago

There was some discussion earlier about making an exception for zero in Rational/Float comparisons. As long as they are distinct objects though then I think that they need to compare unequal under ==:

In [12]: S(0.0)
Out[12]: 0.0

In [13]: S(0)
Out[13]: 0
oscarbenjamin commented 1 month ago

I think that this is the fix:

diff --git a/sympy/core/numbers.py b/sympy/core/numbers.py
index 289c41f362..8f7519d281 100644
--- a/sympy/core/numbers.py
+++ b/sympy/core/numbers.py
@@ -1598,8 +1598,6 @@ def __eq__(self, other):
             # S(0) == S.false is False
             # S(0) == False is True
             return False
-        if not self:
-            return not other
         if other.is_NumberSymbol:
             if other.is_irrational:
                 return False

The prevents S.Zero from comparing equal to zero Floats (or floats):

In [1]: 0.0 == Integer(0)
Out[1]: False

In [2]: Integer(0) == 0.0
Out[2]: False

In [3]: Float(0) == Integer(0)
Out[3]: False

In [4]: Integer(0) == Float(0)
Out[4]: False
oscarbenjamin commented 1 month ago

I think that this is the fix:

That leads to a few test failures:

FAILED sympy/core/tests/test_numbers.py::test_Float - assert 0 == 0.0
FAILED sympy/core/tests/test_evalf.py::test_issue_17681 - OverflowError: cannot convert float infinity to integer
FAILED sympy/functions/elementary/tests/test_integers.py::test_ceiling - OverflowError: cannot convert float infinity to integer
FAILED sympy/functions/elementary/tests/test_trigonometric.py::test_sin_rewrite - assert -0.558931746279103 + 6.46234853557053e-26*I == -0.558931746279103 + 2.58493941422821e-26*I
FAILED sympy/functions/elementary/tests/test_trigonometric.py::test_cos - assert False is None
FAILED sympy/geometry/tests/test_point.py::test_arguments - assert Point2D(0, 10.0000000000000) == Point2D(0.0, 10.0000000000000)
FAILED sympy/functions/elementary/tests/test_trigonometric.py::test_tan_rewrite - assert 0.674050331723157 + 4.14132307267789e-25*I == 0.674050331723157 + 3.67372234649511e-25*I
FAILED sympy/polys/matrices/tests/test_linsolve.py::test__linsolve_float - assert {y: 0, x: 0} == {x: 0.0, y: 0.0}
FAILED sympy/functions/elementary/tests/test_trigonometric.py::test_issue_17461 - AttributeError: 'mpc' object has no attribute '_mpf_'. Did you mean: '_mpc_'?
FAILED sympy/physics/quantum/tests/test_qubit.py::test_measure_normalize - assert [(|000>, 0), ...101>, 0), ...] == [(|110>, a*co...conjugate(b))]
FAILED sympy/physics/quantum/tests/test_qubit.py::test_measure_all - assert [(|00>, 0), (...0), (|11>, 1)] == [(|11>, 1)]
FAILED sympy/integrals/tests/test_integrals.py::test_issue_20782 - assert 0 == 0.0
FAILED sympy/polys/tests/test_polytools.py::test_nroots - assert 0 == 0.0
FAILED sympy/solvers/tests/test_numeric.py::test_issue_6408 - assert 0 == 0.0
FAILED sympy/solvers/tests/test_numeric.py::test_issue_6408_integral - assert 0 == 0.0
FAILED sympy/utilities/tests/test_wester.py::test_D1 - assert (0.0 / sqrt(2)) == 0.0
oscarbenjamin commented 1 month ago

This is what it takes to get the tests passing:

diff --git a/sympy/core/evalf.py b/sympy/core/evalf.py
index c5337c446b..8cef9078c5 100644
--- a/sympy/core/evalf.py
+++ b/sympy/core/evalf.py
@@ -1494,7 +1494,7 @@ def evalf(x: 'Expr', prec: int, options: OPT_DICT) -> TMP_RES:
         re, im = as_real_imag()
         if re.has(re_) or im.has(im_):
             raise NotImplementedError
-        if re == 0.0:
+        if not re:
             re = None
             reprec = None
         elif re.is_number:
@@ -1502,7 +1502,7 @@ def evalf(x: 'Expr', prec: int, options: OPT_DICT) -> TMP_RES:
             reprec = prec
         else:
             raise NotImplementedError
-        if im == 0.0:
+        if not im:
             im = None
             imprec = None
         elif im.is_number:
diff --git a/sympy/core/numbers.py b/sympy/core/numbers.py
index 289c41f362..8f7519d281 100644
--- a/sympy/core/numbers.py
+++ b/sympy/core/numbers.py
@@ -1598,8 +1598,6 @@ def __eq__(self, other):
             # S(0) == S.false is False
             # S(0) == False is True
             return False
-        if not self:
-            return not other
         if other.is_NumberSymbol:
             if other.is_irrational:
                 return False
diff --git a/sympy/core/tests/test_numbers.py b/sympy/core/tests/test_numbers.py
index 5d79e7b472..b5092f0be5 100644
--- a/sympy/core/tests/test_numbers.py
+++ b/sympy/core/tests/test_numbers.py
@@ -455,13 +455,27 @@ def eq(a, b):
         t = Float("1.0E-15")
         return (-t < a - b < t)

-    zeros = (0, S.Zero, 0., Float(0))
-    for i, j in permutations(zeros[:-1], 2):
-        assert i == j
-    for i, j in permutations(zeros[-2:], 2):
-        assert i == j
-    for z in zeros:
-        assert z in zeros
+    equal_pairs = [
+        (0, 0.0), # This is just how Python works...
+        (0, S.Zero),
+        (0.0, Float(0)),
+    ]
+    unequal_pairs = [
+        (0.0, S.Zero),
+        (0, Float(0)),
+        (S.Zero, Float(0)),
+    ]
+    for p1, p2 in equal_pairs:
+        assert (p1 == p2) is True
+        assert (p1 != p2) is False
+        assert (p2 == p1) is True
+        assert (p2 != p1) is False
+    for p1, p2 in unequal_pairs:
+        assert (p1 == p2) is False
+        assert (p1 != p2) is True
+        assert (p2 == p1) is False
+        assert (p2 != p1) is True
+
     assert S.Zero.is_zero

     a = Float(2) ** Float(3)
diff --git a/sympy/geometry/tests/test_point.py b/sympy/geometry/tests/test_point.py
index abe63874a8..1f2b2768eb 100644
--- a/sympy/geometry/tests/test_point.py
+++ b/sympy/geometry/tests/test_point.py
@@ -418,7 +418,7 @@ def test_arguments():
     a = Point(0, 1)
     assert a/10.0 == Point(0, 0.1, evaluate=False)
     a = Point(0, 1)
-    assert a*10.0 == Point(0.0, 10.0, evaluate=False)
+    assert a*10.0 == Point(0, 10.0, evaluate=False)

     # test evaluate=False when changing dimensions
     u = Point(.1, .2, evaluate=False)
diff --git a/sympy/integrals/tests/test_integrals.py b/sympy/integrals/tests/test_integrals.py
index 8436d6127c..9f7552637d 100644
--- a/sympy/integrals/tests/test_integrals.py
+++ b/sympy/integrals/tests/test_integrals.py
@@ -2080,7 +2080,7 @@ def test_issue_20782():
     assert integrate(fun1, L) == 1
     assert integrate(fun2, L) == 0
     assert integrate(-fun1, L) == -1
-    assert integrate(-fun2, L) == 0.
+    assert integrate(-fun2, L) == 0
     assert integrate(fun_sum, L) == 1.
     assert integrate(-fun_sum, L) == -1.

diff --git a/sympy/physics/quantum/qubit.py b/sympy/physics/quantum/qubit.py
index fb75b4c496..10f3df00ec 100644
--- a/sympy/physics/quantum/qubit.py
+++ b/sympy/physics/quantum/qubit.py
@@ -493,7 +493,7 @@ def matrix_to_qubit(matrix):
             element = matrix[0, i]
         if format in ('numpy', 'scipy.sparse'):
             element = complex(element)
-        if element != 0.0:
+        if element:
             # Form Qubit array; 0 in bit-locations where i is 0, 1 in
             # bit-locations where i is 1
             qubit_array = [int(i & (1 << x) != 0) for x in range(nqubits)]
@@ -582,7 +582,7 @@ def measure_all(qubit, format='sympy', normalize=True):
         size = max(m.shape)  # Max of shape to account for bra or ket
         nqubits = int(math.log(size)/math.log(2))
         for i in range(size):
-            if m[i] != 0.0:
+            if m[i]:
                 results.append(
                     (Qubit(IntQubit(i, nqubits=nqubits)), m[i]*conjugate(m[i]))
                 )
diff --git a/sympy/polys/matrices/tests/test_linsolve.py b/sympy/polys/matrices/tests/test_linsolve.py
index 9d8cd7eb9f..25300ef2cb 100644
--- a/sympy/polys/matrices/tests/test_linsolve.py
+++ b/sympy/polys/matrices/tests/test_linsolve.py
@@ -32,7 +32,8 @@ def test__linsolve_float():
         y - x,
         y - 0.0216 * x
     ]
-    sol = {x:0.0, y:0.0}
+    # Should _linsolve return floats here?
+    sol = {x:0, y:0}
     assert _linsolve(eqs, (x, y)) == sol

     # Other cases should be close to eps
diff --git a/sympy/polys/tests/test_polytools.py b/sympy/polys/tests/test_polytools.py
index 1ba0e5a69b..c0672af667 100644
--- a/sympy/polys/tests/test_polytools.py
+++ b/sympy/polys/tests/test_polytools.py
@@ -3126,7 +3126,7 @@ def test_nroots():
     eps = Float("1e-5")

     assert re(roots[0]).epsilon_eq(-0.75487, eps) is S.true
-    assert im(roots[0]) == 0.0
+    assert im(roots[0]) == 0
     assert re(roots[1]) == Float(-0.5, 5)
     assert im(roots[1]).epsilon_eq(-0.86602, eps) is S.true
     assert re(roots[2]) == Float(-0.5, 5)
@@ -3139,7 +3139,7 @@ def test_nroots():
     eps = Float("1e-6")

     assert re(roots[0]).epsilon_eq(-0.75487, eps) is S.false
-    assert im(roots[0]) == 0.0
+    assert im(roots[0]) == 0
     assert re(roots[1]) == Float(-0.5, 5)
     assert im(roots[1]).epsilon_eq(-0.86602, eps) is S.false
     assert re(roots[2]) == Float(-0.5, 5)
diff --git a/sympy/solvers/tests/test_numeric.py b/sympy/solvers/tests/test_numeric.py
index f40bab6965..12abd38c80 100644
--- a/sympy/solvers/tests/test_numeric.py
+++ b/sympy/solvers/tests/test_numeric.py
@@ -73,12 +73,12 @@ def getroot(x0):

 def test_issue_6408():
     x = Symbol('x')
-    assert nsolve(Piecewise((x, x < 1), (x**2, True)), x, 2) == 0.0
+    assert nsolve(Piecewise((x, x < 1), (x**2, True)), x, 2) == 0

 def test_issue_6408_integral():
     x, y = symbols('x y')
-    assert nsolve(Integral(x*y, (x, 0, 5)), y, 2) == 0.0
+    assert nsolve(Integral(x*y, (x, 0, 5)), y, 2) == 0

 @conserve_mpmath_dps
diff --git a/sympy/utilities/tests/test_wester.py b/sympy/utilities/tests/test_wester.py
index 848dbdae82..c5699a4eb0 100644
--- a/sympy/utilities/tests/test_wester.py
+++ b/sympy/utilities/tests/test_wester.py
@@ -269,7 +269,7 @@ def test_C24():

 def test_D1():
-    assert 0.0 / sqrt(2) == 0.0
+    assert 0.0 / sqrt(2) == 0

 def test_D2():
oscarbenjamin commented 1 month ago

@posita I have opened a PR gh-26910 which I think fixes the main inconsistency. I am not sure if that covers all the cases you identified since you printed out a very long list of cases but only referred explicitly to one or two cases.

posita commented 1 month ago

This looks like #26910 addresses the cases raised in https://github.com/sympy/sympy/issues/26817#issuecomment-2266804738 and https://github.com/sympy/sympy/issues/26817#issuecomment-2266931853 , and it's helpful to understand @oscarbenjamin's explanation in https://github.com/sympy/sympy/issues/26817#issuecomment-2267102388 . From that perspective, #26910 makes sense.

However, I strongly urge designers to carefully consider applying different semantics to some built in operators but not others. That has all the hallmarks of a foot gun. If both sets of semantics are valuable, I would expect (or at least hope) that they would be clearly, intuitively, and obviously demarcated by syntax/call site, meaning something like one of the following:

class MyNum:
    # Built-in operators *only* deal with structural equivalence
    def __eq__(self, other):
        return self._structurally_equal(other)
    def __ne__(self, other):
        return not self._structurally_equal(other)
    def __le__(self, other):
        return NotImplemented
    def __lt__(self, other):
        return NotImplemented
    def __ge__(self, other):
        return NotImplemented
    def __gt__(self, other):
        return NotImplemented
    # Secondary stand-ins *only* deal with value comparisons
    def value_eq(self, other):
        return self._value_cmp(other) == 0
    def value_ne(self, other):
        return self._value_cmp(other) != 0
    def value_le(self, other):
        return self._value_cmp(other) <= 0
    def value_lt(self, other):
        return self._value_cmp(other) < 0
    def value_ge(self, other):
        return self._value_cmp(other) >= 0
    def value_gt(self, other):
        return self._value_cmp(other) > 0
class MyNum:
    # Built-in operators *only* deal with value comparisons
    def __eq__(self, other):
        return self._value_cmp(other) == 0
    def __ne__(self, other):
        return self._value_cmp(other) != 0
    def __le__(self, other):
        return self._value_cmp(other) <= 0
    def __lt__(self, other):
        return self._value_cmp(other) < 0
    def __ge__(self, other):
        return self._value_cmp(other) >= 0
    def __gt__(self, other):
        return self._value_cmp(other) > 0
    # Secondary stand-ins *only* deal with structural equivalence
    def structure_eq(self, other):
        return self._structurally_equal(other)
    def structure_ne(self, other):
        return not self._structurally_equal(other)

My guess is that the second approach (i.e., that built-in operators address value comparisons) is likely what most users would expect. (Also, returning NotImplemented for four of the six built-in operators, but then having all six represented through secondary methods is strong signal that built-ins should be used for value comparisons.)

I doubt anyone could reasonably expect mixed semantics that split between (__eq__, __ne__) and (__le__, __lt__, __ge__, __gt__). That just seems like the the worst of all possible worlds. Assuming the split semantics foot gun is still a bug (and I think it should be), I think this is a legit issue and should likely remain open, even if #26910 is merged, unless I've misunderstood the fix and @oscarbenjamin's comments above?

oscarbenjamin commented 1 month ago

I doubt anyone could reasonably expect mixed semantics that split between (__eq__, __ne__) and (__le__, __lt__, __ge__, __gt__)

Maybe but that split already exists and there is an awkwardness in Python here. The __eq__ method is used by basic data structures like sets and dicts. If we want a == b then we have to accept that e.g. {a,b}={a}:

In [3]: {1, 1.0}
Out[3]: {1}

In [4]: {1.0, 1}
Out[4]: {1.0}

In [5]: {S(1), S(1.0)} # With sympy types
Out[5]: {1, 1.0}

In [6]: {S(1.0), S(1)}
Out[6]: {1.0, 1}

The decision that == means structural equality in SymPy was made long ago. The change now makes the implementation of that decision consistent.

oscarbenjamin commented 1 month ago

The decision that == means structural equality in SymPy was made long ago.

For example:

In [12]: e1 = (x + 1)**2

In [13]: e2 = expand(e1)

In [14]: e1
Out[14]: 
       2
(x + 1) 

In [15]: e2
Out[15]: 
 2          
x  + 2⋅x + 1

In [16]: e1 == e2  # Why is this False?
Out[16]: False

In [17]: Eq(e1, e2)
Out[17]: 
       2    2          
(x + 1)  = x  + 2⋅x + 1

In [18]: Eq(e1, e2).simplify()
Out[18]: True

In [19]: e1 > e2
Out[19]: 
       2    2          
(x + 1)  > x  + 2⋅x + 1

In [20]: (e1 > e2).simplify()
Out[20]: False

In [21]: (e1 >= e2).simplify()
Out[21]: True
oscarbenjamin commented 1 month ago

One thing that we could do to make this more palatable is have comparisons with Python's int and float behave differently from comparisons with only SymPy types. Then:

Integer(0) == int(0) # True
Integer(0) == float(0) # True
Float(0) == int(0) # True
Float(0) == float(0) # True

However we would still have

Integer(0) == Float(0) # False

The primary reason that this last one is important is because it applies when looking at the args in a larger expression. There are many places in SymPy that want to collect expressions together in a set or a dict or that want to check if one expression is equal to another during manipulations. Generic operations that recurse down through expression trees need to be able to compare expressions to see whether they are the same expression and a == b is the way to do that. In those contexts though we always work with sympified types and do not need to worry about native Python int/float.

oscarbenjamin commented 1 month ago

A decision is needed here because this is the only outstanding item for SymPy 1.13.2.

oscarbenjamin commented 1 month ago

Now that gh-26910 is merged I am backporting a fix to the 1.13 branch.

We can still consider other changes if this is problematic but the basic inconsistency is fixed:

In [1]: Float(0) == Integer(0)
Out[1]: False

In [2]: Integer(0) == Float(0)
Out[2]: False