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 +84 -0
- cfmath/_backend.py +259 -0
- cfmath/_poly.py +98 -0
- cfmath/_version.py +24 -0
- cfmath/archyperbolic.py +240 -0
- cfmath/arctrig.py +246 -0
- cfmath/constants.py +284 -0
- cfmath/convergents.py +53 -0
- cfmath/core.py +916 -0
- cfmath/debug.py +146 -0
- cfmath/exponential.py +226 -0
- cfmath/gosper.py +1202 -0
- cfmath/gosper.txt +23 -0
- cfmath/gosper_cf.py +631 -0
- cfmath/gosper_generalized.py +421 -0
- cfmath/hyperbolic.py +224 -0
- cfmath/logarithm.py +495 -0
- cfmath/mod.py +123 -0
- cfmath/polyratio.py +264 -0
- cfmath/power.py +703 -0
- cfmath/py.typed +0 -0
- cfmath/quadratic.py +396 -0
- cfmath/special.py +191 -0
- cfmath/trig.py +491 -0
- cfmath-1.0.0rc1.dist-info/METADATA +223 -0
- cfmath-1.0.0rc1.dist-info/RECORD +31 -0
- cfmath-1.0.0rc1.dist-info/WHEEL +5 -0
- cfmath-1.0.0rc1.dist-info/licenses/LICENSE +21 -0
- cfmath-1.0.0rc1.dist-info/scm_file_list.json +75 -0
- cfmath-1.0.0rc1.dist-info/scm_version.json +8 -0
- cfmath-1.0.0rc1.dist-info/top_level.txt +1 -0
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'
|
cfmath/archyperbolic.py
ADDED
|
@@ -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))
|