Skip to content

PrecisionReal: keep 64 bits of a low-precision value, as WMA does - #2060

Merged
tombouctou merged 5 commits into
masterfrom
low-precision-storage
Oct 19, 2026
Merged

tombouctou merged 5 commits into
masterfrom
low-precision-storage

Conversation

@tombouctou

@tombouctou tombouctou commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Part of step 6 of the outline in #2047. On top of #2057 (and the PRs below it): this diff is against its branch.

A low-precision number was stored with only the bits its precision needs: 1.4`2 was 1.400390625. WMA keeps at least about 64 bits of the value, and the difference shows when numbers cancel:

Input before this PR WMA 15
Precision[1.5`20 - 1.4`2] 0.852051 0.853872 0.853872
Accuracy[3.1416`2] 1.50298 1.50285 1.50285
SetPrecision[1.4`2, 20] 1.4003906250000000000 1.4000000000000000000 1.4000000000000000000
SetPrecision[N[Pi, 3], 18] 3.14160156250000000 3.14159265358979324 3.14159265358979324
SetPrecision[Sin[1.4`2], 18] 0.985473632812500000 0.985449729988460181 0.985449729988460181

Design

The extra bits must not be mistaken for precision. Many builtins rebuild a number from x.value or read x.precision (bits), and the code that still uses the minimum precision (complex numbers, special functions, Equal) works from those. So value stays the value rounded to the precision, and the 64-bit value is kept beside it:

  • PrecisionReal._extended, a new slot: the value with MIN_STORED_BITS (64) bits, kept only when the precision needs fewer. value, precision, sameQ, the output and every other reader are unchanged.
  • PrecisionReal.from_value(value, precision10) rounds to the precision and keeps the 64-bit value. The parser, N of exact numbers and constants, Plus, Times, the functions with a condition number (Power, Sqrt: precision from the condition number, as in WMA #2052), SetPrecision and N's bounds use it.
  • extended_value is read by the arithmetic that tracks the error (Plus, Times, the condition-number helper, SetPrecision), and for the Log10 in Accuracy and in Plus's accuracy. These now work at no fewer than 64 bits.
  • The object cache key includes the extended value: 1.4`2 and 1.4001`2 round to the same 10 bits but are different numbers. (They are still SameQ and Equal, as in WMA.)
  • Constants: N[Pi, d] with d up to machine precision came from numpy, with 53 bits; the 64-bit value is taken from sympy or mpmath.

Numbers with 19 or more digits need 64 bits or more anyway, so nothing changes for them.

Not changed: WMA's InputForm prints all the stored digits (3.1415926535897932385`3.), while Mathics3 prints the digits the precision gives (3.142`3.). That is a question of output, not of this PR.

Tests

  • test_low_precision_storage in test/builtin/atomic/test_numbers.py, with WMA 15 values.
  • The Accuracy doctest Accuracy[3.1416`2] expected 1.50298, which is 2 - Log10[3.140625]: the rounded value. It now expects WMA's 1.50285.

Full run on kimmy, next to #2057 at the same time: doctests all pass. The pytest failures are the order-dependent ones (17 here, 30 on #2057).

Benchmark

#2057 this PR
Total[Table[N[1/k, 50], {k, 2000}]] 2582 ms 2599 ms
Times @@ Table[N[1 + 1/k, 50], {k, 2000}] 3090 ms 3094 ms
Do[Sqrt[N[k, 50]], {k, 2000}] 5363 ms 5391 ms
Total[Table[1./k, {k, 2000}]] (machine reals) 1830 ms 1834 ms
Total[Table[N[1/k, 5], {k, 2000}]] 2613 ms 2803 ms
Do[x = Sin[N[k, 5]] + 1.5`3, {k, 2000}] 5040 ms 5567 ms

At 50 digits nothing changes. Low-precision arithmetic is 7–10% slower, since it now works with 64 bits and keeps the extra value.

🤖 Generated with Claude Code

tombouctou and others added 3 commits October 20, 2026 02:13
Part of step 6 of #2047 (MATH-227), on top of the SetPrecision PR.

1.4`2 was stored with the 10 bits its precision needs, as 1.400390625,
so it cancelled wrongly: Precision[1.5`20 - 1.4`2] was 0.852, 0.854 in
WMA. WMA keeps at least about 64 bits of the value.

- A PrecisionReal whose precision needs fewer than MIN_STORED_BITS (64)
  bits keeps its value with 64 bits in a new slot, _extended.
  value, precision, sameQ, the output and the code that still uses the
  minimum precision are unchanged: they see the value rounded to the
  precision, so nothing can mistake the extra bits for precision.
- PrecisionReal.from_value builds such a number. The parser, N of exact
  numbers and of constants, Plus, Times, the functions with a condition
  number, SetPrecision and N's bounds use it, and read extended_value.
- Accuracy and Plus take Log10 of the extended value:
  Accuracy[3.1416`2] is 1.50285, as in WMA (the doctest had 1.50298,
  the Log10 of 3.140625).
- The object cache key includes the extended value: 1.4`2 and 1.4001`2
  round to the same 10 bits but are different numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As other module constants are (review on #2057).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tombouctou
tombouctou force-pushed the low-precision-storage branch from 80be2d0 to e2a1418 Compare October 19, 2026 22:14
@rocky

rocky commented Oct 19, 2026

Copy link
Copy Markdown
Member

...

Benchmark

...
At 50 digits nothing changes. Low-precision arithmetic is 7–10% slower, since it now works with 64 bits and keeps the extra value.

One reason I personally am not worried about this is that we don't really know how often Precision is used, let alone Low precision. And I am guessing that it is not much.

Base automatically changed from set-precision to master October 19, 2026 22:34
@rocky

rocky commented Oct 19, 2026

Copy link
Copy Markdown
Member

@tombouctou

In this branch I am seeing:

In[1]:= SetPrecision[1.5 + 2.5 I, 20] === SetPrecision[1.5 + 2.5 I, 20.3]
Out[1]= True

Is this expected? Should these be different objects in the cache?

Suggested test for adaption:

@pytest.mark.parametrize(
    ("str_expr", "str_expected", "msg"),
    [
        (
            "SetPrecision[1.5 + 2.5 I, 20] === SetPrecision[1.5 + 2.5 I, 20.3]",
            "False",
            "different low-precision complex values must not collide in the object cache",
        ),
        (
            "Precision[SetAccuracy[1.5 + 2.5 I, 20]]",
            "20.",
            "complex values should preserve the same accuracy semantics after SetAccuracy",
        ),
        (
            "Precision /@ {Re[#], Im[#]} &[SetPrecision[1.5 + 2.5 I, 20.3]]",
            "{20.1619, 20.3837}",
            "both parts should share the same accuracy target",
        ),
    ],
)
def test_low_precision_complex_cache_regression(str_expr, str_expected, msg):
    """Guard against recursive cache collisions in complex numbers."""
    check_evaluation(str_expr, str_expected, failure_message=msg)
``

@tombouctou

Copy link
Copy Markdown
Collaborator Author

Thanks for checking. Yes, True is expected; WMA 15 gives the same:

In[1]:= SetPrecision[1.5 + 2.5 I, 20] === SetPrecision[1.5 + 2.5 I, 20.3]
Out[1]= True

In[2]:= SameQ[1.5`20, 1.5`20.3]
Out[2]= True

SameQ on approximate numbers is not object identity: it holds for numbers that agree to their precision. These two are different objects in the cache: Precision /@ {…} gives {20., 20.3}, and the real parts are PrecisionReals with precisions 19.8619 and 20.1619. The collision you may have in mind was real, but it was fixed in #2057: the Complex cache key now includes the parts' real-valued precisions.

I added your tests in test_low_precision_complex_cache_regression, with the WMA values:

  • The === case expects True, as in WMA. Next to it, a case checks that the two numbers keep different precisions, {20., 20.3}, which is the guard against a collision.
  • Precision[SetAccuracy[1.5 + 2.5 I, 20]] is 20.4647 in WMA, not 20.. SetAccuracy gives each part accuracy 20 + Log10[2]/2, so that the complex number has accuracy 20; its precision is then the accuracy plus Log10[Abs[1.5 + 2.5 I]].
  • {20.1619, 20.3837} for the parts, unchanged.

About the benchmark: agreed. The slowdown is only for numbers with fewer than about 19 digits, and only in the code that tracks the error.

Suggested in review, with the WMA 15 values. SetPrecision[1.5 + 2.5 I, 20]
and SetPrecision[1.5 + 2.5 I, 20.3] are SameQ, as in WMA (SameQ holds
for numbers that agree to their precision), but they are two numbers
with precisions 20. and 20.3, not one cached object.
Precision[SetAccuracy[1.5 + 2.5 I, 20]] is 20.4647 in WMA: each part
gets accuracy 20 + Log10[2]/2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit to tombouctou/mathics-core that referenced this pull request Oct 19, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rocky

rocky commented Oct 19, 2026

Copy link
Copy Markdown
Member

Thanks for the explanation and improvements. LGTM. Merge when you are satisified.

@tombouctou
tombouctou merged commit 745e71f into master Oct 19, 2026
15 checks passed
@tombouctou
tombouctou deleted the low-precision-storage branch October 19, 2026 23:17
rocky pushed a commit that referenced this pull request Oct 21, 2026
…WMA (#2064)

Since #2060 a PrecisionReal with fewer than 64 bits of precision keeps
its
value with 64 bits (``_extended``), but InputForm and FullForm printed
only
the digits that the precision gives, so the printed form did not read
back
as the same number.

As in WMA, InputForm and FullForm now print the shortest decimal string
that
reads back as the kept value; OutputForm and StandardForm still print
the
digits of the precision. WMA 15:

    InputForm[3.1415926535897932385`3]  ->  3.1415926535897932385`3.
    InputForm[-4.326563712`2]           ->  -4.326563712`2.
    InputForm[-4.32`4]                  ->  -4.32`4.

(the last two are in test/format/format_tests-WMA.yaml; the Mathics
expectation for -4.326563712`2 in format_tests.yaml is updated to
match).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants