diff --git a/build/pkgs/configure/checksums.ini b/build/pkgs/configure/checksums.ini index 7376965e60d..958540b27ac 100644 --- a/build/pkgs/configure/checksums.ini +++ b/build/pkgs/configure/checksums.ini @@ -1,3 +1,3 @@ tarball=configure-VERSION.tar.gz -sha1=259cd70efeb5bfc37ab38d67a9d84e8fc368e603 -sha256=fe3d338452726766f95ba9df3325d8cd03548c0e45d2c909006b395ad31b8326 +sha1=23d03e2490d2ca11df54205b30070146aff646d2 +sha256=ddb970bc230a14466f0cd6646e7f0a8b65dabcd4f8f21dd97fbb16b2a1040d27 diff --git a/build/pkgs/configure/package-version.txt b/build/pkgs/configure/package-version.txt index e8e6262e9c4..246341f9998 100644 --- a/build/pkgs/configure/package-version.txt +++ b/build/pkgs/configure/package-version.txt @@ -1 +1 @@ -79b13118ab80f055d3054745134cd02bb2660127 +c587f28bff5c719735489d324ec8e5a2b54076b3 diff --git a/pkgs/sagemath-categories/known-test-failures.json b/pkgs/sagemath-categories/known-test-failures.json index 80705635b7e..23e9f1adeeb 100644 --- a/pkgs/sagemath-categories/known-test-failures.json +++ b/pkgs/sagemath-categories/known-test-failures.json @@ -807,6 +807,10 @@ "failed": true, "ntests": 0 }, + "sage.doctest.check_tolerance": { + "failed": true, + "ntests": 19 + }, "sage.doctest.control": { "failed": true, "ntests": 230 @@ -821,13 +825,21 @@ "failed": true, "ntests": 413 }, + "sage.doctest.marked_output": { + "failed": true, + "ntests": 21 + }, "sage.doctest.parsing": { "failed": true, - "ntests": 313 + "ntests": 275 }, "sage.doctest.reporting": { "ntests": 115 }, + "sage.doctest.rif_tol": { + "failed": true, + "ntests": 18 + }, "sage.doctest.sources": { "failed": true, "ntests": 343 diff --git a/src/sage/doctest/check_tolerance.py b/src/sage/doctest/check_tolerance.py new file mode 100644 index 00000000000..356369151aa --- /dev/null +++ b/src/sage/doctest/check_tolerance.py @@ -0,0 +1,259 @@ +# sage_setup: distribution = sagemath-repl +""" +Check tolerance when parsing docstrings +""" + +# **************************************************************************** +# Copyright (C) 2012-2018 David Roe +# 2012 Robert Bradshaw +# 2012 William Stein +# 2013 R. Andrew Ohana +# 2013 Volker Braun +# 2013-2018 Jeroen Demeyer +# 2016-2021 Frédéric Chapoton +# 2017-2018 Erik M. Bray +# 2020 Marc Mezzarobba +# 2020-2023 Matthias Koeppe +# 2022 John H. Palmieri +# 2022 Sébastien Labbé +# 2023 Kwankyu Lee +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + +import re +from sage.doctest.rif_tol import RIFtol, add_tolerance +from sage.doctest.marked_output import MarkedOutput + + +# Regex pattern for float without the (optional) leading sign +float_without_sign = r'((\d*\.?\d+)|(\d+\.?))([eE][+-]?\d+)?' + + +# Regular expression for floats +float_regex = re.compile(r'\s*([+-]?\s*' + float_without_sign + r')') + + +class ToleranceExceededError(BaseException): + pass + + +def check_tolerance_real_domain(want: MarkedOutput, got: str) -> tuple[str, str]: + """ + Compare want and got over real domain with tolerance + + INPUT: + + - ``want`` -- a string, what you want + - ``got`` -- a string, what you got + + OUTPUT: + + The strings to compare, but with matching float numbers replaced by asterisk. + + EXAMPLES:: + + sage: from sage.doctest.check_tolerance import check_tolerance_real_domain + sage: from sage.doctest.marked_output import MarkedOutput + sage: check_tolerance_real_domain( + ....: MarkedOutput('foo:0.2').update(abs_tol=0.3), + ....: 'bar:0.4') + ['foo:*', 'bar:*'] + sage: check_tolerance_real_domain( + ....: MarkedOutput('foo:0.2').update(abs_tol=0.3), + ....: 'bar:0.6') + Traceback (most recent call last): + ... + sage.doctest.check_tolerance.ToleranceExceededError + """ + # First check that the number of occurrences of floats appearing match + want_str = [g[0] for g in float_regex.findall(want)] + got_str = [g[0] for g in float_regex.findall(got)] + if len(want_str) != len(got_str): + raise ToleranceExceededError() + + # Then check the numbers + want_values = [RIFtol(g) for g in want_str] + want_intervals = [add_tolerance(v, want) for v in want_values] + got_values = [RIFtol(g) for g in got_str] + # The doctest is not successful if one of the "want" and "got" + # intervals have an empty intersection + if not all(a.overlaps(b) for a, b in zip(want_intervals, got_values)): + raise ToleranceExceededError() + + # Then check the part of the doctests without the numbers + # Continue the check process with floats replaced by stars + want = float_regex.sub('*', want) + got = float_regex.sub('*', got) + return [want, got] + + +# match 1.0 or 1.0 + I or 1.0 + 2.0*I +real_plus_optional_imag = ''.join([ + r'\s*(?P[+-]?\s*', + float_without_sign, + r')(\s*(?P[+-]\s*', + float_without_sign, + r')\*I|\s*(?P[+-])\s*I)?', +]) + + +# match - 2.0*I +only_imag = ''.join([ + r'\s*(?P[+-]?\s*', + float_without_sign, + r')\*I', +]) + + +# match I or -I (no digits), require a non-word part before and after for specificity +imaginary_unit = r'(?P^|\W)(?P[+-]?)I(?P$|\W)' + + +complex_regex = re.compile(''.join([ + '(', + only_imag, + '|', + imaginary_unit, + '|', + real_plus_optional_imag, + ')', +])) + + +def complex_match_to_real_and_imag(m: re.Match) -> tuple[str, str]: + """ + Extract real and imaginary part from match + + INPUT: + + - ``m`` -- match from ``complex_regex`` + + OUTPUT: + + Pair of real and complex parts (as string) + + EXAMPLES:: + + sage: from sage.doctest.check_tolerance import complex_match_to_real_and_imag, complex_regex + sage: complex_match_to_real_and_imag(complex_regex.match('1.0')) + ('1.0', '0') + sage: complex_match_to_real_and_imag(complex_regex.match('-1.0 - I')) + ('-1.0', '-1') + sage: complex_match_to_real_and_imag(complex_regex.match('1.0 - 3.0*I')) + ('1.0', '- 3.0') + sage: complex_match_to_real_and_imag(complex_regex.match('1.0*I')) + ('0', '1.0') + sage: complex_match_to_real_and_imag(complex_regex.match('- 2.0*I')) + ('0', '- 2.0') + sage: complex_match_to_real_and_imag(complex_regex.match('-I')) + ('0', '-1') + sage: for match in complex_regex.finditer('[1, -1, I, -1, -I]'): + ....: print(complex_match_to_real_and_imag(match)) + ('1', '0') + ('-1', '0') + ('0', '1') + ('-1', '0') + ('0', '-1') + sage: for match in complex_regex.finditer('[1, -1.3, -1.5 + 0.1*I, 0.5 - 0.1*I, -1.5*I]'): + ....: print(complex_match_to_real_and_imag(match)) + ('1', '0') + ('-1.3', '0') + ('-1.5', '+ 0.1') + ('0.5', '- 0.1') + ('0', '-1.5') + """ + real = m.group('real') + if real is not None: + real_imag_coeff = m.group('real_imag_coeff') + real_imag_unit = m.group('real_imag_unit') + if real_imag_coeff is not None: + return (real, real_imag_coeff) + elif real_imag_unit is not None: + return (real, real_imag_unit + '1') + else: + return (real, '0') + only_imag = m.group('only_imag') + if only_imag is not None: + return ('0', only_imag) + unit_imag = m.group('unit_imag') + if unit_imag is not None: + return ('0', unit_imag + '1') + assert False, 'unreachable' + + +def complex_star_repl(m: re.Match): + """ + Replace the complex number in the match with '*' + """ + if m.group('unit_imag') is not None: + # preserve the matched non-word part + return ''.join([ + (m.group('unit_imag_pre') or '').strip(), + '*', + (m.group('unit_imag_post') or '').strip(), + ]) + else: + return '*' + + +def check_tolerance_complex_domain(want: MarkedOutput, got: str) -> tuple[str, str]: + """ + Compare want and got over complex domain with tolerance + + INPUT: + + - ``want`` -- a string, what you want + - ``got`` -- a string, what you got + + OUTPUT: + + The strings to compare, but with matching complex numbers replaced by asterisk. + + EXAMPLES:: + + sage: from sage.doctest.check_tolerance import check_tolerance_complex_domain + sage: from sage.doctest.marked_output import MarkedOutput + sage: check_tolerance_complex_domain( + ....: MarkedOutput('foo:[0.2 + 0.1*I]').update(abs_tol=0.3), + ....: 'bar:[0.4]') + ['foo:[*]', 'bar:[*]'] + sage: check_tolerance_complex_domain( + ....: MarkedOutput('foo:-0.5 - 0.1*I').update(abs_tol=2), + ....: 'bar:1') + ['foo:*', 'bar:*'] + sage: check_tolerance_complex_domain( + ....: MarkedOutput('foo:[1.0*I]').update(abs_tol=0.3), + ....: 'bar:[I]') + ['foo:[*]', 'bar:[*]'] + sage: check_tolerance_complex_domain(MarkedOutput('foo:0.2 + 0.1*I').update(abs_tol=0.3), 'bar:0.6') + Traceback (most recent call last): + ... + sage.doctest.check_tolerance.ToleranceExceededError + """ + want_str = [] + for match in complex_regex.finditer(want): + want_str.extend(complex_match_to_real_and_imag(match)) + got_str = [] + for match in complex_regex.finditer(got): + got_str.extend(complex_match_to_real_and_imag(match)) + if len(want_str) != len(got_str): + raise ToleranceExceededError() + + # Then check the numbers + want_values = [RIFtol(g) for g in want_str] + want_intervals = [add_tolerance(v, want) for v in want_values] + got_values = [RIFtol(g) for g in got_str] + # The doctest is not successful if one of the "want" and "got" + # intervals have an empty intersection + if not all(a.overlaps(b) for a, b in zip(want_intervals, got_values)): + raise ToleranceExceededError() + + # Then check the part of the doctests without the numbers + # Continue the check process with floats replaced by stars + want = complex_regex.sub(complex_star_repl, want) + got = complex_regex.sub(complex_star_repl, got) + return [want, got] diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index 4908959e497..34c7e1299c5 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -970,7 +970,7 @@ def expand_files_into_sources(self): sage: DC = DocTestController(DD, [dirname]) sage: DC.expand_files_into_sources() sage: len(DC.sources) - 12 + 15 sage: DC.sources[0].options.optional True @@ -1071,13 +1071,16 @@ def sort_sources(self): sage.doctest.util sage.doctest.test sage.doctest.sources + sage.doctest.rif_tol sage.doctest.reporting sage.doctest.parsing_test sage.doctest.parsing + sage.doctest.marked_output sage.doctest.forker sage.doctest.fixtures sage.doctest.external sage.doctest.control + sage.doctest.check_tolerance sage.doctest.all sage.doctest """ diff --git a/src/sage/doctest/marked_output.py b/src/sage/doctest/marked_output.py new file mode 100644 index 00000000000..e2f08443ea7 --- /dev/null +++ b/src/sage/doctest/marked_output.py @@ -0,0 +1,102 @@ +# sage_setup: distribution = sagemath-repl +""" +Helper for attaching tolerance information to strings +""" + +# **************************************************************************** +# Copyright (C) 2012-2018 David Roe +# 2012 Robert Bradshaw +# 2012 William Stein +# 2013 R. Andrew Ohana +# 2013 Volker Braun +# 2013-2018 Jeroen Demeyer +# 2016-2021 Frédéric Chapoton +# 2017-2018 Erik M. Bray +# 2020 Marc Mezzarobba +# 2020-2023 Matthias Koeppe +# 2022 John H. Palmieri +# 2022 Sébastien Labbé +# 2023 Kwankyu Lee +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + + +class MarkedOutput(str): + """ + A subclass of string with context for whether another string + matches it. + + EXAMPLES:: + + sage: from sage.doctest.marked_output import MarkedOutput + sage: s = MarkedOutput("abc") + sage: s.rel_tol + 0 + sage: s.update(rel_tol = .05) + 'abc' + sage: s.rel_tol + 0.0500000000000000 + + sage: MarkedOutput("56 µs") + '56 \xb5s' + """ + random = False + rel_tol = 0 + abs_tol = 0 + tol = 0 + + def update(self, **kwds): + """ + EXAMPLES:: + + sage: from sage.doctest.marked_output import MarkedOutput + sage: s = MarkedOutput("0.0007401") + sage: s.update(abs_tol = .0000001) + '0.0007401' + sage: s.rel_tol + 0 + sage: s.abs_tol + 1.00000000000000e-7 + """ + self.__dict__.update(kwds) + return self + + def __reduce__(self): + """ + Pickling. + + EXAMPLES:: + + sage: from sage.doctest.marked_output import MarkedOutput + sage: s = MarkedOutput("0.0007401") + sage: s.update(abs_tol = .0000001) + '0.0007401' + sage: t = loads(dumps(s)) # indirect doctest + sage: t == s + True + sage: t.abs_tol + 1.00000000000000e-7 + """ + return make_marked_output, (str(self), self.__dict__) + + +def make_marked_output(s, D): + """ + Auxiliary function for pickling. + + EXAMPLES:: + + sage: from sage.doctest.marked_output import make_marked_output + sage: s = make_marked_output("0.0007401", {'abs_tol':.0000001}) + sage: s + '0.0007401' + sage: s.abs_tol + 1.00000000000000e-7 + """ + ans = MarkedOutput(s) + ans.__dict__.update(D) + return ans diff --git a/src/sage/doctest/parsing.py b/src/sage/doctest/parsing.py index 92dd31384b3..d9b054ae2dd 100644 --- a/src/sage/doctest/parsing.py +++ b/src/sage/doctest/parsing.py @@ -43,53 +43,14 @@ from sage.misc.cachefunc import cached_function from sage.repl.preparse import preparse, strip_string_literals +from sage.doctest.rif_tol import RIFtol, add_tolerance +from sage.doctest.marked_output import MarkedOutput +from sage.doctest.check_tolerance import ( + ToleranceExceededError, check_tolerance_real_domain, + check_tolerance_complex_domain, float_regex) from .external import available_software, external_software -_RIFtol = None - - -def RIFtol(*args): - """ - Create an element of the real interval field used for doctest tolerances. - - It allows large numbers like 1e1000, it parses strings with spaces - like ``RIF(" - 1 ")`` out of the box and it carries a lot of - precision. The latter is useful for testing libraries using - arbitrary precision but not guaranteed rounding such as PARI. We use - 1044 bits of precision, which should be good to deal with tolerances - on numbers computed with 1024 bits of precision. - - The interval approach also means that we do not need to worry about - rounding errors and it is also very natural to see a number with - tolerance as an interval. - - EXAMPLES:: - - sage: from sage.doctest.parsing import RIFtol - sage: RIFtol(-1, 1) - 0.? - sage: RIFtol(" - 1 ") - -1 - sage: RIFtol("1e1000") - 1.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000?e1000 - """ - global _RIFtol - if _RIFtol is None: - try: - # We need to import from sage.all to avoid circular imports. - from sage.rings.real_mpfi import RealIntervalField - except ImportError: - from warnings import warn - warn("RealIntervalField not available, ignoring all tolerance specifications in doctests") - - def fake_RIFtol(*args): - return 0 - _RIFtol = fake_RIFtol - else: - _RIFtol = RealIntervalField(1044) - return _RIFtol(*args) - # This is the correct pattern to match ISO/IEC 6429 ANSI escape sequences: ansi_escape_sequence = re.compile(r"(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])") @@ -691,83 +652,6 @@ def reduce_hex(fingerprints): return "%032x" % res -class MarkedOutput(str): - """ - A subclass of string with context for whether another string - matches it. - - EXAMPLES:: - - sage: from sage.doctest.parsing import MarkedOutput - sage: s = MarkedOutput("abc") - sage: s.rel_tol - 0 - sage: s.update(rel_tol = .05) - 'abc' - sage: s.rel_tol - 0.0500000000000000 - - sage: MarkedOutput("56 µs") - '56 \xb5s' - """ - random = False - rel_tol = 0 - abs_tol = 0 - tol = 0 - - def update(self, **kwds): - """ - EXAMPLES:: - - sage: from sage.doctest.parsing import MarkedOutput - sage: s = MarkedOutput("0.0007401") - sage: s.update(abs_tol = .0000001) - '0.0007401' - sage: s.rel_tol - 0 - sage: s.abs_tol - 1.00000000000000e-7 - """ - self.__dict__.update(kwds) - return self - - def __reduce__(self): - """ - Pickling. - - EXAMPLES:: - - sage: from sage.doctest.parsing import MarkedOutput - sage: s = MarkedOutput("0.0007401") - sage: s.update(abs_tol = .0000001) - '0.0007401' - sage: t = loads(dumps(s)) # indirect doctest - sage: t == s - True - sage: t.abs_tol - 1.00000000000000e-7 - """ - return make_marked_output, (str(self), self.__dict__) - - -def make_marked_output(s, D): - """ - Auxiliary function for pickling. - - EXAMPLES:: - - sage: from sage.doctest.parsing import make_marked_output - sage: s = make_marked_output("0.0007401", {'abs_tol':.0000001}) - sage: s - '0.0007401' - sage: s.abs_tol - 1.00000000000000e-7 - """ - ans = MarkedOutput(s) - ans.__dict__.update(D) - return ans - - class OriginalSource(): r""" Context swapping out the pre-parsed source with the original for @@ -985,7 +869,7 @@ def parse(self, string, *args): sage: ex.want '0.893515349287690\n' sage: type(ex.want) - + sage: ex.want.tol 2.000000000000000000...?e-11 @@ -1323,7 +1207,7 @@ class SageOutputChecker(doctest.OutputChecker): sage: ex.want '0.893515349287690\n' sage: type(ex.want) - + sage: ex.want.tol 2.000000000000000000...?e-11 sage: OC.check_output(ex.want, '0.893515349287690', optflag) @@ -1363,56 +1247,6 @@ def human_readable(match): return '' return ansi_escape_sequence.subn(human_readable, string)[0] - def add_tolerance(self, wantval, want): - """ - Enlarge the real interval element ``wantval`` according to - the tolerance options in ``want``. - - INPUT: - - - ``wantval`` -- a real interval element - - ``want`` -- a :class:`MarkedOutput` describing the tolerance - - OUTPUT: an interval element containing ``wantval`` - - EXAMPLES:: - - sage: from sage.doctest.parsing import MarkedOutput, SageOutputChecker - sage: OC = SageOutputChecker() - sage: want_tol = MarkedOutput().update(tol=0.0001) - sage: want_abs = MarkedOutput().update(abs_tol=0.0001) - sage: want_rel = MarkedOutput().update(rel_tol=0.0001) - sage: OC.add_tolerance(RIF(pi.n(64)), want_tol).endpoints() # needs sage.symbolic - (3.14127849432443, 3.14190681285516) - sage: OC.add_tolerance(RIF(pi.n(64)), want_abs).endpoints() # needs sage.symbolic - (3.14149265358979, 3.14169265358980) - sage: OC.add_tolerance(RIF(pi.n(64)), want_rel).endpoints() # needs sage.symbolic - (3.14127849432443, 3.14190681285516) - sage: OC.add_tolerance(RIF(1e1000), want_tol) - 1.000?e1000 - sage: OC.add_tolerance(RIF(1e1000), want_abs) - 1.000000000000000?e1000 - sage: OC.add_tolerance(RIF(1e1000), want_rel) - 1.000?e1000 - sage: OC.add_tolerance(0, want_tol) - 0.000? - sage: OC.add_tolerance(0, want_abs) - 0.000? - sage: OC.add_tolerance(0, want_rel) - 0 - """ - if want.tol: - if wantval == 0: - return RIFtol(want.tol) * RIFtol(-1, 1) - else: - return wantval * (1 + RIFtol(want.tol) * RIFtol(-1, 1)) - elif want.abs_tol: - return wantval + RIFtol(want.abs_tol) * RIFtol(-1, 1) - elif want.rel_tol: - return wantval * (1 + RIFtol(want.rel_tol) * RIFtol(-1, 1)) - else: - return wantval - def check_output(self, want, got, optionflags): r""" Check to see if the output matches the desired output. @@ -1509,6 +1343,11 @@ def check_output(self, want, got, optionflags): sage: 0 # rel tol 1 1 + Abs tol checks over the complex domain:: + + sage: [1, -1.3, -1.5 + 0.1*I, 0.5 - 0.1*I, -1.5*I] # abs tol 1.0 + [1, -1, -1, 1, -I] + Spaces before numbers or between the sign and number are ignored:: sage: print("[ - 1, 2]") # abs tol 1e-10 @@ -1539,34 +1378,17 @@ def check_output(self, want, got, optionflags): sage: OC.check_output(ex.want, 'Long-step dual simplex will be used\n1.3090169943749475', optflag) True """ - # Regular expression for floats - float_regex = re.compile(r'\s*([+-]?\s*((\d*\.?\d+)|(\d+\.?))([eE][+-]?\d+)?)') - got = self.human_readable_escape_sequences(got) - - if isinstance(want, MarkedOutput): - if want.random: - return True - elif want.tol or want.rel_tol or want.abs_tol: - # First check that the number of occurrences of floats appearing match - want_str = [g[0] for g in float_regex.findall(want)] - got_str = [g[0] for g in float_regex.findall(got)] - if len(want_str) != len(got_str): - return False - - # Then check the numbers - want_values = [RIFtol(g) for g in want_str] - want_intervals = [self.add_tolerance(v, want) for v in want_values] - got_values = [RIFtol(g) for g in got_str] - # The doctest is not successful if one of the "want" and "got" - # intervals have an empty intersection - if not all(a.overlaps(b) for a, b in zip(want_intervals, got_values)): - return False - - # Then check the part of the doctests without the numbers - # Continue the check process with floats replaced by stars - want = float_regex.sub('*', want) - got = float_regex.sub('*', got) + try: + if isinstance(want, MarkedOutput): + if want.random: + return True + elif want.tol or want.rel_tol: + want, got = check_tolerance_real_domain(want, got) + elif want.abs_tol: + want, got = check_tolerance_complex_domain(want, got) + except ToleranceExceededError: + return False if doctest.OutputChecker.check_output(self, want, got, optionflags): return True @@ -1823,9 +1645,6 @@ def output_difference(self, example, got, optionflags): Tolerance exceeded: 0.0 vs 10.05, tolerance +infinity > 1e-1 """ - # Regular expression for floats - float_regex = re.compile(r'\s*([+-]?\s*((\d*\.?\d+)|(\d+\.?))([eE][+-]?\d+)?)') - got = self.human_readable_escape_sequences(got) want = example.want diff = doctest.OutputChecker.output_difference(self, example, got, optionflags) @@ -1847,7 +1666,7 @@ def fail(x, y, actual, desired): for wstr, gstr in zip(want_str, got_str): w = RIFtol(wstr) g = RIFtol(gstr) - if not g.overlaps(self.add_tolerance(w, want)): + if not g.overlaps(add_tolerance(w, want)): if want.tol: if not w: fail(wstr, gstr, abs(g), want.tol) diff --git a/src/sage/doctest/rif_tol.py b/src/sage/doctest/rif_tol.py new file mode 100644 index 00000000000..619b2e500cd --- /dev/null +++ b/src/sage/doctest/rif_tol.py @@ -0,0 +1,123 @@ +# sage_setup: distribution = sagemath-repl +""" +Helpers for tolerance checking in doctests +""" + +# **************************************************************************** +# Copyright (C) 2012-2018 David Roe +# 2012 Robert Bradshaw +# 2012 William Stein +# 2013 R. Andrew Ohana +# 2013 Volker Braun +# 2013-2018 Jeroen Demeyer +# 2016-2021 Frédéric Chapoton +# 2017-2018 Erik M. Bray +# 2020 Marc Mezzarobba +# 2020-2023 Matthias Koeppe +# 2022 John H. Palmieri +# 2022 Sébastien Labbé +# 2023 Kwankyu Lee +# +# Distributed under the terms of the GNU General Public License (GPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + +from sage.doctest.marked_output import MarkedOutput + + +_RIFtol = None + + +def RIFtol(*args): + """ + Create an element of the real interval field used for doctest tolerances. + + It allows large numbers like 1e1000, it parses strings with spaces + like ``RIF(" - 1 ")`` out of the box and it carries a lot of + precision. The latter is useful for testing libraries using + arbitrary precision but not guaranteed rounding such as PARI. We use + 1044 bits of precision, which should be good to deal with tolerances + on numbers computed with 1024 bits of precision. + + The interval approach also means that we do not need to worry about + rounding errors and it is also very natural to see a number with + tolerance as an interval. + + EXAMPLES:: + + sage: from sage.doctest.parsing import RIFtol + sage: RIFtol(-1, 1) + 0.? + sage: RIFtol(" - 1 ") + -1 + sage: RIFtol("1e1000") + 1.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000?e1000 + """ + global _RIFtol + if _RIFtol is None: + try: + # We need to import from sage.all to avoid circular imports. + from sage.rings.real_mpfi import RealIntervalField + except ImportError: + from warnings import warn + warn("RealIntervalField not available, ignoring all tolerance specifications in doctests") + + def fake_RIFtol(*args): + return 0 + _RIFtol = fake_RIFtol + else: + _RIFtol = RealIntervalField(1044) + return _RIFtol(*args) + + +def add_tolerance(wantval, want: MarkedOutput): + """ + Enlarge the real interval element ``wantval`` according to + the tolerance options in ``want``. + + INPUT: + + - ``wantval`` -- a real interval element + - ``want`` -- a :class:`MarkedOutput` describing the tolerance + + OUTPUT: an interval element containing ``wantval`` + + EXAMPLES:: + + sage: from sage.doctest.parsing import MarkedOutput, SageOutputChecker + sage: from sage.doctest.rif_tol import add_tolerance + sage: want_tol = MarkedOutput().update(tol=0.0001) + sage: want_abs = MarkedOutput().update(abs_tol=0.0001) + sage: want_rel = MarkedOutput().update(rel_tol=0.0001) + sage: add_tolerance(RIF(pi.n(64)), want_tol).endpoints() # needs sage.symbolic + (3.14127849432443, 3.14190681285516) + sage: add_tolerance(RIF(pi.n(64)), want_abs).endpoints() # needs sage.symbolic + (3.14149265358979, 3.14169265358980) + sage: add_tolerance(RIF(pi.n(64)), want_rel).endpoints() # needs sage.symbolic + (3.14127849432443, 3.14190681285516) + sage: add_tolerance(RIF(1e1000), want_tol) + 1.000?e1000 + sage: add_tolerance(RIF(1e1000), want_abs) + 1.000000000000000?e1000 + sage: add_tolerance(RIF(1e1000), want_rel) + 1.000?e1000 + sage: add_tolerance(0, want_tol) + 0.000? + sage: add_tolerance(0, want_abs) + 0.000? + sage: add_tolerance(0, want_rel) + 0 + """ + if want.tol: + if wantval == 0: + return RIFtol(want.tol) * RIFtol(-1, 1) + else: + return wantval * (1 + RIFtol(want.tol) * RIFtol(-1, 1)) + elif want.abs_tol: + return wantval + RIFtol(want.abs_tol) * RIFtol(-1, 1) + elif want.rel_tol: + return wantval * (1 + RIFtol(want.rel_tol) * RIFtol(-1, 1)) + else: + return wantval