Skip to content

PrecisionReal: keep a real-valued precision, as WMA does (#2047) - #2050

Merged
rocky merged 3 commits into
masterfrom
real-precision
Oct 19, 2026
Merged

rocky merged 3 commits into
masterfrom
real-precision

Conversation

@tombouctou

Copy link
Copy Markdown
Collaborator

Steps 1–5 of the outline in #2047: PrecisionReal keeps a real-valued precision, as WMA does, and Plus and Times carry it through.

Input master this PR WMA 15
Precision[1.5`20.3] 20. 20.3 20.3
Precision[1.5`5.497 + 1.5`5.497] 5. 5.497 5.497
Precision[1.5`5.497 * 1.5`5.497] 5. 5.19597 5.19597
Precision[1.5`20.3 + 3.2`30] 20. 20.796 20.796
Precision[2 + 1.5`20.3] 20. 20.668 20.668
Precision[N[Pi, 30] N[E, 30]] 30. 29.699 29.699
Precision[N[Pi, 30] E] 30. 30. 30.
Precision[N[Pi, {20, 5}]] 5. 5.49715 5.49715
Accuracy[1.5`5 - 1.5`5] 15. 4.52288 4.52288
ToString[1.5`20.3, InputForm] 1.5`20. 1.5`20.3 1.5`20.3
3.14159265358979323846264`20.6 20 digits 21 digits 21 digits

Changes

  • PrecisionReal (mathics/core/atoms/numerics.py) has a new slot, _precision10. It is None unless the precision differs from what the binary precision of the sympy.Float already gives, so numbers without a fractional precision look the same as before. precision10 returns the decimal precision; for a zero, as before, it is the accuracy. The object cache is keyed by (sympy.Float, precision10). __neg__, do_copy and pickling keep the value. precision still returns binary digits.
  • Parser: x`p and xa`` keep the real-valued precision the parser already computed. The binary precision is rounded up from it.
  • Precision, Accuracy read precision10.
  • Plus, Times: when the numbers are exact or nonzero PrecisionReals, the errors add up, as in WMA. For Plus the absolute errors add: the accuracy is -Log10[Total[10^-acc_i]]. For Times the relative errors add: the precision is -Log10[Total[10^-prec_i]]. A sum that cancels down to no digits becomes a zero with that accuracy, as in WMA. The computation itself still runs in mpmath at a whole number of bits. Machine numbers, complex numbers and zero factors take the code path they took before.
  • Exact constants: numerify (used by Plus, Times, Power) evaluates exact numeric elements like E or Sqrt[2] with 16 more digits than the inexact ones. Their error then vanishes next to the error of the inexact ones, so N[Pi, 30] E keeps precision 30., as in WMA. Without this, E would count as one more 30-digit number and give 29.699. Nothing changes when a machine number is involved.
  • N[expr, {prec, acc}] (N: evaluate with {precision, accuracy}; give a numeric precision to N rules as a machine real #2028) stores the precision it computes instead of rounding it down to whole bits.
  • Equal: the tolerance ("the last 7 binary digits") is taken from the decimal precision, not from the binary digits it was rounded up to. Otherwise 13.14164 == 13.14134 (a doctest, True in WMA) would turn False. For numbers with a whole-digit precision the tolerance is the same as before.
  • Output: the number of digits shown is the precision rounded, as in WMA, and InputForm shows the stored precision.
  • eval_add_numbers and eval_multiply_numbers had three identical copies: mathics/eval/numeric.py, mathics/eval/arithmetic.py and mathics/eval/arithfns/basic.py. Plus and Times use the one in basic.py, while Abs and Sign use the one in numeric.py. Now there is one, in numeric.py, which the other two modules import, so the new rule applies everywhere.

Tests

  • New test_real_valued_precision in test/builtin/atomic/test_numbers.py, with the WMA 15 values above.
  • In test_precision, Precision[{{a, 2, 3.2`}, {2.13, 3.25, "a"}}] expected 3.. That was only 2.13 rounded down: its precision is `3.32222`. The case now compares with Precision[2.13]. WMA itself answers MachinePrecision here, because the list holds a machine number; the case above it has the same difference, which this PR doesn't change.
  • test_n_precision_and_accuracy (N: evaluate with {precision, accuracy}; give a numeric precision to N rules as a machine real #2028) and test_gamma expected whole-digit precisions. They now expect the WMA 15 values, e.g. Precision[N[Pi, {20, 5}]] is 5.49715 and the Gamma quotient from issue 203 has precision 99.699.

Full run (Python 3.14, pytest -n 4 and docpipeline.py), this branch next to master 0e0aba0 on the same runner:

  • Doctests: 4604 passed, 0 failed.
  • pytest: 45 failed on this branch, 50 on master. All of them depend on the order the tests run in (contexts, packages, patterns, images): every file with a failure passes when run on its own, on both branches. The two sets also differ from run to run.

Benchmark

Local micro-benchmark (best of 5, Python 3.14, macOS):

master this PR
Total[Table[N[1/k, 50], {k, 2000}]] 1299 ms 1308 ms
Times @@ Table[N[1 + 1/k, 50], {k, 2000}] 1476 ms 1574 ms
Do[x = N[k, 50] + Pi, {k, 2000}] 1803 ms 1896 ms
Total[Table[1./k, {k, 2000}]] (machine reals, control) 903 ms 932 ms

So 1–7% on arbitrary-precision sums and products, against 3% noise on the machine-number control. The memory cost is one slot per PrecisionReal.

Not in this PR (step 6 of the outline)

  • N[Pi, 20.3] still gives precision 20.: N with a non-integer precision.
  • Functions other than Plus and Times, such as Sqrt and Power, keep the minimum precision of their arguments instead of WMA's condition-number adjustment.
  • SetPrecision, SetAccuracy.
  • A low-precision input is stored with a few bits, so its binary value is off from the decimal one. That shows when it cancels: Precision[1.5`20 - 1.4`2] gives 0.852 here and 0.854 in WMA.

🤖 Generated with Claude Code

tombouctou and others added 3 commits October 19, 2026 12:13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real.precision is Optional[int] (inherited from Number); the helper now
takes MachineReal or PrecisionReal, whose precision is always an int.

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

rocky commented Oct 19, 2026

Copy link
Copy Markdown
Member

Personally, I think this is really excellent. Very well thought out and executed. It is exacting, and this level of detail distinguishes us from virtually all of the other Mathematica lookalikes.

And it also removes some code duplication.

This is a great improvement over the kind of code I experienced when I first encountered this project.

Thanks @tombouctou!

However, @mmatera, you had concerns. So I'm waiting to hear your thoughts.

@rocky
rocky requested a review from mmatera October 19, 2026 10:10
@rocky

rocky commented Oct 19, 2026

Copy link
Copy Markdown
Member

LGTM

@rocky
rocky merged commit 9f166b1 into master Oct 19, 2026
15 checks passed
@rocky
rocky deleted the real-precision branch October 19, 2026 12:43
tombouctou added a commit that referenced this pull request Oct 19, 2026
Part of step 6 of #2047 (MATH-227).

- N[expr, p] gives each arbitrary-precision number in the result
  precision p, as WMA does, also for a non-integer p:
  Precision[N[Pi, 20.3]] is 20.3 (was 20.).
- When the computation loses digits, it is done again with more digits,
  up to $MaxExtraPrecision more: Precision[N[Pi E, 20]] is 20. again (it
  became 19.699 with #2050), and Precision[N[Pi - 314159/100000, 20]] is
  20. (was 13.6). A number whose input has fewer digits keeps them.
- New $MaxExtraPrecision (50. by default) and N::meprec, as in WMA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit that referenced this pull request Oct 19, 2026
Part of step 6 of #2047 (MATH-227), on top of the hyperbolic PR.

- SetPrecision[expr, p] sets every number in expr to precision p, and
  evaluates numeric constants such as Pi to it. An inexact number keeps
  its binary value: SetPrecision[0.1, 20] is 0.10000000000000000555. As
  in WMA, an exact exponent stays exact (SetPrecision[Sqrt[2], 20] has
  precision 20.301), and a machine or arbitrary-precision zero becomes 0.
  p can be MachinePrecision, or Infinity for the exact binary values.
- SetAccuracy[expr, a] does the same with accuracy a; a number with no
  digit left becomes a zero of accuracy a.
- Complex numbers: both parts get the same accuracy, so that the number
  has precision (or accuracy) p, as in WMA.
- Messages SetPrecision::precbd, ::precsm, ::preclg and
  SetAccuracy::accbd, as in WMA.
- Complex: the object cache was keyed on the parts' values and whole-bit
  precision, so two numbers differing only in a real-valued precision
  (#2050) were the same object. The key now has the parts' precisions.
- number._get_float_inf becomes get_float_inf, for SetPrecision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rocky pushed a commit that referenced this pull request Oct 19, 2026
…MA (#2051)

Part of step 6 of the outline in #2047, after #2050: `N[expr, p]` gives
precision `p`, as WMA does.

| Input | master | this PR | WMA 15 |
|---|---|---|---|
| `Precision[N[Pi, 20.3]]` | `20.` | `20.3` | `20.3` |
| `Precision[N[Pi, 41/2]]` | `20.` | `20.5` | `20.5` |
| `Precision[N[Pi E, 20]]` | `19.699` | `20.` | `20.` |
| `Precision[N[Pi - 314159/100000, 20]]` | `13.6257` | `20.` | `20.` |
| ``Precision[N[1.5`10, 20.3]]`` | `10.` | `10.` | `10.` |
| ``Precision /@ N[{Pi, 1.5`10, 2.5`30.2}, 20.3]`` | `{20., 10., 20.}` |
`{20.3, 10., 20.3}` | `{20.3, 10., 20.3}` |
| `$MaxExtraPrecision` | `$MaxExtraPrecision` | `50.` | `50.` |

`N[Pi E, 20]` giving `19.699` came in with #2050: `N` makes `Pi` and `E`
20-digit numbers, and `Times` now adds their errors.

## Changes

- **`N[expr, p]`** (`eval_NValues_to_precision` in
`mathics/eval/nevaluator.py`) first evaluates at `p`, as before. If a
nonzero arbitrary-precision number in the result has fewer than `p`
digits, it evaluates again with the digits lost added, up to
`$MaxExtraPrecision` more than `p`. It goes on while some number short
of `p` digits gains digits; one that gains nothing has an input with
fewer than `p` digits, and keeps them. Then every number above `p` is
set to precision `p`.
- The N rules see the working precision. So a retry is used only if it
has the same form as the first result, up to the values of numbers: with
`N[a, 20] = 11`, `N[a Pi, 20]` must not turn back into `a 3.14…` at 21
digits. The first pass is still at exactly `p`, so a rule given for `p`
applies as before.
- **`$MaxExtraPrecision`**, new, `50.` by default, no attributes.
Assigning it anything other than a non-negative number or `Infinity`
gives `$MaxExtraPrecision::precset`, as in WMA.
- **`N::meprec`** when the extra digits are not enough. As in WMA, there
is no message when `$MaxExtraPrecision` is `0`.

`N[expr, {p, a}]` and the internal `eval_N` are unchanged.

## Tests

- `test_n_real_valued_precision` and `test_max_extra_precision` in
`test/builtin/atomic/test_numbers.py`, with WMA 15 values.
- Doctests for `$MaxExtraPrecision`.
- The `RootSum` doctest `N[Integrate[1/(x^5 + 11 x + 1), {x, 1, 3}],
50]` expected `…857550894`. On master that number has precision 49.66,
so its last digits were off. Now it is `…857553407`, as in WMA
(`…8575534094…`; `NIntegrate` at 70 digits agrees).

Full run on kimmy (Python 3, `pytest -n 4` and `docpipeline.py`), next
to master 9f166b1:

- Doctests: 4611 passed, 0 failed (master: 4604; the 7 new ones are for
`$MaxExtraPrecision`).
- pytest: 34 failed on this branch, 58 on master. As noted in #2050,
these depend on the order the tests run in. The 12 that fail only on
this branch (`test_tensor.py`, `test_testing_expressions.py`) pass when
their files run on their own.

## Benchmark

kimmy, best of 7, both runs at the same time:

| | master | this PR |
|---|---|---|
| `Total[Table[N[1/k, 50], {k, 2000}]]` | 2636 ms | 2586 ms |
| `Times @@ Table[N[1 + 1/k, 50], {k, 2000}]` | 3113 ms | 3104 ms |
| `Do[x = N[k, 50] + Pi, {k, 2000}]` | 5512 ms | 5277 ms |
| `Total[Table[1./k, {k, 2000}]]` (machine reals, control) | 1921 ms |
1850 ms |
| `Do[N[Pi E + Sqrt[2], 30], {k, 300}]` | 1118 ms | 1643 ms |
| `Do[N[Sin[k] + Cos[k], 30], {k, 300}]` | 787 ms | 935 ms |

The benchmark from #2050 doesn't change. An `N[expr, p]` whose first
pass loses digits now costs about one more evaluation (the last two
rows): that is the second pass.

## Still to do from step 6

- Functions other than `Plus` and `Times` (`Sqrt`, `Power`, `Exp`, …):
precision from the condition number.
- `SetPrecision`, `SetAccuracy`.
- Low-precision inputs stored with few bits (``Precision[1.5`20 -
1.4`2]``).
- `Precision` of a list with a machine number.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit that referenced this pull request Oct 19, 2026
Part of step 6 of #2047 (MATH-227), on top of the hyperbolic PR.

- SetPrecision[expr, p] sets every number in expr to precision p, and
  evaluates numeric constants such as Pi to it. An inexact number keeps
  its binary value: SetPrecision[0.1, 20] is 0.10000000000000000555. As
  in WMA, an exact exponent stays exact (SetPrecision[Sqrt[2], 20] has
  precision 20.301), and a machine or arbitrary-precision zero becomes 0.
  p can be MachinePrecision, or Infinity for the exact binary values.
- SetAccuracy[expr, a] does the same with accuracy a; a number with no
  digit left becomes a zero of accuracy a.
- Complex numbers: both parts get the same accuracy, so that the number
  has precision (or accuracy) p, as in WMA.
- Messages SetPrecision::precbd, ::precsm, ::preclg and
  SetAccuracy::accbd, as in WMA.
- Complex: the object cache was keyed on the parts' values and whole-bit
  precision, so two numbers differing only in a real-valued precision
  (#2050) were the same object. The key now has the parts' precisions.
- number._get_float_inf becomes get_float_inf, for SetPrecision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit that referenced this pull request Oct 19, 2026
Part of step 6 of #2047 (MATH-227), on top of the hyperbolic PR.

- SetPrecision[expr, p] sets every number in expr to precision p, and
  evaluates numeric constants such as Pi to it. An inexact number keeps
  its binary value: SetPrecision[0.1, 20] is 0.10000000000000000555. As
  in WMA, an exact exponent stays exact (SetPrecision[Sqrt[2], 20] has
  precision 20.301), and a machine or arbitrary-precision zero becomes 0.
  p can be MachinePrecision, or Infinity for the exact binary values.
- SetAccuracy[expr, a] does the same with accuracy a; a number with no
  digit left becomes a zero of accuracy a.
- Complex numbers: both parts get the same accuracy, so that the number
  has precision (or accuracy) p, as in WMA.
- Messages SetPrecision::precbd, ::precsm, ::preclg and
  SetAccuracy::accbd, as in WMA.
- Complex: the object cache was keyed on the parts' values and whole-bit
  precision, so two numbers differing only in a real-valued precision
  (#2050) were the same object. The key now has the parts' precisions.
- number._get_float_inf becomes get_float_inf, for SetPrecision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit that referenced this pull request Oct 19, 2026
A follow-up to #2050 (MATH-227).

- Plus: a PrecisionReal zero adds its absolute error, 10^-accuracy, as
  any other term does. Precision[1.5`20 + 0``10] is 10.1761 (was 10.),
  and a sum of zeros is a zero with the combined accuracy.
- Times: a product with PrecisionReal zeros is a zero whose accuracy is
  the sum of theirs, less Log10 of the other factors:
  Accuracy[1.5`20 * 0``10] is 9.82391 (was 15.).
- Times: an exact 0 times arbitrary-precision numbers is an exact 0, as
  in WMA (it gave 0.`15.*^-15).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit that referenced this pull request Oct 19, 2026
Part of step 6 of #2047 (MATH-227), on top of the hyperbolic PR.

- SetPrecision[expr, p] sets every number in expr to precision p, and
  evaluates numeric constants such as Pi to it. An inexact number keeps
  its binary value: SetPrecision[0.1, 20] is 0.10000000000000000555. As
  in WMA, an exact exponent stays exact (SetPrecision[Sqrt[2], 20] has
  precision 20.301), and a machine or arbitrary-precision zero becomes 0.
  p can be MachinePrecision, or Infinity for the exact binary values.
- SetAccuracy[expr, a] does the same with accuracy a; a number with no
  digit left becomes a zero of accuracy a.
- Complex numbers: both parts get the same accuracy, so that the number
  has precision (or accuracy) p, as in WMA.
- Messages SetPrecision::precbd, ::precsm, ::preclg and
  SetAccuracy::accbd, as in WMA.
- Complex: the object cache was keyed on the parts' values and whole-bit
  precision, so two numbers differing only in a real-valued precision
  (#2050) were the same object. The key now has the parts' precisions.
- number._get_float_inf becomes get_float_inf, for SetPrecision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit that referenced this pull request Oct 19, 2026
Part of step 6 of #2047 (MATH-227), on top of the hyperbolic PR.

- SetPrecision[expr, p] sets every number in expr to precision p, and
  evaluates numeric constants such as Pi to it. An inexact number keeps
  its binary value: SetPrecision[0.1, 20] is 0.10000000000000000555. As
  in WMA, an exact exponent stays exact (SetPrecision[Sqrt[2], 20] has
  precision 20.301), and a machine or arbitrary-precision zero becomes 0.
  p can be MachinePrecision, or Infinity for the exact binary values.
- SetAccuracy[expr, a] does the same with accuracy a; a number with no
  digit left becomes a zero of accuracy a.
- Complex numbers: both parts get the same accuracy, so that the number
  has precision (or accuracy) p, as in WMA.
- Messages SetPrecision::precbd, ::precsm, ::preclg and
  SetAccuracy::accbd, as in WMA.
- Complex: the object cache was keyed on the parts' values and whole-bit
  precision, so two numbers differing only in a real-valued precision
  (#2050) were the same object. The key now has the parts' precisions.
- number._get_float_inf becomes get_float_inf, for SetPrecision.

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