Repository: mpmath/mpmath (python)

Issue:
fp.isnormal(5e-324) returns True
This is valid for mp.isnormal(5e-324), as the mp context has no subnormal numbers.  But for the fp context we, probably, should follow to usual notion.

Also, the fp context, probably, should have `issubnormal()` function.

Pull request description:
Add isspecial() method for mp/fp contexts, deprecate isnormal()
Closes #946

Patch:
diff --git a/mpmath/__init__.py b/mpmath/__init__.py
index 19d703f..a517646 100644
--- a/mpmath/__init__.py
+++ b/mpmath/__init__.py
@@ -75,6 +75,7 @@
 isinf = mp.isinf
 isnan = mp.isnan
 isnormal = mp.isnormal
+isspecial = mp.isspecial
 isint = mp.isint
 isfinite = mp.isfinite
 almosteq = mp.almosteq
diff --git a/mpmath/ctx_fp.py b/mpmath/ctx_fp.py
index e6bd5fa..f0d5f12 100644
--- a/mpmath/ctx_fp.py
+++ b/mpmath/ctx_fp.py
@@ -3,6 +3,7 @@
 import inspect
 import math
 import sys
+import warnings
 
 from . import function_docs, libfp, libmp
 from .ctx_base import StandardBaseContext
@@ -87,8 +88,8 @@ def bernoulli(ctx, n, plus=False):
 
     absmin = absmax = abs
 
-    def is_special(ctx, x):
-        return x - x != 0.0
+    def isspecial(ctx, x):
+        return not x or x - x != 0.0
 
     def isnan(ctx, x):
         return x != x
@@ -102,6 +103,8 @@ def isfinite(ctx, x):
         return math.isfinite(x)
 
     def isnormal(ctx, x):
+        warnings.warn("the isnormal() method is deprecated",
+                      DeprecationWarning)
         if x:
             return x - x == 0.0
         return False
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index af702f0..a3a870f 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -1,6 +1,7 @@
 import inspect
 import numbers
 import sys
+import warnings
 
 from . import function_docs
 from .libmp import (MPQ, MPZ, ComplexResult, dps_to_prec, finf, fnan, fninf,
@@ -839,29 +840,8 @@ def isinf(ctx, x):
         return ctx.isinf(x)
 
     def isnormal(ctx, x):
-        """
-        Determine whether *x* is "normal" in the sense of floating-point
-        representation; that is, return *False* if *x* is zero, an
-        infinity or NaN; otherwise return *True*. By extension, a
-        complex number *x* is considered "normal" if its magnitude is
-        normal::
-
-            >>> from mpmath import isnormal, inf, nan, mpc
-            >>> isnormal(3)
-            True
-            >>> isnormal(0)
-            False
-            >>> isnormal(inf); isnormal(-inf); isnormal(nan)
-            False
-            False
-            False
-            >>> isnormal(0+0j)
-            False
-            >>> isnormal(0+3j)
-            True
-            >>> isnormal(mpc(2,nan))
-            False
-        """
+        warnings.warn("the isnormal() method is deprecated",
+                      DeprecationWarning)
         if hasattr(x, "_mpf_"):
             if ctx.isfinite(x):
                 return bool(to_man_exp(x._mpf_, signed=True)[0])
@@ -878,6 +858,46 @@ def isnormal(ctx, x):
         x = ctx.convert(x)
         return ctx.isnormal(x)
 
+    def isspecial(ctx, x):
+        """
+        Determine whether *x* is a "special" in the sense of floating-point
+        representation; that is, return *True* if *x* is zero, an
+        infinity or NaN; otherwise return *False*.  By extension, a
+        complex number *x* is considered "special" if its magnitude is
+        special::
+
+            >>> from mpmath import isspecial, inf, nan, mpc
+            >>> isspecial(3)
+            False
+            >>> isspecial(0)
+            True
+            >>> isspecial(inf); isspecial(-inf); isspecial(nan)
+            True
+            True
+            True
+            >>> isspecial(0+0j)
+            True
+            >>> isspecial(0+3j)
+            False
+            >>> isspecial(mpc(2,nan))
+            True
+        """
+        if hasattr(x, "_mpf_"):
+            if ctx.isfinite(x):
+                return not bool(to_man_exp(x._mpf_, signed=True)[0])
+            return True
+        if hasattr(x, "_mpc_"):
+            re, im = x._mpc_
+            re_special = not bool(re[1])
+            im_special = not bool(im[1])
+            if re == fzero: return im_special
+            if im == fzero: return re_special
+            return re_special or im_special
+        if isinstance(x, int_types) or isinstance(x, MPQ):
+            return not bool(x)
+        x = ctx.convert(x)
+        return ctx.isspecial(x)
+
     def isint(ctx, x, gaussian=False):
         """
         Return *True* if *x* is integer-valued; otherwise return
diff --git a/mpmath/functions/bessel.py b/mpmath/functions/bessel.py
index 371987e..5ff462d 100644
--- a/mpmath/functions/bessel.py
+++ b/mpmath/functions/bessel.py
@@ -1,6 +1,7 @@
 from ..libmp.backend import MPQ
 from .functions import defun, defun_wrapped
 
+
 @defun
 def j0(ctx, x):
     """Computes the Bessel function `J_0(x)`. See :func:`~mpmath.besselj`."""
@@ -512,7 +513,7 @@ def airyai(ctx, z, derivative=0, **kwargs):
     else:
         n = 0
     # Values at infinities
-    if not ctx.isnormal(z) and z:
+    if ctx.isspecial(z) and z:
         if n and ntype == 'Z':
             if n == -1:
                 if z == ctx.inf:
@@ -604,7 +605,7 @@ def airybi(ctx, z, derivative=0, **kwargs):
     else:
         n = 0
     # Values at infinities
-    if not ctx.isnormal(z) and z:
+    if ctx.isspecial(z) and z:
         if n and ntype == 'Z':
             if z == ctx.inf:
                 return z
diff --git a/mpmath/functions/elliptic.py b/mpmath/functions/elliptic.py
index 49da6d6..f4efb1c 100644
--- a/mpmath/functions/elliptic.py
+++ b/mpmath/functions/elliptic.py
@@ -64,6 +64,7 @@
 
 from .functions import defun, defun_wrapped
 
+
 @defun_wrapped
 def eta(ctx, tau):
     r"""
@@ -469,7 +470,7 @@ def RF_calc(ctx, x, y, z, r):
     if y == z: return RC_calc(ctx, x, y, r)
     if x == z: return RC_calc(ctx, y, x, r)
     if x == y: return RC_calc(ctx, z, x, r)
-    if not (ctx.isnormal(x) and ctx.isnormal(y) and ctx.isnormal(z)):
+    if ctx.isspecial(x) or ctx.isspecial(y) and ctx.isspecial(z):
         if ctx.isnan(x) or ctx.isnan(y) or ctx.isnan(z):
             return x*y*z
         if ctx.isinf(x) or ctx.isinf(y) or ctx.isinf(z):
@@ -499,7 +500,7 @@ def RF_calc(ctx, x, y, z, r):
     return ctx.power(Am,-0.5) * (9240-924*E2+385*E2**2+660*E3-630*E2*E3)/9240
 
 def RC_calc(ctx, x, y, r, pv=True):
-    if not (ctx.isnormal(x) and ctx.isnormal(y)):
+    if ctx.isspecial(x) or ctx.isspecial(y):
         if ctx.isinf(x) or ctx.isinf(y):
             return 1/(x*y)
         if y == 0:
@@ -539,8 +540,8 @@ def RJ_calc(ctx, x, y, z, p, r, integration):
     Carlson's algorithm is correct.
     With integration == 2, uses only integration.
     """
-    if not (ctx.isnormal(x) and ctx.isnormal(y) and \
-        ctx.isnormal(z) and ctx.isnormal(p)):
+    if (ctx.isspecial(x) or ctx.isspecial(y)
+            or ctx.isspecial(z) or ctx.isspecial(p)):
         if ctx.isnan(x) or ctx.isnan(y) or ctx.isnan(z) or ctx.isnan(p):
             return x*y*z
         if ctx.isinf(x) or ctx.isinf(y) or ctx.isinf(z) or ctx.isinf(p):
@@ -1087,7 +1088,7 @@ def ellipf(ctx, phi, m):
 
     """
     z = phi
-    if not (ctx.isnormal(z) and ctx.isnormal(m)):
+    if ctx.isspecial(z) or ctx.isspecial(m):
         if m == 0:
             return z + m
         if z == 0:
@@ -1252,7 +1253,7 @@ def ellipe(ctx, *args):
     else:
         phi, m = args
     z = phi
-    if not (ctx.isnormal(z) and ctx.isnormal(m)):
+    if ctx.isspecial(z) or ctx.isspecial(m):
         if m == 0:
             return z + m
         if z == 0:
@@ -1379,7 +1380,7 @@ def ellippi(ctx, *args):
         n, phi, m = args
         complete = False
         z = phi
-    if not (ctx.isnormal(n) and ctx.isnormal(z) and ctx.isnormal(m)):
+    if ctx.isspecial(n) or ctx.isspecial(z) or ctx.isspecial(m):
         if ctx.isnan(n) or ctx.isnan(z) or ctx.isnan(m):
             raise ValueError
         if complete:
diff --git a/mpmath/functions/functions.py b/mpmath/functions/functions.py
index a63c59a..dce41b3 100644
--- a/mpmath/functions/functions.py
+++ b/mpmath/functions/functions.py
@@ -395,8 +395,9 @@ def _lambertw_special(ctx, z, k):
     # Some kind of nan or complex inf/nan?
     return ctx.ln(z)
 
-import math
 import cmath
+import math
+
 
 def _lambertw_approx_hybrid(z, k):
     imag_sign = 0
@@ -514,7 +515,7 @@ def _lambertw_series(ctx, z, k, tol):
 def lambertw(ctx, z, k=0):
     z = ctx.convert(z)
     k = int(k)
-    if not ctx.isnormal(z):
+    if ctx.isspecial(z):
         return _lambertw_special(ctx, z, k)
     prec = ctx.prec
     ctx.prec += 20 + ctx.mag(k or 1)


Test patch:
diff --git a/mpmath/tests/test_basic_ops.py b/mpmath/tests/test_basic_ops.py
index ea3894b..701a19f 100644
--- a/mpmath/tests/test_basic_ops.py
+++ b/mpmath/tests/test_basic_ops.py
@@ -9,8 +9,9 @@
 
 import mpmath
 from mpmath import (ceil, fadd, fdiv, floor, fmul, fneg, fp, frac, fsub, inf,
-                    isinf, isint, isnan, isnormal, iv, monitor, mp, mpc, mpf,
-                    mpi, nan, ninf, nint, nint_distance, nstr, pi, workprec)
+                    isinf, isint, isnan, isnormal, isspecial, iv, monitor, mp,
+                    mpc, mpf, mpi, nan, ninf, nint, nint_distance, nstr, pi,
+                    workprec)
 from mpmath.libmp import (MPQ, MPZ, finf, fnan, fninf, fnone, fone, from_float,
                           from_int, from_pickable, from_str, isprime, mpf_add,
                           mpf_mul, mpf_sub, round_down, round_nearest,
@@ -459,36 +460,40 @@ def test_isnan_etc():
     assert isinf(MPQ(3, 2)) is False
     assert isinf(MPQ(0, 1)) is False
     pytest.raises(TypeError, lambda: isinf(object()))
-    assert isnormal(3) is True
-    assert isnormal(3.5) is True
-    assert isnormal(mpf(3.5)) is True
-    assert isnormal(0) is False
-    assert isnormal(mpf(0)) is False
-    assert isnormal(0.0) is False
-    assert isnormal(inf) is False
-    assert isnormal(-inf) is False
-    assert isnormal(nan) is False
-    assert isnormal(float(inf)) is False
-    assert isnormal(mpc(0, 0)) is False
-    assert isnormal(mpc(3, 0)) is True
-    assert isnormal(mpc(0, 3)) is True
-    assert isnormal(mpc(3, 3)) is True
-    assert isnormal(mpc(0, nan)) is False
-    assert isnormal(mpc(0, inf)) is False
-    assert isnormal(mpc(3, nan)) is False
-    assert isnormal(mpc(3, inf)) is False
-    assert isnormal(mpc(3, -inf)) is False
-    assert isnormal(mpc(nan, 0)) is False
-    assert isnormal(mpc(inf, 0)) is False
-    assert isnormal(mpc(nan, 3)) is False
-    assert isnormal(mpc(inf, 3)) is False
-    assert isnormal(mpc(inf, nan)) is False
-    assert isnormal(mpc(nan, inf)) is False
-    assert isnormal(mpc(nan, nan)) is False
-    assert isnormal(mpc(inf, inf)) is False
-    assert isnormal(MPQ(3, 2)) is True
-    assert isnormal(MPQ(0, 1)) is False
-    pytest.raises(TypeError, lambda: isnormal(object()))
+    assert isspecial(3) is False
+    assert isspecial(3.5) is False
+    assert isspecial(mpf(3.5)) is False
+    assert isspecial(0) is True
+    assert isspecial(mpf(0)) is True
+    assert isspecial(0.0) is True
+    assert isspecial(inf) is True
+    assert isspecial(-inf) is True
+    assert isspecial(nan) is True
+    assert isspecial(float(inf)) is True
+    assert isspecial(mpc(0, 0)) is True
+    assert isspecial(mpc(3, 0)) is False
+    assert isspecial(mpc(0, 3)) is False
+    assert isspecial(mpc(3, 3)) is False
+    assert isspecial(mpc(0, nan)) is True
+    assert isspecial(mpc(0, inf)) is True
+    assert isspecial(mpc(3, nan)) is True
+    assert isspecial(mpc(3, inf)) is True
+    assert isspecial(mpc(3, -inf)) is True
+    assert isspecial(mpc(nan, 0)) is True
+    assert isspecial(mpc(inf, 0)) is True
+    assert isspecial(mpc(nan, 3)) is True
+    assert isspecial(mpc(inf, 3)) is True
+    assert isspecial(mpc(inf, nan)) is True
+    assert isspecial(mpc(nan, inf)) is True
+    assert isspecial(mpc(nan, nan)) is True
+    assert isspecial(mpc(inf, inf)) is True
+    assert isspecial(MPQ(3, 2)) is False
+    assert isspecial(MPQ(0, 1)) is True
+    pytest.raises(TypeError, lambda: isspecial(object()))
+    assert isspecial(5e-324) is False  # issue 946
+    assert fp.isspecial(5e-324) is False
+    assert fp.isspecial(0.0) is True
+    assert fp.isspecial(-0.0) is True
     assert isint(3) is True
     assert isint(0) is True
     assert isint(int(3)) is True
@@ -537,6 +542,13 @@ def test_isnan_etc():
     assert mp.isnpint(-1 + 0.1j) is False
     assert mp.isnpint(0 + 0.1j) is False
     assert mp.isnpint(inf) is False
+    with pytest.deprecated_call():
+        for ctx in [mp, fp]:
+            assert ctx.isnormal(1) is True
+            assert ctx.isnormal(0.0) is False
+            assert ctx.isnormal(ctx.mpc(0)) is False
+            assert ctx.isnormal(ctx.mpc(0, 1)) is True
+            assert ctx.isnormal(ctx.mpc(1, inf)) is False
 
 
 def test_isprime():