Repository: mpmath/mpmath (python)

Issue:
Add an additional line in ctx_mp_python.py line 679 to convert also numpy ndarrays of length 1
Hi,

I would suggest adding the line

        if isinstance(x, np.ndarray) and len(x) == 1: x = x[0]

as line 679 in ctx_mp_python.py to convert numpy ndarrays of length 1.

Justification:
The following code fails without this:

```
from mpmath import *
from scipy.optimize import fsolve
fsolve (lambda x: exp(x) - 5, 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/scipy/optimize/_minpack_py.py", line 170, in fsolve
    res = _root_hybr(_wrapped_func, x0, args, jac=fprime, **options)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/scipy/optimize/_minpack_py.py", line 238, in _root_hybr
    shape, dtype = _check_func('fsolve', 'func', func, x0, args, n, (n,))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/scipy/optimize/_minpack_py.py", line 23, in _check_func
    res = atleast_1d(thefunc(*((x0[:numinputs],) + args)))
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/scipy/optimize/_minpack_py.py", line 158, in _wrapped_func
    return func(*fargs)
           ^^^^^^^^^^^^
  File "<stdin>", line 1, in <lambda>
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/mpmath/ctx_mp_python.py", line 991, in f
    x = ctx.convert(x)
        ^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/mpmath/ctx_mp_python.py", line 650, in convert
    if type(x).__module__ == 'numpy': return ctx.npconvert(x)
                                             ^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/mpmath/ctx_mp_python.py", line 683, in npconvert
    raise TypeError("cannot create mpf from " + repr(x))
TypeError: cannot create mpf from array([1])

```

Of course, one could use findroot instead of fsolve above. But there are instances in which fsolve works and findroot does not:

```
n_1, n_obs_1, n_2, n_obs_2 = 62380000, 34539224.0, 62390000, 34542106.0
def f_1 (N0, N1):
    return N0 * (1 - exp (-1 * n_1 / (N0 + 2 * N1))) + N1 * (1 - exp (-2 * n_1 / (N0 + 2 * N1))) - n_obs_1,  \
           N0 * (1 - exp (-1 * n_2 / (N0 + 2 * N1))) + N1 * (1 - exp (-2 * n_2 / (N0 + 2 * N1))) - n_obs_2

findroot (f_1, (27109937, 28500053))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/mpmath/calculus/optimization.py", line 969, in findroot
    for x, error in iterations:
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/mpmath/calculus/optimization.py", line 660, in __iter__
    s = self.ctx.lu_solve(Jx, fxn)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/mpmath/matrices/linalg.py", line 224, in lu_solve
    A, p = ctx.LU_decomp(A)
           ^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/mpmath/matrices/linalg.py", line 149, in LU_decomp
    raise ZeroDivisionError('matrix is numerically singular')
ZeroDivisionError: matrix is numerically singular

def f_2 (param):
    N0, N1 = param
    return [N0 * (1 - exp (-1 * n_1 / (N0 + 2 * N1))) + N1 * (1 - exp (-2 * n_1 / (N0 + 2 * N1))) - n_obs_1,  \
            N0 * (1 - exp (-1 * n_2 / (N0 + 2 * N1))) + N1 * (1 - exp (-2 * n_2 / (N0 + 2 * N1))) - n_obs_2]

fsolve (f_2, [27109937, 28500053])
<stdin>:1: RuntimeWarning: The iteration is not making good progress, as measured by the 
  improvement from the last ten iterations.
array([36079713.35263181, 14271606.52089586])
```

Finally, there are instances in which fsolve runs into an overflow with the imports from math but not from mpmath:
```
from math import *
from scipy.optimize import fsolve

def f (param):
  N0, N1, N2 = param
  return [N0 * (1 - exp (-1 * n_1 / (N0 + 2 * N1 + 3 * N2))) + N1 * (1 - exp (-2 * n_1 / (N0 + 2 * N1 + 3 * N2))) + N2 * (1 - exp (-3 * n_1 / (N0 + 2 * N1 + 3 * N2))) - n_obs_1, 
          N0 * (1 - exp (-1 * n_2 / (N0 + 2 * N1 + 3 * N2))) + N1 * (1 - exp (-2 * n_2 / (N0 + 2 * N1 + 3 * N2))) + N2 * (1 - exp (-3 * n_1 / (N0 + 2 * N1 + 3 * N2))) - n_obs_2, 
          N0 * (1 - exp (-1 * n_3 / (N0 + 2 * N1 + 3 * N2))) + N1 * (1 - exp (-2 * n_3 / (N0 + 2 * N1 + 3 * N2))) + N2 * (1 - exp (-3 * n_1 / (N0 + 2 * N1 + 3 * N2))) - n_obs_3]

n_1, n_obs_1, n_2, n_obs_2, n_3, n_obs_3 = 62380000, 34539224.0, 62390000, 34542106.0, 62400000, 34544973.0
initial_estimate = [27376700.0, 18251133.333333332, 9125566.666666666]
fsolve (f, initial_estimate)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/scipy/optimize/_minpack_py.py", line 170, in fsolve
    res = _root_hybr(_wrapped_func, x0, args, jac=fprime, **options)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/scipy/optimize/_minpack_py.py", line 249, in _root_hybr
    retval = _minpack._hybrd(func, x0, args, 1, xtol, maxfev,
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/da/cbt/GX/schuisv1/python/envs/library-complexity-test/lib/python3.12/site-packages/scipy/optimize/_minpack_py.py", line 158, in _wrapped_func
    return func(*fargs)
           ^^^^^^^^^^^^
  File "<stdin>", line 3, in f
OverflowError: math range error
from mpmath import *
>>> fsolve (f, initial_estimate)
<stdin>:3: RuntimeWarning: invalid value encountered in multiply
<stdin>:4: RuntimeWarning: invalid value encountered in multiply
<stdin>:5: RuntimeWarning: invalid value encountered in multiply
<stdin>:1: RuntimeWarning: The iteration is not making good progress, as measured by the 
  improvement from the last ten iterations.
array([27376700.        , 18251133.33333333,  9125566.66666667])
```
Best regards,
Sven


Pull request description:
Misc fixes
* move up blog2_10 constant
* add hypothesis test deps
* drop test_mpf_fmt_special()
* support conversion from scalar ndarray's, closes #824

Patch:
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index e1a7bf6..2d28777 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -702,9 +702,10 @@ def npconvert(ctx, x):
         scalar.
         """
         import numpy as np
-        if isinstance(x, np.integer): return ctx.make_mpf(from_int(int(x)))
-        if isinstance(x, np.floating): return ctx.make_mpf(from_npfloat(x))
-        if isinstance(x, np.complexfloating):
+        if isinstance(x, np.ndarray) and x.ndim == 0: x = x.item()
+        if isinstance(x, (np.integer, int)): return ctx.make_mpf(from_int(int(x)))
+        if isinstance(x, (np.floating, float)): return ctx.make_mpf(from_npfloat(x))
+        if isinstance(x, (np.complexfloating, complex)):
             return ctx.make_mpc((from_npfloat(x.real), from_npfloat(x.imag)))
         raise TypeError("cannot create mpf from " + repr(x))
 
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index 9054b6a..7cabdb2 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -43,12 +43,12 @@ class ComplexResult(ValueError):
 def prec_to_dps(n):
     """Return number of accurate decimals that can be represented
     with a precision of n bits."""
-    return max(1, int(round(int(n)/3.3219280948873626)-1))
+    return max(1, int(round(int(n)/blog2_10)-1))
 
 def dps_to_prec(n):
     """Return the number of bits required to represent n decimals
     accurately."""
-    return max(1, int(round((int(n)+1)*3.3219280948873626)))
+    return max(1, int(round((int(n)+1)*blog2_10)))
 
 def repr_dps(n):
     """Return the number of decimal digits required to represent
@@ -79,6 +79,7 @@ def repr_dps(n):
 fninf = (1, MPZ_ZERO, -789, -3)
 
 math_float_inf = math.inf
+blog2_10 = 3.3219280948873626
 
 
 #----------------------------------------------------------------------------#
@@ -361,7 +362,7 @@ def from_Decimal(x, prec=0, rnd=round_fast):
     if x.is_nan(): return fnan
     if x.is_infinite(): return fninf if x.is_signed() else finf
     if not prec:
-        prec = int(len(x.as_tuple()[1])*3.3219280948873626)
+        prec = int(len(x.as_tuple()[1])*blog2_10)
     return from_str(str(x), prec, rnd)
 
 def to_float(s, strict=False, rnd=round_fast):
@@ -1062,7 +1063,7 @@ def to_digits_exp(s, dps, base=10):
         return '', '0', 0
 
     if base == 10:
-        blog2 = 3.3219280948873626
+        blog2 = blog2_10
     elif pow(2, blog2 := int(math.log2(base))) == base:
         pass
     else:
@@ -1360,8 +1361,6 @@ def from_str(x, prec=0, rnd=round_fast, base=0):
 #                              String formatting                             #
 #----------------------------------------------------------------------------#
 
-blog2_10 = 3.3219280948873626
-
 _FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
     (?:
         (?P<fill_char>.)?
diff --git a/pyproject.toml b/pyproject.toml
index 79c9226..bb3aca9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,7 +31,8 @@ Homepage = 'https://mpmath.org/'
 Documentation = 'http://mpmath.org/doc/current/'
 [project.optional-dependencies]
 tests = ['pytest>=6', 'numpy; python_version<"3.13"', 'packaging',
-         'matplotlib; python_version<"3.13"', 'pexpect', 'ipython']
+         'matplotlib; python_version<"3.13"', 'pexpect', 'ipython',
+         'hypothesis']
 develop = ['mpmath[tests]', 'flake518>=1.5; python_version>="3.9"',
            'pytest-cov', 'wheel', 'build']
 gmpy = ['gmpy2>=2.2; platform_python_implementation!="PyPy"']


Test patch:
diff --git a/mpmath/tests/test_convert.py b/mpmath/tests/test_convert.py
index 76cdfb5..678af08 100644
--- a/mpmath/tests/test_convert.py
+++ b/mpmath/tests/test_convert.py
@@ -277,7 +277,10 @@ def test_compatibility():
     assert sqrt(Fraction(2, 3)).ae(sqrt(mpf('2/3')))
     assert sqrt(Decimal(2)/Decimal(3)).ae(sqrt(mpf('2/3')))
     mp.prec = oldprec
-    pytest.raises(TypeError, lambda: mpmathify(np.array(1)))
+    assert mpmathify(np.array(123)) == mpf(123)
+    assert mpmathify(np.array(1.25)) == mpf(1.25)
+    assert mpmathify(np.array(0.5+1j)) == mpc(0.5+1j)
+    pytest.raises(TypeError, lambda: mpmathify(np.array([1])))
 
 def test_issue465():
     assert mpf(Fraction(1, 3)) == mpf('0.33333333333333331')
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
index e208d1b..ecec9b7 100644
--- a/mpmath/tests/test_format.py
+++ b/mpmath/tests/test_format.py
@@ -75,6 +75,8 @@ def test_mpf_fmt_cpython():
     float.__format__, when dps is default.
     '''
 
+    # 'f' code formatting.
+
     # zeros
     assert f'{mp.mpf(0):.0f}' == '0'
     assert f'{mp.mpf(0):.1f}' == '0.0'
@@ -82,6 +84,22 @@ def test_mpf_fmt_cpython():
     assert f'{mp.mpf(0):.3f}' == '0.000'
     assert f'{mp.mpf(0):.50f}' == '0.00000000000000000000000000000000000000000000000000'
 
+    # nan, infs
+    assert f'{inf:f}' == 'inf'
+    assert f'{inf:+f}' == '+inf'
+    assert f'{inf:F}' == 'INF'
+    assert f'{inf:+F}' == '+INF'
+
+    assert f'{ninf:f}' == '-inf'
+    assert f'{ninf:+f}' == '-inf'
+    assert f'{ninf:F}' == '-INF'
+    assert f'{ninf:+F}' == '-INF'
+
+    assert f'{nan:f}' == 'nan'
+    assert f'{nan:+f}' == '+nan'
+    assert f'{nan:F}' == 'NAN'
+    assert f'{nan:+F}' == '+NAN'
+
     # precision 0;  result should never include a .
     assert f'{mp.mpf(1.5):.0f}' == '2'
     assert f'{mp.mpf(2.5):.0f}' == '2'
@@ -165,6 +183,22 @@ def test_mpf_fmt_cpython():
     assert f'{mp.mpf(0):.10e}' == '0.0000000000e+00'
     assert f'{mp.mpf(0):.50e}' == '0.00000000000000000000000000000000000000000000000000e+00'
 
+    # nan, infs
+    assert f'{inf:e}' == 'inf'
+    assert f'{inf:+e}' == '+inf'
+    assert f'{inf:E}' == 'INF'
+    assert f'{inf:+E}' == '+INF'
+
+    assert f'{ninf:e}' == '-inf'
+    assert f'{ninf:+e}' == '-inf'
+    assert f'{ninf:E}' == '-INF'
+    assert f'{ninf:+E}' == '-INF'
+
+    assert f'{nan:e}' == 'nan'
+    assert f'{nan:+e}' == '+nan'
+    assert f'{nan:E}' == 'NAN'
+    assert f'{nan:+E}' == '+NAN'
+
     # precision 0.  no decimal point in the output
     assert f'{mp.mpf(0.01):.0e}' == '1e-02'
     assert f'{mp.mpf(0.1):.0e}' == '1e-01'
@@ -287,6 +321,22 @@ def test_mpf_fmt_cpython():
     assert f'{mp.mpf(0):.50g}' == '0'
     assert f'{mp.mpf(0):.100g}' == '0'
 
+    # nan, infs
+    assert f'{inf:g}' == 'inf'
+    assert f'{inf:+g}' == '+inf'
+    assert f'{inf:G}' == 'INF'
+    assert f'{inf:+G}' == '+INF'
+
+    assert f'{ninf:g}' == '-inf'
+    assert f'{ninf:+g}' == '-inf'
+    assert f'{ninf:G}' == '-INF'
+    assert f'{ninf:+G}' == '-INF'
+
+    assert f'{nan:g}' == 'nan'
+    assert f'{nan:+g}' == '+nan'
+    assert f'{nan:G}' == 'NAN'
+    assert f'{nan:+G}' == '+NAN'
+
     # precision 0 doesn't make a lot of sense for the 'g' code (what does
     # it mean to have no significant digits?); in practice, it's interpreted
     # as identical to precision 1
@@ -422,6 +472,11 @@ def test_mpf_float():
             num = -num
         assert fmt_str.format(num) == fmt_str.format(mp.mpf(num))
 
+    # Test the same random formats with special numbers
+    for num in (inf, -inf, nan, 0):
+        fmt_str = random_fmt()
+        assert fmt_str.format(fp.mpf(num)) == fmt_str.format(mp.mpf(num))
+
 
 def test_mpf_fmt():
     '''
@@ -505,58 +560,6 @@ def test_mpf_fmt():
         assert f"{mp.mpf('0.24'):=+020.2g}" == '+0000000000000000.24'
 
 
-def test_mpf_fmt_special():
-    assert f'{inf:f}' == 'inf'
-    assert f'{inf:+f}' == '+inf'
-    assert f'{inf:F}' == 'INF'
-    assert f'{inf:+F}' == '+INF'
-
-    assert f'{ninf:f}' == '-inf'
-    assert f'{ninf:+f}' == '-inf'
-    assert f'{ninf:F}' == '-INF'
-    assert f'{ninf:+F}' == '-INF'
-
-    assert f'{nan:f}' == 'nan'
-    assert f'{nan:+f}' == '+nan'
-    assert f'{nan:F}' == 'NAN'
-    assert f'{nan:+F}' == '+NAN'
-
-    assert f'{inf:e}' == 'inf'
-    assert f'{inf:+e}' == '+inf'
-    assert f'{inf:E}' == 'INF'
-    assert f'{inf:+E}' == '+INF'
-
-    assert f'{ninf:e}' == '-inf'
-    assert f'{ninf:+e}' == '-inf'
-    assert f'{ninf:E}' == '-INF'
-    assert f'{ninf:+E}' == '-INF'
-
-    assert f'{nan:e}' == 'nan'
-    assert f'{nan:+e}' == '+nan'
-    assert f'{nan:E}' == 'NAN'
-    assert '{:+E}'.format(nan) == '+NAN'
-
-    assert f'{inf:g}' == 'inf'
-    assert f'{inf:+g}' == '+inf'
-    assert f'{inf:G}' == 'INF'
-    assert f'{inf:+G}' == '+INF'
-
-    assert f'{ninf:g}' == '-inf'
-    assert f'{ninf:+g}' == '-inf'
-    assert f'{ninf:G}' == '-INF'
-    assert f'{ninf:+G}' == '-INF'
-
-    assert f'{nan:g}' == 'nan'
-    assert f'{nan:+g}' == '+nan'
-    assert f'{nan:G}' == 'NAN'
-    assert f'{nan:+G}' == '+NAN'
-
-    # Test the same random formats with special numbers
-    for num in (float('inf'), -float('inf'), float('nan'), 0):
-        fmt_str = random_fmt()
-        assert fmt_str.format(fp.mpf(num)) == fmt_str.format(mp.mpf(num))
-
-
 def test_errors():
     with pytest.raises(ValueError):
         # wrong format type