Repository: sympy/sympy
Pull request: Float(0).is_integer is True, Float(1.1).is_integer is False, Float(1) is None
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->

yet another attempt to fix #23731
alternate to #25865, #25864, #25856

#### Brief description of what is fixed or changed

```python
>>> Float(0).is_integer  # same as master
True
>>> print(Float(1).is_integer)  # False in master
None
>>> Float(1.5).is_integer  # same as master
False
```

#### Other comments

There was a long discussion of whether `Float` should be classified as `rational` [here](https://groups.google.com/g/sympy/c/RUYOxxwMgac/m/4-1JXtny32EJ). One major concern was that float/Float objects do not behave as Rationals in operations, e.g.
```python
>>> S(1e-16) + S(2) == 2.0
True
>>> (x+y).n(subs={x:1e-16,y:2}) == 2.
True
```

#### Release Notes

<!-- Write the release notes for this release below between the BEGIN and END
statements. The basic format is a bulleted list with the name of the subpackage
and the release note for this PR. For example:

* solvers
  * Added a new solver for logarithmic equations.

* functions
  * Fixed a bug with log of integers. Formerly, `log(-x)` incorrectly gave `-log(x)`.

* physics.units
  * Corrected a semantical error in the conversion between volt and statvolt which
    reported the volt as being larger than the statvolt.

or if no release note(s) should be included use:

NO ENTRY

See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
information on how to write release notes. The bot will check your release
notes automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
- core
  - `Float.is_integer` is now `None` except for `0.0` and `Floats` with a non-zero fraction. Previously, only `Float(0)` was True
  - Fixed Eq incorrectly evaluating to `False` in some cases with floats.
<!-- END RELEASE NOTES -->


Problem statement:
`is_zero` broken for integer/float comparison
Not sure if this is a recent regression, but comparing an integer-assumption symbol to a Float (or a non-integer to an `Integer`) returns the wrong value:

```python
>>> i = Symbol('x', integer=True)
>>> Eq(i, 1.0)
False
>>> Eq(i, 1)
Eq(x, 1)

>>> ni = Symbol('x', integer=False)
>>> Eq(ni, 1)
False
>>> Eq(ni, 1.0)
Eq(x, 1.0)
```

Upon further inspection, it has to do with the difference being checked in `is_eq`: `_dif.is_zero` returns False, even though that should not always be true.

Hints from issue discussion:
It's been this way all the way back to SymPy 1.2 and is because of this:
```python
In [1]: Float(1)
Out[1]: 1.00000000000000

In [2]: Float(1).is_integer
Out[2]: False
```
That in turn is because of this:
https://github.com/sympy/sympy/blob/997749da1838628eaeb077c7ab32da6666912e13/sympy/core/numbers.py#L1239-L1240
Apparently any Float other than the zero Float has `is_integer -> False`...

That code dates back to #1743 according to `git blame`. There doesn't seem to be any discussion there about `is_integer` at all there. Looking at the commit before though every `Float` had `integer=False`:
https://github.com/sympy/sympy/blob/0608bc62619a572ef8bf6bb4f154b435d21ad4ec/sympy/core/numbers.py#L564

The line originates from 99b21ff58ad2e2ba83172512d7f513a7c37e50c3 which is some kind of rewrite commit that obliterates all the history so I can't find the source any earlier.

In any case at worst `Float(1).is_integer` should give `None` which would prevent the `Eq` from evaluating. The fix to do that is straight forward:
```diff
diff --git a/sympy/core/numbers.py b/sympy/core/numbers.py
index 0303786..b673e63 100644
--- a/sympy/core/numbers.py
+++ b/sympy/core/numbers.py
@@ -1237,7 +1237,10 @@ def _eval_is_infinite(self):
         return False
 
     def _eval_is_integer(self):
-        return self._mpf_ == fzero
+        if self._mpf_ == fzero:
+            return True
+        elif self._mpf_[2] < 0:
+            return False
 
     def _eval_is_negative(self):
         if self._mpf_ in (_mpf_ninf, _mpf_inf):
```
That only fails one test in core:
https://github.com/sympy/sympy/blob/997749da1838628eaeb077c7ab32da6666912e13/sympy/core/tests/test_numbers.py#L496
`None` is probably the best. Floats can have properties that might break the assumptions, e.g., `x + 1 == x` is possible for Float `x`. That was probably the motivation for making it `False`, but `None` is better since some floats do behave like integers in some contexts. 
Maybe it's unrelated, but after some playing around I found that even an arbitrary symbol with integer=False is assumed as nonzero:
```python
>>> sp.Symbol('x', integer=False).is_zero
False
```

Is that intended?
> an arbitrary symbol with integer=False is assumed as nonzero

Zero is an integer so if `integer=False` then `zero=False`.
Alright, makes sense. This should not naturally happen in our usage because integer is never explicitly set to False, only None or True.
I tried setting Float.is_integer to None at https://github.com/sympy/sympy/pull/25856 to see what happens. 

If it breaks too many things, then we should try to be more precise, specifically, return True when the Float exactly represents an integer (like 3.0), False when it has a nonzero fractional part, and None when it is too large to represent the integer exactly (like 1e20). I do worry that strange things could happen around the boundaries of exactly representable integers if we do this, though. 
`is_integer` was intended to be the symbolic equivalent of `is_Integer`. With the improved behavior of `==` (that discriminates between 1 and 1.0 and reports them as unequal since they are not structurally the same) but we allow `Eq(1, 1.0)` to be True. Conceptually, Float and Rational are the same except that the former is more granular, the latter bounded by memory.
A possible fix for the current behavior of `Equality` is to replace any Floats with a Dummy that has `rational=True` assumption.

```
>>> i = var('i',integer=1)
>>> Eq(i/2, S.Half)
Eq(i/2, 1/2) 
>>> Eq(i/2, .5)
Eq(i/2, 0.5)
>>> Eq(i, 1.0)
Eq(i, 1.0)

>>> ni = var('ni',integer=0)
>>> Eq(ni, 1.0)
False
>>> Eq(ni, 1)
False
```

Commit message: Update sympy/assumptions/handlers/ntheory.py

Patch to review:
diff --git a/sympy/assumptions/handlers/ntheory.py b/sympy/assumptions/handlers/ntheory.py
index 4f1397b283ee..ccb91f726e2e 100644
--- a/sympy/assumptions/handlers/ntheory.py
+++ b/sympy/assumptions/handlers/ntheory.py
@@ -5,7 +5,7 @@
 from sympy.assumptions import Q, ask
 from sympy.core import Add, Basic, Expr, Float, Mul, Pow, S
 from sympy.core.numbers import (ImaginaryUnit, Infinity, Integer, NaN,
-    NegativeInfinity, NumberSymbol, Rational)
+    NegativeInfinity, NumberSymbol, Rational, int_valued)
 from sympy.functions import Abs, im, re
 from sympy.ntheory import isprime
 
@@ -119,13 +119,15 @@ def _(expr, assumptions):
 
 def _EvenPredicate_number(expr, assumptions):
     # helper method
+    if isinstance(expr, (float, Float)):
+        if int_valued(expr):
+            return None
+        return False
     try:
         i = int(expr.round())
-        if not (expr - i).equals(0):
-            raise TypeError
     except TypeError:
         return False
-    if isinstance(expr, (float, Float)):
+    if not (expr - i).equals(0):
         return False
     return i % 2 == 0
 
diff --git a/sympy/assumptions/tests/test_query.py b/sympy/assumptions/tests/test_query.py
index 40ed2a8c2d21..b892d724ee0d 100644
--- a/sympy/assumptions/tests/test_query.py
+++ b/sympy/assumptions/tests/test_query.py
@@ -89,7 +89,7 @@ def test_int_12():
 def test_float_1():
     z = 1.0
     assert ask(Q.commutative(z)) is True
-    assert ask(Q.integer(z)) is False
+    assert ask(Q.integer(z)) is None
     assert ask(Q.rational(z)) is None
     assert ask(Q.real(z)) is True
     assert ask(Q.complex(z)) is True
@@ -98,10 +98,10 @@ def test_float_1():
     assert ask(Q.positive(z)) is True
     assert ask(Q.negative(z)) is False
     assert ask(Q.even(z)) is False
-    assert ask(Q.odd(z)) is False
+    assert ask(Q.odd(z)) is None
     assert ask(Q.finite(z)) is True
-    assert ask(Q.prime(z)) is False
-    assert ask(Q.composite(z)) is False
+    assert ask(Q.prime(z)) is None
+    assert ask(Q.composite(z)) is None
     assert ask(Q.hermitian(z)) is True
     assert ask(Q.antihermitian(z)) is False
 
@@ -2307,11 +2307,11 @@ def test_check_old_assumption():
 
 
 def test_issue_9636():
-    assert ask(Q.integer(1.0)) is False
-    assert ask(Q.prime(3.0)) is False
-    assert ask(Q.composite(4.0)) is False
-    assert ask(Q.even(2.0)) is False
-    assert ask(Q.odd(3.0)) is False
+    assert ask(Q.integer(1.0)) is None
+    assert ask(Q.prime(3.0)) is None
+    assert ask(Q.composite(4.0)) is None
+    assert ask(Q.even(2.0)) is None
+    assert ask(Q.odd(3.0)) is None
 
 
 def test_autosimp_used_to_fail():
diff --git a/sympy/core/numbers.py b/sympy/core/numbers.py
index d4e112a0c943..f3839e22d1cd 100644
--- a/sympy/core/numbers.py
+++ b/sympy/core/numbers.py
@@ -749,8 +749,11 @@ class Float(Number):
 
     _mpf_: tuple[int, int, int, int]
 
-    # A Float represents many real numbers,
-    # both rational and irrational.
+    # A Float, though rational in form, does not behave like
+    # a rational in all Python expressions so we deal with
+    # exceptions (where we want to deal with the rational
+    # form of the Float as a rational) at the source rather
+    # than assigning a mathematically loaded category of 'rational'
     is_rational = None
     is_irrational = None
     is_number = True
@@ -969,7 +972,10 @@ def _eval_is_infinite(self):
         return False
 
     def _eval_is_integer(self):
-        return self._mpf_ == fzero
+        if self._mpf_ == fzero:
+            return True
+        if not int_valued(self):
+            return False
 
     def _eval_is_negative(self):
         if self._mpf_ in (_mpf_ninf, _mpf_inf):
diff --git a/sympy/core/relational.py b/sympy/core/relational.py
index cce1dfe76ccd..5ccfc7e9632e 100644
--- a/sympy/core/relational.py
+++ b/sympy/core/relational.py
@@ -4,6 +4,7 @@
 from .sorting import ordered
 from .evalf import EvalfMixin
 from .function import AppliedUndef
+from .numbers import int_valued
 from .singleton import S
 from .sympify import _sympify, SympifyError
 from .parameters import global_parameters
@@ -1571,6 +1572,15 @@ def split_real_imag(expr):
             if z:
                 return True
 
+        # is_zero cannot help decide integer/rational with Float
+        c, t = dif.as_coeff_Add()
+        if c.is_Float:
+            if int_valued(c):
+                if t.is_integer is False:
+                    return False
+            elif t.is_rational is False:
+                return False
+
         n2 = _n2(lhs, rhs)
         if n2 is not None:
             return _sympify(n2 == 0)
diff --git a/sympy/core/tests/test_numbers.py b/sympy/core/tests/test_numbers.py
index 1834524a4423..82a221cf4417 100644
--- a/sympy/core/tests/test_numbers.py
+++ b/sympy/core/tests/test_numbers.py
@@ -504,7 +504,7 @@ def eq(a, b):
     # rationality properties
     # if the integer test fails then the use of intlike
     # should be removed from gamma_functions.py
-    assert Float(1).is_integer is False
+    assert Float(1).is_integer is None
     assert Float(1).is_rational is None
     assert Float(1).is_irrational is None
     assert sqrt(2).n(15).is_rational is None
diff --git a/sympy/core/tests/test_relational.py b/sympy/core/tests/test_relational.py
index 723ec4cafe69..6ab6405c49db 100644
--- a/sympy/core/tests/test_relational.py
+++ b/sympy/core/tests/test_relational.py
@@ -5,7 +5,7 @@
 from sympy.assumptions.ask import Q
 from sympy.core.add import Add
 from sympy.core.basic import Basic
-from sympy.core.expr import Expr
+from sympy.core.expr import Expr, unchanged
 from sympy.core.function import Function
 from sympy.core.mul import Mul
 from sympy.core.numbers import (Float, I, Rational, nan, oo, pi, zoo)
@@ -1252,6 +1252,19 @@ def test_weak_strict():
     assert eq.strict == Lt(x, 1)
     assert eq.weak == eq
 
+
+def test_issue_23731():
+    i = symbols('i', integer=True)
+    assert unchanged(Eq, i, 1.0)
+    assert unchanged(Eq, i/2, 0.5)
+    ni = symbols('ni', integer=False)
+    assert Eq(ni, 1) == False
+    assert unchanged(Eq, ni, .1)
+    assert Eq(ni, 1.0) == False
+    nr = symbols('nr', rational=False)
+    assert Eq(nr, .1) == False
+
+
 def test_rewrite_Add():
     from sympy.testing.pytest import warns_deprecated_sympy
     with warns_deprecated_sympy():
diff --git a/sympy/functions/combinatorial/numbers.py b/sympy/functions/combinatorial/numbers.py
index 52f73a2660d9..df0fa1ebd4e8 100644
--- a/sympy/functions/combinatorial/numbers.py
+++ b/sympy/functions/combinatorial/numbers.py
@@ -539,8 +539,8 @@ def _calc_bernoulli(n):
 
     # We implement a specialized memoization scheme to handle each
     # case modulo 6 separately
-    _cache = {0: S.One, 2: Rational(1, 6), 4: Rational(-1, 30)}
-    _highest = {0: 0, 2: 2, 4: 4}
+    _cache = {0: S.One, 1: Rational(1, 2), 2: Rational(1, 6), 4: Rational(-1, 30)}
+    _highest = {0: 0, 1: 1, 2: 2, 4: 4}
 
     @classmethod
     def eval(cls, n, x=None):
diff --git a/sympy/matrices/expressions/matexpr.py b/sympy/matrices/expressions/matexpr.py
index 5fce1478c40c..5cee3a8f1eda 100644
--- a/sympy/matrices/expressions/matexpr.py
+++ b/sympy/matrices/expressions/matexpr.py
@@ -235,7 +235,8 @@ def _eval_derivative(self, x):
     @classmethod
     def _check_dim(cls, dim):
         """Helper function to check invalid matrix dimensions"""
-        ok = check_assumptions(dim, integer=True, nonnegative=True)
+        ok = not dim.is_Float and check_assumptions(
+            dim, integer=True, nonnegative=True)
         if ok is False:
             raise ValueError(
                 "The dimension specification {} should be "
diff --git a/sympy/matrices/expressions/sets.py b/sympy/matrices/expressions/sets.py
index 90816c684def..de29b1db14fe 100644
--- a/sympy/matrices/expressions/sets.py
+++ b/sympy/matrices/expressions/sets.py
@@ -57,7 +57,8 @@ def _contains(self, other):
     @classmethod
     def _check_dim(cls, dim):
         """Helper function to check invalid matrix dimensions"""
-        ok = check_assumptions(dim, integer=True, nonnegative=True)
+        ok = not dim.is_Float and check_assumptions(
+            dim, integer=True, nonnegative=True)
         if ok is False:
             raise ValueError(
                 "The dimension specification {} should be "
diff --git a/sympy/sets/tests/test_fancysets.py b/sympy/sets/tests/test_fancysets.py
index 08f097b9346c..b23c2a99fce0 100644
--- a/sympy/sets/tests/test_fancysets.py
+++ b/sympy/sets/tests/test_fancysets.py
@@ -114,7 +114,8 @@ def test_ImageSet():
     harmonics = ImageSet(Lambda(x, 1/x), S.Naturals)
     assert Rational(1, 5) in harmonics
     assert Rational(.25) in harmonics
-    assert 0.25 not in harmonics
+    assert harmonics.contains(.25) == Contains(
+        0.25, ImageSet(Lambda(x, 1/x), S.Naturals), evaluate=False)
     assert Rational(.3) not in harmonics
     assert (1, 2) not in harmonics
 
@@ -1268,7 +1269,8 @@ def test_Rationals():
         Rational(1, 3), 3, Rational(-1, 3), -3, Rational(2, 3)]
     assert Basic() not in S.Rationals
     assert S.Half in S.Rationals
-    assert S.Rationals.contains(0.5) == Contains(0.5, S.Rationals, evaluate=False)
+    assert S.Rationals.contains(0.5) == Contains(
+        0.5, S.Rationals, evaluate=False)
     assert 2 in S.Rationals
     r = symbols('r', rational=True)
     assert r in S.Rationals
diff --git a/sympy/stats/stochastic_process_types.py b/sympy/stats/stochastic_process_types.py
index 5b72a6c3893f..e1cdd0ea8442 100644
--- a/sympy/stats/stochastic_process_types.py
+++ b/sympy/stats/stochastic_process_types.py
@@ -1644,7 +1644,7 @@ class BernoulliProcess(DiscreteTimeStochasticProcess):
     >>> B = BernoulliProcess("B", p=0.7, success=1, failure=0)
     >>> B.state_space
     {0, 1}
-    >>> (B.p).round(2)
+    >>> B.p.round(2)
     0.70
     >>> B.success
     1


Code review comments:
diff_hunk: @@ -98,10 +98,10 @@ def test_float_1():
     assert ask(Q.positive(z)) is True
     assert ask(Q.negative(z)) is False
     assert ask(Q.even(z)) is False
path: sympy/assumptions/tests/test_query.py
text: @author:
```suggestion
    assert ask(Q.even(z)) is None
```