freealg 0.7.6__py3-none-any.whl → 0.7.8__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/_continuation_algebraic.py +14 -6
- freealg/_algebraic_form/_discriminant.py +226 -0
- freealg/_algebraic_form/_homotopy.py +208 -66
- freealg/_algebraic_form/_moments.py +450 -0
- freealg/_algebraic_form/algebraic_form.py +23 -25
- {freealg-0.7.6.dist-info → freealg-0.7.8.dist-info}/METADATA +1 -1
- {freealg-0.7.6.dist-info → freealg-0.7.8.dist-info}/RECORD +12 -10
- {freealg-0.7.6.dist-info → freealg-0.7.8.dist-info}/WHEEL +0 -0
- {freealg-0.7.6.dist-info → freealg-0.7.8.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.6.dist-info → freealg-0.7.8.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.7.6.dist-info → freealg-0.7.8.dist-info}/top_level.txt +0 -0
freealg/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.7.
|
|
1
|
+
__version__ = "0.7.8"
|
|
@@ -227,7 +227,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
227
227
|
dtype=float)
|
|
228
228
|
AN = numpy.vstack([AN, L])
|
|
229
229
|
|
|
230
|
-
_,
|
|
230
|
+
_, svals, vhN = numpy.linalg.svd(AN, full_matrices=False)
|
|
231
231
|
y = vhN[-1, :]
|
|
232
232
|
coef_scaled = N @ y
|
|
233
233
|
|
|
@@ -245,7 +245,8 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
245
245
|
dtype=float)
|
|
246
246
|
As_aug = numpy.vstack([As_aug, L])
|
|
247
247
|
|
|
248
|
-
_,
|
|
248
|
+
_, svals, vh = numpy.linalg.svd(As_aug,
|
|
249
|
+
full_matrices=False)
|
|
249
250
|
coef_scaled = vh[-1, :]
|
|
250
251
|
coef = coef_scaled / s_col
|
|
251
252
|
else:
|
|
@@ -255,7 +256,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
255
256
|
dtype=float)
|
|
256
257
|
As = numpy.vstack([As, L])
|
|
257
258
|
|
|
258
|
-
_,
|
|
259
|
+
_, svals, vh = numpy.linalg.svd(As, full_matrices=False)
|
|
259
260
|
coef_scaled = vh[-1, :]
|
|
260
261
|
coef = coef_scaled / s_col
|
|
261
262
|
|
|
@@ -265,7 +266,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
265
266
|
L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
|
|
266
267
|
As = numpy.vstack([As, L])
|
|
267
268
|
|
|
268
|
-
_,
|
|
269
|
+
_, svals, vh = numpy.linalg.svd(As, full_matrices=False)
|
|
269
270
|
coef_scaled = vh[-1, :]
|
|
270
271
|
coef = coef_scaled / s_col
|
|
271
272
|
|
|
@@ -275,7 +276,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
275
276
|
L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
|
|
276
277
|
As = numpy.vstack([As, L])
|
|
277
278
|
|
|
278
|
-
_,
|
|
279
|
+
_, svals, vh = numpy.linalg.svd(As, full_matrices=False)
|
|
279
280
|
coef_scaled = vh[-1, :]
|
|
280
281
|
coef = coef_scaled / s_col
|
|
281
282
|
|
|
@@ -286,7 +287,14 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
286
287
|
if normalize:
|
|
287
288
|
full = _normalize_coefficients(full)
|
|
288
289
|
|
|
289
|
-
|
|
290
|
+
# Diagnostic metrics
|
|
291
|
+
fit_metrics = {
|
|
292
|
+
's_min': svals[-1],
|
|
293
|
+
'gap_ratio': float(svals[-2] / svals[-1]),
|
|
294
|
+
'n_small': float(int(numpy.sum(svals <= svals[0] * 1e-12))),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return full, fit_metrics
|
|
290
298
|
|
|
291
299
|
|
|
292
300
|
# =============================
|
|
@@ -0,0 +1,226 @@
|
|
|
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 as np
|
|
15
|
+
import numpy.polynomial.polynomial as poly
|
|
16
|
+
|
|
17
|
+
__all__ = ['compute_singular_points']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =========
|
|
21
|
+
# ploy trim
|
|
22
|
+
# =========
|
|
23
|
+
|
|
24
|
+
def _poly_trim(p, tol):
|
|
25
|
+
|
|
26
|
+
p = np.asarray(p, dtype=complex).ravel()
|
|
27
|
+
if p.size == 0:
|
|
28
|
+
return np.zeros(1, dtype=complex)
|
|
29
|
+
k = p.size - 1
|
|
30
|
+
while k > 0 and abs(p[k]) <= tol:
|
|
31
|
+
k -= 1
|
|
32
|
+
return p[:k + 1].copy()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ============
|
|
36
|
+
# poly is zero
|
|
37
|
+
# ============
|
|
38
|
+
|
|
39
|
+
def _poly_is_zero(p, tol):
|
|
40
|
+
|
|
41
|
+
p = _poly_trim(p, tol)
|
|
42
|
+
return (p.size == 1) and (abs(p[0]) <= tol)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ========
|
|
46
|
+
# poly add
|
|
47
|
+
# ========
|
|
48
|
+
|
|
49
|
+
def _poly_add(a, b, tol):
|
|
50
|
+
|
|
51
|
+
return _poly_trim(poly.polyadd(a, b), tol)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ========
|
|
55
|
+
# poly sub
|
|
56
|
+
# ========
|
|
57
|
+
|
|
58
|
+
def _poly_sub(a, b, tol):
|
|
59
|
+
|
|
60
|
+
return _poly_trim(poly.polysub(a, b), tol)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# =======
|
|
64
|
+
# ply mul
|
|
65
|
+
# =======
|
|
66
|
+
|
|
67
|
+
def _poly_mul(a, b, tol):
|
|
68
|
+
|
|
69
|
+
return _poly_trim(poly.polymul(a, b), tol)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ==============
|
|
73
|
+
# poly div exact
|
|
74
|
+
# ==============
|
|
75
|
+
|
|
76
|
+
def _poly_div_exact(a, b, tol):
|
|
77
|
+
|
|
78
|
+
a = _poly_trim(a, tol)
|
|
79
|
+
b = _poly_trim(b, tol)
|
|
80
|
+
if _poly_is_zero(b, tol):
|
|
81
|
+
raise ZeroDivisionError("poly division by zero")
|
|
82
|
+
|
|
83
|
+
q, r = poly.polydiv(a, b)
|
|
84
|
+
r = _poly_trim(r, tol)
|
|
85
|
+
|
|
86
|
+
# Bareiss expects exact division; with floats it's only approximate.
|
|
87
|
+
# If the remainder is small, drop it.
|
|
88
|
+
scale = max(1.0, np.linalg.norm(a))
|
|
89
|
+
if np.linalg.norm(r) > 1e3 * tol * scale:
|
|
90
|
+
# Fallback: still drop remainder (keeps algorithm running).
|
|
91
|
+
# This is acceptable because we only need the resultant roots
|
|
92
|
+
# robustly, not exact symbolic coefficients.
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
return _poly_trim(q, tol)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ================
|
|
99
|
+
# det bareiss poly
|
|
100
|
+
# ================
|
|
101
|
+
|
|
102
|
+
def _det_bareiss_poly(M, tol):
|
|
103
|
+
|
|
104
|
+
n = len(M)
|
|
105
|
+
A = [[_poly_trim(M[i][j], tol) for j in range(n)] for i in range(n)]
|
|
106
|
+
|
|
107
|
+
denom = np.array([1.0], dtype=complex)
|
|
108
|
+
sign = 1.0
|
|
109
|
+
|
|
110
|
+
for k in range(n - 1):
|
|
111
|
+
if _poly_is_zero(A[k][k], tol):
|
|
112
|
+
piv = -1
|
|
113
|
+
for i in range(k + 1, n):
|
|
114
|
+
if not _poly_is_zero(A[i][k], tol):
|
|
115
|
+
piv = i
|
|
116
|
+
break
|
|
117
|
+
if piv == -1:
|
|
118
|
+
return np.array([0.0], dtype=complex)
|
|
119
|
+
A[k], A[piv] = A[piv], A[k]
|
|
120
|
+
sign *= -1.0
|
|
121
|
+
|
|
122
|
+
pivot = A[k][k]
|
|
123
|
+
for i in range(k + 1, n):
|
|
124
|
+
for j in range(k + 1, n):
|
|
125
|
+
num = _poly_sub(_poly_mul(A[i][j], pivot, tol),
|
|
126
|
+
_poly_mul(A[i][k], A[k][j], tol),
|
|
127
|
+
tol)
|
|
128
|
+
if k > 0:
|
|
129
|
+
A[i][j] = _poly_div_exact(num, denom, tol)
|
|
130
|
+
else:
|
|
131
|
+
A[i][j] = _poly_trim(num, tol)
|
|
132
|
+
|
|
133
|
+
denom = pivot
|
|
134
|
+
|
|
135
|
+
return _poly_trim(sign * A[n - 1][n - 1], tol)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ===================
|
|
139
|
+
# cluster real points
|
|
140
|
+
# ===================
|
|
141
|
+
|
|
142
|
+
def _cluster_real_points(x, eps):
|
|
143
|
+
|
|
144
|
+
x = np.asarray(x, dtype=float).ravel()
|
|
145
|
+
if x.size == 0:
|
|
146
|
+
return x
|
|
147
|
+
x = np.sort(x)
|
|
148
|
+
uniq = []
|
|
149
|
+
for v in x:
|
|
150
|
+
if (len(uniq) == 0) or (abs(v - uniq[-1]) > eps):
|
|
151
|
+
uniq.append(float(v))
|
|
152
|
+
else:
|
|
153
|
+
uniq[-1] = 0.5 * (uniq[-1] + float(v))
|
|
154
|
+
return np.asarray(uniq, dtype=float)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# =======================
|
|
158
|
+
# compute singular points
|
|
159
|
+
# =======================
|
|
160
|
+
|
|
161
|
+
def compute_singular_points(a_coeffs, tol=1e-12, real_tol=None):
|
|
162
|
+
"""
|
|
163
|
+
a_coeffs[i,j] is coefficient of z^i m^j, shape (deg_z+1, s+1).
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
|
|
168
|
+
z_bp : complex array, roots of Disc_m(P)(z)
|
|
169
|
+
a_s_zero : complex array, roots of leading coefficient a_s(z)
|
|
170
|
+
support : list of (a,b) from real-ish branch points paired consecutively
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
a_coeffs = np.asarray(a_coeffs)
|
|
174
|
+
s = a_coeffs.shape[1] - 1
|
|
175
|
+
if s < 1:
|
|
176
|
+
return (np.array([], dtype=complex),
|
|
177
|
+
np.array([], dtype=complex),
|
|
178
|
+
[])
|
|
179
|
+
|
|
180
|
+
if real_tol is None:
|
|
181
|
+
real_tol = 1e3 * tol
|
|
182
|
+
|
|
183
|
+
a = [_poly_trim(a_coeffs[:, j], tol) for j in range(s + 1)]
|
|
184
|
+
|
|
185
|
+
a_s = a[s]
|
|
186
|
+
a_s_zero = np.roots(a_s[::-1]) if a_s.size > 1 else \
|
|
187
|
+
np.array([], dtype=complex)
|
|
188
|
+
|
|
189
|
+
b = []
|
|
190
|
+
for j in range(s):
|
|
191
|
+
b.append(_poly_trim((j + 1) * a[j + 1], tol))
|
|
192
|
+
|
|
193
|
+
mdeg = s
|
|
194
|
+
ndeg = s - 1
|
|
195
|
+
N = mdeg + ndeg # 2s-1
|
|
196
|
+
|
|
197
|
+
z0 = np.array([0.0], dtype=complex)
|
|
198
|
+
M = [[z0 for _ in range(N)] for __ in range(N)]
|
|
199
|
+
|
|
200
|
+
for r in range(ndeg):
|
|
201
|
+
for j in range(mdeg + 1):
|
|
202
|
+
M[r][r + j] = a[j]
|
|
203
|
+
|
|
204
|
+
for r in range(mdeg):
|
|
205
|
+
rr = ndeg + r
|
|
206
|
+
for j in range(ndeg + 1):
|
|
207
|
+
M[rr][r + j] = b[j]
|
|
208
|
+
|
|
209
|
+
res = _det_bareiss_poly(M, tol)
|
|
210
|
+
if res.size <= 1:
|
|
211
|
+
z_bp = np.array([], dtype=complex)
|
|
212
|
+
else:
|
|
213
|
+
z_bp = np.roots(res[::-1])
|
|
214
|
+
|
|
215
|
+
support = []
|
|
216
|
+
if z_bp.size > 0:
|
|
217
|
+
zr = z_bp[np.abs(z_bp.imag) <= real_tol].real
|
|
218
|
+
zr = _cluster_real_points(zr, eps=1e2 * real_tol)
|
|
219
|
+
m2 = (zr.size // 2) * 2
|
|
220
|
+
for k in range(0, m2, 2):
|
|
221
|
+
a0 = float(zr[k])
|
|
222
|
+
b0 = float(zr[k + 1])
|
|
223
|
+
if b0 > a0:
|
|
224
|
+
support.append((a0, b0))
|
|
225
|
+
|
|
226
|
+
return z_bp, a_s_zero, support
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
# =======
|
|
4
4
|
|
|
5
5
|
import numpy
|
|
6
|
+
from ._moments import AlgebraicStieltjesMoments
|
|
6
7
|
|
|
7
8
|
__all__ = ['stieltjes_poly']
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
# =====================
|
|
11
|
-
#
|
|
12
|
+
# select root
|
|
12
13
|
# =====================
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
+
def select_root(roots, z, target):
|
|
15
16
|
"""
|
|
16
|
-
Select the
|
|
17
|
+
Select the root among Herglotz candidates at a given z closest to a
|
|
18
|
+
given target
|
|
17
19
|
|
|
18
20
|
Parameters
|
|
19
21
|
----------
|
|
@@ -22,9 +24,9 @@ def stieltjes_select_root(roots, z, w_prev=None):
|
|
|
22
24
|
z : complex
|
|
23
25
|
Evaluation point. The Stieltjes/Herglotz branch satisfies
|
|
24
26
|
sign(Im(m)) = sign(Im(z)) away from the real axis.
|
|
25
|
-
|
|
26
|
-
Previous continuation value used to enforce continuity
|
|
27
|
-
|
|
27
|
+
target : complex
|
|
28
|
+
Previous continuation value used to enforce continuity, or
|
|
29
|
+
target value.
|
|
28
30
|
|
|
29
31
|
Returns
|
|
30
32
|
-------
|
|
@@ -40,11 +42,6 @@ def stieltjes_select_root(roots, z, w_prev=None):
|
|
|
40
42
|
|
|
41
43
|
desired_sign = numpy.sign(z.imag)
|
|
42
44
|
|
|
43
|
-
if w_prev is None:
|
|
44
|
-
target = -1.0 / z
|
|
45
|
-
else:
|
|
46
|
-
target = complex(w_prev)
|
|
47
|
-
|
|
48
45
|
# Apply a soft Herglotz sign filter: prefer roots with Im(w) having the
|
|
49
46
|
# same sign as Im(z), allowing tiny numerical violations near the axis.
|
|
50
47
|
imag_roots = numpy.imag(roots)
|
|
@@ -62,77 +59,222 @@ def stieltjes_select_root(roots, z, w_prev=None):
|
|
|
62
59
|
# stieltjes poly
|
|
63
60
|
# ==============
|
|
64
61
|
|
|
65
|
-
|
|
62
|
+
class StieltjesPoly(object):
|
|
66
63
|
"""
|
|
67
|
-
|
|
64
|
+
Stieltjes-branch evaluator for an algebraic equation P(z, m) = 0.
|
|
65
|
+
|
|
66
|
+
This class represents the Stieltjes-branch solution m(z) of an algebraic
|
|
67
|
+
equation defined by a polynomial relation
|
|
68
68
|
|
|
69
|
-
The coefficients `a` define a polynomial relation
|
|
70
69
|
P(z, m) = 0,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
|
|
71
|
+
where P is a polynomial in z and m with monomial-basis coefficients.
|
|
72
|
+
The coefficient matrix ``a`` is fixed at construction time, and all
|
|
73
|
+
quantities depending only on ``a`` are precomputed. Evaluation at a
|
|
74
|
+
complex point ``z`` is performed via :meth:`evaluate`. The instance is
|
|
75
|
+
also callable; :meth:`__call__` supports scalar or vector inputs and
|
|
76
|
+
applies :meth:`evaluate` elementwise.
|
|
77
|
+
|
|
78
|
+
The Stieltjes branch is selected by initializing in the appropriate
|
|
79
|
+
half-plane using an asymptotic Stieltjes estimate and then performing
|
|
80
|
+
homotopy continuation along a straight-line path in the complex plane.
|
|
74
81
|
|
|
75
82
|
Parameters
|
|
76
83
|
----------
|
|
77
|
-
z : complex
|
|
78
|
-
Evaluation point. Must be a single value.
|
|
79
84
|
a : ndarray, shape (L, K)
|
|
80
|
-
Coefficient matrix defining P(z, m) in the monomial basis.
|
|
85
|
+
Coefficient matrix defining P(z, m) in the monomial basis. For fixed
|
|
86
|
+
z, the coefficients of the polynomial in m are assembled from powers
|
|
87
|
+
of z.
|
|
81
88
|
eps : float or None, optional
|
|
82
89
|
If Im(z) == 0, use z + i*eps as the boundary evaluation point.
|
|
83
90
|
If None and Im(z) == 0, eps is set to 1e-8 * max(1, |z|).
|
|
84
|
-
height : float,
|
|
91
|
+
height : float, default = 2.0
|
|
85
92
|
Imaginary height used for the starting point z0 in the same
|
|
86
93
|
half-plane as the evaluation point.
|
|
87
|
-
steps : int,
|
|
94
|
+
steps : int, default = 100
|
|
88
95
|
Number of continuation steps along the homotopy path.
|
|
96
|
+
order : int, default = 15
|
|
97
|
+
Number of moments in Stieltjes estimate
|
|
89
98
|
|
|
90
|
-
|
|
99
|
+
Methods
|
|
91
100
|
-------
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
evaluate(z)
|
|
102
|
+
Evaluate the Stieltjes-branch solution m(z) at a single complex point.
|
|
103
|
+
|
|
104
|
+
__call__(z)
|
|
105
|
+
If ``z`` is scalar, returns ``evaluate(z, ...)``.
|
|
106
|
+
If ``z`` is array-like, returns an array of the same shape, where each
|
|
107
|
+
entry is computed by calling ``evaluate`` on the corresponding element.
|
|
108
|
+
|
|
109
|
+
Notes
|
|
110
|
+
-----
|
|
111
|
+
If an input ``z`` value is real (Im(z) == 0), the evaluation is interpreted
|
|
112
|
+
as a boundary value by replacing that element with z + i*eps. If ``eps`` is
|
|
113
|
+
None, eps is chosen per element as 1e-8 * max(1, |z|).
|
|
95
114
|
"""
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
def __init__(self, a, eps=None, height=2.0, steps=100, order=15):
|
|
117
|
+
a = numpy.asarray(a)
|
|
118
|
+
if a.ndim != 2:
|
|
119
|
+
raise ValueError("a must be a 2D array.")
|
|
120
|
+
|
|
121
|
+
self.a = a
|
|
122
|
+
self.a_l, _ = a.shape
|
|
123
|
+
self.eps = eps
|
|
124
|
+
self.height = height
|
|
125
|
+
self.steps = steps
|
|
126
|
+
self.order = order
|
|
127
|
+
|
|
128
|
+
# Objects depending only on a
|
|
129
|
+
self.mom = AlgebraicStieltjesMoments(a)
|
|
130
|
+
self._zpows_exp = numpy.arange(self.a_l)
|
|
131
|
+
self.rad = 1.0 + self.height * self.mom.radius(self.order)
|
|
132
|
+
|
|
133
|
+
def _poly_coeffs_m(self, z_val):
|
|
134
|
+
z_powers = z_val ** self._zpows_exp
|
|
135
|
+
return (z_powers @ self.a)[::-1]
|
|
136
|
+
|
|
137
|
+
def _poly_roots(self, z_val):
|
|
138
|
+
coeffs = numpy.asarray(self._poly_coeffs_m(z_val),
|
|
139
|
+
dtype=numpy.complex128)
|
|
114
140
|
return numpy.roots(coeffs)
|
|
115
141
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
def evaluate(self, z, eps=None, height=2.0, steps=100, order=15):
|
|
143
|
+
"""
|
|
144
|
+
Evaluate the Stieltjes-branch solution m(z) at a single point.
|
|
145
|
+
|
|
146
|
+
Parameters are as in the original function, except ``a`` is fixed at
|
|
147
|
+
construction time.
|
|
148
|
+
"""
|
|
149
|
+
z = complex(z)
|
|
150
|
+
|
|
151
|
+
if steps < 1:
|
|
152
|
+
raise ValueError("steps must be a positive integer.")
|
|
153
|
+
|
|
154
|
+
# Boundary-value interpretation on the real axis
|
|
155
|
+
if z.imag == 0.0:
|
|
156
|
+
if self.eps is None:
|
|
157
|
+
eps_loc = 1e-8 * max(1.0, abs(z))
|
|
158
|
+
else:
|
|
159
|
+
eps_loc = float(self.eps)
|
|
160
|
+
z_eval = z + 1j * eps_loc
|
|
161
|
+
else:
|
|
162
|
+
z_eval = z
|
|
163
|
+
|
|
164
|
+
half_sign = numpy.sign(z_eval.imag)
|
|
165
|
+
if half_sign == 0.0:
|
|
166
|
+
half_sign = 1.0
|
|
167
|
+
|
|
168
|
+
# If z is outside radius of convergence, no homotopy
|
|
169
|
+
# necessary
|
|
170
|
+
if numpy.abs(z) > self.rad:
|
|
171
|
+
target = self.mom.stieltjes(z, self.order)
|
|
172
|
+
return select_root(self._poly_roots(z), z, target)
|
|
173
|
+
|
|
174
|
+
z0 = 1j * float(half_sign) * self.rad
|
|
175
|
+
target = self.mom.stieltjes(z0, self.order)
|
|
176
|
+
|
|
177
|
+
# Initialize at z0
|
|
178
|
+
w_prev = select_root(self._poly_roots(z0), z0, target)
|
|
179
|
+
|
|
180
|
+
# Straight-line homotopy continuation
|
|
181
|
+
for tau in numpy.linspace(0.0, 1.0, int(self.steps) + 1)[1:]:
|
|
182
|
+
z_tau = z0 + tau * (z_eval - z0)
|
|
183
|
+
w_prev = select_root(self._poly_roots(z_tau), z_tau, w_prev)
|
|
184
|
+
|
|
185
|
+
return w_prev
|
|
186
|
+
|
|
187
|
+
def __call__(self, z):
|
|
188
|
+
# Scalar fast-path
|
|
189
|
+
if numpy.isscalar(z):
|
|
190
|
+
return self.evaluate(z)
|
|
191
|
+
|
|
192
|
+
# Array-like: evaluate elementwise, preserving shape
|
|
193
|
+
z_arr = numpy.asarray(z)
|
|
194
|
+
out = numpy.empty(z_arr.shape, dtype=numpy.complex128)
|
|
195
|
+
|
|
196
|
+
# Iterate over indices so we can pass Python scalars into evaluate()
|
|
197
|
+
for idx in numpy.ndindex(z_arr.shape):
|
|
198
|
+
out[idx] = self.evaluate(z_arr[idx])
|
|
199
|
+
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# def stieltjes_poly(z, a, eps=None, height=2., steps=100, order=15):
|
|
204
|
+
# """
|
|
205
|
+
# Evaluate the Stieltjes-branch solution m(z) of an algebraic equation.
|
|
206
|
+
|
|
207
|
+
# The coefficients `a` define a polynomial relation
|
|
208
|
+
# P(z, m) = 0,
|
|
209
|
+
# where P is a polynomial in z and m with monomial-basis coefficients
|
|
210
|
+
# arranged so that for fixed z, the coefficients of the polynomial in m
|
|
211
|
+
# can be assembled from powers of z.
|
|
212
|
+
|
|
213
|
+
# Parameters
|
|
214
|
+
# ----------
|
|
215
|
+
# z : complex
|
|
216
|
+
# Evaluation point. Must be a single value.
|
|
217
|
+
# a : ndarray, shape (L, K)
|
|
218
|
+
# Coefficient matrix defining P(z, m) in the monomial basis.
|
|
219
|
+
# eps : float or None, optional
|
|
220
|
+
# If Im(z) == 0, use z + i*eps as the boundary evaluation point.
|
|
221
|
+
# If None and Im(z) == 0, eps is set to 1e-8 * max(1, |z|).
|
|
222
|
+
# height : float, default = 2.0
|
|
223
|
+
# Imaginary height used for the starting point z0 in the same
|
|
224
|
+
# half-plane as the evaluation point.
|
|
225
|
+
# steps : int, default = 100
|
|
226
|
+
# Number of continuation steps along the homotopy path.
|
|
227
|
+
# order : int, default = 15
|
|
228
|
+
# Number of moments in Stieltjes estimate
|
|
229
|
+
|
|
230
|
+
# Returns
|
|
231
|
+
# -------
|
|
232
|
+
# w : complex
|
|
233
|
+
# Value of the Stieltjes-branch solution m(z) (or m(z+i*eps) if z is
|
|
234
|
+
# real).
|
|
235
|
+
# """
|
|
236
|
+
|
|
237
|
+
# z = complex(z)
|
|
238
|
+
# a = numpy.asarray(a)
|
|
239
|
+
|
|
240
|
+
# if a.ndim != 2:
|
|
241
|
+
# raise ValueError('a must be a 2D array.')
|
|
242
|
+
|
|
243
|
+
# if steps < 1:
|
|
244
|
+
# raise ValueError("steps must be a positive integer.")
|
|
245
|
+
|
|
246
|
+
# a_l, _ = a.shape
|
|
247
|
+
# mom = AlgebraicStieltjesMoments(a)
|
|
248
|
+
|
|
249
|
+
# def poly_coeffs_m(z_val):
|
|
250
|
+
# z_powers = z_val ** numpy.arange(a_l)
|
|
251
|
+
# return (z_powers @ a)[::-1]
|
|
252
|
+
|
|
253
|
+
# def poly_roots(z_val):
|
|
254
|
+
# coeffs = numpy.asarray(poly_coeffs_m(z_val), dtype=numpy.complex128)
|
|
255
|
+
# return numpy.roots(coeffs)
|
|
256
|
+
|
|
257
|
+
# # If user asked for a real-axis value, interpret as boundary value from C+.
|
|
258
|
+
# if z.imag == 0.0:
|
|
259
|
+
# if eps is None:
|
|
260
|
+
# eps = 1e-8 * max(1.0, abs(z))
|
|
261
|
+
# z_eval = z + 1j * float(eps)
|
|
262
|
+
# else:
|
|
263
|
+
# z_eval = z
|
|
264
|
+
|
|
265
|
+
# half_sign = numpy.sign(z_eval.imag)
|
|
266
|
+
# if half_sign == 0.0:
|
|
267
|
+
# half_sign = 1.0
|
|
268
|
+
|
|
269
|
+
# z0 = 1j * float(half_sign) * (1. + height * mom.radius(order))
|
|
270
|
+
# target = mom.stieltjes(z0, order)
|
|
271
|
+
|
|
272
|
+
# # Initialize at z0 via asymptotic / Im-sign selection.
|
|
273
|
+
# w_prev = select_root(poly_roots(z0), z0, target)
|
|
274
|
+
|
|
275
|
+
# # Straight-line homotopy from z0 to z_eval.
|
|
276
|
+
# for tau in numpy.linspace(0.0, 1.0, int(steps) + 1)[1:]:
|
|
277
|
+
# z_tau = z0 + tau * (z_eval - z0)
|
|
278
|
+
# w_prev = select_root(poly_roots(z_tau), z_tau, w_prev)
|
|
279
|
+
|
|
280
|
+
# return w_prev
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# =========
|
|
5
|
+
# Moments
|
|
6
|
+
# =========
|
|
7
|
+
|
|
8
|
+
class MomentsESD(object):
|
|
9
|
+
"""
|
|
10
|
+
Moments :math:`\\mu_n(t)` generated from eigenvalues, under
|
|
11
|
+
free decompression, where
|
|
12
|
+
|
|
13
|
+
.. math::
|
|
14
|
+
|
|
15
|
+
m_n = \\mu_n(0) = \\mathbb{E}[\\lambda^n],
|
|
16
|
+
|
|
17
|
+
and :math:`\\lambda` denotes an eigenvalue sample.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
|
|
22
|
+
eig : array_like
|
|
23
|
+
1D array of eigenvalues (or samples). Internally it is converted to a
|
|
24
|
+
floating-point :class:`numpy.ndarray`.
|
|
25
|
+
|
|
26
|
+
Attributes
|
|
27
|
+
----------
|
|
28
|
+
|
|
29
|
+
eig : numpy.ndarray
|
|
30
|
+
Eigenvalue samples.
|
|
31
|
+
|
|
32
|
+
Methods
|
|
33
|
+
-------
|
|
34
|
+
|
|
35
|
+
m
|
|
36
|
+
Compute the raw moment :math:`m_n = \\mathbb{E}[\\lambda^n]`.
|
|
37
|
+
|
|
38
|
+
coeffs
|
|
39
|
+
Compute the coefficient vector :math:`a_n`.
|
|
40
|
+
|
|
41
|
+
__call__
|
|
42
|
+
Evaluate :math:`\\mu_n(t)` for a given :math:`n` and :math:`t`.
|
|
43
|
+
|
|
44
|
+
Notes
|
|
45
|
+
-----
|
|
46
|
+
|
|
47
|
+
The recursion memoizes:
|
|
48
|
+
|
|
49
|
+
* Moments ``_m[n] = m_n``.
|
|
50
|
+
* Coefficients ``_a[n] = a_n`` where ``a_n`` has length ``n`` and contains
|
|
51
|
+
:math:`(a_{n,0}, \\dots, a_{n,n-1})`.
|
|
52
|
+
|
|
53
|
+
The coefficient row :math:`a_n` is computed using an intermediate quantity
|
|
54
|
+
:math:`R_{n,k}` formed via discrete convolutions of previous rows.
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
|
|
59
|
+
.. code-block:: python
|
|
60
|
+
|
|
61
|
+
>>> import numpy as np
|
|
62
|
+
>>> eig = np.array([1.0, 2.0, 3.0])
|
|
63
|
+
>>> mu = Moments(eig)
|
|
64
|
+
>>> mu(3, t=0.0) # equals m_3
|
|
65
|
+
12.0
|
|
66
|
+
>>> mu(3, t=0.1)
|
|
67
|
+
14.203...
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# ====
|
|
71
|
+
# init
|
|
72
|
+
# ====
|
|
73
|
+
|
|
74
|
+
def __init__(self, eig):
|
|
75
|
+
"""
|
|
76
|
+
Initialization.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
self.eig = numpy.asarray(eig, dtype=float)
|
|
80
|
+
|
|
81
|
+
# Memoized moments m_n
|
|
82
|
+
self._m = {0: 1.0}
|
|
83
|
+
|
|
84
|
+
# Memoized coefficients a[n] = array of length n
|
|
85
|
+
# (a_{n,0},...,a_{n,n-1})
|
|
86
|
+
self._a = {0: numpy.array([1.0])}
|
|
87
|
+
|
|
88
|
+
# ----------
|
|
89
|
+
# moments
|
|
90
|
+
# ----------
|
|
91
|
+
|
|
92
|
+
def m(self, n):
|
|
93
|
+
"""
|
|
94
|
+
Compute raw moment :math:`m_n`.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
|
|
99
|
+
n : int
|
|
100
|
+
Order of the moment.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
|
|
105
|
+
m_n : float
|
|
106
|
+
The raw moment :math:`m_n = \\mathbb{E}[\\lambda^n]`, estimated by
|
|
107
|
+
the sample mean of ``eig**n``.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
if n not in self._m:
|
|
111
|
+
self._m[n] = numpy.mean(self.eig ** n)
|
|
112
|
+
return self._m[n]
|
|
113
|
+
|
|
114
|
+
# -------------
|
|
115
|
+
# coefficients
|
|
116
|
+
# -------------
|
|
117
|
+
|
|
118
|
+
def coeffs(self, n):
|
|
119
|
+
"""
|
|
120
|
+
Get coefficients :math:`a_n` for :math:`\\mu_n(t)`.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
|
|
125
|
+
n : int
|
|
126
|
+
Order of :math:`\\mu_n(t)`.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
|
|
131
|
+
a_n : numpy.ndarray
|
|
132
|
+
Array of shape ``(n,)`` containing :math:`(a_{n,0}, \\dots, a_{n,n-1})`.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if n in self._a:
|
|
136
|
+
return self._a[n]
|
|
137
|
+
|
|
138
|
+
# Ensure previous rows exist
|
|
139
|
+
for r in range(1, n):
|
|
140
|
+
if r not in self._a:
|
|
141
|
+
self._compute_row(r)
|
|
142
|
+
|
|
143
|
+
self._compute_row(n)
|
|
144
|
+
return self._a[n]
|
|
145
|
+
|
|
146
|
+
def _compute_row(self, n):
|
|
147
|
+
"""
|
|
148
|
+
Compute and memoize the coefficient row :math:`a_n`.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
|
|
153
|
+
n : int
|
|
154
|
+
Row index to compute.
|
|
155
|
+
|
|
156
|
+
Notes
|
|
157
|
+
-----
|
|
158
|
+
|
|
159
|
+
For :math:`n=1`, the row is
|
|
160
|
+
|
|
161
|
+
.. math::
|
|
162
|
+
|
|
163
|
+
a_{1,0} = m_1.
|
|
164
|
+
|
|
165
|
+
For :math:`n \\ge 2`, let :math:`R_n` be a length ``n-1`` array defined
|
|
166
|
+
by convolution of previous rows:
|
|
167
|
+
|
|
168
|
+
.. math::
|
|
169
|
+
|
|
170
|
+
R_n = \\sum_{i=1}^{n-1} (a_i * a_{n-i})\\big|_{0:(n-2)}.
|
|
171
|
+
|
|
172
|
+
Then for :math:`k = 0, \\dots, n-2`,
|
|
173
|
+
|
|
174
|
+
.. math::
|
|
175
|
+
|
|
176
|
+
a_{n,k} = \\frac{1 + k/2}{(n-1-k)} R_{n,k},
|
|
177
|
+
|
|
178
|
+
and the last coefficient is chosen so that :math:`\\mu_n(0)=m_n`:
|
|
179
|
+
|
|
180
|
+
.. math::
|
|
181
|
+
|
|
182
|
+
a_{n,n-1} = m_n - \\sum_{k=0}^{n-2} a_{n,k}.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if n in self._a:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if n == 1:
|
|
189
|
+
self._a[1] = numpy.array([self.m(1)])
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# Ensure all smaller rows exist
|
|
193
|
+
for r in range(1, n):
|
|
194
|
+
if r not in self._a:
|
|
195
|
+
self._compute_row(r)
|
|
196
|
+
|
|
197
|
+
a_n = numpy.zeros(n, dtype=float)
|
|
198
|
+
|
|
199
|
+
# Compute R_{n,k} via convolutions:
|
|
200
|
+
# R_n = sum_{i=1}^{n-1} convolve(a[i], a[n-i]) truncated to length n-1
|
|
201
|
+
R = numpy.zeros(n - 1, dtype=float)
|
|
202
|
+
for i in range(1, n):
|
|
203
|
+
conv = numpy.convolve(self._a[i], self._a[n - i])
|
|
204
|
+
R += conv[: n - 1]
|
|
205
|
+
|
|
206
|
+
k = numpy.arange(n - 1, dtype=float)
|
|
207
|
+
factors = (1.0 + 0.5 * k) / (n - 1 - k)
|
|
208
|
+
a_n[: n - 1] = factors * R
|
|
209
|
+
|
|
210
|
+
# k = n-1 from the initial condition mu_n(0) = m_n
|
|
211
|
+
a_n[n - 1] = self.m(n) - a_n[: n - 1].sum()
|
|
212
|
+
|
|
213
|
+
self._a[n] = a_n
|
|
214
|
+
|
|
215
|
+
# ----------
|
|
216
|
+
# evaluate
|
|
217
|
+
# ----------
|
|
218
|
+
|
|
219
|
+
def __call__(self, n, t=0.0):
|
|
220
|
+
"""
|
|
221
|
+
Evaluate :math:`\\mu_n(t)`.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
|
|
226
|
+
n : int
|
|
227
|
+
Order of :math:`\\mu_n(t)`.
|
|
228
|
+
|
|
229
|
+
t : float, default=0.0
|
|
230
|
+
Deformation parameter.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
|
|
235
|
+
mu_n : float
|
|
236
|
+
The value of :math:`\\mu_n(t)`.
|
|
237
|
+
|
|
238
|
+
Notes
|
|
239
|
+
-----
|
|
240
|
+
|
|
241
|
+
This function evaluates
|
|
242
|
+
|
|
243
|
+
.. math::
|
|
244
|
+
|
|
245
|
+
\\mu_n(t) = \\sum_{k=0}^{n-1} a_{n,k} \\, e^{k t}.
|
|
246
|
+
|
|
247
|
+
For ``n == 0``, it returns ``1.0``.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
if n == 0:
|
|
251
|
+
return 1.0
|
|
252
|
+
|
|
253
|
+
a_n = self.coeffs(n)
|
|
254
|
+
k = numpy.arange(n, dtype=float)
|
|
255
|
+
return numpy.dot(a_n, numpy.exp(k * t))
|
|
256
|
+
|
|
257
|
+
# ===========================
|
|
258
|
+
# Algebraic Stieltjes Moments
|
|
259
|
+
# ===========================
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class AlgebraicStieltjesMoments(object):
|
|
263
|
+
"""
|
|
264
|
+
Given coefficients a[i,j] for P(z,m)=sum_{i,j} a[i,j] z^i m^j,
|
|
265
|
+
compute the large-|z| branch
|
|
266
|
+
m(z) = sum_{k>=0} mu_series[k] / z^{k+1}.
|
|
267
|
+
|
|
268
|
+
Convention here: choose mu0 (the leading coefficient) by solving the
|
|
269
|
+
leading-diagonal equation and (by default) picking the root closest
|
|
270
|
+
to -1, i.e. m(z) ~ -1/z.
|
|
271
|
+
|
|
272
|
+
The returned 'moments(N)' are normalized density moments:
|
|
273
|
+
mu_density[k] = mu_series[k] / mu_series[0]
|
|
274
|
+
so mu_density[0] = 1.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(self, a, mu0=None):
|
|
278
|
+
self.a = numpy.asarray(a)
|
|
279
|
+
# Ensure valid
|
|
280
|
+
self.a[-1, 0] = 0.0
|
|
281
|
+
if self.a.ndim != 2:
|
|
282
|
+
raise ValueError("a must be a 2D NumPy array with a[i,j]=a_{ij}.")
|
|
283
|
+
|
|
284
|
+
self.I = self.a.shape[0] - 1
|
|
285
|
+
self.J = self.a.shape[1] - 1
|
|
286
|
+
|
|
287
|
+
nz = numpy.argwhere(self.a != 0)
|
|
288
|
+
if nz.size == 0:
|
|
289
|
+
raise ValueError("All coefficients are zero.")
|
|
290
|
+
|
|
291
|
+
# r = max(i-j) over nonzero terms
|
|
292
|
+
self.r = int(numpy.max(nz[:, 0] - nz[:, 1]))
|
|
293
|
+
|
|
294
|
+
# Group coefficients by diagonal offset s = r - (i-j) >= 0
|
|
295
|
+
# diag[s] is list of (j, a_ij) for which i-j = r-s
|
|
296
|
+
self.diag = {}
|
|
297
|
+
for i, j in nz:
|
|
298
|
+
i = int(i)
|
|
299
|
+
j = int(j)
|
|
300
|
+
coeff = self.a[i, j]
|
|
301
|
+
s = self.r - (i - j)
|
|
302
|
+
if s >= 0:
|
|
303
|
+
self.diag.setdefault(int(s), []).append((j, coeff))
|
|
304
|
+
|
|
305
|
+
# Choose mu0 (series leading coefficient). This should be
|
|
306
|
+
# -1 for m(z) ~ -1/z, but it may only hold approximately.
|
|
307
|
+
if mu0 is None:
|
|
308
|
+
self.mu0 = self._solve_mu0()
|
|
309
|
+
else:
|
|
310
|
+
self.mu0 = mu0
|
|
311
|
+
|
|
312
|
+
# Precompute mu0^p up to p=J
|
|
313
|
+
self.mu0pow = [1]
|
|
314
|
+
for _ in range(self.J):
|
|
315
|
+
self.mu0pow.append(self.mu0pow[-1] * self.mu0)
|
|
316
|
+
|
|
317
|
+
# Linear coefficient A0 = sum_{i-j=r} j a_ij mu0^{j-1}
|
|
318
|
+
self.A0 = 0
|
|
319
|
+
for j, coeff in self.diag.get(0, []):
|
|
320
|
+
if j > 0:
|
|
321
|
+
self.A0 += j * coeff * self.mu0pow[j - 1]
|
|
322
|
+
if self.A0 == 0:
|
|
323
|
+
raise ValueError("A0 is zero for this mu0; the sequential recursion is degenerate.")
|
|
324
|
+
|
|
325
|
+
# Stored series moments mu_series[0..]
|
|
326
|
+
self._mu = [self.mu0]
|
|
327
|
+
|
|
328
|
+
# Convolution table c[j][n] = coefficient of w^n in (S(w))^j,
|
|
329
|
+
# where S(w) = sum_{t>=0} mu_series[t] w^t and m(z)=w S(w), w=1/z.
|
|
330
|
+
#
|
|
331
|
+
# We store c as lists growing in n: c[j][n] for j=0..J.
|
|
332
|
+
self._c = [[0] for _ in range(self.J + 1)]
|
|
333
|
+
self._c[0][0] = 1
|
|
334
|
+
for j in range(1, self.J + 1):
|
|
335
|
+
self._c[j][0] = self.mu0pow[j]
|
|
336
|
+
|
|
337
|
+
def _solve_mu0(self):
|
|
338
|
+
# Leading diagonal polynomial L(m) = sum_{i-j=r} a_ij m^j.
|
|
339
|
+
# That means i = j + r, so coefficient is a[j+r, j] if in bounds.
|
|
340
|
+
coeffs = numpy.zeros(self.J + 1, dtype=numpy.complex128)
|
|
341
|
+
for j in range(self.J + 1):
|
|
342
|
+
i = j + self.r
|
|
343
|
+
if 0 <= i <= self.I:
|
|
344
|
+
coeffs[j] = self.a[i, j]
|
|
345
|
+
|
|
346
|
+
if not numpy.any(coeffs != 0):
|
|
347
|
+
raise ValueError("Leading diagonal polynomial is identically zero; cannot determine mu0.")
|
|
348
|
+
|
|
349
|
+
deg = int(numpy.max(numpy.nonzero(coeffs)[0]))
|
|
350
|
+
roots = numpy.roots(coeffs[:deg + 1][::-1]) # descending powers for numpy.roots
|
|
351
|
+
|
|
352
|
+
# Targetting mu0 = -1 for ~ -1/z asymptotics
|
|
353
|
+
mu0 = roots[numpy.argmin(numpy.abs(roots + 1))]
|
|
354
|
+
|
|
355
|
+
if abs(mu0.imag) < 1e-12:
|
|
356
|
+
mu0 = mu0.real
|
|
357
|
+
return mu0
|
|
358
|
+
|
|
359
|
+
def _ensure(self, N):
|
|
360
|
+
# Compute mu_series up to index N (inclusive)
|
|
361
|
+
while len(self._mu) <= N:
|
|
362
|
+
k = len(self._mu) # compute mu_k
|
|
363
|
+
|
|
364
|
+
# Compute f[j] = coefficient of w^k in (S_trunc(w))^j,
|
|
365
|
+
# 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 j*mu_k*mu0^{j-1}.
|
|
367
|
+
f = [0] * (self.J + 1)
|
|
368
|
+
f[0] = 0
|
|
369
|
+
for j in range(1, self.J + 1):
|
|
370
|
+
ssum = 0
|
|
371
|
+
# sum_{t=1..k-1} mu_t * c[j-1, k-t]
|
|
372
|
+
for t in range(1, k):
|
|
373
|
+
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} mu_t*c[j-1,k-t] + mu_k*c[j-1,0]
|
|
375
|
+
# with mu_k=0 for f, and c[j-1,k]=f[j-1]
|
|
376
|
+
f[j] = self.mu0 * f[j - 1] + ssum
|
|
377
|
+
|
|
378
|
+
# Build the linear equation for mu_k:
|
|
379
|
+
# A0*mu_k + rest = 0
|
|
380
|
+
rest = 0
|
|
381
|
+
|
|
382
|
+
# s=0 diagonal contributes coeff*(f[j]) (the mu_k-free part)
|
|
383
|
+
for j, coeff in self.diag.get(0, []):
|
|
384
|
+
if j == 0:
|
|
385
|
+
# only affects k=0, but we never come here with k=0
|
|
386
|
+
continue
|
|
387
|
+
rest += coeff * f[j]
|
|
388
|
+
|
|
389
|
+
# lower diagonals s=1..k contribute coeff*c[j,k-s] (already known since k-s < k)
|
|
390
|
+
for s in range(1, k + 1):
|
|
391
|
+
entries = self.diag.get(s)
|
|
392
|
+
if not entries:
|
|
393
|
+
continue
|
|
394
|
+
n = k - s
|
|
395
|
+
for j, coeff in entries:
|
|
396
|
+
if j == 0:
|
|
397
|
+
if n == 0:
|
|
398
|
+
rest += coeff
|
|
399
|
+
else:
|
|
400
|
+
rest += coeff * self._c[j][n]
|
|
401
|
+
|
|
402
|
+
mu_k = -rest / self.A0
|
|
403
|
+
self._mu.append(mu_k)
|
|
404
|
+
|
|
405
|
+
# Now append the new column k to c using the full convolution recurrence:
|
|
406
|
+
# c[j,k] = sum_{t=0..k} mu_t * c[j-1,k-t]
|
|
407
|
+
for j in range(self.J + 1):
|
|
408
|
+
self._c[j].append(0)
|
|
409
|
+
|
|
410
|
+
self._c[0][k] = 0
|
|
411
|
+
for j in range(1, self.J + 1):
|
|
412
|
+
val = 0
|
|
413
|
+
for t in range(0, k + 1):
|
|
414
|
+
val += self._mu[t] * self._c[j - 1][k - t]
|
|
415
|
+
self._c[j][k] = val
|
|
416
|
+
|
|
417
|
+
# --- API ---
|
|
418
|
+
|
|
419
|
+
def __call__(self, k):
|
|
420
|
+
self._ensure(k)
|
|
421
|
+
return self._mu[k] / self._mu[0]
|
|
422
|
+
|
|
423
|
+
def moments(self, N):
|
|
424
|
+
# normalized density moments so moment 0 is 1
|
|
425
|
+
self._ensure(N)
|
|
426
|
+
mu0 = self._mu[0]
|
|
427
|
+
return numpy.array([self._mu[k] / mu0 for k in range(N + 1)])
|
|
428
|
+
|
|
429
|
+
def radius(self, N):
|
|
430
|
+
# Estimate the radius of convergence of the Stieltjes
|
|
431
|
+
# series
|
|
432
|
+
if N < 3:
|
|
433
|
+
raise RuntimeError("Order is too small, choose a larger value of N")
|
|
434
|
+
self._ensure(N)
|
|
435
|
+
return max([numpy.abs(self._mu[j] / self._mu[j-1]) for j in range(2,N+1)])
|
|
436
|
+
|
|
437
|
+
def stieltjes(self, z, N):
|
|
438
|
+
# Estimate Stieltjes transform (root) using moment
|
|
439
|
+
# expansion
|
|
440
|
+
z = numpy.asarray(z)
|
|
441
|
+
mu = self.moments(N)
|
|
442
|
+
return -numpy.sum(z[..., numpy.newaxis]**(-numpy.arange(N+1)-1) * mu,
|
|
443
|
+
axis=-1)
|
|
444
|
+
|
|
445
|
+
def target_pt(self, N=15):
|
|
446
|
+
# Obtain an estimate of the Stieltjes transform at a
|
|
447
|
+
# single point z where the estimate is likely reliable
|
|
448
|
+
z = 1j + 2j * self.radius(N)
|
|
449
|
+
return z, self.stieltjes(z, N)
|
|
450
|
+
|
|
@@ -20,7 +20,9 @@ from ._continuation_algebraic import sample_z_joukowski, \
|
|
|
20
20
|
from ._edge import evolve_edges, merge_edges
|
|
21
21
|
from ._decompress import decompress_newton
|
|
22
22
|
from ._decompress2 import decompress_coeffs
|
|
23
|
-
from ._homotopy import
|
|
23
|
+
from ._homotopy import StieltjesPoly
|
|
24
|
+
from ._discriminant import compute_singular_points
|
|
25
|
+
from ._moments import MomentsESD
|
|
24
26
|
from .._free_form._support import supp
|
|
25
27
|
from .._free_form._plot_util import plot_density
|
|
26
28
|
|
|
@@ -143,6 +145,7 @@ class AlgebraicForm(object):
|
|
|
143
145
|
self.A = None
|
|
144
146
|
self.eig = None
|
|
145
147
|
self.stieltjes = None
|
|
148
|
+
self.moments = None
|
|
146
149
|
self.support = support
|
|
147
150
|
self.delta = delta # Offset above real axis to apply Plemelj formula
|
|
148
151
|
|
|
@@ -177,6 +180,7 @@ class AlgebraicForm(object):
|
|
|
177
180
|
# Use empirical Stieltjes function
|
|
178
181
|
self.stieltjes = lambda z: \
|
|
179
182
|
numpy.mean(1.0/(self.eig-z[:, numpy.newaxis]), axis=-1)
|
|
183
|
+
self.moments = MomentsESD(self.eig)
|
|
180
184
|
|
|
181
185
|
# Support
|
|
182
186
|
if support is None:
|
|
@@ -262,11 +266,13 @@ class AlgebraicForm(object):
|
|
|
262
266
|
|
|
263
267
|
# Fitting (w_inf = None means adaptive weight selection)
|
|
264
268
|
m1_fit = self.stieltjes(z_fit)
|
|
265
|
-
a_coeffs = fit_polynomial_relation(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
269
|
+
a_coeffs, fit_metrics = fit_polynomial_relation(
|
|
270
|
+
z_fit, m1_fit, s=deg_m, deg_z=deg_z, ridge_lambda=reg,
|
|
271
|
+
triangular=triangular, normalize=normalize, mu=mu,
|
|
272
|
+
mu_reg=mu_reg)
|
|
273
|
+
|
|
274
|
+
# Compute global branhc points, zeros of leading a_j, and support
|
|
275
|
+
branch_points, a_s_zero, support = compute_singular_points(a_coeffs)
|
|
270
276
|
|
|
271
277
|
self.a_coeffs = a_coeffs
|
|
272
278
|
|
|
@@ -282,9 +288,13 @@ class AlgebraicForm(object):
|
|
|
282
288
|
eta=max(y_eps, 1e-2), n_x=128,
|
|
283
289
|
max_bad_frac=0.05)
|
|
284
290
|
|
|
291
|
+
status['branch_points'] = branch_points
|
|
292
|
+
status['a_s_zero'] = a_s_zero
|
|
285
293
|
status['res_max'] = float(res_max)
|
|
286
294
|
status['res_99_9'] = float(res_99_9)
|
|
295
|
+
status['fit_metrics'] = fit_metrics
|
|
287
296
|
self.status = status
|
|
297
|
+
self.stieltjes = StieltjesPoly(self.a_coeffs)
|
|
288
298
|
|
|
289
299
|
if verbose:
|
|
290
300
|
print(f'fit residual max : {res_max:>0.4e}')
|
|
@@ -309,7 +319,7 @@ class AlgebraicForm(object):
|
|
|
309
319
|
else:
|
|
310
320
|
print('\nStieltjes sanity check: OK')
|
|
311
321
|
|
|
312
|
-
return a_coeffs, status
|
|
322
|
+
return a_coeffs, support, status
|
|
313
323
|
|
|
314
324
|
# =============
|
|
315
325
|
# generate grid
|
|
@@ -389,13 +399,7 @@ class AlgebraicForm(object):
|
|
|
389
399
|
x = self._generate_grid(1.25)
|
|
390
400
|
|
|
391
401
|
# Preallocate density to zero
|
|
392
|
-
rho =
|
|
393
|
-
|
|
394
|
-
for idx, x_i in enumerate(x):
|
|
395
|
-
m_i = stieltjes_poly(x_i, self.a_coeffs)
|
|
396
|
-
rho[idx] = m_i.imag
|
|
397
|
-
|
|
398
|
-
rho = rho / numpy.pi
|
|
402
|
+
rho = self.stieltjes(x).imag / numpy.pi
|
|
399
403
|
|
|
400
404
|
# if self.method == 'jacobi':
|
|
401
405
|
# rho[mask] = jacobi_density(x[mask], self.psi, self.support,
|
|
@@ -663,12 +667,9 @@ class AlgebraicForm(object):
|
|
|
663
667
|
# Decompression ratio equal to e^{t}.
|
|
664
668
|
alpha = numpy.atleast_1d(size) / self.n
|
|
665
669
|
|
|
666
|
-
def m_fn(z):
|
|
667
|
-
return stieltjes_poly(z, self.a_coeffs)
|
|
668
|
-
|
|
669
670
|
# Lower and upper bound on new support
|
|
670
|
-
hilb_lb = (1.0 /
|
|
671
|
-
hilb_ub = (1.0 /
|
|
671
|
+
hilb_lb = (1.0 / self.stieltjes(self.lam_m + self.delta * 1j).item()).real
|
|
672
|
+
hilb_ub = (1.0 / self.stieltjes(self.lam_p + self.delta * 1j).item()).real
|
|
672
673
|
lb = self.lam_m - (numpy.max(alpha) - 1) * hilb_lb
|
|
673
674
|
ub = self.lam_p - (numpy.max(alpha) - 1) * hilb_ub
|
|
674
675
|
|
|
@@ -689,9 +690,7 @@ class AlgebraicForm(object):
|
|
|
689
690
|
z_query = x + 1j * self.delta
|
|
690
691
|
|
|
691
692
|
# Initial condition at t=0 (physical branch)
|
|
692
|
-
|
|
693
|
-
stieltjes = numpy.vectorize(m_fn)
|
|
694
|
-
w0_list = stieltjes(z_query)
|
|
693
|
+
w0_list = self.stieltjes(z_query)
|
|
695
694
|
|
|
696
695
|
# Times
|
|
697
696
|
t = numpy.log(alpha)
|
|
@@ -721,9 +720,8 @@ class AlgebraicForm(object):
|
|
|
721
720
|
for i in range(alpha.size):
|
|
722
721
|
coeffs_i = decompress_coeffs(self.a_coeffs,
|
|
723
722
|
numpy.log(alpha[i]))
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
rho[i, j] = m_j.imag
|
|
723
|
+
stieltjes_i = StieltjesPoly(coeffs_i)
|
|
724
|
+
rho[i, :] = stieltjes_i.imag
|
|
727
725
|
|
|
728
726
|
rho = rho / numpy.pi
|
|
729
727
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
freealg/__init__.py,sha256=SjcYb6HWmaclnnM-m1eC1honZRyfNBWYDYBx23kSdjo,833
|
|
2
|
-
freealg/__version__.py,sha256=
|
|
2
|
+
freealg/__version__.py,sha256=uC8wB9mRblQ0jUBAOUyCQLUQJ39MC2xybVLB_8ZsevU,22
|
|
3
3
|
freealg/_util.py,sha256=RzccUCORgzrI9NdNqwMVugiHU0uDKkJFcIyjFMUOnv8,2518
|
|
4
4
|
freealg/_algebraic_form/__init__.py,sha256=MIB_jVgw2qI-JW_ypqaFSeNAB6c4GvpjNySnap_a6hg,398
|
|
5
5
|
freealg/_algebraic_form/_constraints.py,sha256=37U7nvtCTocuS7l_nfUznkPi195PY7eXFzeiikrv3B0,2448
|
|
6
|
-
freealg/_algebraic_form/_continuation_algebraic.py,sha256=
|
|
6
|
+
freealg/_algebraic_form/_continuation_algebraic.py,sha256=KundB9VfX61a35VRxLFyuvB5A51QdT4PD2ffAMjrKR0,19383
|
|
7
7
|
freealg/_algebraic_form/_decompress.py,sha256=gGtixLOVxlMy5S-NsXgoA7lIrB7u7nUZImQk1mIDo3s,21101
|
|
8
8
|
freealg/_algebraic_form/_decompress2.py,sha256=Ng9w9xmGe9M-DApp35IeNeQlvszfzT4NZx5BQn0lQ3I,2459
|
|
9
|
+
freealg/_algebraic_form/_discriminant.py,sha256=755pproom6-xThFARaH20m4GuBwwZS2rc0Y80Yg6NzY,5331
|
|
9
10
|
freealg/_algebraic_form/_edge.py,sha256=7l9QyLJDxaEY4WB6MCUFtfEZSf04wyHwH7YPHFJXSbM,10690
|
|
10
|
-
freealg/_algebraic_form/_homotopy.py,sha256=
|
|
11
|
+
freealg/_algebraic_form/_homotopy.py,sha256=YmNpRbKYKHQIYQpRsyFqUIQhaCr4bvxSQLyPwg61iO8,9174
|
|
12
|
+
freealg/_algebraic_form/_moments.py,sha256=xSQdJhPUtSnXOLrd2JDmz4O_soDCtmcDt5_iSeiNOZg,12404
|
|
11
13
|
freealg/_algebraic_form/_sheets_util.py,sha256=6OLzWQKu-gN8rxM2rbpbN8TjNZFmD8UJ-8t9kcZdkCo,4174
|
|
12
|
-
freealg/_algebraic_form/algebraic_form.py,sha256=
|
|
14
|
+
freealg/_algebraic_form/algebraic_form.py,sha256=fmcyunM4fkRjuVjQpDtgwBBs5wTbShPyNGGSpqAynIM,32775
|
|
13
15
|
freealg/_free_form/__init__.py,sha256=5cnSX7kHci3wKx6-BEFhmVY_NjjmQAq1JjWPTEqETTg,611
|
|
14
16
|
freealg/_free_form/_chebyshev.py,sha256=zkyVA8NLf7uUKlJdLz4ijd_SurdsqUgkA5nHGWSybaE,6916
|
|
15
17
|
freealg/_free_form/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
|
|
@@ -42,9 +44,9 @@ freealg/distributions/_wigner.py,sha256=epgx6ne6R_7to5j6-QsWIAVFJQFquWMmYgnZYMN4
|
|
|
42
44
|
freealg/visualization/__init__.py,sha256=NLq_zwueF7ytZ8sl8zLPqm-AODxxXNvfMozHGmmklcE,435
|
|
43
45
|
freealg/visualization/_glue_util.py,sha256=2oKnEYjUOS4OZfivmciVLauVr53kyHMwi6c2zRKilTQ,693
|
|
44
46
|
freealg/visualization/_rgb_hsv.py,sha256=rEskxXxSlKKxIrHRslVkgxHtD010L3ge9YtcVsOPl8E,3650
|
|
45
|
-
freealg-0.7.
|
|
46
|
-
freealg-0.7.
|
|
47
|
-
freealg-0.7.
|
|
48
|
-
freealg-0.7.
|
|
49
|
-
freealg-0.7.
|
|
50
|
-
freealg-0.7.
|
|
47
|
+
freealg-0.7.8.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
|
|
48
|
+
freealg-0.7.8.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
|
|
49
|
+
freealg-0.7.8.dist-info/METADATA,sha256=b5v5ekXtGJh4o-fcwxpF58bMWc8oZs4qrgfVMDO79h0,5516
|
|
50
|
+
freealg-0.7.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
51
|
+
freealg-0.7.8.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
|
|
52
|
+
freealg-0.7.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|