Skip to content

Hyperbolic functions and their inverses: precision from the condition number, as in WMA - #2056

Merged
tombouctou merged 4 commits into
masterfrom
hyperbolic-real-valued-precision
Oct 19, 2026
Merged

tombouctou merged 4 commits into
masterfrom
hyperbolic-real-valued-precision

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 the inverse trigonometric PR, #2055: this diff is against its branch.

The hyperbolic functions and their inverses get their precision from the condition number Abs[x f'[x] / f[x]], as WMA does.

One WMA detail: for Tanh and Coth with Abs[x] < 1, WMA uses the condition number of Sinh[x] / Cosh[x] with both errors added, x (Tanh[x] + Coth[x]). From Abs[x] >= 1 on it uses the true one. I checked at 0.05, 0.3, 0.5, 0.9, 0.99, 1., 1.01, 1.1, 1.2, 1.4, 2. and 5.; the switch is exactly at 1. This PR follows it, so Precision[Tanh[0.3`20]] is 19.9519, as in WMA, not 20.0258. For large x, x (1 - Tanh[x]^2) / Tanh[x] would cancel, so it is computed as 2 x / Sinh[2 x].

Input before this PR WMA 15
Precision[Sinh[25.`20]] 20. 18.6021 18.6021
Precision[Tanh[0.3`20]] 20. 19.9519 19.9519
Precision[Tanh[25.`20]] 20. 39.7147 39.7147
Precision[ArcTanh[0.99`20]] 20. 18.7259 18.7259
Precision[ArcSech[0.99`20]] 20. 18.3018 18.3018

Zeros

With the rule from #2054 for an inexact zero argument, as in WMA 15: Sinh, Tanh, ArcSinh and ArcTanh of 020are020, and Cosh[020]andSech[020] are 1 with precision 40.301. Csch of 0. or 020`` is now ComplexInfinity, as `Coth[0.]` already was; it stayed unevaluated. Tested in `test_hyperbolic_real_valued_precision`.

Tests

test_hyperbolic_real_valued_precision in test/builtin/atomic/test_numbers.py, with WMA 15 values. Full run on kimmy: doctests all pass. The pytest failures are the ones that depend on the order the tests run in; the failing files that touch numbers pass on their own.

🤖 Generated with Claude Code

@rocky

rocky commented Oct 19, 2026

Copy link
Copy Markdown
Member

LGTM - merge when you feel is ready.

Decide if you want to add edge-case tests around exact zero / near-zero / pole-adjacent values, where formulas divide by value or by sqrt(1-x^2).

@tombouctou
tombouctou force-pushed the arctrig-real-valued-precision branch from 694522f to 48907e4 Compare October 19, 2026 21:49
@tombouctou
tombouctou force-pushed the hyperbolic-real-valued-precision branch from 6eb4ce9 to 77e79f6 Compare October 19, 2026 21:49
tombouctou and others added 4 commits October 20, 2026 02:02
… number

Part of step 6 of #2047 (MATH-227), on top of the inverse trig PR.

As in WMA, the relative error of x is multiplied by Abs[x f'[x] / f[x]].
For Tanh and Coth, WMA uses the condition number of Sinh[x] / Cosh[x],
with both errors added, when Abs[x] < 1: Precision[Tanh[0.3`20]] is
19.9519, not 20.0258. This follows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As asked in review: mpmath numbers in and out, as for Power.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As for Cot and Csc: Csch of an inexact zero is ComplexInfinity, as in
WMA (Csch[0.] stayed unevaluated; Coth[0.] already had the rule). The
hyperbolic functions of 0``20 get their error from how much they change
near 0: Sinh[0``20] is 0``20, Cosh[0``20] is 1 with precision 40.301.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As suggested in review. Coth and Csch set has_poles, and Tanh and Sech
are bounded by 1 (see #2054). ArcCosh[1.`20] and ArcSech[1.`20] are
0``9.849, as in WMA, and the values next to 1 or to a pole are tested
too, as are 0`20 (an exact 0) and 0.0`20 (a machine 0.).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tombouctou added a commit that referenced this pull request Oct 19, 2026
As suggested in review, edge cases next to poles and where the formulas
divide by the value or by Sqrt[1 - x^2]. As in WMA:

- Where the condition factor does not exist (a division by zero, an
  infinite factor, or a value of 0), the error is how much f changes
  within the error of its argument, as was already done for an inexact
  zero argument; a side where f has no real value is left out. This is
  what the inverse functions need at the ends of their domain (#2055,
  #2056), where ArcSin[1.`20] raised ZeroDivisionError.
- A function with poles gives ComplexInfinity when no digit is left:
  Tan[N[Pi/2, 20]] and Tan[1.5707963`5] (was 0.`1.). Tan, Sec, Cot and
  Csc set has_poles.
- A bounded function has an error no larger than its bound:
  Sin[1.`20*^30] is 0``0. Sin and Cos set value_bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tombouctou
tombouctou force-pushed the arctrig-real-valued-precision branch from 48907e4 to 58715b3 Compare October 19, 2026 22:04
@tombouctou
tombouctou force-pushed the hyperbolic-real-valued-precision branch from 77e79f6 to 03d2771 Compare October 19, 2026 22:04
@tombouctou

Copy link
Copy Markdown
Collaborator Author

Thanks, added (03d2771, with the shared part in #2054), checked against WMA 15:

  • Exact and machine zeros: Sinh[020]is0, and Cosh[0.020] is a machine 1..
  • Near zero: Sinh[020]is020, and Cosh[020]`` is 1 with precision 40.301.
  • Poles: Coth and Csch of 0. or 020`` are ComplexInfinity; `Csch[0.]` stayed unevaluated before. `Coth` and `Csch` are also marked as having poles: like `Tan` in Sin, Cos, Tan, Cot, Sec, Csc: precision from the condition number, as in WMA #2054, they give `ComplexInfinity` when no digit is left.
  • Ends of the domain, where the formula divides by Sqrt[x^2 - 1]: ArcCosh[1.`20] and ArcSech[1.`20] are 09.849, from the fallback in #2054. Next to the end, ArcCosh[1.0000000120]`` has precision 12.301, and next to a pole ``ArcTanh[0.9999999920]`` has 13.2813.
  • Bounds: Tanh and Sech are bounded by 1, so when no digit is left their error is at most 1, as for Sin (Sin[1.`20*^30] is 00). Sinh[1000.`2]is unbounded and gives0``-434.993``.

Left as it was: ArcTanh[1.`20] is Infinity here and Indeterminate in WMA. That comes from SymPy, not from this code.

Base automatically changed from arctrig-real-valued-precision to master October 19, 2026 22:33
@tombouctou
tombouctou merged commit ad3a9b5 into master Oct 19, 2026
9 checks passed
@tombouctou
tombouctou deleted the hyperbolic-real-valued-precision branch October 19, 2026 22:33
tombouctou added a commit that referenced this pull request Oct 20, 2026
…n WMA (#2063)

A follow-up to #2056 (part of #2047).

`ArcTanh` and `ArcCoth` have poles at 1 and -1. For an inexact argument
there, SymPy gave `Infinity`. Near a pole, the old path gave `0.` or a
complex number even when the argument's error covered the pole. In WMA,
if the error of an inexact argument covers a pole, the result is
`Indeterminate`: within that error, the function goes to `Infinity` on
one side and takes an imaginary part on the other. A machine number has
no error, so only `1.` and `-1.` themselves are covered. Exact
`ArcTanh[1]` is still `Infinity`.

This is a new `MPMathFunction` attribute, `indeterminate_at`, checked by
`covers_point` in `mathics/eval/numeric.py`.

| Input | master | this PR | WMA 15 |
|---|---|---|---|
| ``ArcTanh[1.`20]`` | `Infinity` | `Indeterminate` | `Indeterminate` |
| `ArcTanh[-1.]` | `-Infinity` | `Indeterminate` | `Indeterminate` |
| `ArcCoth[1.]` | `Infinity` | `Indeterminate` | `Indeterminate` |
| ``ArcTanh[1.0000001`5]`` | `Infinity` | `Indeterminate` |
`Indeterminate` |
| ``ArcTanh[0.9999999`5]`` | `0.` | `Indeterminate` | `Indeterminate` |
| ``ArcTanh[1.000001`6]`` | `7.27805 - 1.5708 I` | `Indeterminate` |
`Indeterminate` |
| ``Precision[ArcTanh[0.999999`6.1]]`` | `1.26163` | `1.26163` |
`1.26163` |
| `ArcTanh[1]` | `Infinity` | `Infinity` | `Infinity` |

Right at the boundary, WMA gives ``ArcTanh[0.999999`6]`` = ``0``-6.``,
which looks like an artifact. Here it is a number with precision about
1.16.

## Tests

In `test_hyperbolic_real_valued_precision`
(`test/builtin/atomic/test_numbers.py`), with WMA 15 values, plus a
doctest in `ArcTanh`. Full run on kimmy next to master: all 4620
doctests pass. The pytest failures are the ones that depend on test
order: each failing file passes when run on its own.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: R. Bernstein <rocky@users.noreply.github.com>
rocky pushed a commit that referenced this pull request Oct 26, 2026
The rest of #2047 step 6, for complex numbers: complex arithmetic, and
functions with a complex value at a real argument, now get their
precision the way WMA does.

In WMA each part of a `Complex` keeps its own error. `Accuracy` combines
the two errors in quadrature, and `Precision` is that accuracy plus
`Log10` of the modulus; an exact part also counts toward the modulus.
Before this PR, Mathics3 gave a complex result the precision of its
least precise input, or machine precision, which is why ``1.5`20.3 +
2.5`20.3 I`` had precision 15.

- `Plus` and `Times` on complex numbers with exact or `PrecisionReal`
parts work out each part with the real rules. `Times` multiplies as a
balanced tree and squares a pair of equal factors. `z^n` squares
starting from the top bit of `n`, and `1/z` is `(a - b I)/(a^2 + b^2)`.
This is the order WMA uses, and it changes the last digits of the
precision. ``2.`20 I`` keeps an exact zero real part.
- A function with a complex value at a real argument (``ArcSin[2.`20]``,
``Log[-1.`20]``, ``ArcCosh[0``20]``, ``ArcSec[0.5`20]``, …) gives each
part the error from that part of `x f'[x]`, as #2052–#2056 do for real
values. A part that does not change gets `2^-54` of the error, so the
`Pi` part of ``Log[-1.`20]`` is 16 digits more precise than the real
part, as in WMA. At a zero or a branch point, the change of `f` within
the error goes to the nonzero parts.
- A negative `PrecisionReal` to a `Rational` power: `x^(1/q)` shares the
error of the whole value between its parts, and `x^(p/q)` is `x^n
(x^(1/q))^r`. Checked against WMA for `p/q` from −5/2 to 7/3.
- `Log[x]` is no longer rewritten as `Log[E, x]`: the numerified `E`
base added an error of its own.
- An inexact zero real part is now shown, as in WMA (``0``19.4 + 2.`19.7
I``). `N[I, 30]` is ``1.`30 I``.

| Input | master | this PR | WMA 15 |
|---|---|---|---|
| ``Precision[ArcSec[0.5`20]]`` | `15.1196` | `20.0571` | `20.0571` |
| ``Precision[1.5`20.3 + 2.5`20.3 I]`` | `15.` | `20.3` | `20.3` |
| ``ArcCosh[0``20]`` | ``1.5707963…`20.*I`` | ``1.5707963…`20.1961*I`` |
``1.5707963…`20.1961*I`` |
| ``Sqrt[-2.`20]`` | ``1.4142135…`20.*I`` | ``1.4142135…`20.301*I`` |
``1.4142135…`20.301*I`` |
| ``Precision[Log[-1.`20]]`` | `15.4971` | `20.4971` | `20.4971` |
| ``Accuracy[Im[Log[-1.`20]]] - Accuracy[Re[Log[-1.`20]]]`` | `4.50285`
| `16.2556` | `16.2556` |
| ``Precision[(1.`20 + 2.`25 I) (3.`22 - 1.`18 I)]`` | `15.` | `18.4939`
| `18.4939` |
| ``Precision[(1.`20 + 2.`25 I)^5]`` | `15.` | `19.3049` | `19.3049` |
| ``Precision[(-8.`20)^(1/3)]`` | `20.` | `20.4771` | `20.4771` |

Not in this PR: functions of a complex argument (``Sqrt[1.`20 + I]``,
``Exp[1.`20 I]``), a negative base with an inexact exponent, and `N` of
an exact complex number with two nonzero parts, where WMA splits the
precision between the parts.

## Tests

`test_complex_precision` and `test_complex_valued_precision` in
`test/builtin/atomic/test_numbers.py`, next to
`test_real_valued_precision`, with WMA 15 values. Full run on kimmy: all
doctests pass. pytest under xdist (`-n 4`) fails a different handful of
tests on each run, on master as well, because those tests depend on
symbols that earlier tests in the same worker define or clear (`f`, `F`,
`x`). With `--dist loadfile`, master fails 15 (`test_forms.py`,
`test_options.py`, `test_testing_expressions.py`, `test_latex.py`) and
this branch fails 7 (`test_list.py`, `test_diffeqns.py`,
`test_constructing.py`). None of them is about numbers, and each of
these files passes when run on its own.

Benchmark from #2050 on kimmy, best of 5, ms:

| Case | master | this PR |
|---|---|---|
| ``Total[Table[N[1/k, 50], {k, 2000}]]`` | 2645 | 2738 |
| ``Times @@ Table[N[1 + 1/k, 50], {k, 2000}]`` | 3255 | 3324 |
| ``Do[x = N[k, 50] + Pi, {k, 2000}]`` | 5388 | 5238 |
| ``Total[Table[1./k, {k, 2000}]]`` | 1988 | 2056 |
| ``Total[Table[N[1/k + I/(k+1), 50], {k, 2000}]]`` | 4884 | 5127 |
| ``Times @@ Table[N[1 + I/k, 50], {k, 500}]`` | 883 | 1070 |
| ``Table[N[1 + I/k, 30]^5, {k, 500}]`` | 1435 | 1820 |
| ``Table[1 / N[1 + I/k, 30], {k, 500}]`` | 1592 | 1670 |
| ``Table[ArcSin[N[1 + k, 30]], {k, 500}]`` | 1112 | 1504 |
| ``Table[Log[N[-k, 30]], {k, 500}]`` | 1421 | 1322 |

The real cases are unchanged within noise. Complex `Times`, `z^5` and
`ArcSin` outside its domain are 20–35% slower: their parts are now
computed from real operations, and `ArcSin` takes more derivatives.

🤖 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