From 4582b13795a4ab8f4c19ea8640dcd24eb8b86012 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sun, 1 Jun 2025 13:19:59 +0300 Subject: [PATCH] Support rounding property for the mp context --- CHANGES | 3 ++- conftest.py | 5 +++-- docs/basics.rst | 12 ++++++++++++ mpmath/ctx_mp.py | 7 +++++-- mpmath/ctx_mp_python.py | 16 +++++++++++++--- mpmath/tests/test_basic_ops.py | 12 +++++++++++- mpmath/tests/test_functions.py | 4 ++-- 7 files changed, 48 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index 8268a2cd..bc7a9630 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/conftest.py b/conftest.py index 3163aafc..edd8cf04 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/docs/basics.rst b/docs/basics.rst index db941313..684a049a 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 37c4a98d..4fd35fbd 100644 --- a/mpmath/ctx_mp.py +++ b/mpmath/ctx_mp.py @@ -18,7 +18,7 @@ from .libmp import (MPQ, MPZ_ONE, ComplexResult, dps_to_prec, finf, fnan, 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 @@ class MPContext(BaseMPContext, StandardBaseContext): 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 fe0968cd..255322ab 100644 --- a/mpmath/ctx_mp_python.py +++ b/mpmath/ctx_mp_python.py @@ -130,13 +130,14 @@ class _mpf(mpnumeric): 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 @@ class PythonMPContext: 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 @@ class PythonMPContext: 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 diff --git a/mpmath/tests/test_basic_ops.py b/mpmath/tests/test_basic_ops.py index 701a19f4..ca914767 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 967902b6..3486363d 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())