freealg 0.7.12__py3-none-any.whl → 0.7.14__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/_cusp.py +357 -0
- freealg/_algebraic_form/_cusp_wrap.py +268 -0
- freealg/_algebraic_form/_decompress2.py +2 -0
- freealg/_algebraic_form/_decompress4.py +739 -0
- freealg/_algebraic_form/_decompress5.py +738 -0
- freealg/_algebraic_form/_decompress6.py +492 -0
- freealg/_algebraic_form/_decompress7.py +355 -0
- freealg/_algebraic_form/_decompress8.py +369 -0
- freealg/_algebraic_form/_decompress9.py +363 -0
- freealg/_algebraic_form/_decompress_new.py +431 -0
- freealg/_algebraic_form/_decompress_new_2.py +1631 -0
- freealg/_algebraic_form/_decompress_util.py +172 -0
- freealg/_algebraic_form/_homotopy2.py +289 -0
- freealg/_algebraic_form/_homotopy3.py +215 -0
- freealg/_algebraic_form/_homotopy4.py +320 -0
- freealg/_algebraic_form/_homotopy5.py +185 -0
- freealg/_algebraic_form/_moments.py +0 -1
- freealg/_algebraic_form/_support.py +132 -177
- freealg/_algebraic_form/algebraic_form.py +21 -2
- freealg/distributions/_compound_poisson.py +464 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/METADATA +1 -1
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/RECORD +27 -11
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/WHEEL +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.14.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileType: SOURCE
|
|
4
|
+
#
|
|
5
|
+
# Robust Stieltjes branch evaluation for algebraic P(z,m)=0.
|
|
6
|
+
#
|
|
7
|
+
# This version is tailored for empirical polynomial fits where spurious
|
|
8
|
+
# small-Im roots can appear outside the true support and create fake bulks
|
|
9
|
+
# when using rho = Im m(x+i*eta)/pi.
|
|
10
|
+
#
|
|
11
|
+
# Core idea: pick the physical branch by combining
|
|
12
|
+
# (i) Herglotz half-plane constraint,
|
|
13
|
+
# (ii) asymptotic constraint z*m ~ -1,
|
|
14
|
+
# (iii) 1D continuity along x (Viterbi),
|
|
15
|
+
# and DO NOT globally reward large |Im(m)| (which can fabricate density).
|
|
16
|
+
|
|
17
|
+
import numpy
|
|
18
|
+
|
|
19
|
+
__all__ = ["StieltjesPoly"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =========================
|
|
23
|
+
# Poly -> roots in m for z
|
|
24
|
+
# =========================
|
|
25
|
+
|
|
26
|
+
def _poly_m_coeffs(a_coeffs, z):
|
|
27
|
+
"""Return coefficients b_j for \sum_j b_j m^j = 0 at fixed z.
|
|
28
|
+
|
|
29
|
+
a_coeffs[i,j] is coeff of z^i m^j.
|
|
30
|
+
Returns b of length (deg_m+1) with b[j] = \sum_i a[i,j] z^i.
|
|
31
|
+
"""
|
|
32
|
+
a = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
33
|
+
deg_z = a.shape[0] - 1
|
|
34
|
+
deg_m = a.shape[1] - 1
|
|
35
|
+
|
|
36
|
+
# Horner in z for each m-power j
|
|
37
|
+
z = complex(z)
|
|
38
|
+
b = numpy.zeros((deg_m + 1,), dtype=numpy.complex128)
|
|
39
|
+
# b[j] = a[0,j] + a[1,j] z + ... + a[deg_z,j] z^{deg_z}
|
|
40
|
+
# do Horner: (((a[deg_z,j] z + a[deg_z-1,j]) z + ...) z + a[0,j])
|
|
41
|
+
for j in range(deg_m + 1):
|
|
42
|
+
acc = 0.0 + 0.0j
|
|
43
|
+
for i in range(deg_z, -1, -1):
|
|
44
|
+
acc = acc * z + a[i, j]
|
|
45
|
+
b[j] = acc
|
|
46
|
+
|
|
47
|
+
return b
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _roots_m(a_coeffs, z):
|
|
51
|
+
"""All algebraic roots in m at fixed z."""
|
|
52
|
+
b = _poly_m_coeffs(a_coeffs, z)
|
|
53
|
+
|
|
54
|
+
# Drop leading zeros in highest power to keep numpy.roots stable
|
|
55
|
+
# numpy.roots expects highest degree first.
|
|
56
|
+
coeffs = b.copy()
|
|
57
|
+
# find highest nonzero index
|
|
58
|
+
nz = numpy.flatnonzero(numpy.abs(coeffs) > 0.0)
|
|
59
|
+
if nz.size == 0:
|
|
60
|
+
return numpy.array([], dtype=numpy.complex128)
|
|
61
|
+
j_max = int(nz.max())
|
|
62
|
+
coeffs = coeffs[: j_max + 1]
|
|
63
|
+
|
|
64
|
+
# reverse to highest-first
|
|
65
|
+
return numpy.roots(coeffs[::-1])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ==============================
|
|
69
|
+
# Physical root selection scalar
|
|
70
|
+
# ==============================
|
|
71
|
+
|
|
72
|
+
def _pick_physical_root_scalar(z, roots, target=None, tol_im=1e-12):
|
|
73
|
+
"""Pick the physical root among candidates for a single z.
|
|
74
|
+
|
|
75
|
+
Rules:
|
|
76
|
+
1) Prefer roots with sign(Im m) = sign(Im z) (Herglotz).
|
|
77
|
+
2) Break ties by asymptotic closeness to -1/z.
|
|
78
|
+
3) If target provided, also enforce continuity by closeness to target.
|
|
79
|
+
|
|
80
|
+
Returns complex root.
|
|
81
|
+
"""
|
|
82
|
+
z = complex(z)
|
|
83
|
+
roots = numpy.asarray(roots, dtype=numpy.complex128).ravel()
|
|
84
|
+
if roots.size == 0:
|
|
85
|
+
return numpy.nan + 1j * numpy.nan
|
|
86
|
+
|
|
87
|
+
s = numpy.sign(z.imag)
|
|
88
|
+
if s == 0.0:
|
|
89
|
+
s = 1.0
|
|
90
|
+
|
|
91
|
+
im_s = numpy.imag(roots) * s
|
|
92
|
+
cand = roots[im_s > -tol_im]
|
|
93
|
+
if cand.size == 0:
|
|
94
|
+
cand = roots
|
|
95
|
+
|
|
96
|
+
# asymptotic target
|
|
97
|
+
m_asym = -1.0 / z
|
|
98
|
+
|
|
99
|
+
if target is None:
|
|
100
|
+
score = numpy.abs(cand - m_asym)
|
|
101
|
+
else:
|
|
102
|
+
t = complex(target)
|
|
103
|
+
# continuity dominates, asymptotic helps in ambiguous regions
|
|
104
|
+
score = numpy.abs(cand - t) + 0.15 * numpy.abs(cand - m_asym)
|
|
105
|
+
|
|
106
|
+
return cand[int(numpy.argmin(score))]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# =====================
|
|
110
|
+
# Viterbi along a line
|
|
111
|
+
# =====================
|
|
112
|
+
|
|
113
|
+
def _viterbi_track(z_list, roots_list, mL, mR, *,
|
|
114
|
+
lam_space=1.0,
|
|
115
|
+
lam_edge=20.0,
|
|
116
|
+
lam_asym=0.25,
|
|
117
|
+
lam_time=0.0,
|
|
118
|
+
# Hinge penalty against "tiny-im" traps ONLY
|
|
119
|
+
lam_tiny_im=0.0,
|
|
120
|
+
tiny_im=1e-7,
|
|
121
|
+
tol_im=1e-12,
|
|
122
|
+
m_prev=None):
|
|
123
|
+
"""Choose one root per z via dynamic programming.
|
|
124
|
+
|
|
125
|
+
z_list: (nz,)
|
|
126
|
+
roots_list: list of arrays of candidate roots, each (k_i,)
|
|
127
|
+
mL,mR: boundary anchors (complex)
|
|
128
|
+
m_prev: optional previous-time chosen path, shape (nz,)
|
|
129
|
+
|
|
130
|
+
Returns m_path (nz,) and ok (nz,) boolean indicating finite.
|
|
131
|
+
"""
|
|
132
|
+
z_list = numpy.asarray(z_list, dtype=numpy.complex128).ravel()
|
|
133
|
+
nz = z_list.size
|
|
134
|
+
|
|
135
|
+
# Determine a fixed K by padding with NaNs (max roots)
|
|
136
|
+
K = max((r.size for r in roots_list), default=0)
|
|
137
|
+
if K == 0:
|
|
138
|
+
return (numpy.full((nz,), numpy.nan + 1j * numpy.nan, dtype=numpy.complex128),
|
|
139
|
+
numpy.zeros((nz,), dtype=bool))
|
|
140
|
+
|
|
141
|
+
R = numpy.full((nz, K), numpy.nan + 1j * numpy.nan, dtype=numpy.complex128)
|
|
142
|
+
for i, r in enumerate(roots_list):
|
|
143
|
+
if r.size:
|
|
144
|
+
R[i, : r.size] = r
|
|
145
|
+
|
|
146
|
+
# feasible mask: finite and Herglotz half-plane (soft)
|
|
147
|
+
s = numpy.sign(z_list.imag)
|
|
148
|
+
s[s == 0.0] = 1.0
|
|
149
|
+
IM = (numpy.imag(R) * s[:, None])
|
|
150
|
+
feasible = numpy.isfinite(R) & (IM > -tol_im)
|
|
151
|
+
|
|
152
|
+
# unary cost
|
|
153
|
+
unary = numpy.full((nz, K), numpy.inf, dtype=numpy.float64)
|
|
154
|
+
# continuity to asymptotic (-1/z) discourages fake branches outside support
|
|
155
|
+
m_asym = -1.0 / z_list
|
|
156
|
+
|
|
157
|
+
for i in range(nz):
|
|
158
|
+
zi = z_list[i]
|
|
159
|
+
mi = m_asym[i]
|
|
160
|
+
for k in range(K):
|
|
161
|
+
if not feasible[i, k]:
|
|
162
|
+
continue
|
|
163
|
+
w = R[i, k]
|
|
164
|
+
c = 0.0
|
|
165
|
+
|
|
166
|
+
# asymptotic penalty (small weight)
|
|
167
|
+
if lam_asym != 0.0:
|
|
168
|
+
c += float(lam_asym) * float(numpy.abs(zi * w + 1.0))
|
|
169
|
+
|
|
170
|
+
# optional hinge against tiny imag (ONLY below threshold)
|
|
171
|
+
if lam_tiny_im != 0.0:
|
|
172
|
+
im = abs(w.imag)
|
|
173
|
+
floor = max(float(tiny_im), 0.25 * abs(zi.imag))
|
|
174
|
+
if im < floor:
|
|
175
|
+
c += float(lam_tiny_im) * float(((floor / max(im, 1e-16)) - 1.0) ** 2)
|
|
176
|
+
|
|
177
|
+
# optional time consistency
|
|
178
|
+
if (lam_time != 0.0) and (m_prev is not None) and numpy.isfinite(m_prev[i]):
|
|
179
|
+
c += float(lam_time) * float(numpy.abs(w - m_prev[i]))
|
|
180
|
+
|
|
181
|
+
unary[i, k] = c
|
|
182
|
+
|
|
183
|
+
# boundary anchors
|
|
184
|
+
if numpy.isfinite(mL):
|
|
185
|
+
unary[0, :] += float(lam_edge) * numpy.abs(R[0, :] - mL)
|
|
186
|
+
if numpy.isfinite(mR):
|
|
187
|
+
unary[-1, :] += float(lam_edge) * numpy.abs(R[-1, :] - mR)
|
|
188
|
+
|
|
189
|
+
# pairwise cost
|
|
190
|
+
dp = numpy.full((nz, K), numpy.inf, dtype=numpy.float64)
|
|
191
|
+
prev = numpy.full((nz, K), -1, dtype=numpy.int64)
|
|
192
|
+
|
|
193
|
+
dp[0, :] = unary[0, :]
|
|
194
|
+
|
|
195
|
+
for i in range(1, nz):
|
|
196
|
+
for k in range(K):
|
|
197
|
+
if not numpy.isfinite(unary[i, k]):
|
|
198
|
+
continue
|
|
199
|
+
# transition from any j
|
|
200
|
+
best_val = numpy.inf
|
|
201
|
+
best_j = -1
|
|
202
|
+
wk = R[i, k]
|
|
203
|
+
for j in range(K):
|
|
204
|
+
if not numpy.isfinite(dp[i - 1, j]):
|
|
205
|
+
continue
|
|
206
|
+
wj = R[i - 1, j]
|
|
207
|
+
if not numpy.isfinite(wj):
|
|
208
|
+
continue
|
|
209
|
+
val = dp[i - 1, j] + float(lam_space) * float(numpy.abs(wk - wj))
|
|
210
|
+
if val < best_val:
|
|
211
|
+
best_val = val
|
|
212
|
+
best_j = j
|
|
213
|
+
if best_j >= 0:
|
|
214
|
+
dp[i, k] = best_val + unary[i, k]
|
|
215
|
+
prev[i, k] = best_j
|
|
216
|
+
|
|
217
|
+
# backtrack
|
|
218
|
+
k_end = int(numpy.argmin(dp[-1, :]))
|
|
219
|
+
m_path = numpy.full((nz,), numpy.nan + 1j * numpy.nan, dtype=numpy.complex128)
|
|
220
|
+
if not numpy.isfinite(dp[-1, k_end]):
|
|
221
|
+
return m_path, numpy.zeros((nz,), dtype=bool)
|
|
222
|
+
|
|
223
|
+
k = k_end
|
|
224
|
+
for i in range(nz - 1, -1, -1):
|
|
225
|
+
m_path[i] = R[i, k]
|
|
226
|
+
k = prev[i, k]
|
|
227
|
+
if (i > 0) and (k < 0):
|
|
228
|
+
# cannot continue
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
ok = numpy.isfinite(m_path)
|
|
232
|
+
return m_path, ok
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ============
|
|
236
|
+
# StieltjesPoly
|
|
237
|
+
# ============
|
|
238
|
+
|
|
239
|
+
class StieltjesPoly(object):
|
|
240
|
+
"""Callable m(z) for P(z,m)=0 using robust branch selection."""
|
|
241
|
+
|
|
242
|
+
def __init__(self, a_coeffs, *,
|
|
243
|
+
viterbi_opt=None):
|
|
244
|
+
self.a_coeffs = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
245
|
+
self.viterbi_opt = dict(viterbi_opt or {})
|
|
246
|
+
|
|
247
|
+
# ----------
|
|
248
|
+
# scalar eval
|
|
249
|
+
# ----------
|
|
250
|
+
|
|
251
|
+
def evaluate_scalar(self, z, target=None):
|
|
252
|
+
r = _roots_m(self.a_coeffs, z)
|
|
253
|
+
return _pick_physical_root_scalar(z, r, target=target)
|
|
254
|
+
|
|
255
|
+
# ---------------
|
|
256
|
+
# vectorized eval
|
|
257
|
+
# ---------------
|
|
258
|
+
|
|
259
|
+
def __call__(self, z):
|
|
260
|
+
z = numpy.asarray(z, dtype=numpy.complex128)
|
|
261
|
+
scalar = (z.ndim == 0)
|
|
262
|
+
if scalar:
|
|
263
|
+
z = z.reshape((1,))
|
|
264
|
+
|
|
265
|
+
# If 1D, do Viterbi tracking in the given order
|
|
266
|
+
if z.ndim == 1:
|
|
267
|
+
z_list = z.ravel()
|
|
268
|
+
|
|
269
|
+
# roots for each point
|
|
270
|
+
roots_list = [_roots_m(self.a_coeffs, zi) for zi in z_list]
|
|
271
|
+
|
|
272
|
+
# boundary anchors via scalar selection
|
|
273
|
+
mL = self.evaluate_scalar(z_list[0])
|
|
274
|
+
mR = self.evaluate_scalar(z_list[-1])
|
|
275
|
+
|
|
276
|
+
opt = {
|
|
277
|
+
"lam_space": 1.0,
|
|
278
|
+
"lam_edge": 20.0,
|
|
279
|
+
"lam_asym": 0.25,
|
|
280
|
+
"lam_time": 0.0,
|
|
281
|
+
"lam_tiny_im": 0.0,
|
|
282
|
+
"tiny_im": 1e-7,
|
|
283
|
+
"tol_im": 1e-12,
|
|
284
|
+
}
|
|
285
|
+
opt.update(self.viterbi_opt)
|
|
286
|
+
|
|
287
|
+
m_path, ok = _viterbi_track(
|
|
288
|
+
z_list,
|
|
289
|
+
roots_list,
|
|
290
|
+
mL,
|
|
291
|
+
mR,
|
|
292
|
+
lam_space=opt["lam_space"],
|
|
293
|
+
lam_edge=opt["lam_edge"],
|
|
294
|
+
lam_asym=opt["lam_asym"],
|
|
295
|
+
lam_time=opt["lam_time"],
|
|
296
|
+
lam_tiny_im=opt["lam_tiny_im"],
|
|
297
|
+
tiny_im=opt["tiny_im"],
|
|
298
|
+
tol_im=opt["tol_im"],
|
|
299
|
+
m_prev=None,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# fallback pointwise for any failures
|
|
303
|
+
if not numpy.all(ok):
|
|
304
|
+
out = m_path.copy()
|
|
305
|
+
for i in numpy.flatnonzero(~ok):
|
|
306
|
+
out[i] = self.evaluate_scalar(z_list[i], target=None)
|
|
307
|
+
m_path = out
|
|
308
|
+
|
|
309
|
+
out = m_path.reshape(z.shape)
|
|
310
|
+
return out.reshape(()) if scalar else out
|
|
311
|
+
|
|
312
|
+
# For nd>1: evaluate pointwise with asymptotic tie-break
|
|
313
|
+
out = numpy.empty(z.size, dtype=numpy.complex128)
|
|
314
|
+
zf = z.ravel()
|
|
315
|
+
prev = None
|
|
316
|
+
for i in range(zf.size):
|
|
317
|
+
out[i] = self.evaluate_scalar(zf[i], target=prev)
|
|
318
|
+
prev = out[i]
|
|
319
|
+
out = out.reshape(z.shape)
|
|
320
|
+
return out.reshape(()) if scalar else out
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileType: SOURCE
|
|
4
|
+
#
|
|
5
|
+
# Robust Stieltjes branch evaluation for algebraic P(z,m)=0 using
|
|
6
|
+
# global 1D dynamic programming (Viterbi) along a complex line.
|
|
7
|
+
|
|
8
|
+
import numpy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _poly_coeffs_in_m(a_coeffs, z):
|
|
12
|
+
a = a_coeffs
|
|
13
|
+
dz = a.shape[0] - 1
|
|
14
|
+
s = a.shape[1] - 1
|
|
15
|
+
zp = numpy.array([z**i for i in range(dz + 1)], dtype=numpy.complex128)
|
|
16
|
+
coeff_m = numpy.empty(s + 1, dtype=numpy.complex128)
|
|
17
|
+
for j in range(s + 1):
|
|
18
|
+
coeff_m[j] = numpy.dot(a[:, j], zp)
|
|
19
|
+
return coeff_m
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _roots_m(a_coeffs, z):
|
|
23
|
+
coeff_m = _poly_coeffs_in_m(a_coeffs, z)
|
|
24
|
+
c = coeff_m[::-1]
|
|
25
|
+
while c.size > 1 and numpy.abs(c[0]) == 0:
|
|
26
|
+
c = c[1:]
|
|
27
|
+
if c.size <= 1:
|
|
28
|
+
return numpy.array([], dtype=numpy.complex128)
|
|
29
|
+
return numpy.roots(c)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _pick_anchor(z, roots, tol_im, lam_asym):
|
|
33
|
+
if roots.size == 0:
|
|
34
|
+
return numpy.nan + 1j * numpy.nan
|
|
35
|
+
sgn = 1.0 if numpy.imag(z) >= 0 else -1.0
|
|
36
|
+
ok = (sgn * numpy.imag(roots) > tol_im)
|
|
37
|
+
if numpy.any(ok):
|
|
38
|
+
cand = roots[ok]
|
|
39
|
+
else:
|
|
40
|
+
cand = roots
|
|
41
|
+
cost = lam_asym * numpy.abs(z * cand + 1.0)
|
|
42
|
+
return cand[int(numpy.argmin(cost))]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _viterbi_1d(z_list, roots_all, *, lam_space, lam_asym,
|
|
46
|
+
lam_tiny_im, tiny_im, tol_im):
|
|
47
|
+
n, s = roots_all.shape
|
|
48
|
+
big = 1.0e300
|
|
49
|
+
|
|
50
|
+
cost0 = numpy.zeros((n, s), dtype=float)
|
|
51
|
+
back = numpy.zeros((n, s), dtype=numpy.int64)
|
|
52
|
+
dp = numpy.full((n, s), big, dtype=float)
|
|
53
|
+
|
|
54
|
+
for k in range(n):
|
|
55
|
+
z = z_list[k]
|
|
56
|
+
r = roots_all[k]
|
|
57
|
+
sgn = 1.0 if numpy.imag(z) >= 0 else -1.0
|
|
58
|
+
|
|
59
|
+
ok = (sgn * numpy.imag(r) > tol_im)
|
|
60
|
+
cost0[k, ~ok] += big * 1.0e-6
|
|
61
|
+
|
|
62
|
+
if lam_tiny_im != 0.0 and tiny_im is not None:
|
|
63
|
+
imabs = numpy.abs(numpy.imag(r))
|
|
64
|
+
hing = numpy.maximum(0.0, float(tiny_im) - imabs)
|
|
65
|
+
cost0[k] += lam_tiny_im * hing
|
|
66
|
+
|
|
67
|
+
if lam_asym != 0.0:
|
|
68
|
+
cost0[k] += lam_asym * numpy.abs(z * r + 1.0)
|
|
69
|
+
|
|
70
|
+
m0 = _pick_anchor(z_list[0], roots_all[0], tol_im, lam_asym)
|
|
71
|
+
mN = _pick_anchor(z_list[-1], roots_all[-1], tol_im, lam_asym)
|
|
72
|
+
|
|
73
|
+
init = cost0[0] + lam_space * numpy.abs(roots_all[0] - m0)
|
|
74
|
+
dp[0] = init
|
|
75
|
+
|
|
76
|
+
for k in range(1, n):
|
|
77
|
+
r = roots_all[k]
|
|
78
|
+
rp = roots_all[k - 1]
|
|
79
|
+
for j in range(s):
|
|
80
|
+
trans = dp[k - 1] + lam_space * numpy.abs(r[j] - rp)
|
|
81
|
+
idx = int(numpy.argmin(trans))
|
|
82
|
+
dp[k, j] = trans[idx] + cost0[k, j]
|
|
83
|
+
back[k, j] = idx
|
|
84
|
+
|
|
85
|
+
last = dp[-1] + lam_space * numpy.abs(roots_all[-1] - mN)
|
|
86
|
+
jn = int(numpy.argmin(last))
|
|
87
|
+
|
|
88
|
+
path = numpy.empty(n, dtype=numpy.complex128)
|
|
89
|
+
for k in range(n - 1, -1, -1):
|
|
90
|
+
path[k] = roots_all[k, jn]
|
|
91
|
+
if k > 0:
|
|
92
|
+
jn = int(back[k, jn])
|
|
93
|
+
return path
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class StieltjesPoly(object):
|
|
97
|
+
"""Callable m(z) for P(z,m)=0 using robust branch selection."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, a_coeffs, *, viterbi_opt=None):
|
|
100
|
+
self.a_coeffs = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
101
|
+
self.viterbi_opt = dict(viterbi_opt or {})
|
|
102
|
+
|
|
103
|
+
def evaluate_scalar(self, z, target=None):
|
|
104
|
+
r = _roots_m(self.a_coeffs, z)
|
|
105
|
+
if r.size == 0:
|
|
106
|
+
return numpy.nan + 1j * numpy.nan
|
|
107
|
+
tol_im = float(self.viterbi_opt.get("tol_im", 1e-14))
|
|
108
|
+
lam_asym = float(self.viterbi_opt.get("lam_asym", 1.0))
|
|
109
|
+
sgn = 1.0 if numpy.imag(z) >= 0 else -1.0
|
|
110
|
+
ok = (sgn * numpy.imag(r) > tol_im)
|
|
111
|
+
cand = r[ok] if numpy.any(ok) else r
|
|
112
|
+
cost = lam_asym * numpy.abs(z * cand + 1.0)
|
|
113
|
+
if target is not None and numpy.isfinite(target):
|
|
114
|
+
lam_space = float(self.viterbi_opt.get("lam_space", 1.0))
|
|
115
|
+
cost = cost + lam_space * numpy.abs(cand - target)
|
|
116
|
+
return cand[int(numpy.argmin(cost))]
|
|
117
|
+
|
|
118
|
+
def __call__(self, z):
|
|
119
|
+
z = numpy.asarray(z, dtype=numpy.complex128)
|
|
120
|
+
scalar = (z.ndim == 0)
|
|
121
|
+
if scalar:
|
|
122
|
+
z = z.reshape((1,))
|
|
123
|
+
|
|
124
|
+
if z.ndim == 1 and z.size >= 2:
|
|
125
|
+
z_list = z.ravel()
|
|
126
|
+
s = self.a_coeffs.shape[1] - 1
|
|
127
|
+
roots_all = numpy.empty((z_list.size, s), dtype=numpy.complex128)
|
|
128
|
+
ok_all = numpy.ones(z_list.size, dtype=bool)
|
|
129
|
+
for k in range(z_list.size):
|
|
130
|
+
r = _roots_m(self.a_coeffs, z_list[k])
|
|
131
|
+
if r.size != s:
|
|
132
|
+
ok_all[k] = False
|
|
133
|
+
if r.size == 0:
|
|
134
|
+
roots_all[k] = numpy.nan + 1j * numpy.nan
|
|
135
|
+
elif r.size < s:
|
|
136
|
+
rr = numpy.empty(s, dtype=numpy.complex128)
|
|
137
|
+
rr[:] = numpy.nan + 1j * numpy.nan
|
|
138
|
+
rr[:r.size] = r
|
|
139
|
+
roots_all[k] = rr
|
|
140
|
+
else:
|
|
141
|
+
roots_all[k] = r[:s]
|
|
142
|
+
else:
|
|
143
|
+
roots_all[k] = r
|
|
144
|
+
|
|
145
|
+
opt = {
|
|
146
|
+
"lam_space": 1.0,
|
|
147
|
+
"lam_asym": 1.0,
|
|
148
|
+
"lam_tiny_im": 200.0,
|
|
149
|
+
"tiny_im": None,
|
|
150
|
+
"tol_im": 1e-14,
|
|
151
|
+
}
|
|
152
|
+
opt.update(self.viterbi_opt)
|
|
153
|
+
|
|
154
|
+
if opt["tiny_im"] is None:
|
|
155
|
+
opt["tiny_im"] = 0.5 * numpy.abs(numpy.imag(z_list[0]))
|
|
156
|
+
|
|
157
|
+
m_path = _viterbi_1d(
|
|
158
|
+
z_list, roots_all,
|
|
159
|
+
lam_space=float(opt["lam_space"]),
|
|
160
|
+
lam_asym=float(opt["lam_asym"]),
|
|
161
|
+
lam_tiny_im=float(opt["lam_tiny_im"]),
|
|
162
|
+
tiny_im=float(opt["tiny_im"]),
|
|
163
|
+
tol_im=float(opt["tol_im"]),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if not numpy.all(ok_all):
|
|
167
|
+
out = m_path.copy()
|
|
168
|
+
prev = None
|
|
169
|
+
for i in range(z_list.size):
|
|
170
|
+
if not ok_all[i] or not numpy.isfinite(out[i]):
|
|
171
|
+
out[i] = self.evaluate_scalar(z_list[i], target=prev)
|
|
172
|
+
prev = out[i]
|
|
173
|
+
m_path = out
|
|
174
|
+
|
|
175
|
+
out = m_path.reshape(z.shape)
|
|
176
|
+
return out.reshape(()) if scalar else out
|
|
177
|
+
|
|
178
|
+
out = numpy.empty(z.size, dtype=numpy.complex128)
|
|
179
|
+
zf = z.ravel()
|
|
180
|
+
prev = None
|
|
181
|
+
for i in range(zf.size):
|
|
182
|
+
out[i] = self.evaluate_scalar(zf[i], target=prev)
|
|
183
|
+
prev = out[i]
|
|
184
|
+
out = out.reshape(z.shape)
|
|
185
|
+
return out.reshape(()) if scalar else out
|