Repository: mpmath/mpmath
Pull request #963 (2025-06-01 10:21:54)

Support rounding property for the mp context
See also #683


Patch:
diff --git a/CHANGES b/CHANGES
index 8268a2c..bc7a963 100644
--- a/CHANGES
+++ b/CHANGES
@@ -44,7 +44,7 @@ Features:
 * Support 'b' (binary) format type for mpf/mpc, see #867 (Sergey B Kirpichev)
 * Implement mpf.__floordiv__() and mpf.__divmod__(), see #873 (Sergey B
   Kirpichev)
-* Add parameters for MPContext constructor, see #876 (Sergey B Kirpichev)
+* Add parameters for MPContext constructor, see #876 and #963 (Sergey B Kirpichev)
 * Add MPFR-compatible aliases for rounding modes, see #892 (Sergey B
   Kirpichev)
 * Support negative indexes in matrix, see #897 (Riccardo Orsi)
@@ -59,6 +59,7 @@ Features:
 * Use PyREPL, as fallback (no IPython), see #941 (Sergey B Kirpichev)
 * Add exp2() and log2(), see #948 (Sergey B Kirpichev)
 * Add isspecial() method for contexts, see #949 (Sergey B Kirpichev)
+* Support rounding property for the mp context, see #963 (Sergey B Kirpichev)
 
 Compatibility:
 
diff --git a/docs/basics.rst b/docs/basics.rst
index db94131..684a049 100644
--- a/docs/basics.rst
+++ b/docs/basics.rst
@@ -80,6 +80,7 @@ Mpmath uses a global working precision; it does not keep track of the precision
     Mpmath settings:
       mp.prec = 53                [default: 53]
       mp.dps = 15                 [default: 15]
+      mp.rounding = 'n'           [default: 'n']
       mp.trap_complex = False     [default: False]
 
 The term **prec** denotes the binary precision (measured in bits) while **dps** (short for *decimal places*) is the decimal precision. Binary and decimal precision are related roughly according to the formula ``prec = 3.33*dps``. For example, it takes a precision of roughly 333 bits to hold an approximation of pi that is accurate to 100 decimal places (actually slightly more than 333 bits is used).
@@ -120,6 +121,17 @@ Or why not 1 googolplex:
 
 The (binary) exponent is stored exactly and is independent of the precision.
 
+The ``rounding`` property control default rounding mode for the context:
+
+    >>> mp.rounding  # round to nearest
+    'n'
+    >>> sin(1)
+    mpf('0.8414709848078965')
+    >>> mp.rounding = 'u'  # round up
+    >>> sin(1)
+    mpf('0.84147098480789662')
+    >>> mp.rounding = 'n'
+
 Temporarily changing the precision
 ..................................
 
diff --git a/mpmath/ctx_mp.py b/mpmath/ctx_mp.py
index 37c4a98..4fd35fb 100644
--- a/mpmath/ctx_mp.py
+++ b/mpmath/ctx_mp.py
@@ -18,7 +18,7 @@
                     mpf_degree, mpf_div, mpf_e, mpf_euler, mpf_glaisher,
                     mpf_khinchin, mpf_ln2, mpf_ln10, mpf_mertens, mpf_mul,
                     mpf_neg, mpf_phi, mpf_pi, mpf_rand, mpf_sub, mpf_twinprime,
-                    repr_dps, to_man_exp, to_str)
+                    repr_dps, to_man_exp, to_str, round_nearest)
 
 
 get_complex = re.compile(r"""
@@ -44,12 +44,14 @@ class MPContext(BaseMPContext, StandardBaseContext):
     Context for multiprecision arithmetic with a global precision.
     """
 
-    def __init__(ctx, prec=sys.float_info.mant_dig, trap_complex=False):
+    def __init__(ctx, prec=sys.float_info.mant_dig,
+                 rounding=round_nearest, trap_complex=False):
         BaseMPContext.__init__(ctx)
         ctx.pretty = False
         ctx.types = [ctx.mpf, ctx.mpc, ctx.constant]
         ctx.default()
         ctx._set_prec(prec)
+        ctx._set_rounding(rounding)
         ctx.trap_complex = trap_complex
         StandardBaseContext.__init__(ctx)
 
@@ -359,6 +361,7 @@ def __str__(ctx):
         lines = ["Mpmath settings:",
             ("  mp.prec = %s" % ctx.prec).ljust(30) + f"[default: {sys.float_info.mant_dig}]",
             ("  mp.dps = %s" % ctx.dps).ljust(30) + f"[default: {sys.float_info.dig}]",
+            ("  mp.rounding = '%s'" % ctx.rounding).ljust(30) + f"[default: 'n']",
             ("  mp.trap_complex = %s" % ctx.trap_complex).ljust(30) + "[default: False]",
         ]
         return "\n".join(lines)
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index fe0968c..255322a 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -130,13 +130,14 @@ def as_integer_ratio(self):
     def __reduce__(self): return _make_mpf, (self._mpf_,)
 
     def __repr__(s):
+        rounding = s.context._rounding
         if s.context.pretty:
             ndigits = (s.context._repr_digits
                        if s.context._pretty_repr_dps else s.context._str_digits)
-            return to_str(s._mpf_, ndigits)
-        return "mpf('%s')" % to_str(s._mpf_, s.context._repr_digits)
+            return to_str(s._mpf_, ndigits, rounding=rounding)
+        return "mpf('%s')" % to_str(s._mpf_, s.context._repr_digits, rounding=rounding)
 
-    def __str__(s): return to_str(s._mpf_, s.context._str_digits)
+    def __str__(s): return to_str(s._mpf_, s.context._str_digits, rounding=s.context._rounding)
     def __hash__(s): return mpf_hash(s._mpf_)
     def __int__(s): return int(to_int(s._mpf_))
     def __float__(s): return to_float(s._mpf_, rnd=s.context._prec_rounding[1])
@@ -730,6 +731,7 @@ def make_mpc(ctx, v):
 
     def default(ctx):
         ctx._prec = ctx._prec_rounding[0] = sys.float_info.mant_dig
+        ctx._rounding = ctx._prec_rounding[1]
         ctx._dps = sys.float_info.dig
         ctx.trap_complex = False
 
@@ -741,8 +743,16 @@ def _set_dps(ctx, n):
         ctx._prec = ctx._prec_rounding[0] = dps_to_prec(n)
         ctx._dps = max(1, int(n))
 
+    def _set_rounding(ctx, r):
+        try:
+            ctx._prec_rounding[1] = ctx._parse_prec({'rounding': r})[1]
+            ctx._rounding = ctx._prec_rounding[1]
+        except KeyError:
+            raise ValueError('invalid rounding mode')
+
     prec = property(lambda ctx: ctx._prec, _set_prec)
     dps = property(lambda ctx: ctx._dps, _set_dps)
+    rounding = property(lambda ctx: ctx._rounding, _set_rounding)
 
     def _set_pretty_dps(ctx, v):
         ctx._pretty_repr_dps = True if v == 'repr' else False


Test patch:
diff --git a/conftest.py b/conftest.py
index 3163aaf..edd8cf0 100644
--- a/conftest.py
+++ b/conftest.py
@@ -24,7 +24,8 @@ def pytest_configure(config):
 @pytest.fixture(autouse=True)
 def reset_mp_globals():
     from mpmath import mp, iv
-    mp.dps = 15
+    mp.prec = sys.float_info.mant_dig
     mp.pretty = False
-    iv.dps = 15
+    mp.rounding = 'n'
+    iv.prec = mp.prec
     iv.pretty = False
diff --git a/mpmath/tests/test_basic_ops.py b/mpmath/tests/test_basic_ops.py
index 701a19f..ca91476 100644
--- a/mpmath/tests/test_basic_ops.py
+++ b/mpmath/tests/test_basic_ops.py
@@ -288,11 +288,11 @@ def test_arithmetic_functions():
                 assert fneg(z1) == -(+z1)
 
 def test_exact_integer_arithmetic():
-    # XXX: re-fix this so that all operations are tested with all rounding modes
     random.seed(0)
     for prec in [6, 10, 25, 40, 100, 250, 725]:
         for rounding in ['d', 'u', 'f', 'c', 'n']:
             mp.dps = prec
+            mp.rounding = rounding
             M = 10**(prec-2)
             M2 = 10**(prec//2-2)
             for i in range(10):
@@ -665,3 +665,13 @@ def test_round_bulk(x, n):
             return
         assert nstr(mr, n=14, base=16, strip_zeros=False,
                     show_zero_exponent=True, binary_exp=True) == xr.hex()
+
+
+def test_rounding_prop():
+    assert mp.rounding == 'n'
+    assert mp.sin(1) == mpf('0x1.aed548f090ceep-1')
+    mp.rounding = 'u'
+    assert mp.rounding == 'u'
+    assert mp.sin(1) == mpf('0x1.aed548f090cefp-1')
+    with pytest.raises(ValueError):
+        mp.rounding = 'x'
diff --git a/mpmath/tests/test_functions.py b/mpmath/tests/test_functions.py
index 967902b..3486363 100644
--- a/mpmath/tests/test_functions.py
+++ b/mpmath/tests/test_functions.py
@@ -653,9 +653,8 @@ def test_root():
             r = nthroot(a, -n)
             r1 = pow(a, -mpf(1)/n)
             assert r.ae(r1)
-    # XXX: this is broken right now
     # tests for nthroot rounding
-    for rnd in ['nearest', 'up', 'down']:
+    for rnd in ['n', 'u', 'd']:
         mp.rounding = rnd
         for n in [-5, -3, 3, 5]:
             prec = 50
@@ -667,6 +666,7 @@ def test_root():
                 mp.prec = prec
                 r = nthroot(b, n)
                 assert r == a
+    mp.rounding = 'n'
     mp.dps = 30
     for n in range(3, 21):
         a = (random.random() + j*random.random())
diff --git a/mpmath/tests/test_functions2.py b/mpmath/tests/test_functions2.py
index 05edd39..1de0903 100644
--- a/mpmath/tests/test_functions2.py
+++ b/mpmath/tests/test_functions2.py
@@ -617,10 +617,7 @@ def test_hyper_u():
     assert hyperu(2,6,pi).ae(0.55804439825913399130)
     assert (hyperu((3,2),8,100+201j)*10**4).ae(-0.3797318333856738798 - 2.9974928453561707782j)
     assert (hyperu((5,2),(-1,2),-5000)*10**10).ae(-5.6681877926881664678j)
-    # XXX: fails because of undetected cancellation in low level series code
-    # Alternatively: could use asymptotic series here, if convergence test
-    # tweaked back to recognize this one
-    #assert (hyperu((5,2),(-1,2),-500)*10**7).ae(-1.82526906001593252847j)
+    assert (hyperu((5,2),(-1,2),-500)*10**7).ae(-1.82526906001593252847j)
 
 def test_hyper_2f0():
     assert hyper([1,2],[],3) == hyp2f0(1,2,3)
diff --git a/mpmath/tests/test_interval.py b/mpmath/tests/test_interval.py
index f39a270..531d34d 100644
--- a/mpmath/tests/test_interval.py
+++ b/mpmath/tests/test_interval.py
@@ -371,9 +371,8 @@ def test_interval_ae():
 def test_interval_nstr():
     iv.dps = n = 30
     x = mpi(1, 2)
-    # FIXME: error_dps should not be necessary
-    assert iv.nstr(x, n, mode='plusminus', error_dps=6) == '1.5 +- 0.5'
-    assert iv.nstr(x, n, mode='plusminus', use_spaces=False, error_dps=6) == '1.5+-0.5'
+    assert iv.nstr(x, n, mode='plusminus') == '1.5 +- 0.5'
+    assert iv.nstr(x, n, mode='plusminus', use_spaces=False) == '1.5+-0.5'
     assert iv.nstr(x, n, mode='percent') == '1.5 (33.33%)'
     assert iv.nstr(x, n, mode='brackets', use_spaces=False) == '[1.0,2.0]'
     assert iv.nstr(x, n, mode='brackets' , brackets=('<', '>')) == '<1.0, 2.0>'