freealg 0.7.6__tar.gz → 0.7.8__tar.gz
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-0.7.6 → freealg-0.7.8}/PKG-INFO +1 -1
- freealg-0.7.8/freealg/__version__.py +1 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_continuation_algebraic.py +14 -6
- freealg-0.7.8/freealg/_algebraic_form/_discriminant.py +226 -0
- freealg-0.7.8/freealg/_algebraic_form/_homotopy.py +280 -0
- freealg-0.7.8/freealg/_algebraic_form/_moments.py +450 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/algebraic_form.py +23 -25
- {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/PKG-INFO +1 -1
- {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/SOURCES.txt +2 -0
- freealg-0.7.6/freealg/__version__.py +0 -1
- freealg-0.7.6/freealg/_algebraic_form/_homotopy.py +0 -138
- {freealg-0.7.6 → freealg-0.7.8}/AUTHORS.txt +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/CHANGELOG.rst +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/LICENSE.txt +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/MANIFEST.in +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/README.rst +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/__init__.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/__init__.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_constraints.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_decompress.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_decompress2.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_edge.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_sheets_util.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/__init__.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_chebyshev.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_damp.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_decompress.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_density_util.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_jacobi.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_linalg.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_pade.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_plot_util.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_sample.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_series.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_support.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/free_form.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/__init__.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_continuation_genus0.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_continuation_genus1.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_elliptic_functions.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_sphere_maps.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_torus_maps.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/geometric_form.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/_util.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/__init__.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_chiral_block.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_deformed_marchenko_pastur.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_deformed_wigner.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_kesten_mckay.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_marchenko_pastur.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_meixner.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_wachter.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_wigner.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/visualization/__init__.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/visualization/_glue_util.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg/visualization/_rgb_hsv.py +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/dependency_links.txt +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/not-zip-safe +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/requires.txt +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/top_level.txt +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/pyproject.toml +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/requirements.txt +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/setup.cfg +0 -0
- {freealg-0.7.6 → freealg-0.7.8}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
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
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# =======
|
|
2
|
+
# Imports
|
|
3
|
+
# =======
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
from ._moments import AlgebraicStieltjesMoments
|
|
7
|
+
|
|
8
|
+
__all__ = ['stieltjes_poly']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# =====================
|
|
12
|
+
# select root
|
|
13
|
+
# =====================
|
|
14
|
+
|
|
15
|
+
def select_root(roots, z, target):
|
|
16
|
+
"""
|
|
17
|
+
Select the root among Herglotz candidates at a given z closest to a
|
|
18
|
+
given target
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
roots : array_like of complex
|
|
23
|
+
Candidate roots for m at the given z.
|
|
24
|
+
z : complex
|
|
25
|
+
Evaluation point. The Stieltjes/Herglotz branch satisfies
|
|
26
|
+
sign(Im(m)) = sign(Im(z)) away from the real axis.
|
|
27
|
+
target : complex
|
|
28
|
+
Previous continuation value used to enforce continuity, or
|
|
29
|
+
target value.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
w : complex
|
|
34
|
+
Selected root corresponding to the Stieltjes branch.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
z = complex(z)
|
|
38
|
+
roots = numpy.asarray(roots, dtype=numpy.complex128).ravel()
|
|
39
|
+
|
|
40
|
+
if roots.size == 0:
|
|
41
|
+
raise ValueError("roots must contain at least one candidate root.")
|
|
42
|
+
|
|
43
|
+
desired_sign = numpy.sign(z.imag)
|
|
44
|
+
|
|
45
|
+
# Apply a soft Herglotz sign filter: prefer roots with Im(w) having the
|
|
46
|
+
# same sign as Im(z), allowing tiny numerical violations near the axis.
|
|
47
|
+
imag_roots = numpy.imag(roots)
|
|
48
|
+
|
|
49
|
+
good = roots[numpy.sign(imag_roots) == desired_sign]
|
|
50
|
+
if good.size == 0:
|
|
51
|
+
good = roots[(imag_roots * desired_sign) > -1e-12]
|
|
52
|
+
|
|
53
|
+
candidates = good if good.size > 0 else roots
|
|
54
|
+
idx = int(numpy.argmin(numpy.abs(candidates - target)))
|
|
55
|
+
return candidates[idx]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ==============
|
|
59
|
+
# stieltjes poly
|
|
60
|
+
# ==============
|
|
61
|
+
|
|
62
|
+
class StieltjesPoly(object):
|
|
63
|
+
"""
|
|
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
|
+
|
|
69
|
+
P(z, m) = 0,
|
|
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.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
a : ndarray, shape (L, K)
|
|
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.
|
|
88
|
+
eps : float or None, optional
|
|
89
|
+
If Im(z) == 0, use z + i*eps as the boundary evaluation point.
|
|
90
|
+
If None and Im(z) == 0, eps is set to 1e-8 * max(1, |z|).
|
|
91
|
+
height : float, default = 2.0
|
|
92
|
+
Imaginary height used for the starting point z0 in the same
|
|
93
|
+
half-plane as the evaluation point.
|
|
94
|
+
steps : int, default = 100
|
|
95
|
+
Number of continuation steps along the homotopy path.
|
|
96
|
+
order : int, default = 15
|
|
97
|
+
Number of moments in Stieltjes estimate
|
|
98
|
+
|
|
99
|
+
Methods
|
|
100
|
+
-------
|
|
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|).
|
|
114
|
+
"""
|
|
115
|
+
|
|
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)
|
|
140
|
+
return numpy.roots(coeffs)
|
|
141
|
+
|
|
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
|