From 40a446d2e83aacf388311865e8f09be4d601c5e6 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 30 Jan 2023 16:38:32 -0300 Subject: [PATCH] More compatibility improvements in Accuracy and Precision. Adding tests. (#769) This PR is another fix coming from issues found when I was working on #766. In particular, the way in which Mathics handles Precision and Accuracy for real numbers with 0. nominal value. Also, a bug that prevents parsing Real numbers with fixed accuracy was fixed. --- CHANGES.rst | 9 +- SYMBOLS_MANIFEST.txt | 12 +-- mathics/builtin/atomic/numbers.py | 113 ++++++++++------------- mathics/builtin/numbers/constants.py | 62 ++++++++++++- mathics/core/expression.py | 7 +- mathics/core/number.py | 46 ++++++---- mathics/core/parser/convert.py | 15 +++- mathics/eval/numbers.py | 99 ++++++++++++++++++++ test/builtin/atomic/test_numbers.py | 129 +++++++++++++++++++-------- 9 files changed, 359 insertions(+), 133 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 774387872..5cb2870b8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -38,6 +38,8 @@ New Builtins #. ``Kurtosis`` #. ``ListLogPlot`` #. ``LogPlot`` +#. ``$MaxMachineNumber`` +#. ``$MinMachineNumber`` #. ``NumberLinePlot`` #. ``PauliMatrix`` #. ``Remove`` @@ -92,9 +94,10 @@ Bugs #. Units and Quantities were sometimes failing. Also they were omitted from documentation. #. Better handling of ``Infinite`` quantities. -#. Fix ``Precision`` compatibility with WMA. +#. Improved ``Precision`` and ``Accuracy``compatibility with WMA. In particular, ``Precision[0.]`` and ``Accuracy[0.]`` +#. Accuracy in numbers using the notation ``` n.nnn``acc ``` now is properly handled. + - PyPI Package requirements +++++++++++++++++++++++++ @@ -115,6 +118,8 @@ Enhancements #. ``Grid`` compatibility with WMA was improved. Now it supports non-uniform list of lists and lists with general elements. #. Support for BigEndian Big TIFF + + 5.0.2 ----- diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index eb74417e7..859bb0561 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -40,7 +40,9 @@ System`$Machine System`$MachineEpsilon System`$MachineName System`$MachinePrecision +System`$MaxMachineNumber System`$MaxPrecision +System`$MinMachineNumber System`$MinPrecision System`$ModuleNumber System`$OperatingSystem @@ -245,8 +247,8 @@ System`Continue System`ContinuedFraction System`Convert`B64Dump`B64Decode System`Convert`B64Dump`B64Encode -System`ConvertersDump`$extensionMappings -System`ConvertersDump`$formatMappings +System`ConvertersDump`$ExtensionMappings +System`ConvertersDump`$FormatMappings System`CoprimeQ System`CopyDirectory System`CopyFile @@ -613,7 +615,6 @@ System`MachinePrecision System`Magenta System`MakeBoxes System`ManhattanDistance -System`Manipulate System`MantissaExponent System`Map System`MapAt @@ -687,10 +688,10 @@ System`Null System`NullSpace System`Number System`NumberForm +System`NumberLinePlot System`NumberQ System`NumberString System`Numerator -System`NumberLinePlot System`NumericFunction System`NumericQ System`O @@ -739,7 +740,6 @@ System`Pi System`Pick System`PieChart System`Piecewise -System`PillowImageFilter System`Pink System`PixelValue System`PixelValuePositions @@ -781,7 +781,6 @@ System`Print System`PrintTrace System`Private`$ContextPathStack System`Private`$ContextStack -System`Private`ManipulateParameter System`Product System`ProductLog System`Projection @@ -830,6 +829,7 @@ System`Reap System`Record System`Rectangle System`RectangleBox +System`Rectangular System`Red System`RegularExpression System`RegularPolygon diff --git a/mathics/builtin/atomic/numbers.py b/mathics/builtin/atomic/numbers.py index d874957f3..c87d21f6b 100644 --- a/mathics/builtin/atomic/numbers.py +++ b/mathics/builtin/atomic/numbers.py @@ -24,32 +24,35 @@ from mathics.builtin.base import Builtin, Predefined, Test from mathics.core.atoms import ( - Complex, Integer, Integer0, Integer10, MachineReal, - MachineReal0, Number, Rational, - Real, ) from mathics.core.attributes import A_LISTABLE, A_PROTECTED from mathics.core.convert.python import from_python from mathics.core.expression import Expression from mathics.core.list import ListExpression -from mathics.core.number import dps, machine_epsilon, machine_precision +from mathics.core.number import ( + MACHINE_PRECISION_VALUE, + machine_epsilon, + machine_precision, +) from mathics.core.symbols import Symbol, SymbolDivide from mathics.core.systemsymbols import ( SymbolIndeterminate, SymbolInfinity, SymbolLog, + SymbolMachinePrecision, SymbolN, SymbolPrecision, SymbolRealDigits, SymbolRound, ) from mathics.eval.nevaluator import eval_N +from mathics.eval.numbers import eval_Accuracy, eval_Precision SymbolIntegerDigits = Symbol("IntegerDigits") SymbolIntegerExponent = Symbol("IntegerExponent") @@ -157,7 +160,8 @@ class Accuracy(Builtin):
examines the number of significant digits of $expr$ after the \ decimal point in the number x. - This is rather a proof-of-concept than a full implementation. + Notice that the result could be slighly different than the obtained\ + in WMA, due to differencs in the internal representation of the real numbers. Accuracy of a real number is estimated from its value and its precision: @@ -183,11 +187,17 @@ class Accuracy(Builtin): = Infinity >> Accuracy[F[1.3, Pi, A]] - = 14.8861 + = ... 'Accuracy' for the value 0 is a fixed-precision Real number: >> 0``2 = 0.00 + >> Accuracy[0.``2] + = 2. + + For 0.`, the accuracy satisfies: + >> Accuracy[0.`] == $MachinePrecision - Log[10, $MinMachineNumber] + = True In compound expressions, the 'Accuracy' is fixed by the number with the lowest 'Accuracy': @@ -203,33 +213,10 @@ class Accuracy(Builtin): def eval(self, z, evaluation): "Accuracy[z_]" - if isinstance(z, Real): - if z.is_zero: - return MachineReal(dps(z.get_precision())) - z_f = z.to_python() - log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10) - return MachineReal(dps(z.get_precision()) - log10_z) - - if isinstance(z, Complex): - acc_real = self.eval(z.real, evaluation) - acc_imag = self.eval(z.imag, evaluation) - if acc_real is SymbolInfinity: - return acc_imag - if acc_imag is SymbolInfinity: - return acc_real - return Real(min(acc_real.to_python(), acc_imag.to_python())) - - if isinstance(z, Expression): - result = None - for element in z.elements: - candidate = self.eval(element, evaluation) - if isinstance(candidate, Real): - candidate_f = candidate.to_python() - if result is None or candidate_f < result: - result = candidate_f - if result is not None: - return Real(result) - return SymbolInfinity + acc = eval_Accuracy(z) + if acc is None: + return SymbolInfinity + return MachineReal(acc) class ExactNumberQ(Test): @@ -927,8 +914,8 @@ class Precision(Builtin):
'Precision[$expr$]'
examines the number of significant digits of $expr$. - - This is rather a proof-of-concept than a full implementation. + Notice that the result could be slighly different than the obtained\ + in WMA, due to differencs in the internal representation of the real numbers. The precision of an exact number, e.g. an Integer, is 'Infinity': @@ -954,43 +941,39 @@ class Precision(Builtin): >> Precision[{{1, 1.`},{1.`5, 1.`10}}] = 5. + For non-zero Real values, it holds in general: + + 'Accuracy'[$z$] == 'Precision'[$z$] + 'Log'[$z$] + + >> (Accuracy[z] == Precision[z] + Log[z])/.z-> 37.` + = True + + The case of `0.` values is special. Following WMA, in a Machine Real\ + representation, the precision is set to 'MachinePrecision': + >> Precision[0.] + = MachinePrecision + + On the other hand, for a Precision Real with fixed accuracy,\ + the precision is evaluated to 0.: + >> Precision[0.``3] + = 0. + See also :'Accuracy': /doc/reference-of-built-in-symbols/atomic-elements-of-expressions/representation-of-numbers/accuracy/. """ - rules = { - "Precision[z_?MachineNumberQ]": "MachinePrecision", - } - summary_text = "find the precision of a number" def eval(self, z, evaluation): "Precision[z_]" - if isinstance(z, Real): - if z.is_zero: - return MachineReal0 - return MachineReal(dps(z.get_precision())) - - if isinstance(z, Complex): - prec_real = self.eval(z.real, evaluation) - prec_imag = self.eval(z.imag, evaluation) - if prec_real is SymbolInfinity: - return prec_imag - if prec_imag is SymbolInfinity: - return prec_real - - return Real(min(prec_real.to_python(), prec_imag.to_python())) - - if isinstance(z, Expression): - result = None - for element in z.elements: - candidate = self.eval(element, evaluation) - if isinstance(candidate, Real): - candidate_f = candidate.to_python() - if result is None or candidate_f < result: - result = candidate_f - if result is not None: - return Real(result) - return SymbolInfinity + if isinstance(z, MachineReal): + return SymbolMachinePrecision + + prec = eval_Precision(z) + if prec is None: + return SymbolInfinity + if prec == MACHINE_PRECISION_VALUE: + return SymbolMachinePrecision + return MachineReal(prec) diff --git a/mathics/builtin/numbers/constants.py b/mathics/builtin/numbers/constants.py index 3383802fa..3c3caeb10 100644 --- a/mathics/builtin/numbers/constants.py +++ b/mathics/builtin/numbers/constants.py @@ -19,7 +19,14 @@ from mathics.builtin.base import Builtin, Predefined, SympyObject from mathics.core.atoms import MachineReal, PrecisionReal from mathics.core.attributes import A_CONSTANT, A_PROTECTED, A_READ_PROTECTED -from mathics.core.number import PrecisionValueError, get_precision, machine_precision +from mathics.core.evaluation import Evaluation +from mathics.core.number import ( + MAX_MACHINE_NUMBER, + MIN_MACHINE_NUMBER, + PrecisionValueError, + get_precision, + machine_precision, +) from mathics.core.symbols import Atom, Symbol, strip_context from mathics.core.systemsymbols import SymbolIndeterminate @@ -575,6 +582,59 @@ class Overflow(Builtin): summary_text = "overflow in numeric evaluation" +class MaxMachineNumber(Predefined): + """ + Largest normalizable machine number ( + :WMA: + https://reference.wolfram.com/language/ref/$MaxMachineNumber.html + ) + +
+
'$MaxMachineNumber' +
Represents the largest positive number that can be represented \ + as a normalized machine number in the system. +
+ + The product of '$MaxMachineNumber' and '$MinMachineNumber' is a constant: + >> $MaxMachineNumber * $MinMachineNumber + = 4. + + """ + + name = "$MaxMachineNumber" + summary_text = "largest normalized positive machine number" + + def evaluate(self, evaluation: Evaluation) -> MachineReal: + return MachineReal(MAX_MACHINE_NUMBER) + + +class MinMachineNumber(Predefined): + """ + Smallest normalizable machine number ( + :WMA: + https://reference.wolfram.com/language/ref/$MinMachineNumber.html + ) + +
+
'$MinMachineNumber' +
Represents the smallest positive number that can be represented \ + as a normalized machine number in the system. +
+ + 'MachinePrecision' minus the 'Log' base 10 of this number is the\ + 'Accuracy' of 0`: + >> MachinePrecision -Log[10., $MinMachineNumber]==Accuracy[0`] + = True + + """ + + name = "$MinMachineNumber" + summary_text = "smallest normalized positive machine number" + + def evaluate(self, evaluation: Evaluation) -> MachineReal: + return MachineReal(MIN_MACHINE_NUMBER) + + class Pi(_MPMathConstant, _SympyConstant): """ diff --git a/mathics/core/expression.py b/mathics/core/expression.py index e67359131..e8b22a47e 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -54,6 +54,7 @@ SymbolDirectedInfinity, SymbolFunction, SymbolMinus, + SymbolOverflow, SymbolPattern, SymbolPower, SymbolSequence, @@ -1266,7 +1267,11 @@ def rules(): yield rule for rule in rules(): - result = rule.apply(new, evaluation, fully=False) + try: + result = rule.apply(new, evaluation, fully=False) + except OverflowError: + evaluation.message("General", "ovfl") + return Expression(SymbolOverflow), False if result is not None: if not isinstance(result, EvalMixin): return result, False diff --git a/mathics/core/number.py b/mathics/core/number.py index ba166ed61..2a5b3297e 100644 --- a/mathics/core/number.py +++ b/mathics/core/number.py @@ -2,28 +2,33 @@ # cython: language_level=3 import string -from math import ceil, log, log2 +from math import ceil, log +from sys import float_info from typing import List, Optional import mpmath import sympy +from mathics.core.element import BaseElement from mathics.core.symbols import ( SymbolMachinePrecision, SymbolMaxPrecision, SymbolMinPrecision, ) -C = log2(10) # ~ 3.3219280948873626 +C = mpmath.log(10.0, 2.0) # ~ 3.3219280948873626 -# Number of bits of machine precision. -# Note this is a float, not an int. -# WMA uses real values for precision, to take into account the internal representation of numbers. -# This is why $MachinePrecision is not 16, but 15.9546` -machine_precision = 53.0 -machine_digits = int(machine_precision / C) +machine_precision = float_info.mant_dig +machine_digits = float_info.dig -machine_epsilon = 2 ** (1 - machine_precision) +MACHINE_EPSILON = float_info.epsilon +MACHINE_PRECISION_VALUE = float_info.mant_dig / C +MIN_MACHINE_NUMBER = float_info.min +MAX_MACHINE_NUMBER = float_info.max +ZERO_MACHINE_ACCURACY = -mpmath.log(MIN_MACHINE_NUMBER, 10.0) + MACHINE_PRECISION_VALUE + +# backward compatibility +machine_epsilon = MACHINE_EPSILON def reconstruct_digits(bits) -> int: @@ -118,13 +123,22 @@ def prec(dps) -> int: return max(1, int(round((int(dps) + 1) * C))) -def min_prec(*args): - result = None - for arg in args: - prec = arg.get_precision() - if result is None or (prec is not None and prec < result): - result = prec - return result +def min_prec(*args: BaseElement) -> Optional[float]: + """ + Returns the precision of the expression with the minimum precision. + If all the expressions are exact or non numeric, return None. + + If one of the expressions is an inexact value with zero + nominal value, then its accuracy is used instead. For example, + ```min_prec(1, 0.``4) ``` returns 4. + + Notice that this behaviour is different that the one obtained + using mathics.core.numbers.eval_Precision. + """ + args_prec = (arg.get_precision() for arg in args) + return min( + (arg_prec for arg_prec in args_prec if arg_prec is not None), default=None + ) def pickle_mp(value): diff --git a/mathics/core/parser/convert.py b/mathics/core/parser/convert.py index 6fae9d1c5..7c6e5c0eb 100644 --- a/mathics/core/parser/convert.py +++ b/mathics/core/parser/convert.py @@ -83,14 +83,16 @@ def convert_Number(self, node: AST_Number) -> tuple: if suffix is None: # MachineReal/PrecisionReal is determined by number of digits # in the mantissa - d = len(man) - 2 # one less for decimal point - if d < reconstruct_digits(machine_precision): + # if the number of digits is less than 17, then MachineReal is used. + # If more digits are provided, then PrecisionReal is used. + digits = len(man) - 2 + if digits < reconstruct_digits(machine_precision): return "MachineReal", sign * float(s) else: return ( "PrecisionReal", ("DecimalString", str("-" + s if sign == -1 else s)), - d, + digits, ) elif suffix == "": return "MachineReal", sign * float(s) @@ -118,10 +120,15 @@ def convert_Number(self, node: AST_Number) -> tuple: # so ``` 0`3 === 0 ``` and ``` 0.`3 === 0.`4 ``` if node.value == "0": return "Integer", 0 + + s_float = float(s) + prec = float(suffix) + if s_float == 0.0: + return "MachineReal", sign * s_float return ( "PrecisionReal", ("DecimalString", str("-" + s if sign == -1 else s)), - float(suffix), + prec, ) # Put into standard form mantissa * base ^ n diff --git a/mathics/eval/numbers.py b/mathics/eval/numbers.py index 4e1db1dc3..69a8371c4 100644 --- a/mathics/eval/numbers.py +++ b/mathics/eval/numbers.py @@ -1,10 +1,109 @@ +from typing import Optional + +import mpmath import sympy +from mathics.core.atoms import Complex, MachineReal, PrecisionReal from mathics.core.convert.sympy import from_sympy +from mathics.core.element import BaseElement from mathics.core.expression import Expression +from mathics.core.number import MACHINE_PRECISION_VALUE, ZERO_MACHINE_ACCURACY, dps from mathics.core.symbols import SymbolPlus +def eval_Accuracy(z: BaseElement) -> Optional[float]: + """ + Determine the accuracy of an expression expr. + If z is a Real value, returns a Python float value + representing the difference between the number of + significant decimal figures (Precision) and log_10(z). + + For example, + ``` + 12.345`2 + ``` + which is equivalent to 12.`2 has an accuracy of: + ``` + 0.908509 == 2. - log(10, 12.345) + ``` + + If the expression contains Real values, returns + the minimal accuracy of all the numbers in the expression. + + Otherwise returns None, representing infinite accuracy. + """ + if isinstance(z, MachineReal): + if z.is_zero: + return ZERO_MACHINE_ACCURACY + z_f = z.to_python() + log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10) + return MACHINE_PRECISION_VALUE - log10_z + + if isinstance(z, PrecisionReal): + if z.is_zero: + return float(dps(z.get_precision())) + z_f = z.to_python() + log10_z = mpmath.log((-z_f if z_f < 0 else z_f), 10) + return dps(z.get_precision()) - log10_z + + if isinstance(z, Complex): + acc_real = eval_Accuracy(z.real) + acc_imag = eval_Accuracy(z.imag) + if acc_real is None: + return acc_imag + if acc_imag is None: + return acc_real + return min(acc_real, acc_imag) + + if isinstance(z, Expression): + elem_accuracies = (eval_Accuracy(z_elem) for z_elem in z.elements) + return min((acc for acc in elem_accuracies if acc is not None), default=None) + return None + + +def eval_Precision(z: BaseElement) -> Optional[float]: + """ + Determine the precision of an expression expr. + If z is a Real value, returns the number of significant + decimal figures of z. For example, + ``` + 12.345`2 + ``` + which is equivalent to 12.`2 has a precision of 2. + + If the expression contains Real values, returns + the minimal accuracy of all the numbers in the expression. + + If z is PrecisionReal(0.), the precision is 0. In that case, + the field "precision" is interpreted as "accuracy". + + Otherwise returns None, representing infinite precision. + """ + + if isinstance(z, MachineReal): + return MACHINE_PRECISION_VALUE + + if isinstance(z, PrecisionReal): + if z.is_zero: + return 0.0 + return float(dps(z.get_precision())) + + if isinstance(z, Complex): + prec_real = eval_Precision(z.real) + prec_imag = eval_Precision(z.imag) + if prec_real is None: + return prec_imag + if prec_imag is None: + return prec_real + return min(prec_real, prec_imag) + + if isinstance(z, Expression): + elem_prec = (eval_Precision(z_elem) for z_elem in z.elements) + return min((prec for prec in elem_prec if prec is not None), default=None) + + return None + + def cancel(expr): if expr.has_form("Plus", None): return Expression(SymbolPlus, *[cancel(element) for element in expr.elements]) diff --git a/test/builtin/atomic/test_numbers.py b/test/builtin/atomic/test_numbers.py index 9039abc63..da539617c 100644 --- a/test/builtin/atomic/test_numbers.py +++ b/test/builtin/atomic/test_numbers.py @@ -4,9 +4,15 @@ In particular, RealDigits[] and N[] """ - from test.helper import check_evaluation +import pytest + +from mathics.core.number import MACHINE_PRECISION_VALUE, ZERO_MACHINE_ACCURACY + +ZERO_MACHINE_ACCURACY_STR = str(ZERO_MACHINE_ACCURACY) +DEFAUT_ACCURACY_10_STR = str(MACHINE_PRECISION_VALUE - 1) + def test_realdigits(): for str_expr, str_expected in ( @@ -177,43 +183,90 @@ def test_n(): check_evaluation(str_expr, str_expected) -def test_accuracy(): - for str_expr, str_expected in ( - ("0`4", "0"), - ("Accuracy[0.0]", "15."), - ("Accuracy[0.000000000000000000000000000000000000]", "36."), - ("Accuracy[-0.0]", "15."), - # In WMA, this gives 36. Seems to be a rounding issue - # ("Accuracy[-0.000000000000000000000000000000000000]", "36."), - ("1.0000000000000000 // Accuracy", "15."), - ("1.00000000000000000 // Accuracy", "17."), - # Returns the accuracy of ```2.4``` - (" 0.4 + 2.4 I // Accuracy", "14.6198"), - ("Accuracy[2 + 3 I]", "Infinity"), - ('Accuracy["abc"]', "Infinity"), +@pytest.mark.parametrize( + ("str_expr", "str_expected"), + [ + # Accuracy for 0 + ("0", "Infinity"), + ("0.", ZERO_MACHINE_ACCURACY_STR), + ("0.00", ZERO_MACHINE_ACCURACY_STR), + ("0.00`", ZERO_MACHINE_ACCURACY_STR), + ("0.00`2", ZERO_MACHINE_ACCURACY_STR), + ("0.00`20", ZERO_MACHINE_ACCURACY_STR), + ("0.00000000000000000000", "20."), + ("0.``2", "2."), + ("0.``20", "20."), + ("-0.`2", ZERO_MACHINE_ACCURACY_STR), + ("-0.`20", ZERO_MACHINE_ACCURACY_STR), + ("-0.``2", "2."), + ("-0.``20", "20."), + # Now for non-zero numbers + ("10", "Infinity"), + ("10.", DEFAUT_ACCURACY_10_STR), + ("10.00", DEFAUT_ACCURACY_10_STR), + ("10.00`", DEFAUT_ACCURACY_10_STR), + ("10.00`2", "1."), + ("10.00`20", "19."), + ("10.00000000000000000000", "20."), + ("10.``2", "2."), + ("10.``20", "20."), + # For some reason, the following test + # would fail in WMA + ("1. I", "Accuracy[1.]"), + (" 0.4 + 2.4 I", "Accuracy[2.4]"), + ("2 + 3 I", "Infinity"), + ('"abc"', "Infinity"), # Returns the accuracy of ``` 3.2`3 ``` - ('Accuracy[F["a", 2, 3.2`3]]', "2.49482"), - ('Accuracy[{{a, 2, 3.2`},{2.1`5, 3.2`3, "a"}}]', "2.49482"), - # Another case of issues with rounding. In Mathics, this returns - # 2.67776 - # ('Accuracy[{{a, 2, 3.2`},{2.1``3, 3.2``5, "a"}}]', '3.'), - ): - check_evaluation(str_expr, str_expected) + ('F["a", 2, 3.2`3]', "Accuracy[3.2`3]"), + ("F[1.3, Pi, A]", "15.8406"), + ('{{a, 2, 3.2`},{2.1`5, 3.2`3, "a"}}', "Accuracy[3.2`3]"), + ('{{a, 2, 3.2`},{2.1``3, 3.2``5, "a"}}', "Accuracy[2.1``3]"), + ("{1, 0.}", ZERO_MACHINE_ACCURACY_STR), + ("{1, 0.``5}", "5."), + ], +) +def test_accuracy(str_expr, str_expected): + check_evaluation(f"Accuracy[{str_expr}]", str_expected) -def test_precision(): - for str_expr, str_expected in ( - ("0`4", "0"), - ("Precision[0.0]", "MachinePrecision"), - ("Precision[0.000000000000000000000000000000000000]", "0."), - ("Precision[-0.0]", "MachinePrecision"), - ("Precision[-0.000000000000000000000000000000000000]", "0."), - ("1.0000000000000000 // Precision", "MachinePrecision"), - ("1.00000000000000000 // Precision", "17."), - (" 0.4 + 2.4 I // Precision", "MachinePrecision"), - ("Precision[2 + 3 I]", "Infinity"), - ('Precision["abc"]', "Infinity"), - ('Precision[F["a", 2, 3.2`3]]', "3."), - ('Precision[{{a,2,3.2`},{2.1`5, 2.`3, "a"}}]', "3."), - ): - check_evaluation(str_expr, str_expected) +@pytest.mark.parametrize( + ("str_expr", "str_expected"), + [ + # Precision for 0 + ("0", "Infinity"), + ("0.", "MachinePrecision"), + ("0.00", "MachinePrecision"), + ("0.00`", "MachinePrecision"), + ("0.00`2", "MachinePrecision"), + ("0.00`20", "MachinePrecision"), + ("0.00000000000000000000", "0."), + ("0.``2", "0."), + ("0.``20", "0."), + ("-0.`2", "MachinePrecision"), + ("-0.`20", "MachinePrecision"), + ("-0.``2", "0."), + ("-0.``20", "0."), + # Now for non-zero numbers + ("10", "Infinity"), + ("10.", "MachinePrecision"), + ("10.00", "MachinePrecision"), + ("10.00`", "MachinePrecision"), + ("10.00`2", "2."), + ("10.00`20", "20."), + ("10.00000000000000000000", "21."), + ("10.``2", "3."), + ("10.``20", "21."), + # Returns the precision of ```2.4``` + (" 0.4 + 2.4 I", "MachinePrecision"), + ("2 + 3 I", "Infinity"), + ('"abc"', "Infinity"), + # Returns the precision of ``` 3.2`3 ``` + ('F["a", 2, 3.2`3]', "3."), + ('{{a, 2, 3.2`},{2.1`5, 3.2`3, "a"}}', "3."), + ('{{a, 2, 3.2`},{2.1``3, 3.2``5, "a"}}', "3."), + ("{1, 0.}", "MachinePrecision"), + ("{1, 0.``5}", "0."), + ], +) +def test_precision(str_expr, str_expected): + check_evaluation(f"Precision[{str_expr}]", str_expected)