Repository: sympy/sympy
Pull request: enhanced int/even/odd detection
<!-- 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. -->

fixes #8648
closes #12399 as alternate

#### Brief description of what is fixed or changed

Consideration of the nominal multiplicity of 2 allows us to return True/False for queries on integer/odd/even as tests indicate.

#### Other comments


#### 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.

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
    * better detection of odd/even/integer for expressions with symbolic powers of even bases
<!-- END RELEASE NOTES -->


Problem statement:
If n is even, n**2/2 should also be even
The following:

``` python
>>> n = Symbol('n', integer=True, even=True)
>>> (n**2/2).is_even
```

should return `True`, but it returns `None` (of course, this is also an enhancement).

That makes me think that perhaps symbolic integers should keep a more complex "assumptions" method, which generalizes "is_even" and "is_odd" to instead contain a dictionary of primes that are known to divide that integer, mapping to minimum and maximum known multiplicity.

I would like to think about it and post a proposition/plan, but I am not sure what is the correct github way of doing this.

Hints from issue discussion:
I have added some handling for this instance in this [PR](https://github.com/sympy/sympy/pull/12320).  I did put thought into generalizing this even more for any integer divisor, but because we don't have the factorizations for the symbols, this does not seem easily possible.

Commit message: document Rational gcd=1 arg

Patch to review:
diff --git a/sympy/core/mul.py b/sympy/core/mul.py
index 2c1d3b1ec8e5..d37b950fbc63 100644
--- a/sympy/core/mul.py
+++ b/sympy/core/mul.py
@@ -1334,6 +1334,7 @@ def _eval_is_zero(self):
     #_eval_is_integer = lambda self: _fuzzy_group(
     #    (a.is_integer for a in self.args), quick_exit=True)
     def _eval_is_integer(self):
+        from sympy import trailing
         is_rational = self._eval_is_rational()
         if is_rational is False:
             return False
@@ -1342,11 +1343,14 @@ def _eval_is_integer(self):
         denominators = []
         for a in self.args:
             if a.is_integer:
-                numerators.append(a)
+                if abs(a) is not S.One:
+                    numerators.append(a)
             elif a.is_Rational:
                 n, d = a.as_numer_denom()
-                numerators.append(n)
-                denominators.append(d)
+                if abs(n) is not S.One:
+                    numerators.append(n)
+                if d is not S.One:
+                    denominators.append(d)
             elif a.is_Pow:
                 b, e = a.as_base_exp()
                 if not b.is_integer or not e.is_integer: return
@@ -1364,13 +1368,35 @@ def _eval_is_integer(self):
         if not denominators:
             return True
 
-        odd = lambda ints: all(i.is_odd for i in ints)
-        even = lambda ints: any(i.is_even for i in ints)
+        odd = lambda ints: all(i.is_odd for i in ints)  # ALL
+        even = lambda ints: any(i.is_even for i in ints)  # ANY
 
         if odd(numerators) and even(denominators):
             return False
         elif even(numerators) and denominators == [2]:
             return True
+        elif all(i.is_even for i in numerators) and odd(denominators
+                ) and (Mul(*denominators, evaluate=False) - 1
+                ).is_positive:
+            return False
+        if len(denominators) == 1:
+            d = denominators[0]
+            if d.is_Integer and d.is_even:
+                # if minimal power of 2 in num vs den is not
+                # negative then we have an integer
+                if (Add(*[i.as_base_exp()[1] for i in
+                        numerators if i.is_even]) - trailing(d.p)
+                        ).is_nonnegative:
+                    return True
+        if len(numerators) == 1:
+            n = numerators[0]
+            if n.is_Integer and n.is_even:
+                # if minimal power of 2 in den vs num is positive
+                # then we have have a non-integer
+                if (Add(*[i.as_base_exp()[1] for i in
+                        denominators if i.is_even]) - trailing(n.p)
+                        ).is_positive:
+                    return False
 
     def _eval_is_polar(self):
         has_polar = any(arg.is_polar for arg in self.args)
@@ -1545,37 +1571,54 @@ def _eval_is_extended_negative(self):
         return self._eval_pos_neg(-1)
 
     def _eval_is_odd(self):
+        from sympy import trailing, fraction
         is_integer = self.is_integer
-
         if is_integer:
+            if self.is_zero:
+                return False
+            n, d = fraction(self)
+            if d.is_Integer and d.is_even:
+                # if minimal power of 2 in num vs den is
+                # positive then we have an even number
+                if (Add(*[i.as_base_exp()[1] for i in
+                        Mul.make_args(n) if i.is_even]) - trailing(d.p)
+                        ).is_positive:
+                    return False
+                return
             r, acc = True, 1
             for t in self.args:
-                if not t.is_integer:
-                    return None
-                elif t.is_even:
+                if abs(t) is S.One:
+                    continue
+                assert t.is_integer
+                if t.is_even:
+                    return False
+                if r is False:
+                    pass
+                elif acc != 1 and (acc + t).is_odd:
                     r = False
-                elif t.is_integer:
-                    if r is False:
-                        pass
-                    elif acc != 1 and (acc + t).is_odd:
-                        r = False
-                    elif t.is_odd is None:
-                        r = None
+                elif t.is_even is None:
+                    r = None
                 acc = t
             return r
-
-        # !integer -> !odd
-        elif is_integer is False:
-            return False
+        return is_integer # !integer -> !odd
 
     def _eval_is_even(self):
+        from sympy import trailing, fraction
         is_integer = self.is_integer
 
         if is_integer:
             return fuzzy_not(self.is_odd)
 
-        elif is_integer is False:
-            return False
+        n, d = fraction(self)
+        if n.is_Integer and n.is_even:
+            # if minimal power of 2 in den vs num is not
+            # negative then this is not an integer and
+            # can't be even
+            if (Add(*[i.as_base_exp()[1] for i in
+                    Mul.make_args(d) if i.is_even]) - trailing(n.p)
+                    ).is_nonnegative:
+                return False
+        return is_integer
 
     def _eval_is_composite(self):
         """
diff --git a/sympy/core/numbers.py b/sympy/core/numbers.py
index 845260626e16..4e4373e9d238 100644
--- a/sympy/core/numbers.py
+++ b/sympy/core/numbers.py
@@ -1572,6 +1572,16 @@ class Rational(Number):
     >>> r.p/r.q
     0.75
 
+    If an unevaluated Rational is desired, ``gcd=1`` can be passed and
+    this will keep common divisors of the numerator and denominator
+    from being eliminated. It is not possible, however, to leave a
+    negative value in the denominator.
+
+    >>> Rational(2, 4, gcd=1)
+    2/4
+    >>> Rational(2, -4, gcd=1).q
+    4
+
     See Also
     ========
     sympy.core.sympify.sympify, sympy.simplify.simplify.nsimplify
diff --git a/sympy/core/tests/test_arit.py b/sympy/core/tests/test_arit.py
index d12eeabe2db0..16580c14fa37 100644
--- a/sympy/core/tests/test_arit.py
+++ b/sympy/core/tests/test_arit.py
@@ -512,6 +512,12 @@ def test_Mul_is_even_odd():
     assert (x*(x + k)).is_odd is False
     assert (x*(x + m)).is_odd is None
 
+    # issue 8648
+    assert (m**2/2).is_even
+    assert (m**2/3).is_even is False
+    assert (2/m**2).is_odd is False
+    assert (2/m).is_odd is None
+
 
 @XFAIL
 def test_evenness_in_ternary_integer_product_with_odd():
@@ -1051,6 +1057,18 @@ def test_Pow_is_integer():
     assert (1/(x + 1)).is_integer is False
     assert (1/(-x - 1)).is_integer is False
 
+    # issue 8648-like
+    k = Symbol('k', even=True)
+    assert (k**3/2).is_integer
+    assert (k**3/8).is_integer
+    assert (k**3/16).is_integer is None
+    assert (2/k).is_integer is None
+    assert (2/k**2).is_integer is False
+    o = Symbol('o', odd=True)
+    assert (k/o).is_integer is None
+    o = Symbol('o', odd=True, prime=True)
+    assert (k/o).is_integer is False
+
 
 def test_Pow_is_real():
     x = Symbol('x', real=True)
diff --git a/sympy/core/tests/test_numbers.py b/sympy/core/tests/test_numbers.py
index 0b9abd816771..a288e4a64da2 100644
--- a/sympy/core/tests/test_numbers.py
+++ b/sympy/core/tests/test_numbers.py
@@ -343,6 +343,11 @@ def test_Rational_new():
     assert Rational(mpq(2, 6)) == Rational(1, 3)
     assert Rational(PythonRational(2, 6)) == Rational(1, 3)
 
+    assert Rational(2, 4, gcd=1).q == 4
+    n = Rational(2, -4, gcd=1)
+    assert n.q == 4
+    assert n.p == -2
+
 
 def test_Number_new():
     """"


Code review comments:
diff_hunk: @@ -1364,13 +1368,35 @@ def _eval_is_integer(self):
         if not denominators:
             return True
 
-        odd = lambda ints: all(i.is_odd for i in ints)
-        even = lambda ints: any(i.is_even for i in ints)
+        odd = lambda ints: all(i.is_odd for i in ints)  # ALL
+        even = lambda ints: any(i.is_even for i in ints)  # ANY
path: sympy/core/mul.py
text: @user1:
I think it would be slightly easier to understand if a third lambda `alleven` was introduced and the above was named `allodd` and `anyeven`. I was about to comment on the all below earlier.

But apart from that I think the changes really makes sense.