fmsl 0.7.0__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.
- fmsl/__init__.py +13 -0
- fmsl/__main__.py +14 -0
- fmsl/algebraic.py +303 -0
- fmsl/bifurcation.py +151 -0
- fmsl/cluster.py +348 -0
- fmsl/compiler.py +469 -0
- fmsl/engine.py +349 -0
- fmsl/gauss.py +107 -0
- fmsl/matrix.py +153 -0
- fmsl/multivar.py +525 -0
- fmsl/plane.py +441 -0
- fmsl/reference.py +119 -0
- fmsl/siletz.py +207 -0
- fmsl/space.py +742 -0
- fmsl-0.7.0.dist-info/METADATA +553 -0
- fmsl-0.7.0.dist-info/RECORD +20 -0
- fmsl-0.7.0.dist-info/WHEEL +5 -0
- fmsl-0.7.0.dist-info/entry_points.txt +2 -0
- fmsl-0.7.0.dist-info/licenses/LICENSE +21 -0
- fmsl-0.7.0.dist-info/top_level.txt +1 -0
fmsl/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""FMSL: certified analysis and safe code generation for formulas with 0/0 traps."""
|
|
2
|
+
from .engine import V, x, Exp, Sin, Cos, Essential
|
|
3
|
+
from .gauss import G, I
|
|
4
|
+
from .compiler import run, compile_text, analyze
|
|
5
|
+
from .siletz import proven_series_error
|
|
6
|
+
|
|
7
|
+
__version__ = "0.7.0"
|
|
8
|
+
# Matrix functions (requires numpy): from fmsl import matrix
|
|
9
|
+
# Two variables / exact arcs / algebraic numbers / iteration:
|
|
10
|
+
# from fmsl import multivar, cluster, algebraic, bifurcation
|
|
11
|
+
# Three variables / exact two-variable ranges: from fmsl import space, plane
|
|
12
|
+
__all__ = ["V", "x", "Exp", "Sin", "Cos", "Essential", "G", "I",
|
|
13
|
+
"run", "compile_text", "analyze", "proven_series_error"]
|
fmsl/__main__.py
ADDED
fmsl/algebraic.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fmsl.algebraic -- exact real algebraic numbers.
|
|
3
|
+
|
|
4
|
+
A real algebraic number alpha is represented by a square-free polynomial p with rational
|
|
5
|
+
coefficients and a rational isolating interval (lo, hi] containing exactly one real root
|
|
6
|
+
of p. Elements of Q(alpha) are polynomials in alpha reduced modulo p.
|
|
7
|
+
|
|
8
|
+
* zero test: g(alpha) = 0 <=> h = gcd(g, p) has its root in (lo, hi] (Sturm count)
|
|
9
|
+
when g(alpha) != 0 but gcd(g, p) is nontrivial, p is replaced by p / h,
|
|
10
|
+
which still has alpha as a root (so p need not be irreducible up front)
|
|
11
|
+
* inverse: extended Euclid once gcd(g, p) = 1
|
|
12
|
+
* sign: interval evaluation of g on (lo, hi], refining by bisection
|
|
13
|
+
All decisions are exact; floating point is used only for display.
|
|
14
|
+
"""
|
|
15
|
+
from fractions import Fraction as Fr
|
|
16
|
+
from numbers import Rational
|
|
17
|
+
|
|
18
|
+
_NUM = (int, Rational)
|
|
19
|
+
|
|
20
|
+
# ------------------------------ univariate polynomials over Q ------------------------------
|
|
21
|
+
def ptrim(p):
|
|
22
|
+
p = list(p)
|
|
23
|
+
while p and p[-1] == 0: p.pop()
|
|
24
|
+
return p
|
|
25
|
+
|
|
26
|
+
def padd(a, b):
|
|
27
|
+
n = max(len(a), len(b))
|
|
28
|
+
return ptrim([(a[i] if i < len(a) else 0) + (b[i] if i < len(b) else 0) for i in range(n)])
|
|
29
|
+
|
|
30
|
+
def psub(a, b): return padd(a, [-v for v in b])
|
|
31
|
+
|
|
32
|
+
def pmul(a, b):
|
|
33
|
+
if not a or not b: return []
|
|
34
|
+
r = [Fr(0)] * (len(a) + len(b) - 1)
|
|
35
|
+
for i, x in enumerate(a):
|
|
36
|
+
if x:
|
|
37
|
+
for j, y in enumerate(b): r[i + j] += x * y
|
|
38
|
+
return ptrim(r)
|
|
39
|
+
|
|
40
|
+
def pdivmod(a, b):
|
|
41
|
+
a = ptrim(a); b = ptrim(b)
|
|
42
|
+
if not b: raise ZeroDivisionError("polynomial division by zero")
|
|
43
|
+
q = [Fr(0)] * max(len(a) - len(b) + 1, 1)
|
|
44
|
+
while len(a) >= len(b) and a:
|
|
45
|
+
k = len(a) - len(b); f = Fr(a[-1]) / b[-1]; q[k] = f
|
|
46
|
+
for i, bv in enumerate(b): a[i + k] -= f * bv
|
|
47
|
+
a = ptrim(a)
|
|
48
|
+
return ptrim(q), a
|
|
49
|
+
|
|
50
|
+
def pgcd(a, b):
|
|
51
|
+
a, b = ptrim(a), ptrim(b)
|
|
52
|
+
while b: a, b = b, pdivmod(a, b)[1]
|
|
53
|
+
return [Fr(v) / a[-1] for v in a] if a else a
|
|
54
|
+
|
|
55
|
+
def pderiv(p): return ptrim([i * p[i] for i in range(1, len(p))])
|
|
56
|
+
|
|
57
|
+
def peval(p, x):
|
|
58
|
+
acc = Fr(0)
|
|
59
|
+
for v in reversed(p): acc = acc * x + v
|
|
60
|
+
return acc
|
|
61
|
+
|
|
62
|
+
def squarefree(p):
|
|
63
|
+
p = ptrim(p)
|
|
64
|
+
return pdivmod(p, pgcd(p, pderiv(p)))[0] if len(p) > 1 else p
|
|
65
|
+
|
|
66
|
+
def _primitive_positive(p):
|
|
67
|
+
"""p scaled by a POSITIVE rational to integer coefficients with gcd 1 (signs unchanged)"""
|
|
68
|
+
from math import gcd
|
|
69
|
+
den = 1
|
|
70
|
+
for v in p:
|
|
71
|
+
v = Fr(v); den = den * v.denominator // gcd(den, v.denominator)
|
|
72
|
+
ints = [int(Fr(v) * den) for v in p]
|
|
73
|
+
g = 0
|
|
74
|
+
for v in ints: g = gcd(g, abs(v))
|
|
75
|
+
return [Fr(v // g) for v in ints] if g > 1 else [Fr(v) for v in ints]
|
|
76
|
+
|
|
77
|
+
def sturm_sequence(p):
|
|
78
|
+
"""Sturm sequence, each member rescaled by a positive constant (keeps the coefficients
|
|
79
|
+
small without changing any sign)"""
|
|
80
|
+
seq = [_primitive_positive(ptrim(p)), _primitive_positive(pderiv(p))]
|
|
81
|
+
while len(seq[-1]) > 1:
|
|
82
|
+
r = pdivmod(seq[-2], seq[-1])[1]
|
|
83
|
+
if not r: break
|
|
84
|
+
seq.append(_primitive_positive([-v for v in r]))
|
|
85
|
+
return [s for s in seq if s]
|
|
86
|
+
|
|
87
|
+
def _changes(vals):
|
|
88
|
+
vals = [v for v in vals if v != 0]
|
|
89
|
+
return sum(1 for a, b in zip(vals, vals[1:]) if (a > 0) != (b > 0))
|
|
90
|
+
|
|
91
|
+
_STURM_CACHE = {}
|
|
92
|
+
|
|
93
|
+
def _sturm_of(p):
|
|
94
|
+
"""(square-free part, Sturm sequence as integer coefficient lists), cached"""
|
|
95
|
+
key = tuple(Fr(v) for v in ptrim(p))
|
|
96
|
+
hit = _STURM_CACHE.get(key)
|
|
97
|
+
if hit is None:
|
|
98
|
+
sq = squarefree(list(key))
|
|
99
|
+
seq = [[int(v) for v in s] for s in sturm_sequence(sq)] if len(sq) > 1 else []
|
|
100
|
+
hit = (sq, seq)
|
|
101
|
+
if len(_STURM_CACHE) > 2000: _STURM_CACHE.clear()
|
|
102
|
+
_STURM_CACHE[key] = hit
|
|
103
|
+
return hit
|
|
104
|
+
|
|
105
|
+
def _sign_at(coeffs, x):
|
|
106
|
+
"""sign of an integer polynomial at the rational x = a/b (b > 0), integer arithmetic only:
|
|
107
|
+
p(a/b) * b^n = sum_i c_i a^i b^(n-i)"""
|
|
108
|
+
x = Fr(x); a, b = x.numerator, x.denominator
|
|
109
|
+
n = len(coeffs) - 1
|
|
110
|
+
tot = 0
|
|
111
|
+
apow, bpow = 1, b ** n
|
|
112
|
+
for i in range(n + 1):
|
|
113
|
+
tot += coeffs[i] * apow * bpow
|
|
114
|
+
apow *= a
|
|
115
|
+
if i < n: bpow //= b
|
|
116
|
+
return (tot > 0) - (tot < 0)
|
|
117
|
+
|
|
118
|
+
def count_roots(p, lo, hi):
|
|
119
|
+
"""distinct real roots of p in (lo, hi]"""
|
|
120
|
+
sq, seq = _sturm_of(p)
|
|
121
|
+
if len(sq) <= 1: return 0
|
|
122
|
+
def ch(x):
|
|
123
|
+
sg = [t for t in (_sign_at(s, x) for s in seq) if t != 0]
|
|
124
|
+
return sum(1 for u, v in zip(sg, sg[1:]) if u != v)
|
|
125
|
+
return ch(lo) - ch(hi)
|
|
126
|
+
|
|
127
|
+
def root_bound(p):
|
|
128
|
+
p = ptrim(p)
|
|
129
|
+
return 1 + max(abs(Fr(v) / p[-1]) for v in p[:-1]) if len(p) > 1 else Fr(1)
|
|
130
|
+
|
|
131
|
+
def isolate_real_roots(p, lo=None, hi=None):
|
|
132
|
+
"""isolating intervals (lo, hi] for the distinct real roots of p (rational p), optionally
|
|
133
|
+
only those in (lo, hi]. Returns list of ('rational', r) or ('interval', lo, hi)."""
|
|
134
|
+
p = squarefree(p)
|
|
135
|
+
if len(p) <= 1: return []
|
|
136
|
+
B = root_bound(p)
|
|
137
|
+
L = -B if lo is None else max(-B, Fr(lo))
|
|
138
|
+
H = B if hi is None else min(B, Fr(hi))
|
|
139
|
+
if L >= H: return []
|
|
140
|
+
out, stack = [], [(L, H)]
|
|
141
|
+
while stack:
|
|
142
|
+
lo, hi = stack.pop()
|
|
143
|
+
n = count_roots(p, lo, hi)
|
|
144
|
+
if n == 0: continue
|
|
145
|
+
if n == 1:
|
|
146
|
+
if peval(p, hi) == 0: out.append(("rational", hi))
|
|
147
|
+
else: out.append(("interval", lo, hi))
|
|
148
|
+
continue
|
|
149
|
+
mid = (lo + hi) / 2
|
|
150
|
+
stack += [(lo, mid), (mid, hi)]
|
|
151
|
+
return sorted(out, key=lambda r: r[1])
|
|
152
|
+
|
|
153
|
+
def _ieval(p, lo, hi):
|
|
154
|
+
"""interval enclosure of p over [lo, hi]"""
|
|
155
|
+
a = b = Fr(0)
|
|
156
|
+
for v in reversed(p):
|
|
157
|
+
cands = (a * lo, a * hi, b * lo, b * hi)
|
|
158
|
+
a, b = min(cands) + v, max(cands) + v
|
|
159
|
+
return a, b
|
|
160
|
+
|
|
161
|
+
# ------------------------------ the number field Q(alpha) ------------------------------
|
|
162
|
+
class AlgField:
|
|
163
|
+
"""alpha = the unique root of the square-free polynomial p in (lo, hi]"""
|
|
164
|
+
def __init__(s, p, lo, hi):
|
|
165
|
+
s.p, s.lo, s.hi = squarefree(p), Fr(lo), Fr(hi)
|
|
166
|
+
assert count_roots(s.p, s.lo, s.hi) == 1, "interval must isolate exactly one root"
|
|
167
|
+
|
|
168
|
+
def refine(s):
|
|
169
|
+
mid = (s.lo + s.hi) / 2
|
|
170
|
+
if count_roots(s.p, s.lo, mid) == 1: s.hi = mid
|
|
171
|
+
else: s.lo = mid
|
|
172
|
+
|
|
173
|
+
def reduce(s, g):
|
|
174
|
+
return pdivmod(g, s.p)[1] if len(ptrim(g)) >= len(s.p) else ptrim(g)
|
|
175
|
+
|
|
176
|
+
def is_zero(s, g):
|
|
177
|
+
r = s.reduce(g)
|
|
178
|
+
if not r: return True
|
|
179
|
+
h = pgcd(r, s.p)
|
|
180
|
+
if len(h) <= 1: return False
|
|
181
|
+
if count_roots(h, s.lo, s.hi) == 1: return True
|
|
182
|
+
s.p = pdivmod(s.p, h)[0] # alpha is a root of the cofactor
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
def sign(s, g):
|
|
186
|
+
if s.is_zero(g): return 0
|
|
187
|
+
r = s.reduce(g)
|
|
188
|
+
while True:
|
|
189
|
+
a, b = _ieval(r, s.lo, s.hi)
|
|
190
|
+
if a > 0: return 1
|
|
191
|
+
if b < 0: return -1
|
|
192
|
+
s.refine()
|
|
193
|
+
|
|
194
|
+
def approx(s, g, tol=Fr(1, 10 ** 40)):
|
|
195
|
+
while s.hi - s.lo > tol: s.refine()
|
|
196
|
+
return peval(s.reduce(g), (s.lo + s.hi) / 2)
|
|
197
|
+
|
|
198
|
+
def inverse(s, g):
|
|
199
|
+
if s.is_zero(g): raise ZeroDivisionError("division by an algebraic number equal to 0")
|
|
200
|
+
a, m = s.reduce(g), s.p
|
|
201
|
+
# extended Euclid: find u with u*a = 1 mod m (gcd(a, m) = 1 after is_zero)
|
|
202
|
+
r0, r1, u0, u1 = m, a, [], [Fr(1)]
|
|
203
|
+
while len(r1) > 1:
|
|
204
|
+
q, r = pdivmod(r0, r1)
|
|
205
|
+
r0, r1 = r1, r
|
|
206
|
+
u0, u1 = u1, psub(u0, pmul(q, u1))
|
|
207
|
+
if not r1: raise ArithmeticError("unexpected common factor")
|
|
208
|
+
return s.reduce([v / r1[0] for v in u1])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class A:
|
|
212
|
+
"""element of Q(alpha) for a fixed real algebraic alpha"""
|
|
213
|
+
__slots__ = ("F", "c")
|
|
214
|
+
|
|
215
|
+
def __init__(s, F, c):
|
|
216
|
+
s.F, s.c = F, F.reduce([Fr(v) for v in c])
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def root(F): return A(F, [0, 1])
|
|
220
|
+
|
|
221
|
+
def _co(s, o):
|
|
222
|
+
if isinstance(o, A):
|
|
223
|
+
if o.F is not s.F: raise NotImplementedError("arithmetic across different algebraic fields")
|
|
224
|
+
return o.c
|
|
225
|
+
if isinstance(o, _NUM): return [Fr(o)] if o else []
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def _wrap(s, c):
|
|
229
|
+
r = A(s.F, c)
|
|
230
|
+
return r.rational() if len(r.c) <= 1 else r
|
|
231
|
+
|
|
232
|
+
def rational(s):
|
|
233
|
+
return s.c[0] if s.c else Fr(0)
|
|
234
|
+
|
|
235
|
+
def __add__(s, o):
|
|
236
|
+
c = s._co(o)
|
|
237
|
+
return NotImplemented if c is None else s._wrap(padd(s.c, c))
|
|
238
|
+
__radd__ = __add__
|
|
239
|
+
def __sub__(s, o):
|
|
240
|
+
c = s._co(o)
|
|
241
|
+
return NotImplemented if c is None else s._wrap(psub(s.c, c))
|
|
242
|
+
def __rsub__(s, o):
|
|
243
|
+
c = s._co(o)
|
|
244
|
+
return NotImplemented if c is None else s._wrap(psub(c, s.c))
|
|
245
|
+
def __neg__(s): return s._wrap([-v for v in s.c])
|
|
246
|
+
def __mul__(s, o):
|
|
247
|
+
c = s._co(o)
|
|
248
|
+
return NotImplemented if c is None else s._wrap(pmul(s.c, c))
|
|
249
|
+
__rmul__ = __mul__
|
|
250
|
+
def __truediv__(s, o):
|
|
251
|
+
c = s._co(o)
|
|
252
|
+
if c is None: return NotImplemented
|
|
253
|
+
if isinstance(o, _NUM):
|
|
254
|
+
if o == 0: raise ZeroDivisionError
|
|
255
|
+
return s._wrap([v / Fr(o) for v in s.c])
|
|
256
|
+
return s._wrap(pmul(s.c, s.F.inverse(c)))
|
|
257
|
+
def __rtruediv__(s, o):
|
|
258
|
+
c = s._co(o)
|
|
259
|
+
return NotImplemented if c is None else s._wrap(pmul(c, s.F.inverse(s.c)))
|
|
260
|
+
def __pow__(s, n):
|
|
261
|
+
r = A(s.F, [1])
|
|
262
|
+
b = s
|
|
263
|
+
if n < 0: b, n = 1 / s, -n
|
|
264
|
+
while n:
|
|
265
|
+
if n & 1: r = r * b
|
|
266
|
+
b = b * b; n >>= 1
|
|
267
|
+
return r if isinstance(r, A) and len(r.c) > 1 else (r.rational() if isinstance(r, A) else r)
|
|
268
|
+
|
|
269
|
+
def sign(s): return s.F.sign(s.c)
|
|
270
|
+
def __bool__(s): return not s.F.is_zero(s.c)
|
|
271
|
+
def __eq__(s, o):
|
|
272
|
+
c = s._co(o)
|
|
273
|
+
return NotImplemented if c is None else s.F.is_zero(psub(s.c, c))
|
|
274
|
+
def __ne__(s, o):
|
|
275
|
+
r = s.__eq__(o)
|
|
276
|
+
return r if r is NotImplemented else not r
|
|
277
|
+
__hash__ = None
|
|
278
|
+
def __lt__(s, o): return (s - o).sign() < 0 if isinstance(s - o, A) else (s - o) < 0
|
|
279
|
+
def __gt__(s, o): return (s - o).sign() > 0 if isinstance(s - o, A) else (s - o) > 0
|
|
280
|
+
def __le__(s, o): return not s.__gt__(o)
|
|
281
|
+
def __ge__(s, o): return not s.__lt__(o)
|
|
282
|
+
def __abs__(s): return -s if s.sign() < 0 else s
|
|
283
|
+
def __float__(s): return float(s.F.approx(s.c))
|
|
284
|
+
def __repr__(s): return f"{float(s):.10g}"
|
|
285
|
+
__str__ = __repr__
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def sign_of(v):
|
|
289
|
+
"""sign of a Fraction or algebraic number"""
|
|
290
|
+
if isinstance(v, A): return v.sign()
|
|
291
|
+
return (v > 0) - (v < 0)
|
|
292
|
+
|
|
293
|
+
def same_value(u, v):
|
|
294
|
+
"""exact equality across Fractions and algebraic numbers. Returns True/False, or None
|
|
295
|
+
if the two numbers live in different algebraic fields and could not be separated."""
|
|
296
|
+
if isinstance(u, A) and isinstance(v, A) and u.F is not v.F:
|
|
297
|
+
for _ in range(200):
|
|
298
|
+
a1, b1 = _ieval(u.F.reduce(u.c), u.F.lo, u.F.hi)
|
|
299
|
+
a2, b2 = _ieval(v.F.reduce(v.c), v.F.lo, v.F.hi)
|
|
300
|
+
if b1 < a2 or b2 < a1: return False
|
|
301
|
+
u.F.refine(); v.F.refine()
|
|
302
|
+
return None
|
|
303
|
+
return u == v
|
fmsl/bifurcation.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fmsl.bifurcation -- bifurcations of iterated maps as balance points.
|
|
3
|
+
|
|
4
|
+
For a one-parameter family of polynomial maps x -> f(x, r):
|
|
5
|
+
fixed points: F1(x, r) = f(x) - x = 0
|
|
6
|
+
period-n points: Qn(x, r) = (f^n(x) - x) / (f(x) - x) (exact polynomial division)
|
|
7
|
+
At a bifurcation point (x0, r0), numerator and denominator of Qn vanish together: a 0/0
|
|
8
|
+
transition point. The new orbits are born along the real branches of Qn = 0 (or F1 = 0)
|
|
9
|
+
through (x0, r0). With u = x - x0 and e = r - r0, every branch has the Puiseux form
|
|
10
|
+
e = c |u|^a + ... (x-side sigma = +-1)
|
|
11
|
+
and is found EXACTLY from the Newton polygon: a is a balance curvature (an edge slope),
|
|
12
|
+
c a real root of the edge's initial form (rational or real algebraic).
|
|
13
|
+
So the size of the new orbit near the bifurcation is |u| ~ (e/c)^(1/a): the exponent and
|
|
14
|
+
the coefficient are both exact.
|
|
15
|
+
"""
|
|
16
|
+
from fractions import Fraction as Fr
|
|
17
|
+
from math import comb
|
|
18
|
+
|
|
19
|
+
from .multivar import _critical_as, _initial_form, _rational_roots, _irrational_roots, _trim
|
|
20
|
+
from .algebraic import A
|
|
21
|
+
|
|
22
|
+
# bivariate polynomials in (x, r): {(i, j): coeff} meaning coeff * x^i * r^j
|
|
23
|
+
def _add(P, Q):
|
|
24
|
+
R = dict(P)
|
|
25
|
+
for k, v in Q.items():
|
|
26
|
+
R[k] = R.get(k, 0) + v
|
|
27
|
+
if R[k] == 0: del R[k]
|
|
28
|
+
return R
|
|
29
|
+
|
|
30
|
+
def _scale(P, s): return {k: v * s for k, v in P.items()}
|
|
31
|
+
|
|
32
|
+
def _mul(P, Q):
|
|
33
|
+
R = {}
|
|
34
|
+
for (a, b), u in P.items():
|
|
35
|
+
for (c, d), v in Q.items():
|
|
36
|
+
k = (a + c, b + d); R[k] = R.get(k, 0) + u * v
|
|
37
|
+
return {k: v for k, v in R.items() if v != 0}
|
|
38
|
+
|
|
39
|
+
def compose(f, g):
|
|
40
|
+
"""f(g(x, r), r)"""
|
|
41
|
+
out, gp, cache = {}, {(0, 0): Fr(1)}, {0: {(0, 0): Fr(1)}}
|
|
42
|
+
maxi = max(i for i, _ in f)
|
|
43
|
+
for i in range(1, maxi + 1):
|
|
44
|
+
gp = _mul(gp, g); cache[i] = gp
|
|
45
|
+
for (i, j), v in f.items():
|
|
46
|
+
out = _add(out, _mul(cache[i], {(0, j): v}))
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
def iterate(f, n):
|
|
50
|
+
g = {(1, 0): Fr(1)}
|
|
51
|
+
for _ in range(n): g = compose(f, g)
|
|
52
|
+
return g
|
|
53
|
+
|
|
54
|
+
def _lead(P):
|
|
55
|
+
"""leading term in lex order (x-degree first, then r-degree)"""
|
|
56
|
+
k = max(P, key=lambda t: (t[0], t[1]))
|
|
57
|
+
return k, P[k]
|
|
58
|
+
|
|
59
|
+
def divide_exact(P, Q):
|
|
60
|
+
"""exact division P / Q in Q[x, r] (lex order); raises if not exact"""
|
|
61
|
+
P, quo = dict(P), {}
|
|
62
|
+
(qi, qj), qc = _lead(Q)
|
|
63
|
+
while P:
|
|
64
|
+
(pi, pj), pc = _lead(P)
|
|
65
|
+
if pi < qi or pj < qj: raise ArithmeticError("not an exact division")
|
|
66
|
+
t = {(pi - qi, pj - qj): pc / qc}
|
|
67
|
+
quo = _add(quo, t)
|
|
68
|
+
P = _add(P, _scale(_mul(t, Q), -1))
|
|
69
|
+
return quo
|
|
70
|
+
|
|
71
|
+
def period_polynomial(f, n):
|
|
72
|
+
"""Q_n = (f^n(x) - x) / (f(x) - x)"""
|
|
73
|
+
x = {(1, 0): Fr(1)}
|
|
74
|
+
return divide_exact(_add(iterate(f, n), _scale(x, -1)), _add(f, _scale(x, -1)))
|
|
75
|
+
|
|
76
|
+
def fixed_polynomial(f):
|
|
77
|
+
return _add(f, {(1, 0): Fr(-1)})
|
|
78
|
+
|
|
79
|
+
def _exact(v): return v if isinstance(v, A) else Fr(v)
|
|
80
|
+
|
|
81
|
+
def shift(P, x0, r0):
|
|
82
|
+
"""P(u + x0, e + r0) as a polynomial in (u, e); x0, r0 rational or real algebraic"""
|
|
83
|
+
x0, r0 = _exact(x0), _exact(r0)
|
|
84
|
+
xp, rp = {0: Fr(1)}, {0: Fr(1)}
|
|
85
|
+
out = {}
|
|
86
|
+
for (i, j), v in P.items():
|
|
87
|
+
for a in range(i + 1):
|
|
88
|
+
for b in range(j + 1):
|
|
89
|
+
if i - a not in xp: xp[i - a] = x0 ** (i - a)
|
|
90
|
+
if j - b not in rp: rp[j - b] = r0 ** (j - b)
|
|
91
|
+
c = v * comb(i, a) * comb(j, b) * xp[i - a] * rp[j - b]
|
|
92
|
+
out[(a, b)] = out.get((a, b), 0) + c
|
|
93
|
+
return {k: v for k, v in out.items() if v != 0}
|
|
94
|
+
|
|
95
|
+
def branches(P):
|
|
96
|
+
"""Real branches of P(u, e) = 0 through (0, 0): list of dicts
|
|
97
|
+
{'a': curvature, 'sigma': side of u, 'c': coefficient (exact)} meaning e = c |u|^a,
|
|
98
|
+
plus axis branches ('u = 0' / 'e = 0') when P vanishes identically on an axis."""
|
|
99
|
+
out = []
|
|
100
|
+
if all(i > 0 for (i, j) in P): out.append({"axis": "u = 0"})
|
|
101
|
+
if all(j > 0 for (i, j) in P): out.append({"axis": "e = 0"})
|
|
102
|
+
for a in sorted(_critical_as(P)):
|
|
103
|
+
for sigma in (1, -1):
|
|
104
|
+
_, form = _initial_form(P, a, sigma)
|
|
105
|
+
if all(not isinstance(v, A) for v in form):
|
|
106
|
+
roots = _rational_roots(form) + _irrational_roots(form)
|
|
107
|
+
else:
|
|
108
|
+
f = _trim(form)
|
|
109
|
+
while f and f[0] == 0: f = f[1:] # drop the c = 0 root
|
|
110
|
+
if len(f) == 2: roots = [-f[0] / f[1]] # linear: exact root in Q(alpha)
|
|
111
|
+
elif len(f) <= 1: roots = []
|
|
112
|
+
else: raise NotImplementedError("initial form of degree > 1 over an algebraic field")
|
|
113
|
+
for c in roots:
|
|
114
|
+
out.append({"a": a, "sigma": sigma, "c": c})
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
def describe_branches(P):
|
|
118
|
+
lines = []
|
|
119
|
+
for b in branches(P):
|
|
120
|
+
if "axis" in b:
|
|
121
|
+
lines.append(f"branch {b['axis']}")
|
|
122
|
+
continue
|
|
123
|
+
a, c, sg = b["a"], b["c"], b["sigma"]
|
|
124
|
+
side = "u > 0" if sg > 0 else "u < 0"
|
|
125
|
+
cval = f"{float(c):.6g} (algebraic)" if isinstance(c, A) else str(c)
|
|
126
|
+
lines.append(f"branch e = {cval} * |u|^{a} on {side} => |u| = (e / {cval})^(1/{a})")
|
|
127
|
+
return lines
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _dx(P): return {(i - 1, j): v * i for (i, j), v in P.items() if i > 0}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def branch_stability(f, n, x0, r0, branch):
|
|
134
|
+
"""Is the period-n orbit born along `branch` attracting? The multiplier
|
|
135
|
+
lambda = d(f^n)/dx along the branch is a Puiseux series lambda0 + k s^m + ... ;
|
|
136
|
+
at the bifurcation |lambda0| = 1, and the sign of k decides (exactly).
|
|
137
|
+
Returns (lambda0, k, m, 'stable' | 'unstable')."""
|
|
138
|
+
from .multivar import _along
|
|
139
|
+
from .algebraic import sign_of
|
|
140
|
+
M = shift(_dx(iterate(f, n)), x0, r0)
|
|
141
|
+
lam0 = M.get((0, 0), 0)
|
|
142
|
+
a, sg, c = branch["a"], branch["sigma"], branch["c"]
|
|
143
|
+
p, q = a.numerator, a.denominator
|
|
144
|
+
ser = _along({k: v for k, v in M.items() if k != (0, 0)}, sg, c, p, q) # u = sg s^q, e = c s^p
|
|
145
|
+
m = next((i for i, v in enumerate(ser) if v != 0), None)
|
|
146
|
+
if m is None: return lam0, 0, None, "neutral to all orders"
|
|
147
|
+
k = ser[m]
|
|
148
|
+
if lam0 == 1: verdict = "stable" if sign_of(k) < 0 else "unstable"
|
|
149
|
+
elif lam0 == -1: verdict = "stable" if sign_of(k) > 0 else "unstable"
|
|
150
|
+
else: verdict = "stable" if abs(float(lam0)) < 1 else "unstable"
|
|
151
|
+
return lam0, k, m, verdict
|