Repository: sympy/sympy

Issue:
# deprecate degree report for numerical expression and non-polynomial expressions unless symbolic generator of interest is given

The `gen` argument of degree can be an integer when the passed `f` is a Poly, but it also allows the integer to select the generator of a numeric expression *even if the expression is not a Poly*. This looks like an oversight and it would be better to treat the numeric expression like the nonnumeric and just raise an error since, without actually computing the Poly, it is not possible to know if the expression has more than one numerical generator.

```python
>>> degree(E+pi**2, 0)
1
>>> degree(E+pi**2, 1)
2
>>> Poly(E + pi**2).gens
(E, pi)
```

The proposal is to treat any non-Number numeric expression as a potentially multivarite expression and require that the symbolic numerical generator of interest be specified.

```python
>>> degree(x + y)
...
TypeError
>>> degree(0)
-oo
>>> degree(1)
0
>>> degree(pi)
1 <--- raise TypeError instead
```
In fact, it would be smart to go one step further and disallow use of the int for univariate expressions which are not polynomial so even though this works...
```python
>>> degree(sqrt(x)+x**2)
2
>>> degree(sqrt(x)+x**2,1)
1
```
it shouldn't because...
```python
>>> (sqrt(x)+x**2).is_polynomial()
False <--- therefore, let's disable reporting of degree since without the Poly to consult the user doesn't know which generator the result is for
>>> Poly(sqrt(x)+x**2).gens
(x, sqrt(x))
```
(Actually, I think it would be better if Poly identified only sqrt(x) as the generator and viewed this as y+y**4 where y=sqrt(x). That is a different issue.)

I doubt that anyone really is depending on this behavior and would like to simply correct this oversight instead of doing a deprecation.

Implementation patch:
diff --git a/sympy/polys/polytools.py b/sympy/polys/polytools.py
index 90540c9048e2..35608f5ae8d6 100644
--- a/sympy/polys/polytools.py
+++ b/sympy/polys/polytools.py
@@ -21,7 +21,6 @@
 from sympy.core.intfunc import ilcm
 from sympy.core.numbers import I, Integer, equal_valued, NegativeInfinity
 from sympy.core.relational import Relational, Equality
-from sympy.core.sorting import ordered
 from sympy.core.symbol import Dummy, Symbol
 from sympy.core.sympify import sympify, _sympify
 from sympy.core.traversal import preorder_traversal, bottom_up
@@ -4907,8 +4906,15 @@ def _update_args(args, key, value):
 def degree(f, gen=0):
     """
     Return the degree of ``f`` in the given variable.
+    When ``f`` is a univariate polynomial, it is not
+    necessary to pass a generator, but if the generator
+    is given as an int it refers to the gen-th generator
+    of the expression which must be passed as a Poly
+    instance.
 
-    The degree of 0 is negative infinity.
+    The degree of 0 with respect to any variable is ``-oo``; if ``f``
+    is a numerical expression equal to 0, but not identically 0,
+    zero may be returned instead (because the variable does not appear).
 
     Examples
     ========
@@ -4916,6 +4922,8 @@ def degree(f, gen=0):
     >>> from sympy import degree
     >>> from sympy.abc import x, y
 
+    >>> degree(x**2)
+    2
     >>> degree(x**2 + y*x + 1, gen=x)
     2
     >>> degree(x**2 + y*x + 1, gen=y)
@@ -4923,6 +4931,20 @@ def degree(f, gen=0):
     >>> degree(0, x)
     -oo
 
+    In contrast to the strict Poly method, if the specified generator is
+    not in Poly's generators, the Poly instance is recast in terms of the
+    generator instead of raising an error.
+
+    >>> from sympy import Poly
+    >>> p = Poly(x + y, x)
+    >>> p.degree(y)
+    Traceback (most recent call last):
+    ...
+    PolynomialError: a valid generator expected, got y
+
+    >>> degree(p, y)
+    1
+
     See also
     ========
 
@@ -4930,36 +4952,34 @@ def degree(f, gen=0):
     degree_list
     """
 
-    f = sympify(f, strict=True)
-    gen_is_Num = sympify(gen, strict=True).is_Number
-    if f.is_Poly:
-        p = f
-        isNum = p.as_expr().is_Number
-    else:
-        isNum = f.is_Number
-        if not isNum:
-            if gen_is_Num:
-                p, _ = poly_from_expr(f)
-            else:
-                p, _ = poly_from_expr(f, gen)
+    _degree = lambda x: Integer(x) if type(x) is int else x
 
-    if isNum:
-        return S.Zero if f else S.NegativeInfinity
-
-    if not gen_is_Num:
-        if f.is_Poly and gen not in p.gens:
-            # try recast without explicit gens
-            p, _ = poly_from_expr(f.as_expr())
-        if gen not in p.gens:
-            return S.Zero
-    elif not f.is_Poly and len(f.free_symbols) > 1:
-        raise TypeError(filldedent('''
-         A symbolic generator of interest is required for a multivariate
-         expression like func = %s, e.g. degree(func, gen = %s) instead of
-         degree(func, gen = %s).
-        ''' % (f, next(ordered(f.free_symbols)), gen)))
-    result = p.degree(gen)
-    return Integer(result) if isinstance(result, int) else S.NegativeInfinity
+    f = sympify(f, strict=True)
+    if type(gen) is int:
+        if isinstance(f, Poly):
+            return _degree(f.degree(gen))
+        if f.is_Number:
+            return S.NegativeInfinity if f.is_zero else S.Zero
+        try:
+            p = Poly(f, expand=False)
+        except GeneratorsNeeded:  # e.g. f = (1+I)**2/2
+            gens = ()  # do not guess what the user intended
+        else:
+            gens = p.gens
+        free = f.free_symbols
+        if not (gen == 0 and len(gens) == 1 and len(free) < 2 and f.is_polynomial(
+                gen := next(iter(gens))) and # <-- assigns gen
+                gen.is_Atom):  # e.g. x or pi
+            raise TypeError(filldedent('''
+                To avoid ambiguity, this expression requires either
+                a symbol (not int) generator or a Poly (which identifies
+                the generators).'''))
+        return p.degree(gen)
+
+    gen = sympify(gen, strict=True)
+    if not isinstance(f, Poly) or gen not in f.gens:
+        f = poly_from_expr(f, gen)[0]
+    return _degree(f.degree(gen))
 
 
 @public


Test patch:
diff --git a/sympy/polys/tests/test_polytools.py b/sympy/polys/tests/test_polytools.py
index 46f1fc327692..431e41f2c725 100644
--- a/sympy/polys/tests/test_polytools.py
+++ b/sympy/polys/tests/test_polytools.py
@@ -72,7 +72,7 @@
 from sympy.functions.elementary.hyperbolic import tanh
 from sympy.functions.elementary.miscellaneous import sqrt
 from sympy.functions.elementary.piecewise import Piecewise
-from sympy.functions.elementary.trigonometric import sin
+from sympy.functions.elementary.trigonometric import cos, sin
 from sympy.matrices.dense import Matrix
 from sympy.matrices.expressions.matexpr import MatrixSymbol
 from sympy.polys.rootoftools import rootof
@@ -1335,10 +1335,9 @@ def test_Poly_degree():
     assert degree(x*y**2, z) == 0
 
     assert degree(pi) == 1
-
+    raises(TypeError, lambda: degree(I))  # Poly sees a number but no gen -> needs gen
     raises(TypeError, lambda: degree(y**2 + x**3))
     raises(TypeError, lambda: degree(y**2 + x**3, 1))
-    raises(PolynomialError, lambda: degree(x, 1.1))
     raises(PolynomialError, lambda: degree(x**2/(x**3 + 1), x))
 
     assert degree(Poly(0,x),z) is -oo
@@ -1348,6 +1347,40 @@ def test_Poly_degree():
     assert degree(Poly(y**2 + x**3, x), z) == 0
     assert degree(Poly(y**2 + x**3 + z**4, x), z) == 4
 
+
+    # optimization
+    big = 2  # make this number 1000 when full expansion can be avoided
+
+    assert degree((x + 1)**big, x) == big  # Large power (fast-path, no expansion)
+    assert degree((x + 1)**big + x**(big-1), x) == big  # Addition without cancellation
+    assert degree((x + 1)**2 - x**2, x) == 1  # Addition with cancellation (fallback required)
+    assert degree(x*(x + 1)**big, x) == big + 1  # Nested multiplication
+    assert degree(y*(x + 1)**big, x) == big  # Generator independence
+
+    # checking 0 detection
+    Z = pi*(1/pi + 1) - 1 - pi
+    assert degree(Z**big, x) is S.NegativeInfinity
+    assert degree(Z**big, pi) is S.NegativeInfinity
+    Z = Add(0, Mul(0, x, evaluate=False), evaluate=False)
+    assert degree(Z, x) is S.NegativeInfinity
+    assert degree(cos(1)**2 + sin(1)**2 - 1, x) == 0  # should be -oo
+
+    # /!\ user was warned; anything could change as Poly is improved-#
+    eq = x + 1/x**2                                                  #
+    raises(PolynomialError, lambda: degree(eq**big, x))              #
+    assert degree(eq**big, 1/x) == 2*big                             #
+                                                                     #
+    eq = exp(x) + 1/exp(2*x)                                         #
+    assert degree(eq, exp(x)) == 1                                   #
+    assert degree(eq, exp(-x)) == 2                                  #
+                                                                     #
+    one = Pow(x, 0, evaluate=False)                                  #
+    raises(PolynomialError, lambda: degree(one, one))                #
+    assert degree(x, 1.1) == degree(x, pi) == 0                      #
+    assert degree(0, 1.1) == -oo                                     #
+    # end of zoo of warnings-----------------------------------------#
+
+
 def test_Poly_degree_list():
     assert Poly(0, x).degree_list() == (-oo,)
     assert Poly(0, x, y).degree_list() == (-oo, -oo)