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 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
@@ -0,0 +1,14 @@
1
+ import sys
2
+ from .compiler import run
3
+
4
+
5
+ def main():
6
+ if len(sys.argv) != 2:
7
+ print("usage: fmsl PROGRAM.fms")
8
+ sys.exit(2)
9
+ with open(sys.argv[1]) as f:
10
+ run(f.read())
11
+
12
+
13
+ if __name__ == "__main__":
14
+ main()
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