freealg 0.7.17__py3-none-any.whl → 0.7.18__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/__init__.py +8 -6
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/_branch_points.py +18 -18
- freealg/_algebraic_form/_continuation_algebraic.py +13 -13
- freealg/_algebraic_form/_cusp.py +15 -15
- freealg/_algebraic_form/_cusp_wrap.py +6 -6
- freealg/_algebraic_form/_decompress.py +16 -16
- freealg/_algebraic_form/_decompress4.py +31 -31
- freealg/_algebraic_form/_decompress5.py +23 -23
- freealg/_algebraic_form/_decompress6.py +13 -13
- freealg/_algebraic_form/_decompress7.py +15 -15
- freealg/_algebraic_form/_decompress8.py +17 -17
- freealg/_algebraic_form/_decompress9.py +18 -18
- freealg/_algebraic_form/_decompress_new.py +17 -17
- freealg/_algebraic_form/_decompress_new_2.py +57 -57
- freealg/_algebraic_form/_decompress_util.py +10 -10
- freealg/_algebraic_form/_decompressible.py +292 -0
- freealg/_algebraic_form/_edge.py +10 -10
- freealg/_algebraic_form/_homotopy4.py +9 -9
- freealg/_algebraic_form/_homotopy5.py +9 -9
- freealg/_algebraic_form/_support.py +19 -19
- freealg/_algebraic_form/algebraic_form.py +262 -468
- freealg/_base_form.py +401 -0
- freealg/_free_form/__init__.py +1 -4
- freealg/_free_form/_density_util.py +1 -1
- freealg/_free_form/_plot_util.py +3 -511
- freealg/_free_form/free_form.py +8 -367
- freealg/_util.py +59 -11
- freealg/distributions/__init__.py +2 -1
- freealg/distributions/_base_distribution.py +163 -0
- freealg/distributions/_chiral_block.py +137 -11
- freealg/distributions/_compound_poisson.py +141 -47
- freealg/distributions/_deformed_marchenko_pastur.py +138 -33
- freealg/distributions/_deformed_wigner.py +98 -9
- freealg/distributions/_fuss_catalan.py +269 -0
- freealg/distributions/_kesten_mckay.py +4 -130
- freealg/distributions/_marchenko_pastur.py +8 -196
- freealg/distributions/_meixner.py +4 -130
- freealg/distributions/_wachter.py +4 -130
- freealg/distributions/_wigner.py +10 -127
- freealg/visualization/__init__.py +2 -2
- freealg/visualization/{_rgb_hsv.py → _domain_coloring.py} +37 -29
- freealg/visualization/_plot_util.py +513 -0
- {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/METADATA +1 -1
- freealg-0.7.18.dist-info/RECORD +74 -0
- freealg-0.7.17.dist-info/RECORD +0 -69
- /freealg/{_free_form/_sample.py → _sample.py} +0 -0
- /freealg/{_free_form/_support.py → _support.py} +0 -0
- {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/WHEEL +0 -0
- {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.7.17.dist-info → freealg-0.7.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
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__ = ['precheck_laurent']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# =====
|
|
20
|
+
# L add
|
|
21
|
+
# =====
|
|
22
|
+
|
|
23
|
+
def L_add(A, B):
|
|
24
|
+
|
|
25
|
+
C = dict(A)
|
|
26
|
+
for p, c in B.items():
|
|
27
|
+
C[p] = C.get(p, 0.0) + c
|
|
28
|
+
if abs(C[p]) == 0:
|
|
29
|
+
C.pop(p, None)
|
|
30
|
+
return C
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# =======
|
|
34
|
+
# L scale
|
|
35
|
+
# =======
|
|
36
|
+
|
|
37
|
+
def L_scale(A, s):
|
|
38
|
+
return {p: s*c for p, c in A.items()}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =====
|
|
42
|
+
# L mul
|
|
43
|
+
# =====
|
|
44
|
+
|
|
45
|
+
def L_mul(A, B, pmin, pmax):
|
|
46
|
+
|
|
47
|
+
C = {}
|
|
48
|
+
for pa, ca in A.items():
|
|
49
|
+
for pb, cb in B.items():
|
|
50
|
+
p = pa + pb
|
|
51
|
+
if p < pmin or p > pmax:
|
|
52
|
+
continue
|
|
53
|
+
C[p] = C.get(p, 0.0) + ca*cb
|
|
54
|
+
|
|
55
|
+
return C
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# =====
|
|
59
|
+
# L pow
|
|
60
|
+
# =====
|
|
61
|
+
|
|
62
|
+
def L_pow(A, k, pmin, pmax):
|
|
63
|
+
|
|
64
|
+
if k == 0:
|
|
65
|
+
return {0: 1.0+0.0j}
|
|
66
|
+
if k == 1:
|
|
67
|
+
return dict(A)
|
|
68
|
+
# fast exponentiation
|
|
69
|
+
out = {0: 1.0+0.0j}
|
|
70
|
+
base = dict(A)
|
|
71
|
+
e = k
|
|
72
|
+
while e > 0:
|
|
73
|
+
if e & 1:
|
|
74
|
+
out = L_mul(out, base, pmin, pmax)
|
|
75
|
+
e >>= 1
|
|
76
|
+
if e:
|
|
77
|
+
base = L_mul(base, base, pmin, pmax)
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============
|
|
82
|
+
# L inv series
|
|
83
|
+
# ============
|
|
84
|
+
|
|
85
|
+
def L_inv_series(A, pmin, pmax):
|
|
86
|
+
"""
|
|
87
|
+
Invert a *power series* around power 0: requires A[0] != 0 and no negative
|
|
88
|
+
powers.
|
|
89
|
+
|
|
90
|
+
Returns power series B with nonnegative powers only.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
if any(p < 0 for p in A.keys()):
|
|
94
|
+
raise ValueError("L_inv_series expects no negative powers")
|
|
95
|
+
|
|
96
|
+
a0 = A.get(0, 0.0)
|
|
97
|
+
if abs(a0) < 1e-18:
|
|
98
|
+
raise ValueError("Need nonzero constant term to invert")
|
|
99
|
+
|
|
100
|
+
# compute up to pmax (nonnegative)
|
|
101
|
+
B = {0: 1.0/a0}
|
|
102
|
+
for n in range(1, pmax+1):
|
|
103
|
+
s = 0.0+0.0j
|
|
104
|
+
for k in range(1, n+1):
|
|
105
|
+
s += A.get(k, 0.0) * B.get(n-k, 0.0)
|
|
106
|
+
B[n] = -s/a0
|
|
107
|
+
|
|
108
|
+
# clip
|
|
109
|
+
B = {p: c for p, c in B.items() if 0 <= p <= pmax}
|
|
110
|
+
return B
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ==================
|
|
114
|
+
# Ptau Laurent alpha
|
|
115
|
+
# ==================
|
|
116
|
+
|
|
117
|
+
def Ptau_Laurent_alpha(a, tau, alpha, mu, pmin, pmax):
|
|
118
|
+
"""
|
|
119
|
+
Build Laurent series of P_tau(z,m) at z=1/w,
|
|
120
|
+
m(w)=-(alpha w + mu1 w^2 + ...).
|
|
121
|
+
|
|
122
|
+
Returns dict power->coeff for powers in [pmin,pmax].
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# m(w)
|
|
126
|
+
m = {1: -(alpha+0.0j)}
|
|
127
|
+
for k, muk in enumerate(mu, start=1):
|
|
128
|
+
m[k+1] = -(muk+0.0j)
|
|
129
|
+
|
|
130
|
+
# y = tau m
|
|
131
|
+
y = L_scale(m, tau)
|
|
132
|
+
|
|
133
|
+
# m = w*m1(w) with m1(w) = -(alpha + mu1 w + mu2 w^2 + ...)
|
|
134
|
+
m1 = {0: -(alpha+0.0j)}
|
|
135
|
+
for k, muk in enumerate(mu, start=1):
|
|
136
|
+
m1[k] = -(muk+0.0j)
|
|
137
|
+
|
|
138
|
+
inv_m1 = L_inv_series(m1, pmin=0, pmax=max(0, pmax))
|
|
139
|
+
|
|
140
|
+
# 1/m = w^{-1} * inv_m1
|
|
141
|
+
inv_m = {p-1: c for p, c in inv_m1.items() if (p-1) >= pmin}
|
|
142
|
+
|
|
143
|
+
# z = 1/w
|
|
144
|
+
z = {-1: 1.0+0.0j}
|
|
145
|
+
|
|
146
|
+
c = (1.0 - 1.0/tau)
|
|
147
|
+
zeta = L_add(z, L_scale(inv_m, c)) # z + c*(1/m)
|
|
148
|
+
|
|
149
|
+
deg_z = a.shape[0]-1
|
|
150
|
+
deg_m = a.shape[1]-1
|
|
151
|
+
|
|
152
|
+
out = {}
|
|
153
|
+
zeta_pows = [L_pow(zeta, i, pmin, pmax) for i in range(deg_z+1)]
|
|
154
|
+
y_pows = [L_pow(y, j, pmin, pmax) for j in range(deg_m+1)]
|
|
155
|
+
|
|
156
|
+
for i in range(deg_z+1):
|
|
157
|
+
for j in range(deg_m+1):
|
|
158
|
+
coeff = a[i, j]
|
|
159
|
+
if abs(coeff) < 1e-18:
|
|
160
|
+
continue
|
|
161
|
+
term = L_mul(zeta_pows[i], y_pows[j], pmin, pmax)
|
|
162
|
+
out = L_add(out, L_scale(term, coeff))
|
|
163
|
+
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ======================
|
|
168
|
+
# solve laurent alpha ls
|
|
169
|
+
# ======================
|
|
170
|
+
|
|
171
|
+
def solve_laurent_alpha_ls(a, tau, K=8, L=None, max_iter=40, tol_step=1e-12):
|
|
172
|
+
"""
|
|
173
|
+
Solve for x = [alpha, mu1..muK] so that Laurent coeffs vanish
|
|
174
|
+
for powers p in [-L, ..., K]. Uses LS on both Re and Im parts (robust).
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
deg_z = a.shape[0]-1
|
|
178
|
+
if L is None:
|
|
179
|
+
L = deg_z + 2
|
|
180
|
+
|
|
181
|
+
pmin, pmax = -L, K
|
|
182
|
+
powers = list(range(pmin, pmax+1))
|
|
183
|
+
|
|
184
|
+
# unknowns: alpha + mu1..muK
|
|
185
|
+
x = numpy.zeros(K+1, dtype=numpy.float64)
|
|
186
|
+
x[0] = 1.0 # alpha init
|
|
187
|
+
|
|
188
|
+
def build_out(xx):
|
|
189
|
+
alpha = float(xx[0])
|
|
190
|
+
mu = xx[1:]
|
|
191
|
+
return Ptau_Laurent_alpha(a, tau, alpha, mu, pmin, pmax)
|
|
192
|
+
|
|
193
|
+
# LS Newton / Gauss-Newton
|
|
194
|
+
for it in range(max_iter):
|
|
195
|
+
out = build_out(x)
|
|
196
|
+
|
|
197
|
+
# complex residual vector r_p = coeff(p)
|
|
198
|
+
r = numpy.array([out.get(p, 0.0+0.0j) for p in powers],
|
|
199
|
+
dtype=numpy.complex128)
|
|
200
|
+
|
|
201
|
+
# stack real+imag (this is the key fix)
|
|
202
|
+
F = numpy.concatenate([r.real, r.imag], axis=0)
|
|
203
|
+
nrm = numpy.linalg.norm(F)
|
|
204
|
+
|
|
205
|
+
if nrm < 1e-12:
|
|
206
|
+
return x, True, out, powers
|
|
207
|
+
|
|
208
|
+
# Jacobian by FD
|
|
209
|
+
eps = 1e-6
|
|
210
|
+
J = numpy.zeros((F.size, x.size), dtype=numpy.float64)
|
|
211
|
+
for k in range(x.size):
|
|
212
|
+
x2 = x.copy()
|
|
213
|
+
x2[k] += eps
|
|
214
|
+
out2 = build_out(x2)
|
|
215
|
+
r2 = numpy.array([out2.get(p, 0.0+0.0j) for p in powers],
|
|
216
|
+
dtype=numpy.complex128)
|
|
217
|
+
F2 = numpy.concatenate([r2.real, r2.imag], axis=0)
|
|
218
|
+
J[:, k] = (F2 - F) / eps
|
|
219
|
+
|
|
220
|
+
# LS step
|
|
221
|
+
dx, *_ = numpy.linalg.lstsq(J, -F, rcond=None)
|
|
222
|
+
|
|
223
|
+
if numpy.linalg.norm(dx) < tol_step:
|
|
224
|
+
return x, False, out, powers
|
|
225
|
+
|
|
226
|
+
x += dx
|
|
227
|
+
|
|
228
|
+
return x, False, out, powers
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ================
|
|
232
|
+
# precheck laurent
|
|
233
|
+
# ================
|
|
234
|
+
|
|
235
|
+
def precheck_laurent(a, tau, K_list=(6, 8, 10), L=3, tol=1e-8, verbose=True):
|
|
236
|
+
"""
|
|
237
|
+
For fixed tau, try several K and pick the best (smallest max |coeff|).
|
|
238
|
+
Returns dict with bestK, alpha, max_abs, ok, and per-K details.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
deg_z = a.shape[0]-1
|
|
242
|
+
if L is None:
|
|
243
|
+
L = deg_z + 2
|
|
244
|
+
|
|
245
|
+
best = None
|
|
246
|
+
perK = []
|
|
247
|
+
|
|
248
|
+
for K in K_list:
|
|
249
|
+
x, ok_solve, out, powers = solve_laurent_alpha_ls(a, tau, K=K, L=L)
|
|
250
|
+
alpha = x[0]
|
|
251
|
+
coeffs = numpy.array([out.get(p, 0.0+0.0j) for p in powers],
|
|
252
|
+
dtype=numpy.complex128)
|
|
253
|
+
max_abs = float(numpy.max(numpy.abs(coeffs)))
|
|
254
|
+
worst_p = powers[int(numpy.argmax(numpy.abs(coeffs)))]
|
|
255
|
+
perK.append((K, alpha, max_abs, worst_p, ok_solve))
|
|
256
|
+
|
|
257
|
+
if best is None or max_abs < best["max_abs"]:
|
|
258
|
+
best = {
|
|
259
|
+
"K": K,
|
|
260
|
+
"alpha": alpha,
|
|
261
|
+
"max_abs": max_abs,
|
|
262
|
+
"worst_p": worst_p,
|
|
263
|
+
"ok_solve": ok_solve
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
alphas = numpy.array([row[1] for row in perK], dtype=float) # alpha per K
|
|
267
|
+
alpha_std = float(numpy.std(alphas))
|
|
268
|
+
alpha_span = float(numpy.max(alphas) - numpy.min(alphas))
|
|
269
|
+
|
|
270
|
+
ok = (best["max_abs"] <= tol) and best["ok_solve"]
|
|
271
|
+
ok = ok and (alpha_std < 1e-3) # or use alpha_span < 3e-3
|
|
272
|
+
|
|
273
|
+
if verbose:
|
|
274
|
+
print(f"--- tau={tau} --- ok={ok} bestK={best['K']} "
|
|
275
|
+
f"max_abs={best['max_abs']:.3e} "
|
|
276
|
+
f"alpha={best['alpha']:.12g} worst_p={best['worst_p']}")
|
|
277
|
+
|
|
278
|
+
print(f" alpha_std={alpha_std:.3e}, alpha_span={alpha_span:.3e}, "
|
|
279
|
+
f"alphas={alphas}")
|
|
280
|
+
|
|
281
|
+
for (K, alpha, max_abs, worst_p, ok_solve) in perK:
|
|
282
|
+
print(f" K={K:2d} max_abs={max_abs:.3e} worst_p={worst_p:2d} "
|
|
283
|
+
f"alpha={alpha:.12g} solve_ok={ok_solve}")
|
|
284
|
+
|
|
285
|
+
res = {
|
|
286
|
+
"best": best,
|
|
287
|
+
"perK": perK,
|
|
288
|
+
"alpha_std": alpha_std,
|
|
289
|
+
"alpha_span": alpha_span
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return ok, res
|
freealg/_algebraic_form/_edge.py
CHANGED
|
@@ -22,7 +22,7 @@ __all__ = ['evolve_edges', 'merge_edges']
|
|
|
22
22
|
# edge newton step
|
|
23
23
|
# ================
|
|
24
24
|
|
|
25
|
-
def _edge_newton_step(t, zeta, y,
|
|
25
|
+
def _edge_newton_step(t, zeta, y, coeffs, max_iter=30, tol=1e-12):
|
|
26
26
|
"""
|
|
27
27
|
"""
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ def _edge_newton_step(t, zeta, y, a_coeffs, max_iter=30, tol=1e-12):
|
|
|
30
30
|
c = tau - 1.0
|
|
31
31
|
|
|
32
32
|
for _ in range(max_iter):
|
|
33
|
-
P, Pz, Py = eval_P_partials(zeta, y,
|
|
33
|
+
P, Pz, Py = eval_P_partials(zeta, y, coeffs)
|
|
34
34
|
|
|
35
35
|
# F1 = P(zeta,y)
|
|
36
36
|
F1 = complex(P)
|
|
@@ -45,11 +45,11 @@ def _edge_newton_step(t, zeta, y, a_coeffs, max_iter=30, tol=1e-12):
|
|
|
45
45
|
eps_z = 1e-8 * (1.0 + abs(zeta))
|
|
46
46
|
eps_y = 1e-8 * (1.0 + abs(y))
|
|
47
47
|
|
|
48
|
-
Pp, Pzp, Pyp = eval_P_partials(zeta + eps_z, y,
|
|
48
|
+
Pp, Pzp, Pyp = eval_P_partials(zeta + eps_z, y, coeffs)
|
|
49
49
|
F1_zp = (complex(Pp) - F1) / eps_z
|
|
50
50
|
F2_zp = (complex((y * y) * Pyp - c * Pzp) - F2) / eps_z
|
|
51
51
|
|
|
52
|
-
Pp, Pzp, Pyp = eval_P_partials(zeta, y + eps_y,
|
|
52
|
+
Pp, Pzp, Pyp = eval_P_partials(zeta, y + eps_y, coeffs)
|
|
53
53
|
F1_yp = (complex(Pp) - F1) / eps_y
|
|
54
54
|
F2_yp = (complex(((y + eps_y) * (y + eps_y)) * Pyp - c * Pzp) - F2) / \
|
|
55
55
|
eps_y
|
|
@@ -102,7 +102,7 @@ def _pick_physical_root(z, roots):
|
|
|
102
102
|
# init edge point from support
|
|
103
103
|
# ============================
|
|
104
104
|
|
|
105
|
-
def _init_edge_point_from_support(x_edge,
|
|
105
|
+
def _init_edge_point_from_support(x_edge, coeffs, eta=1e-3):
|
|
106
106
|
"""
|
|
107
107
|
Initialize (zeta,y) at t=0 for an edge near x_edge.
|
|
108
108
|
|
|
@@ -111,7 +111,7 @@ def _init_edge_point_from_support(x_edge, a_coeffs, eta=1e-3):
|
|
|
111
111
|
"""
|
|
112
112
|
|
|
113
113
|
z = complex(x_edge + 1j * eta)
|
|
114
|
-
roots = eval_roots(numpy.array([z]),
|
|
114
|
+
roots = eval_roots(numpy.array([z]), coeffs)[0]
|
|
115
115
|
y = _pick_physical_root(z, roots)
|
|
116
116
|
|
|
117
117
|
# Move zeta to real axis as initial guess
|
|
@@ -119,7 +119,7 @@ def _init_edge_point_from_support(x_edge, a_coeffs, eta=1e-3):
|
|
|
119
119
|
|
|
120
120
|
# Refine zeta,y to satisfy P=0 and Py=0 at t=0 (branch point)
|
|
121
121
|
# This uses the same Newton system with c=0, i.e. F2 = y^2 Py.
|
|
122
|
-
zeta, y, ok = _edge_newton_step(0.0, zeta, y,
|
|
122
|
+
zeta, y, ok = _edge_newton_step(0.0, zeta, y, coeffs, max_iter=50,
|
|
123
123
|
tol=1e-10)
|
|
124
124
|
|
|
125
125
|
return zeta, y, ok
|
|
@@ -131,7 +131,7 @@ def _init_edge_point_from_support(x_edge, a_coeffs, eta=1e-3):
|
|
|
131
131
|
|
|
132
132
|
def evolve_edges(
|
|
133
133
|
t_grid,
|
|
134
|
-
|
|
134
|
+
coeffs,
|
|
135
135
|
support=None,
|
|
136
136
|
eta=1e-3,
|
|
137
137
|
dt_max=0.1,
|
|
@@ -185,7 +185,7 @@ def evolve_edges(
|
|
|
185
185
|
y = numpy.empty(m, dtype=numpy.complex128)
|
|
186
186
|
|
|
187
187
|
for j in range(m):
|
|
188
|
-
z0, y0, ok0 = _init_edge_point_from_support(endpoints0[j],
|
|
188
|
+
z0, y0, ok0 = _init_edge_point_from_support(endpoints0[j], coeffs,
|
|
189
189
|
eta=eta)
|
|
190
190
|
zeta[j] = z0
|
|
191
191
|
y[j] = y0
|
|
@@ -210,7 +210,7 @@ def evolve_edges(
|
|
|
210
210
|
t = t0 + dt * (ks / float(n_sub))
|
|
211
211
|
for j in range(m):
|
|
212
212
|
zeta[j], y[j], okj = _edge_newton_step(
|
|
213
|
-
t, zeta[j], y[j],
|
|
213
|
+
t, zeta[j], y[j], coeffs, max_iter=max_iter, tol=tol
|
|
214
214
|
)
|
|
215
215
|
ok[it, j] = okj
|
|
216
216
|
|
|
@@ -23,13 +23,13 @@ __all__ = ["StieltjesPoly"]
|
|
|
23
23
|
# Poly -> roots in m for z
|
|
24
24
|
# =========================
|
|
25
25
|
|
|
26
|
-
def _poly_m_coeffs(
|
|
26
|
+
def _poly_m_coeffs(coeffs, z):
|
|
27
27
|
"""Return coefficients b_j for \sum_j b_j m^j = 0 at fixed z.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
coeffs[i,j] is coeff of z^i m^j.
|
|
30
30
|
Returns b of length (deg_m+1) with b[j] = \sum_i a[i,j] z^i.
|
|
31
31
|
"""
|
|
32
|
-
a = numpy.asarray(
|
|
32
|
+
a = numpy.asarray(coeffs, dtype=numpy.complex128)
|
|
33
33
|
deg_z = a.shape[0] - 1
|
|
34
34
|
deg_m = a.shape[1] - 1
|
|
35
35
|
|
|
@@ -47,9 +47,9 @@ def _poly_m_coeffs(a_coeffs, z):
|
|
|
47
47
|
return b
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def _roots_m(
|
|
50
|
+
def _roots_m(coeffs, z):
|
|
51
51
|
"""All algebraic roots in m at fixed z."""
|
|
52
|
-
b = _poly_m_coeffs(
|
|
52
|
+
b = _poly_m_coeffs(coeffs, z)
|
|
53
53
|
|
|
54
54
|
# Drop leading zeros in highest power to keep numpy.roots stable
|
|
55
55
|
# numpy.roots expects highest degree first.
|
|
@@ -239,9 +239,9 @@ def _viterbi_track(z_list, roots_list, mL, mR, *,
|
|
|
239
239
|
class StieltjesPoly(object):
|
|
240
240
|
"""Callable m(z) for P(z,m)=0 using robust branch selection."""
|
|
241
241
|
|
|
242
|
-
def __init__(self,
|
|
242
|
+
def __init__(self, coeffs, *,
|
|
243
243
|
viterbi_opt=None):
|
|
244
|
-
self.
|
|
244
|
+
self.coeffs = numpy.asarray(coeffs, dtype=numpy.complex128)
|
|
245
245
|
self.viterbi_opt = dict(viterbi_opt or {})
|
|
246
246
|
|
|
247
247
|
# ----------
|
|
@@ -249,7 +249,7 @@ class StieltjesPoly(object):
|
|
|
249
249
|
# ----------
|
|
250
250
|
|
|
251
251
|
def evaluate_scalar(self, z, target=None):
|
|
252
|
-
r = _roots_m(self.
|
|
252
|
+
r = _roots_m(self.coeffs, z)
|
|
253
253
|
return _pick_physical_root_scalar(z, r, target=target)
|
|
254
254
|
|
|
255
255
|
# ---------------
|
|
@@ -267,7 +267,7 @@ class StieltjesPoly(object):
|
|
|
267
267
|
z_list = z.ravel()
|
|
268
268
|
|
|
269
269
|
# roots for each point
|
|
270
|
-
roots_list = [_roots_m(self.
|
|
270
|
+
roots_list = [_roots_m(self.coeffs, zi) for zi in z_list]
|
|
271
271
|
|
|
272
272
|
# boundary anchors via scalar selection
|
|
273
273
|
mL = self.evaluate_scalar(z_list[0])
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import numpy
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _poly_coeffs_in_m(
|
|
12
|
-
a =
|
|
11
|
+
def _poly_coeffs_in_m(coeffs, z):
|
|
12
|
+
a = coeffs
|
|
13
13
|
dz = a.shape[0] - 1
|
|
14
14
|
s = a.shape[1] - 1
|
|
15
15
|
zp = numpy.array([z**i for i in range(dz + 1)], dtype=numpy.complex128)
|
|
@@ -19,8 +19,8 @@ def _poly_coeffs_in_m(a_coeffs, z):
|
|
|
19
19
|
return coeff_m
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def _roots_m(
|
|
23
|
-
coeff_m = _poly_coeffs_in_m(
|
|
22
|
+
def _roots_m(coeffs, z):
|
|
23
|
+
coeff_m = _poly_coeffs_in_m(coeffs, z)
|
|
24
24
|
c = coeff_m[::-1]
|
|
25
25
|
while c.size > 1 and numpy.abs(c[0]) == 0:
|
|
26
26
|
c = c[1:]
|
|
@@ -96,12 +96,12 @@ def _viterbi_1d(z_list, roots_all, *, lam_space, lam_asym,
|
|
|
96
96
|
class StieltjesPoly(object):
|
|
97
97
|
"""Callable m(z) for P(z,m)=0 using robust branch selection."""
|
|
98
98
|
|
|
99
|
-
def __init__(self,
|
|
100
|
-
self.
|
|
99
|
+
def __init__(self, coeffs, *, viterbi_opt=None):
|
|
100
|
+
self.coeffs = numpy.asarray(coeffs, dtype=numpy.complex128)
|
|
101
101
|
self.viterbi_opt = dict(viterbi_opt or {})
|
|
102
102
|
|
|
103
103
|
def evaluate_scalar(self, z, target=None):
|
|
104
|
-
r = _roots_m(self.
|
|
104
|
+
r = _roots_m(self.coeffs, z)
|
|
105
105
|
if r.size == 0:
|
|
106
106
|
return numpy.nan + 1j * numpy.nan
|
|
107
107
|
tol_im = float(self.viterbi_opt.get("tol_im", 1e-14))
|
|
@@ -123,11 +123,11 @@ class StieltjesPoly(object):
|
|
|
123
123
|
|
|
124
124
|
if z.ndim == 1 and z.size >= 2:
|
|
125
125
|
z_list = z.ravel()
|
|
126
|
-
s = self.
|
|
126
|
+
s = self.coeffs.shape[1] - 1
|
|
127
127
|
roots_all = numpy.empty((z_list.size, s), dtype=numpy.complex128)
|
|
128
128
|
ok_all = numpy.ones(z_list.size, dtype=bool)
|
|
129
129
|
for k in range(z_list.size):
|
|
130
|
-
r = _roots_m(self.
|
|
130
|
+
r = _roots_m(self.coeffs, z_list[k])
|
|
131
131
|
if r.size != s:
|
|
132
132
|
ok_all[k] = False
|
|
133
133
|
if r.size == 0:
|
|
@@ -15,18 +15,18 @@ import numpy
|
|
|
15
15
|
import numpy.polynomial.polynomial as poly
|
|
16
16
|
from ._homotopy5 import StieltjesPoly
|
|
17
17
|
|
|
18
|
-
__all__ = ['
|
|
18
|
+
__all__ = ['estimate_support']
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
# =====================
|
|
22
22
|
# poly coeffs in m at z
|
|
23
23
|
# =====================
|
|
24
24
|
|
|
25
|
-
def _poly_coeffs_in_m_at_z(
|
|
26
|
-
s =
|
|
25
|
+
def _poly_coeffs_in_m_at_z(coeffs, z):
|
|
26
|
+
s = coeffs.shape[1] - 1
|
|
27
27
|
a = numpy.empty(s + 1, dtype=numpy.complex128)
|
|
28
28
|
for j in range(s + 1):
|
|
29
|
-
a[j] = poly.polyval(z,
|
|
29
|
+
a[j] = poly.polyval(z, coeffs[:, j])
|
|
30
30
|
return a
|
|
31
31
|
|
|
32
32
|
|
|
@@ -34,14 +34,14 @@ def _poly_coeffs_in_m_at_z(a_coeffs, z):
|
|
|
34
34
|
# P and partials
|
|
35
35
|
# ==============
|
|
36
36
|
|
|
37
|
-
def _P_and_partials(
|
|
38
|
-
s =
|
|
37
|
+
def _P_and_partials(coeffs, z, m):
|
|
38
|
+
s = coeffs.shape[1] - 1
|
|
39
39
|
|
|
40
40
|
a = numpy.empty(s + 1, dtype=numpy.complex128)
|
|
41
41
|
da = numpy.empty(s + 1, dtype=numpy.complex128)
|
|
42
42
|
for j in range(s + 1):
|
|
43
|
-
a[j] = poly.polyval(z,
|
|
44
|
-
da[j] = poly.polyval(z, poly.polyder(
|
|
43
|
+
a[j] = poly.polyval(z, coeffs[:, j])
|
|
44
|
+
da[j] = poly.polyval(z, poly.polyder(coeffs[:, j]))
|
|
45
45
|
|
|
46
46
|
mpow = 1.0 + 0.0j
|
|
47
47
|
P = 0.0 + 0.0j
|
|
@@ -67,13 +67,13 @@ def _P_and_partials(a_coeffs, z, m):
|
|
|
67
67
|
# newton edge
|
|
68
68
|
# ===========
|
|
69
69
|
|
|
70
|
-
def _newton_edge(
|
|
70
|
+
def _newton_edge(coeffs, x0, m0, tol=1e-12, max_iter=50):
|
|
71
71
|
x = float(x0)
|
|
72
72
|
m = float(m0)
|
|
73
73
|
|
|
74
74
|
for _ in range(max_iter):
|
|
75
75
|
z = x + 0.0j
|
|
76
|
-
P, Pz, Pm, Pzm, Pmm = _P_and_partials(
|
|
76
|
+
P, Pz, Pm, Pzm, Pmm = _P_and_partials(coeffs, z, m)
|
|
77
77
|
|
|
78
78
|
f0 = float(numpy.real(P))
|
|
79
79
|
f1 = float(numpy.real(Pm))
|
|
@@ -157,12 +157,12 @@ def _bisect_edge(stieltjes_poly, x_lo, x_hi, eta, im_thr, max_iter=60):
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
# ===============
|
|
160
|
-
#
|
|
160
|
+
# estimate support
|
|
161
161
|
# ===============
|
|
162
162
|
|
|
163
|
-
def
|
|
163
|
+
def estimate_support(coeffs, x_min, x_max, n_scan=4000, **kwargs):
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
coeffs = numpy.asarray(coeffs, dtype=numpy.complex128)
|
|
166
166
|
|
|
167
167
|
x_min = float(x_min)
|
|
168
168
|
x_max = float(x_max)
|
|
@@ -182,7 +182,7 @@ def compute_support(a_coeffs, x_min, x_max, n_scan=4000, **kwargs):
|
|
|
182
182
|
'tol_im': 1e-14,
|
|
183
183
|
}
|
|
184
184
|
vopt.update(kwargs.get('viterbi_opt', {}) or {})
|
|
185
|
-
stieltjes = StieltjesPoly(
|
|
185
|
+
stieltjes = StieltjesPoly(coeffs, viterbi_opt=vopt)
|
|
186
186
|
|
|
187
187
|
x_grid = numpy.linspace(x_min, x_max, n_scan)
|
|
188
188
|
z_grid = x_grid + 1j * eta
|
|
@@ -234,18 +234,18 @@ def compute_support(a_coeffs, x_min, x_max, n_scan=4000, **kwargs):
|
|
|
234
234
|
edges_ref = []
|
|
235
235
|
for x0 in edges:
|
|
236
236
|
m0 = float(numpy.real(stieltjes.evaluate_scalar(x0 + 1j * eta)))
|
|
237
|
-
xe, _, ok = _newton_edge(
|
|
237
|
+
xe, _, ok = _newton_edge(coeffs, x0, m0, tol=newton_tol)
|
|
238
238
|
edges_ref.append(float(xe)
|
|
239
239
|
if ok and numpy.isfinite(xe) else float(x0))
|
|
240
240
|
edges = _cluster_edges(edges_ref, edge_x_cluster_tol)
|
|
241
241
|
|
|
242
242
|
edges.sort()
|
|
243
|
-
|
|
243
|
+
est_supp = []
|
|
244
244
|
for k in range(0, edges.size - 1, 2):
|
|
245
245
|
a = float(edges[k])
|
|
246
246
|
b = float(edges[k + 1])
|
|
247
247
|
if b > a:
|
|
248
|
-
|
|
248
|
+
est_supp.append((a, b))
|
|
249
249
|
|
|
250
250
|
info = {
|
|
251
251
|
'x_grid': x_grid,
|
|
@@ -254,11 +254,11 @@ def compute_support(a_coeffs, x_min, x_max, n_scan=4000, **kwargs):
|
|
|
254
254
|
'im_grid': im_grid,
|
|
255
255
|
'im_thr': float(im_thr),
|
|
256
256
|
'edges': edges,
|
|
257
|
-
'
|
|
257
|
+
'est_supp': est_supp,
|
|
258
258
|
'x_min': x_min,
|
|
259
259
|
'x_max': x_max,
|
|
260
260
|
'n_scan': n_scan,
|
|
261
261
|
'scale': scale,
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
return
|
|
264
|
+
return est_supp, info
|