cfmath 1.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cfmath/__init__.py ADDED
@@ -0,0 +1,84 @@
1
+ """cfmath — continued fractions library."""
2
+
3
+ try:
4
+ from ._version import __version__
5
+ except ImportError: # pragma: no cover
6
+ __version__ = "dev"
7
+
8
+ from .archyperbolic import Arccosh, Arcsinh, Arctanh
9
+ from .arctrig import Arccos, Arcsin, Arctan, ArctrigMode
10
+ from .constants import Apery, Catalan, E, EulerGamma, Khinchin, Phi, Pi, Plastic, Tau
11
+ from .convergents import convergent, convergent_pair, convergent_pairs, convergents
12
+ from .core import CF
13
+ from .debug import CountingIterator, describe_source_tree, digits_with_debug
14
+ from .exponential import Exp
15
+ from .gosper import cf_add, cf_div, cf_homographic, cf_max, cf_min, cf_mul, cf_sub
16
+ from .gosper_cf import GosperBi, GosperGeneric, GosperMono
17
+ from .hyperbolic import Cosh, Sinh, Tanh
18
+ from .logarithm import Ln, Log, Log2, Log10
19
+ from .polyratio import PolyTransform
20
+ from .power import Cuberoot, Nthroot, Pow, PowCF, PowInterval, PowIntExponent, PowMode, PowMP
21
+ from .quadratic import Sqrt
22
+ from .special import Gamma, Zeta
23
+ from .trig import Cos, Sin, Tan, TrigMode
24
+
25
+ __all__ = [
26
+ "CF",
27
+ "convergent",
28
+ "convergent_pair",
29
+ "convergent_pairs",
30
+ "convergents",
31
+ "cf_add",
32
+ "cf_sub",
33
+ "cf_mul",
34
+ "cf_div",
35
+ "GosperMono",
36
+ "GosperBi",
37
+ "GosperGeneric",
38
+ "PolyTransform",
39
+ "cf_homographic",
40
+ "cf_min",
41
+ "cf_max",
42
+ "Sqrt",
43
+ "Phi",
44
+ "E",
45
+ "Pi",
46
+ "Tau",
47
+ "Cuberoot",
48
+ "Nthroot",
49
+ "Log2",
50
+ "Ln",
51
+ "Sin",
52
+ "Cos",
53
+ "Tan",
54
+ "TrigMode",
55
+ "Arcsin",
56
+ "Arccos",
57
+ "Arctan",
58
+ "ArctrigMode",
59
+ "Sinh",
60
+ "Cosh",
61
+ "Tanh",
62
+ "Arctanh",
63
+ "Arcsinh",
64
+ "Arccosh",
65
+ "Exp",
66
+ "EulerGamma",
67
+ "Catalan",
68
+ "Apery",
69
+ "Log",
70
+ "Log10",
71
+ "Pow",
72
+ "PowMode",
73
+ "PowIntExponent",
74
+ "PowCF",
75
+ "PowMP",
76
+ "PowInterval",
77
+ "Gamma",
78
+ "Zeta",
79
+ "Plastic",
80
+ "Khinchin",
81
+ "CountingIterator",
82
+ "describe_source_tree",
83
+ "digits_with_debug",
84
+ ]
cfmath/_backend.py ADDED
@@ -0,0 +1,259 @@
1
+ """Shared infrastructure for cfmath implementations.
2
+
3
+ Provides:
4
+ _HAS_MPMATH — True if mpmath is importable
5
+ _lazy_cf(fn) — wrap a batch-compute function into a lazy CF
6
+ _extract_cf_terms — extract CF terms from an mpmath value until precision runs out
7
+ _mpmath_cf(fn) — precision-automatic lazy CF from an mpmath value function
8
+ _mpmath_cf_for_cf_arg — apply an mpmath function to a CF argument via convergent approx
9
+ _coerce_trig_arg — validate and coerce int/Fraction inputs
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from fractions import Fraction
15
+ from typing import Any, Callable, Iterator
16
+
17
+ from .core import CF
18
+
19
+ try:
20
+ import mpmath as _mpmath # noqa: F401 — side-effect: validates import
21
+
22
+ _HAS_MPMATH = True
23
+ except ImportError:
24
+ _HAS_MPMATH = False
25
+
26
+
27
+ def _lazy_cf(
28
+ compute: Callable[[int], list[int]],
29
+ initial: int = 60,
30
+ batch: int = 50,
31
+ debug_source: Any | None = None,
32
+ ) -> CF:
33
+ """Build a lazily-extending CF from a batch-compute function.
34
+
35
+ ``compute(n)`` must return a list of at least *n* CF term integers.
36
+ The first call uses *initial* terms; when those run out, it recomputes
37
+ with *batch* more terms each time.
38
+ """
39
+ terms = compute(initial)
40
+ static = terms[:10]
41
+ rest = iter(terms[10:])
42
+
43
+ def _more() -> Iterator[int]:
44
+ yield from rest
45
+ offset = len(terms)
46
+ while True:
47
+ more = compute(offset + batch)
48
+ yield from more[offset:]
49
+ offset = len(more)
50
+
51
+ cf = CF(static, _source=_more())
52
+ if debug_source is not None:
53
+ cf._debug_source = debug_source
54
+ return cf
55
+
56
+
57
+ def _annotate_cf(cf: CF, debug_source: Any | None) -> CF:
58
+ """Attach debugging provenance to a CF and return it."""
59
+ if debug_source is not None:
60
+ cf._debug_source = debug_source
61
+ return cf
62
+
63
+
64
+ def _extract_cf_terms(x: Any, guard_bits: int = 64) -> list[int]:
65
+ """Extract CF terms from an mpmath value until precision is exhausted.
66
+
67
+ After k extraction steps the effective precision is approximately
68
+ ``mp.prec - 2*log2(q_k)`` bits, where q_k is the k-th convergent denominator.
69
+ We stop before a step would push ``2*log2(q)`` past ``mp.prec - guard_bits``.
70
+
71
+ We track ``log2(q)`` cheaply via the identity
72
+ ``log2(q_{k+1}) ≈ log2(q_k) + log2(1/frac_k)``
73
+ (since ``a_{k+1} = floor(1/frac_k) ≈ 1/frac_k``).
74
+
75
+ This naturally accounts for large terms: π's term-4 value of 292 costs ~8 bits
76
+ without any per-constant tuning, and also catches exhaustion by many
77
+ moderate-sized terms (which a simple ``frac < threshold`` check misses).
78
+ """
79
+ import mpmath
80
+
81
+ available = float(mpmath.mp.prec - guard_bits)
82
+ log_q = 0.0 # tracks log2 of the convergent denominator
83
+ terms: list[int] = []
84
+ while True:
85
+ a = int(mpmath.floor(x))
86
+ frac = x - a
87
+ terms.append(a)
88
+ if frac == 0:
89
+ break
90
+ # Track log2 of the convergent denominator with mpmath, not float(frac).
91
+ # A huge partial quotient makes frac underflow a double to 0.0, which
92
+ # would stop extraction even when mpmath still has the precision to go on
93
+ # (and escalating dps wouldn't help — float underflows at any dps).
94
+ # mpmath.log of a tiny frac is a moderate number; the precision check
95
+ # below — comparing the cost against available bits — is the real and
96
+ # only stopping rule.
97
+ log_q -= float(mpmath.log(frac, 2)) # log2(1/frac) = -log2(frac)
98
+ if 2.0 * log_q > available:
99
+ break
100
+ x = 1 / frac
101
+ return terms
102
+
103
+
104
+ def _mpmath_cf(
105
+ value_fn: Callable[[], Any],
106
+ initial_dps: int = 100,
107
+ scale: float = 2.0,
108
+ guard_bits: int = 64,
109
+ debug_source: Any | None = None,
110
+ ) -> CF:
111
+ """Build a lazy CF from an mpmath value function with automatic precision management.
112
+
113
+ ``value_fn()`` is called with ``mp.dps`` already set; it should return the
114
+ mpmath value to convert (e.g. ``lambda: mpmath.euler``).
115
+
116
+ Always maintains two consecutive precision levels (lo and hi = lo × scale).
117
+ Only terms where lo and hi agree are emitted — the first disagreement is the
118
+ empirical precision boundary, so no per-constant tuning is needed and marginal
119
+ terms are automatically filtered. When the consumer asks for more terms, hi
120
+ becomes the new lo and a fresh hi is computed at the next level.
121
+ """
122
+ import mpmath
123
+
124
+ def _get(dps: int) -> list[int]:
125
+ mpmath.mp.dps = dps
126
+ return _extract_cf_terms(value_fn(), guard_bits)
127
+
128
+ def _agree_up_to(a: list[int], b: list[int]) -> int:
129
+ """First index where a and b differ, or min(len(a), len(b)) if all agree."""
130
+ for i, (x, y) in enumerate(zip(a, b)):
131
+ if x != y:
132
+ return i
133
+ return min(len(a), len(b))
134
+
135
+ # Bootstrap: compute at two consecutive levels, emit only agreed terms.
136
+ lo = _get(initial_dps)
137
+ hi = _get(int(initial_dps * scale))
138
+ first_valid = _agree_up_to(lo, hi)
139
+
140
+ static = hi[: min(10, first_valid)] if first_valid else hi[:1]
141
+
142
+ def _source() -> Iterator[int]:
143
+ cur_lo = hi
144
+ cur_dps = int(initial_dps * scale)
145
+ emitted = len(static)
146
+
147
+ yield from cur_lo[emitted:first_valid]
148
+ emitted = first_valid
149
+
150
+ while True:
151
+ cur_dps = int(cur_dps * scale)
152
+ cur_hi = _get(cur_dps)
153
+ new_valid = _agree_up_to(cur_lo, cur_hi)
154
+
155
+ if new_valid < emitted:
156
+ raise ArithmeticError(
157
+ f"CF term {new_valid} changed from {cur_lo[new_valid]} to "
158
+ f"{cur_hi[new_valid]} at {cur_dps} dps — previously emitted "
159
+ f"term is wrong"
160
+ )
161
+
162
+ new_terms = cur_hi[emitted:new_valid]
163
+ if new_terms:
164
+ yield from new_terms
165
+ emitted = new_valid
166
+
167
+ cur_lo = cur_hi
168
+
169
+ cf = CF(static, _source=_source())
170
+ if debug_source is not None:
171
+ cf._debug_source = debug_source
172
+ return cf
173
+
174
+
175
+ def _cf_terms_from_interval_approximator(
176
+ interval_at_precision: Callable[[int], tuple[Fraction, Fraction]],
177
+ n_terms: int,
178
+ initial: int = 16,
179
+ max_precision: int = 1 << 16,
180
+ ) -> list[int]:
181
+ """Return CF terms once rational intervals prove them.
182
+
183
+ The caller supplies ``interval_at_precision(p)``, which must return
184
+ rational bounds ``lo <= x <= hi``. This helper extracts simple-CF terms
185
+ only while both endpoints force the same next integer. If the interval
186
+ straddles an integer boundary or zero after subtracting a term, the helper
187
+ doubles precision and starts over.
188
+ """
189
+ precision = initial
190
+ while precision <= max_precision:
191
+ lo, hi = interval_at_precision(precision)
192
+ if lo > hi:
193
+ lo, hi = hi, lo
194
+
195
+ terms: list[int] = []
196
+ while len(terms) < n_terms:
197
+ if lo == hi:
198
+ terms.extend(CF.from_rational(lo).terms)
199
+ return terms[:n_terms]
200
+
201
+ a_lo = lo.numerator // lo.denominator
202
+ a_hi = hi.numerator // hi.denominator
203
+ if a_lo != a_hi:
204
+ break
205
+
206
+ a = a_lo
207
+ terms.append(a)
208
+ lo -= a
209
+ hi -= a
210
+
211
+ if lo == hi == 0:
212
+ return terms
213
+ if lo <= 0 <= hi:
214
+ break
215
+
216
+ lo, hi = Fraction(1, hi), Fraction(1, lo)
217
+ if lo > hi:
218
+ lo, hi = hi, lo
219
+
220
+ if len(terms) >= n_terms:
221
+ return terms[:n_terms]
222
+ precision *= 2
223
+
224
+ raise ValueError(f"could not pin {n_terms} CF terms by precision {max_precision}")
225
+
226
+
227
+ def _mpmath_cf_for_cf_arg(x: CF, fn: Callable[[Any], Any]) -> CF:
228
+ """Return CF for fn(x) where x is a CF, using dual-precision verification.
229
+
230
+ Approximates x by a convergent deep enough that the rational-approximation
231
+ error is negligible relative to the mpmath working precision, then emits CF
232
+ terms via _mpmath_cf's agree-at-two-levels protocol.
233
+
234
+ fn must accept a single mpmath.mpf and return an mpmath value (e.g.
235
+ mpmath.sin, mpmath.cosh, mpmath.atan).
236
+ """
237
+ import mpmath
238
+
239
+ from .convergents import convergent
240
+
241
+ def _value_fn() -> Any:
242
+ dps = mpmath.mp.dps
243
+ depth = max(5 * dps, 60)
244
+ try:
245
+ approx = convergent(x, depth)
246
+ except IndexError:
247
+ approx = x.to_fraction()
248
+ return fn(mpmath.mpf(approx.numerator) / mpmath.mpf(approx.denominator))
249
+
250
+ return _mpmath_cf(_value_fn)
251
+
252
+
253
+ def _coerce_trig_arg(x: int | Fraction) -> Fraction:
254
+ """Validate and coerce a trig/hyperbolic function argument to Fraction."""
255
+ if isinstance(x, int):
256
+ return Fraction(x)
257
+ if isinstance(x, Fraction):
258
+ return x
259
+ raise TypeError(f"expected int or Fraction, got {type(x).__name__}")
cfmath/_poly.py ADDED
@@ -0,0 +1,98 @@
1
+ """Integer polynomial arithmetic over ascending-degree coefficient lists.
2
+
3
+ A polynomial is a list of int coefficients, lowest degree first: ``[c0, c1, c2]``
4
+ means ``c0 + c1*x + c2*x**2``. The zero polynomial is ``[0]``.
5
+
6
+ Two callers share these primitives, and they pull in opposite directions. The
7
+ ``PolyTransform`` algebra in ``polyratio`` reduces by GCD, so it needs a *canonical*
8
+ form: no trailing zeros, leading coefficient nonzero, otherwise degree counts and
9
+ pseudo-remainders go wrong. The meta-CF rebuild path in ``gosper`` only ever
10
+ evaluates the polynomials it builds, so trailing zeros are invisible to it but
11
+ the products are many and low-degree, where a scalar/monomial fast path pays off.
12
+
13
+ ``add``/``sub``/``mul`` therefore strip their results (satisfying the first caller)
14
+ and ``mul`` keeps the fast paths (satisfying the second). Stripping is
15
+ evaluation-invariant, so the rebuild path is unaffected by it.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from math import gcd
21
+
22
+
23
+ def strip(p: list[int]) -> list[int]:
24
+ """Drop trailing zero coefficients, keeping at least the constant term."""
25
+ while len(p) > 1 and p[-1] == 0:
26
+ p = p[:-1]
27
+ return p
28
+
29
+
30
+ def content(p: list[int]) -> int:
31
+ """Return the GCD of the coefficients (1 for the zero polynomial).
32
+
33
+ This is the GCD of a set of integers, so it does not care about coefficient
34
+ order; descending-degree callers (power's Nthroot engine) may use it too.
35
+ """
36
+ g = 0
37
+ for c in p:
38
+ g = gcd(g, abs(c))
39
+ return g or 1
40
+
41
+
42
+ def primitive(p: list[int]) -> list[int]:
43
+ """Divide out the content, leaving a primitive polynomial."""
44
+ g = content(p)
45
+ return [c // g for c in p]
46
+
47
+
48
+ def add(p: list[int], q: list[int]) -> list[int]:
49
+ """Add two polynomials."""
50
+ n = max(len(p), len(q))
51
+ return strip([(p[i] if i < len(p) else 0) + (q[i] if i < len(q) else 0) for i in range(n)])
52
+
53
+
54
+ def sub(p: list[int], q: list[int]) -> list[int]:
55
+ """Subtract q from p."""
56
+ n = max(len(p), len(q))
57
+ return strip([(p[i] if i < len(p) else 0) - (q[i] if i < len(q) else 0) for i in range(n)])
58
+
59
+
60
+ def mul(p: list[int], q: list[int]) -> list[int]:
61
+ """Multiply two polynomials.
62
+
63
+ Scalar and single-term (monomial) operands are common in the meta-CF rebuild
64
+ path and get direct fast paths; the general case skips zero coefficients.
65
+ """
66
+ if len(p) == 1:
67
+ k = p[0]
68
+ if k == 0:
69
+ return [0]
70
+ if k == 1:
71
+ return strip(list(q))
72
+ return strip([k * c for c in q])
73
+ if len(q) == 1:
74
+ k = q[0]
75
+ if k == 0:
76
+ return [0]
77
+ if k == 1:
78
+ return strip(list(p))
79
+ return strip([k * c for c in p])
80
+
81
+ nonzero_p = [i for i, c in enumerate(p) if c != 0]
82
+ if len(nonzero_p) == 1:
83
+ shift = nonzero_p[0]
84
+ k = p[shift]
85
+ return strip([0] * shift + [k * c for c in q])
86
+
87
+ nonzero_q = [i for i, c in enumerate(q) if c != 0]
88
+ if len(nonzero_q) == 1:
89
+ shift = nonzero_q[0]
90
+ k = q[shift]
91
+ return strip([0] * shift + [k * c for c in p])
92
+
93
+ out = [0] * (len(p) + len(q) - 1)
94
+ for i in nonzero_p:
95
+ ci = p[i]
96
+ for j in nonzero_q:
97
+ out[i + j] += ci * q[j]
98
+ return strip(out)
cfmath/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.0.0rc1'
22
+ __version_tuple__ = version_tuple = (1, 0, 0, 'rc1')
23
+
24
+ __commit_id__ = commit_id = 'gaa80809af'
@@ -0,0 +1,240 @@
1
+ """Inverse hyperbolic functions as continued fractions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fractions import Fraction
6
+
7
+ from ._backend import _HAS_MPMATH, _annotate_cf, _coerce_trig_arg, _lazy_cf, _mpmath_cf_for_cf_arg
8
+ from .core import CF
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Decimal backends
12
+ # ---------------------------------------------------------------------------
13
+
14
+
15
+ def _arcsinh_terms_from_decimal(x_num: int, x_den: int, n_terms: int) -> list[int]:
16
+ """Compute n_terms CF terms of arcsinh(x_num/x_den).
17
+
18
+ Uses arcsinh(x) = ln(x + sqrt(x²+1)) computed entirely in Decimal.
19
+ Works for all real x.
20
+ """
21
+ import decimal
22
+
23
+ from .logarithm import _decimal_ln2
24
+
25
+ prec = n_terms * 5 + 80
26
+ ctx = decimal.Context(prec=prec, rounding=decimal.ROUND_FLOOR)
27
+ with decimal.localcontext(ctx):
28
+ eps = decimal.Decimal(10) ** (-(prec - 10))
29
+
30
+ def _two_atanh(t: decimal.Decimal) -> decimal.Decimal:
31
+ t2 = t * t
32
+ term = t
33
+ val = t
34
+ k = 1
35
+ while True:
36
+ term *= t2
37
+ delta = term / decimal.Decimal(2 * k + 1)
38
+ val += delta
39
+ if abs(delta) < eps:
40
+ break
41
+ k += 1
42
+ return 2 * val
43
+
44
+ x = decimal.Decimal(x_num) / decimal.Decimal(x_den)
45
+ inner = x + (x * x + 1).sqrt()
46
+
47
+ ln2 = _decimal_ln2(prec)
48
+ n = 0
49
+ reduced = inner
50
+ while reduced >= 2:
51
+ reduced /= 2
52
+ n += 1
53
+ while reduced < decimal.Decimal("0.5"):
54
+ reduced *= 2
55
+ n -= 1
56
+
57
+ ln_val = decimal.Decimal(n) * ln2 + _two_atanh((reduced - 1) / (reduced + 1))
58
+
59
+ terms: list[int] = []
60
+ for _ in range(n_terms):
61
+ a = int(ln_val.to_integral_value(rounding=decimal.ROUND_FLOOR))
62
+ terms.append(a)
63
+ frac = ln_val - a
64
+ if frac <= eps:
65
+ break
66
+ ln_val = decimal.Decimal(1) / frac
67
+
68
+ return terms
69
+
70
+
71
+ def _arccosh_terms_from_decimal(x_num: int, x_den: int, n_terms: int) -> list[int]:
72
+ """Compute n_terms CF terms of arccosh(x_num/x_den).
73
+
74
+ Uses arccosh(x) = ln(x + sqrt(x²-1)) computed entirely in Decimal.
75
+ Requires x ≥ 1.
76
+ """
77
+ import decimal
78
+
79
+ from .logarithm import _decimal_ln2
80
+
81
+ prec = n_terms * 5 + 80
82
+ ctx = decimal.Context(prec=prec, rounding=decimal.ROUND_FLOOR)
83
+ with decimal.localcontext(ctx):
84
+ eps = decimal.Decimal(10) ** (-(prec - 10))
85
+
86
+ def _two_atanh(t: decimal.Decimal) -> decimal.Decimal:
87
+ t2 = t * t
88
+ term = t
89
+ val = t
90
+ k = 1
91
+ while True:
92
+ term *= t2
93
+ delta = term / decimal.Decimal(2 * k + 1)
94
+ val += delta
95
+ if abs(delta) < eps:
96
+ break
97
+ k += 1
98
+ return 2 * val
99
+
100
+ x = decimal.Decimal(x_num) / decimal.Decimal(x_den)
101
+ inner = x + (x * x - 1).sqrt()
102
+
103
+ if inner <= eps:
104
+ return [0]
105
+
106
+ ln2 = _decimal_ln2(prec)
107
+ n = 0
108
+ reduced = inner
109
+ while reduced >= 2:
110
+ reduced /= 2
111
+ n += 1
112
+ while reduced < decimal.Decimal("0.5"):
113
+ reduced *= 2
114
+ n -= 1
115
+
116
+ ln_val = decimal.Decimal(n) * ln2 + _two_atanh((reduced - 1) / (reduced + 1))
117
+
118
+ terms: list[int] = []
119
+ for _ in range(n_terms):
120
+ a = int(ln_val.to_integral_value(rounding=decimal.ROUND_FLOOR))
121
+ terms.append(a)
122
+ frac = ln_val - a
123
+ if frac <= eps:
124
+ break
125
+ ln_val = decimal.Decimal(1) / frac
126
+
127
+ return terms
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # mpmath backends
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ def _arcsinh_terms_mpmath(x_num: int, x_den: int, n_terms: int) -> list[int]:
136
+ """Compute n_terms CF terms of arcsinh(x_num/x_den) using mpmath."""
137
+ import mpmath
138
+
139
+ mpmath.mp.dps = n_terms * 5 + 80
140
+ val = mpmath.asinh(mpmath.mpf(x_num) / mpmath.mpf(x_den))
141
+ terms: list[int] = []
142
+ for _ in range(n_terms):
143
+ a = int(mpmath.floor(val))
144
+ terms.append(a)
145
+ val = 1 / (val - a)
146
+ return terms
147
+
148
+
149
+ def _arccosh_terms_mpmath(x_num: int, x_den: int, n_terms: int) -> list[int]:
150
+ """Compute n_terms CF terms of arccosh(x_num/x_den) using mpmath."""
151
+ import mpmath
152
+
153
+ mpmath.mp.dps = n_terms * 5 + 80
154
+ val = mpmath.acosh(mpmath.mpf(x_num) / mpmath.mpf(x_den))
155
+ terms: list[int] = []
156
+ for _ in range(n_terms):
157
+ a = int(mpmath.floor(val))
158
+ terms.append(a)
159
+ val = 1 / (val - a)
160
+ return terms
161
+
162
+
163
+ def _arctanh_terms_mpmath(x_num: int, x_den: int, n_terms: int) -> list[int]:
164
+ """Compute n_terms CF terms of arctanh(x_num/x_den) using mpmath."""
165
+ import mpmath
166
+
167
+ mpmath.mp.dps = n_terms * 5 + 80
168
+ val = mpmath.atanh(mpmath.mpf(x_num) / mpmath.mpf(x_den))
169
+ terms: list[int] = []
170
+ for _ in range(n_terms):
171
+ a = int(mpmath.floor(val))
172
+ terms.append(a)
173
+ val = 1 / (val - a)
174
+ return terms
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Public functions
179
+ # ---------------------------------------------------------------------------
180
+
181
+
182
+ def Arctanh(x: int | Fraction | CF) -> CF:
183
+ """Inverse hyperbolic tangent of x, as a continued fraction.
184
+
185
+ For int/Fraction, uses arctanh(x) = ln((1+x)/(1-x)) / 2 (|x| < 1 required).
186
+ For CF input, uses the mpmath dual-precision convergent approach.
187
+ """
188
+ if isinstance(x, CF):
189
+ import mpmath
190
+
191
+ return _mpmath_cf_for_cf_arg(x, mpmath.atanh)
192
+ x = _coerce_trig_arg(x)
193
+ if abs(x) >= 1:
194
+ raise ValueError(f"Arctanh argument must satisfy |x| < 1, got {x}")
195
+ if x == 0:
196
+ return _annotate_cf(CF.from_int(0), ("Arctanh", x))
197
+ ratio = (Fraction(1) + x) / (Fraction(1) - x)
198
+ from .logarithm import Ln
199
+
200
+ return _annotate_cf(Ln(ratio) / CF.from_int(2), ("Arctanh", x))
201
+
202
+
203
+ def Arcsinh(x: int | Fraction | CF) -> CF:
204
+ """Inverse hyperbolic sine of x, as a continued fraction.
205
+
206
+ For int/Fraction, uses arcsinh(x) = ln(x + sqrt(x²+1)).
207
+ For CF input, uses the mpmath dual-precision convergent approach.
208
+ """
209
+ if isinstance(x, CF):
210
+ import mpmath
211
+
212
+ return _mpmath_cf_for_cf_arg(x, mpmath.asinh)
213
+ x = _coerce_trig_arg(x)
214
+ if x == 0:
215
+ return _annotate_cf(CF.from_int(0), ("Arcsinh", x))
216
+ num, den = x.numerator, x.denominator
217
+ if _HAS_MPMATH:
218
+ return _lazy_cf(lambda n: _arcsinh_terms_mpmath(num, den, n), debug_source=("Arcsinh", x))
219
+ return _lazy_cf(lambda n: _arcsinh_terms_from_decimal(num, den, n), debug_source=("Arcsinh", x))
220
+
221
+
222
+ def Arccosh(x: int | Fraction | CF) -> CF:
223
+ """Inverse hyperbolic cosine of x, as a continued fraction.
224
+
225
+ For int/Fraction, uses arccosh(x) = ln(x + sqrt(x²-1)) (x ≥ 1 required).
226
+ For CF input, uses the mpmath dual-precision convergent approach.
227
+ """
228
+ if isinstance(x, CF):
229
+ import mpmath
230
+
231
+ return _mpmath_cf_for_cf_arg(x, mpmath.acosh)
232
+ x = _coerce_trig_arg(x)
233
+ if x < 1:
234
+ raise ValueError(f"Arccosh argument must satisfy x ≥ 1, got {x}")
235
+ if x == 1:
236
+ return _annotate_cf(CF.from_int(0), ("Arccosh", x))
237
+ num, den = x.numerator, x.denominator
238
+ if _HAS_MPMATH:
239
+ return _lazy_cf(lambda n: _arccosh_terms_mpmath(num, den, n), debug_source=("Arccosh", x))
240
+ return _lazy_cf(lambda n: _arccosh_terms_from_decimal(num, den, n), debug_source=("Arccosh", x))