freealg 0.7.9__py3-none-any.whl → 0.7.11__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.
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/_branch_points.py +288 -0
- freealg/_algebraic_form/_continuation_algebraic.py +1 -1
- freealg/_algebraic_form/_decompress.py +45 -1
- freealg/_algebraic_form/_homotopy.py +5 -5
- freealg/_algebraic_form/_moments.py +43 -25
- freealg/_algebraic_form/_support.py +309 -0
- freealg/_algebraic_form/algebraic_form.py +120 -46
- {freealg-0.7.9.dist-info → freealg-0.7.11.dist-info}/METADATA +1 -1
- {freealg-0.7.9.dist-info → freealg-0.7.11.dist-info}/RECORD +14 -13
- freealg/_algebraic_form/_discriminant.py +0 -226
- {freealg-0.7.9.dist-info → freealg-0.7.11.dist-info}/WHEEL +0 -0
- {freealg-0.7.9.dist-info → freealg-0.7.11.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.9.dist-info → freealg-0.7.11.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.7.9.dist-info → freealg-0.7.11.dist-info}/top_level.txt +0 -0
freealg/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.7.
|
|
1
|
+
__version__ = "0.7.11"
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileType: SOURCE
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
# the terms of the license found in the LICENSE.txt file in the root directory
|
|
7
|
+
# of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
|
|
16
|
+
__all__ = ['compute_branch_points']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# =========
|
|
20
|
+
# poly trim
|
|
21
|
+
# =========
|
|
22
|
+
|
|
23
|
+
def _poly_trim(p, tol):
|
|
24
|
+
p = numpy.asarray(p, dtype=float)
|
|
25
|
+
if p.size == 0:
|
|
26
|
+
return p
|
|
27
|
+
k = p.size - 1
|
|
28
|
+
while k > 0 and abs(p[k]) <= tol:
|
|
29
|
+
k -= 1
|
|
30
|
+
return p[: k + 1]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ========
|
|
34
|
+
# poly add
|
|
35
|
+
# ========
|
|
36
|
+
|
|
37
|
+
def _poly_add(a, b, tol):
|
|
38
|
+
|
|
39
|
+
n = max(len(a), len(b))
|
|
40
|
+
out = numpy.zeros(n, dtype=float)
|
|
41
|
+
out[: len(a)] += a
|
|
42
|
+
out[: len(b)] += b
|
|
43
|
+
|
|
44
|
+
return _poly_trim(out, tol)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ========
|
|
48
|
+
# poly sub
|
|
49
|
+
# ========
|
|
50
|
+
|
|
51
|
+
def _poly_sub(a, b, tol):
|
|
52
|
+
|
|
53
|
+
n = max(len(a), len(b))
|
|
54
|
+
out = numpy.zeros(n, dtype=float)
|
|
55
|
+
out[: len(a)] += a
|
|
56
|
+
out[: len(b)] -= b
|
|
57
|
+
|
|
58
|
+
return _poly_trim(out, tol)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ========
|
|
62
|
+
# poly mul
|
|
63
|
+
# ========
|
|
64
|
+
|
|
65
|
+
def _poly_mul(a, b, tol):
|
|
66
|
+
|
|
67
|
+
a = _poly_trim(a, tol)
|
|
68
|
+
b = _poly_trim(b, tol)
|
|
69
|
+
if a.size == 0 or b.size == 0:
|
|
70
|
+
return numpy.zeros(1, dtype=float)
|
|
71
|
+
out = numpy.convolve(a, b)
|
|
72
|
+
return _poly_trim(out, tol)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ===============
|
|
76
|
+
# poly div approx
|
|
77
|
+
# ===============
|
|
78
|
+
|
|
79
|
+
def _poly_div_approx(a, b, tol):
|
|
80
|
+
"""
|
|
81
|
+
Polynomial division q,r = a/b in ascending powers (numpy.polynomial
|
|
82
|
+
convention). Returns q (ascending). Remainder is ignored if it is
|
|
83
|
+
small-ish.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
a = _poly_trim(a, tol)
|
|
87
|
+
b = _poly_trim(b, tol)
|
|
88
|
+
if b.size == 0 or (b.size == 1 and abs(b[0]) <= tol):
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
"division by (near) zero polynomial in branch point resultant")
|
|
91
|
+
# numpy.polydiv uses descending powers, so flip.
|
|
92
|
+
qd, rd = numpy.polydiv(a[::-1], b[::-1])
|
|
93
|
+
q = qd[::-1]
|
|
94
|
+
r = rd[::-1]
|
|
95
|
+
# Accept small remainder (Bareiss should be exact in exact arithmetic).
|
|
96
|
+
# If not small, we still proceed with the quotient (robustness over
|
|
97
|
+
# exactness).
|
|
98
|
+
scale = max(1.0, numpy.linalg.norm(a))
|
|
99
|
+
if numpy.linalg.norm(_poly_trim(r, tol)) > 1e6 * tol * scale:
|
|
100
|
+
pass
|
|
101
|
+
return _poly_trim(q, tol)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# =================
|
|
105
|
+
# det baresiss poly
|
|
106
|
+
# =================
|
|
107
|
+
|
|
108
|
+
def _det_bareiss_poly(M, tol):
|
|
109
|
+
"""
|
|
110
|
+
Fraction-free determinant for a matrix with polynomial entries in z.
|
|
111
|
+
Polynomials are stored as 1D arrays of ascending coefficients.
|
|
112
|
+
Returns det as ascending coefficients.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
n = len(M)
|
|
116
|
+
A = [[_poly_trim(M[i][j], tol) for j in range(n)] for i in range(n)]
|
|
117
|
+
denom = numpy.array([1.0], dtype=float)
|
|
118
|
+
|
|
119
|
+
for k in range(n - 1):
|
|
120
|
+
pivot = A[k][k]
|
|
121
|
+
if pivot.size == 1 and abs(pivot[0]) <= tol:
|
|
122
|
+
swap = None
|
|
123
|
+
for i in range(k + 1, n):
|
|
124
|
+
if not (A[i][k].size == 1 and abs(A[i][k][0]) <= tol):
|
|
125
|
+
swap = i
|
|
126
|
+
break
|
|
127
|
+
if swap is None:
|
|
128
|
+
return numpy.zeros(1, dtype=float)
|
|
129
|
+
A[k], A[swap] = A[swap], A[k]
|
|
130
|
+
pivot = A[k][k]
|
|
131
|
+
|
|
132
|
+
for i in range(k + 1, n):
|
|
133
|
+
for j in range(k + 1, n):
|
|
134
|
+
num = _poly_sub(
|
|
135
|
+
_poly_mul(A[i][j], pivot, tol),
|
|
136
|
+
_poly_mul(A[i][k], A[k][j], tol),
|
|
137
|
+
tol,
|
|
138
|
+
)
|
|
139
|
+
if k > 0:
|
|
140
|
+
A[i][j] = _poly_div_approx(num, denom, tol)
|
|
141
|
+
else:
|
|
142
|
+
A[i][j] = _poly_trim(num, tol)
|
|
143
|
+
|
|
144
|
+
denom = pivot
|
|
145
|
+
|
|
146
|
+
for i in range(k + 1, n):
|
|
147
|
+
A[i][k] = numpy.array([0.0], dtype=float)
|
|
148
|
+
A[k][i] = numpy.array([0.0], dtype=float)
|
|
149
|
+
|
|
150
|
+
return _poly_trim(A[n - 1][n - 1], tol)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ======================
|
|
154
|
+
# resultant discriminant
|
|
155
|
+
# ======================
|
|
156
|
+
|
|
157
|
+
def _resultant_discriminant(a_coeffs, tol):
|
|
158
|
+
"""
|
|
159
|
+
Numerically compute Disc_m(P)(z) as a polynomial in z (ascending coeffs),
|
|
160
|
+
via Sylvester determinant evaluation on a circle + interpolation.
|
|
161
|
+
|
|
162
|
+
a_coeffs[i,j] is coeff of z^i m^j, shape (deg_z+1, s+1).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
import numpy
|
|
166
|
+
|
|
167
|
+
a_coeffs = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
168
|
+
deg_z = a_coeffs.shape[0] - 1
|
|
169
|
+
s = a_coeffs.shape[1] - 1
|
|
170
|
+
if s < 1 or deg_z < 0:
|
|
171
|
+
return numpy.zeros(1, dtype=numpy.complex128)
|
|
172
|
+
|
|
173
|
+
# Degree bound: deg_z(Disc) <= (2s-1)*deg_z
|
|
174
|
+
D = (2 * s - 1) * deg_z
|
|
175
|
+
if D <= 0:
|
|
176
|
+
return numpy.zeros(1, dtype=numpy.complex128)
|
|
177
|
+
|
|
178
|
+
def eval_disc(z):
|
|
179
|
+
# Build P(m) coeffs in descending powers of m: p_desc[k] = coeff of
|
|
180
|
+
# m^(s-k)
|
|
181
|
+
p_asc = numpy.zeros(s + 1, dtype=numpy.complex128)
|
|
182
|
+
for j in range(s + 1):
|
|
183
|
+
p_asc[j] = numpy.polyval(a_coeffs[:, j][::-1], z) # a_j(z)
|
|
184
|
+
p_desc = p_asc[::-1]
|
|
185
|
+
|
|
186
|
+
# Q(m) = dP/dm, descending
|
|
187
|
+
q_asc = numpy.zeros(s, dtype=numpy.complex128)
|
|
188
|
+
for j in range(1, s + 1):
|
|
189
|
+
q_asc[j - 1] = j * p_asc[j]
|
|
190
|
+
q_desc = q_asc[::-1]
|
|
191
|
+
|
|
192
|
+
# Sylvester matrix of P (deg s) and Q (deg s-1): size (2s-1)x(2s-1)
|
|
193
|
+
n = 2 * s - 1
|
|
194
|
+
S = numpy.zeros((n, n), dtype=numpy.complex128)
|
|
195
|
+
|
|
196
|
+
# First (s-1) rows: shifts of P
|
|
197
|
+
for r in range(s - 1):
|
|
198
|
+
S[r, r:r + (s + 1)] = p_desc
|
|
199
|
+
|
|
200
|
+
# Next s rows: shifts of Q
|
|
201
|
+
for r in range(s):
|
|
202
|
+
rr = (s - 1) + r
|
|
203
|
+
S[rr, r:r + s] = q_desc
|
|
204
|
+
|
|
205
|
+
return numpy.linalg.det(S)
|
|
206
|
+
|
|
207
|
+
# Sample points on a circle; scale radius using coefficient magnitude
|
|
208
|
+
# (simple heuristic) (This only affects conditioning of interpolation, not
|
|
209
|
+
# correctness.)
|
|
210
|
+
scale = float(numpy.max(numpy.abs(a_coeffs))) \
|
|
211
|
+
if numpy.max(numpy.abs(a_coeffs)) > 0 else 1.0
|
|
212
|
+
R = 1.0 + 0.1 * scale
|
|
213
|
+
|
|
214
|
+
N = D + 1
|
|
215
|
+
k = numpy.arange(N, dtype=float)
|
|
216
|
+
z_samp = R * numpy.exp(2.0j * numpy.pi * k / float(N))
|
|
217
|
+
d_samp = numpy.array([eval_disc(z) for z in z_samp],
|
|
218
|
+
dtype=numpy.complex128)
|
|
219
|
+
|
|
220
|
+
# Interpolate disc(z) = sum_{j=0}^D c[j] z^j (ascending)
|
|
221
|
+
V = (z_samp[:, None] ** numpy.arange(D + 1)[None, :]).astype(
|
|
222
|
+
numpy.complex128)
|
|
223
|
+
c, _, _, _ = numpy.linalg.lstsq(V, d_samp, rcond=None)
|
|
224
|
+
|
|
225
|
+
# Trim tiny coefficients
|
|
226
|
+
c = _poly_trim(c, tol)
|
|
227
|
+
if c.size == 0:
|
|
228
|
+
c = numpy.zeros(1, dtype=numpy.complex128)
|
|
229
|
+
|
|
230
|
+
# If numerics leave small imag, kill it (disc should be real-coeff if
|
|
231
|
+
# a_coeffs real)
|
|
232
|
+
if numpy.linalg.norm(c.imag) <= \
|
|
233
|
+
1e3 * tol * max(1.0, numpy.linalg.norm(c.real)):
|
|
234
|
+
c = c.real.astype(numpy.float64)
|
|
235
|
+
|
|
236
|
+
return c
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# =====================
|
|
240
|
+
# compute branch points
|
|
241
|
+
# =====================
|
|
242
|
+
|
|
243
|
+
def compute_branch_points(a_coeffs, tol=1e-12, real_tol=None):
|
|
244
|
+
"""
|
|
245
|
+
Compute global branch points of the affine curve P(z,m)=0 by
|
|
246
|
+
z-roots of Disc_m(P)(z) = Res_m(P, dP/dm).
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
z_bp : complex ndarray
|
|
251
|
+
a_s_zero : complex ndarray
|
|
252
|
+
info : dict
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
a_coeffs = numpy.asarray(a_coeffs, dtype=float)
|
|
256
|
+
s = a_coeffs.shape[1] - 1
|
|
257
|
+
if s < 1:
|
|
258
|
+
if real_tol is None:
|
|
259
|
+
real_tol = 1e3 * tol
|
|
260
|
+
return \
|
|
261
|
+
numpy.array([], dtype=complex), \
|
|
262
|
+
numpy.array([], dtype=complex), \
|
|
263
|
+
{
|
|
264
|
+
"disc": numpy.zeros(1, dtype=float),
|
|
265
|
+
"tol": float(tol),
|
|
266
|
+
"real_tol": float(real_tol),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if real_tol is None:
|
|
270
|
+
real_tol = 1e3 * tol
|
|
271
|
+
|
|
272
|
+
a_s = _poly_trim(a_coeffs[:, s], tol)
|
|
273
|
+
a_s_zero = numpy.roots(a_s[::-1]) if a_s.size > 1 else \
|
|
274
|
+
numpy.array([], dtype=complex)
|
|
275
|
+
|
|
276
|
+
disc = _resultant_discriminant(a_coeffs, tol)
|
|
277
|
+
if disc.size <= 1:
|
|
278
|
+
z_bp = numpy.array([], dtype=complex)
|
|
279
|
+
else:
|
|
280
|
+
z_bp = numpy.roots(disc[::-1])
|
|
281
|
+
|
|
282
|
+
info = {
|
|
283
|
+
"disc": disc,
|
|
284
|
+
"tol": float(tol),
|
|
285
|
+
"real_tol": float(real_tol),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return z_bp, a_s_zero, info
|
|
@@ -289,7 +289,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
289
289
|
|
|
290
290
|
# Diagnostic metrics
|
|
291
291
|
fit_metrics = {
|
|
292
|
-
's_min': svals[-1],
|
|
292
|
+
's_min': float(svals[-1]),
|
|
293
293
|
'gap_ratio': float(svals[-2] / svals[-1]),
|
|
294
294
|
'n_small': float(int(numpy.sum(svals <= svals[0] * 1e-12))),
|
|
295
295
|
}
|
|
@@ -14,7 +14,50 @@
|
|
|
14
14
|
import numpy
|
|
15
15
|
from ._continuation_algebraic import powers
|
|
16
16
|
|
|
17
|
-
__all__ = ['decompress_newton_old', 'decompress_newton']
|
|
17
|
+
__all__ = ['build_time_grid', 'decompress_newton_old', 'decompress_newton']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ===============
|
|
21
|
+
# build time grid
|
|
22
|
+
# ===============
|
|
23
|
+
|
|
24
|
+
def build_time_grid(sizes, n0, min_n_time=0):
|
|
25
|
+
"""
|
|
26
|
+
sizes: list/array of requested matrix sizes (e.g. [2000,3000,4000,8000])
|
|
27
|
+
n0: initial size (self.n)
|
|
28
|
+
min_n_time: minimum number of time points to run Newton sweep on
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
t_all: sorted time grid to run solver on
|
|
33
|
+
idx_req: indices of requested times inside t_all (same order as sizes)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
sizes = numpy.asarray(sizes, dtype=float)
|
|
37
|
+
alpha = sizes / float(n0)
|
|
38
|
+
t_req = numpy.log(alpha)
|
|
39
|
+
|
|
40
|
+
# Always include t=0 and T=max(t_req)
|
|
41
|
+
T = float(numpy.max(t_req)) if t_req.size else 0.0
|
|
42
|
+
base = numpy.unique(numpy.r_[0.0, t_req, T])
|
|
43
|
+
t_all = numpy.sort(base)
|
|
44
|
+
|
|
45
|
+
# Add points only if needed: split largest gaps
|
|
46
|
+
N = int(min_n_time) if min_n_time is not None else 0
|
|
47
|
+
while t_all.size < N and t_all.size >= 2:
|
|
48
|
+
gaps = numpy.diff(t_all)
|
|
49
|
+
k = int(numpy.argmax(gaps))
|
|
50
|
+
mid = 0.5 * (t_all[k] + t_all[k+1])
|
|
51
|
+
t_all = numpy.sort(numpy.unique(numpy.r_[t_all, mid]))
|
|
52
|
+
|
|
53
|
+
# Map each requested time to an index in t_all (stable, no float drama)
|
|
54
|
+
# (t_req values came from same construction, so they should match exactly;
|
|
55
|
+
# still: use searchsorted + assert)
|
|
56
|
+
idx_req = numpy.searchsorted(t_all, t_req)
|
|
57
|
+
# optional sanity:
|
|
58
|
+
# assert numpy.allclose(t_all[idx_req], t_req, rtol=0, atol=0)
|
|
59
|
+
|
|
60
|
+
return t_all, idx_req
|
|
18
61
|
|
|
19
62
|
|
|
20
63
|
# ===============
|
|
@@ -504,6 +547,7 @@ def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
|
|
|
504
547
|
ok : ndarray of bool, same shape as W
|
|
505
548
|
Convergence flags from the accepted solve at each point.
|
|
506
549
|
"""
|
|
550
|
+
|
|
507
551
|
z_list = numpy.asarray(z_list, dtype=complex).ravel()
|
|
508
552
|
t_grid = numpy.asarray(t_grid, dtype=float).ravel()
|
|
509
553
|
nt = t_grid.size
|
|
@@ -6,12 +6,12 @@ import numpy
|
|
|
6
6
|
from ._moments import AlgebraicStieltjesMoments
|
|
7
7
|
from tqdm import tqdm
|
|
8
8
|
|
|
9
|
-
__all__ = ['
|
|
9
|
+
__all__ = ['StieltjesPoly']
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# ===========
|
|
13
13
|
# select root
|
|
14
|
-
#
|
|
14
|
+
# ===========
|
|
15
15
|
|
|
16
16
|
def select_root(roots, z, target):
|
|
17
17
|
"""
|
|
@@ -202,7 +202,7 @@ class StieltjesPoly(object):
|
|
|
202
202
|
|
|
203
203
|
# Iterate over indices so we can pass Python scalars into evaluate()
|
|
204
204
|
if progress:
|
|
205
|
-
indices = tqdm(numpy.ndindex(z_arr.shape),total=z_arr.size)
|
|
205
|
+
indices = tqdm(numpy.ndindex(z_arr.shape), total=z_arr.size)
|
|
206
206
|
else:
|
|
207
207
|
indices = numpy.ndindex(z_arr.shape)
|
|
208
208
|
for idx in indices:
|
|
@@ -265,7 +265,7 @@ class StieltjesPoly(object):
|
|
|
265
265
|
# coeffs = numpy.asarray(poly_coeffs_m(z_val), dtype=numpy.complex128)
|
|
266
266
|
# return numpy.roots(coeffs)
|
|
267
267
|
|
|
268
|
-
# # If user asked
|
|
268
|
+
# # If user asked a real-axis value, interpret as boundary value from C+.
|
|
269
269
|
# if z.imag == 0.0:
|
|
270
270
|
# if eps is None:
|
|
271
271
|
# eps = 1e-8 * max(1.0, abs(z))
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
# =======
|
|
2
|
+
# Imports
|
|
3
|
+
# =======
|
|
4
|
+
|
|
1
5
|
import numpy
|
|
2
6
|
|
|
3
7
|
|
|
4
|
-
#
|
|
8
|
+
# =======
|
|
5
9
|
# Moments
|
|
6
|
-
#
|
|
10
|
+
# =======
|
|
7
11
|
|
|
8
12
|
class MomentsESD(object):
|
|
9
13
|
"""
|
|
@@ -85,9 +89,9 @@ class MomentsESD(object):
|
|
|
85
89
|
# (a_{n,0},...,a_{n,n-1})
|
|
86
90
|
self._a = {0: numpy.array([1.0])}
|
|
87
91
|
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
92
|
+
# =
|
|
93
|
+
# m
|
|
94
|
+
# =
|
|
91
95
|
|
|
92
96
|
def m(self, n):
|
|
93
97
|
"""
|
|
@@ -111,9 +115,9 @@ class MomentsESD(object):
|
|
|
111
115
|
self._m[n] = numpy.mean(self.eig ** n)
|
|
112
116
|
return self._m[n]
|
|
113
117
|
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
118
|
+
# ======
|
|
119
|
+
# coeffs
|
|
120
|
+
# ======
|
|
117
121
|
|
|
118
122
|
def coeffs(self, n):
|
|
119
123
|
"""
|
|
@@ -129,7 +133,8 @@ class MomentsESD(object):
|
|
|
129
133
|
-------
|
|
130
134
|
|
|
131
135
|
a_n : numpy.ndarray
|
|
132
|
-
Array of shape ``(n,)`` containing :math:`(a_{n,0},
|
|
136
|
+
Array of shape ``(n,)`` containing :math:`(a_{n,0},
|
|
137
|
+
\\dots, a_{n,n-1})`.
|
|
133
138
|
"""
|
|
134
139
|
|
|
135
140
|
if n in self._a:
|
|
@@ -143,6 +148,10 @@ class MomentsESD(object):
|
|
|
143
148
|
self._compute_row(n)
|
|
144
149
|
return self._a[n]
|
|
145
150
|
|
|
151
|
+
# ===========
|
|
152
|
+
# compute row
|
|
153
|
+
# ===========
|
|
154
|
+
|
|
146
155
|
def _compute_row(self, n):
|
|
147
156
|
"""
|
|
148
157
|
Compute and memoize the coefficient row :math:`a_n`.
|
|
@@ -212,9 +221,9 @@ class MomentsESD(object):
|
|
|
212
221
|
|
|
213
222
|
self._a[n] = a_n
|
|
214
223
|
|
|
215
|
-
#
|
|
224
|
+
# --------
|
|
216
225
|
# evaluate
|
|
217
|
-
#
|
|
226
|
+
# --------
|
|
218
227
|
|
|
219
228
|
def __call__(self, n, t=0.0):
|
|
220
229
|
"""
|
|
@@ -254,18 +263,18 @@ class MomentsESD(object):
|
|
|
254
263
|
k = numpy.arange(n, dtype=float)
|
|
255
264
|
return numpy.dot(a_n, numpy.exp(k * t))
|
|
256
265
|
|
|
266
|
+
|
|
257
267
|
# ===========================
|
|
258
268
|
# Algebraic Stieltjes Moments
|
|
259
269
|
# ===========================
|
|
260
270
|
|
|
261
|
-
|
|
262
271
|
class AlgebraicStieltjesMoments(object):
|
|
263
272
|
"""
|
|
264
273
|
Given coefficients a[i,j] for P(z,m)=sum_{i,j} a[i,j] z^i m^j,
|
|
265
274
|
compute the large-|z| branch
|
|
266
275
|
m(z) = sum_{k>=0} mu_series[k] / z^{k+1}.
|
|
267
276
|
|
|
268
|
-
Convention here: choose mu0 (the leading coefficient) by solving the
|
|
277
|
+
Convention here: choose mu0 (the leading coefficient) by solving the
|
|
269
278
|
leading-diagonal equation and (by default) picking the root closest
|
|
270
279
|
to -1, i.e. m(z) ~ -1/z.
|
|
271
280
|
|
|
@@ -281,7 +290,7 @@ class AlgebraicStieltjesMoments(object):
|
|
|
281
290
|
if self.a.ndim != 2:
|
|
282
291
|
raise ValueError("a must be a 2D NumPy array with a[i,j]=a_{ij}.")
|
|
283
292
|
|
|
284
|
-
self.I = self.a.shape[0] - 1
|
|
293
|
+
self.I = self.a.shape[0] - 1 # noqa: E741
|
|
285
294
|
self.J = self.a.shape[1] - 1
|
|
286
295
|
|
|
287
296
|
nz = numpy.argwhere(self.a != 0)
|
|
@@ -320,7 +329,8 @@ class AlgebraicStieltjesMoments(object):
|
|
|
320
329
|
if j > 0:
|
|
321
330
|
self.A0 += j * coeff * self.mu0pow[j - 1]
|
|
322
331
|
if self.A0 == 0:
|
|
323
|
-
raise ValueError("A0 is zero for this mu0; the sequential
|
|
332
|
+
raise ValueError("A0 is zero for this mu0; the sequential " +
|
|
333
|
+
"recursion is degenerate.")
|
|
324
334
|
|
|
325
335
|
# Stored series moments mu_series[0..]
|
|
326
336
|
self._mu = [self.mu0]
|
|
@@ -344,14 +354,17 @@ class AlgebraicStieltjesMoments(object):
|
|
|
344
354
|
coeffs[j] = self.a[i, j]
|
|
345
355
|
|
|
346
356
|
if not numpy.any(coeffs != 0):
|
|
347
|
-
raise ValueError("Leading diagonal polynomial is identically
|
|
357
|
+
raise ValueError("Leading diagonal polynomial is identically " +
|
|
358
|
+
"zero; cannot determine mu0.")
|
|
348
359
|
|
|
349
360
|
deg = int(numpy.max(numpy.nonzero(coeffs)[0]))
|
|
350
|
-
|
|
361
|
+
|
|
362
|
+
# descending powers for numpy.roots
|
|
363
|
+
roots = numpy.roots(coeffs[:deg + 1][::-1])
|
|
351
364
|
|
|
352
365
|
# Targetting mu0 = -1 for ~ -1/z asymptotics
|
|
353
366
|
mu0 = roots[numpy.argmin(numpy.abs(roots + 1))]
|
|
354
|
-
|
|
367
|
+
|
|
355
368
|
if abs(mu0.imag) < 1e-12:
|
|
356
369
|
mu0 = mu0.real
|
|
357
370
|
return mu0
|
|
@@ -363,7 +376,8 @@ class AlgebraicStieltjesMoments(object):
|
|
|
363
376
|
|
|
364
377
|
# Compute f[j] = coefficient of w^k in (S_trunc(w))^j,
|
|
365
378
|
# where S_trunc uses mu_0..mu_{k-1} only (i.e. mu_k treated as 0).
|
|
366
|
-
# Key fact: in the true c[j,k], mu_k can only appear linearly as
|
|
379
|
+
# Key fact: in the true c[j,k], mu_k can only appear linearly as
|
|
380
|
+
# j*mu_k*mu0^{j-1}.
|
|
367
381
|
f = [0] * (self.J + 1)
|
|
368
382
|
f[0] = 0
|
|
369
383
|
for j in range(1, self.J + 1):
|
|
@@ -371,8 +385,9 @@ class AlgebraicStieltjesMoments(object):
|
|
|
371
385
|
# sum_{t=1..k-1} mu_t * c[j-1, k-t]
|
|
372
386
|
for t in range(1, k):
|
|
373
387
|
ssum += self._mu[t] * self._c[j - 1][k - t]
|
|
374
|
-
# recurrence: c[j,k] = mu0*c[j-1,k] + sum_{t=1..k-1}
|
|
375
|
-
#
|
|
388
|
+
# recurrence: c[j,k] = mu0*c[j-1,k] + sum_{t=1..k-1}
|
|
389
|
+
# mu_t*c[j-1,k-t] + mu_k*c[j-1,0] with mu_k=0 for f,
|
|
390
|
+
# and c[j-1,k]=f[j-1]
|
|
376
391
|
f[j] = self.mu0 * f[j - 1] + ssum
|
|
377
392
|
|
|
378
393
|
# Build the linear equation for mu_k:
|
|
@@ -386,7 +401,8 @@ class AlgebraicStieltjesMoments(object):
|
|
|
386
401
|
continue
|
|
387
402
|
rest += coeff * f[j]
|
|
388
403
|
|
|
389
|
-
# lower diagonals s=1..k contribute coeff*c[j,k-s] (already known
|
|
404
|
+
# lower diagonals s=1..k contribute coeff*c[j,k-s] (already known
|
|
405
|
+
# since k-s < k)
|
|
390
406
|
for s in range(1, k + 1):
|
|
391
407
|
entries = self.diag.get(s)
|
|
392
408
|
if not entries:
|
|
@@ -402,7 +418,8 @@ class AlgebraicStieltjesMoments(object):
|
|
|
402
418
|
mu_k = -rest / self.A0
|
|
403
419
|
self._mu.append(mu_k)
|
|
404
420
|
|
|
405
|
-
# Now append the new column k to c using the full convolution
|
|
421
|
+
# Now append the new column k to c using the full convolution
|
|
422
|
+
# recurrence:
|
|
406
423
|
# c[j,k] = sum_{t=0..k} mu_t * c[j-1,k-t]
|
|
407
424
|
for j in range(self.J + 1):
|
|
408
425
|
self._c[j].append(0)
|
|
@@ -430,9 +447,10 @@ class AlgebraicStieltjesMoments(object):
|
|
|
430
447
|
# Estimate the radius of convergence of the Stieltjes
|
|
431
448
|
# series
|
|
432
449
|
if N < 3:
|
|
433
|
-
raise RuntimeError("
|
|
450
|
+
raise RuntimeError("N is too small, choose a larger value.")
|
|
434
451
|
self._ensure(N)
|
|
435
|
-
return max([numpy.abs(self._mu[j] / self._mu[j-1])
|
|
452
|
+
return max([numpy.abs(self._mu[j] / self._mu[j-1])
|
|
453
|
+
for j in range(2, N+1)])
|
|
436
454
|
|
|
437
455
|
def stieltjes(self, z, N):
|
|
438
456
|
# Estimate Stieltjes transform (root) using moment
|