freealg 0.7.10__py3-none-any.whl → 0.7.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/_branch_points.py +288 -0
- freealg/_algebraic_form/_continuation_algebraic.py +1 -1
- freealg/_algebraic_form/_decompress.py +44 -1
- freealg/_algebraic_form/_support.py +309 -0
- freealg/_algebraic_form/algebraic_form.py +106 -35
- {freealg-0.7.10.dist-info → freealg-0.7.11.dist-info}/METADATA +1 -1
- {freealg-0.7.10.dist-info → freealg-0.7.11.dist-info}/RECORD +12 -11
- freealg/_algebraic_form/_discriminant.py +0 -226
- {freealg-0.7.10.dist-info → freealg-0.7.11.dist-info}/WHEEL +0 -0
- {freealg-0.7.10.dist-info → freealg-0.7.11.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.10.dist-info → freealg-0.7.11.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.7.10.dist-info → freealg-0.7.11.dist-info}/top_level.txt +0 -0
freealg/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.7.
|
|
1
|
+
__version__ = "0.7.11"
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileType: SOURCE
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
# the terms of the license found in the LICENSE.txt file in the root directory
|
|
7
|
+
# of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
|
|
16
|
+
__all__ = ['compute_branch_points']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# =========
|
|
20
|
+
# poly trim
|
|
21
|
+
# =========
|
|
22
|
+
|
|
23
|
+
def _poly_trim(p, tol):
|
|
24
|
+
p = numpy.asarray(p, dtype=float)
|
|
25
|
+
if p.size == 0:
|
|
26
|
+
return p
|
|
27
|
+
k = p.size - 1
|
|
28
|
+
while k > 0 and abs(p[k]) <= tol:
|
|
29
|
+
k -= 1
|
|
30
|
+
return p[: k + 1]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ========
|
|
34
|
+
# poly add
|
|
35
|
+
# ========
|
|
36
|
+
|
|
37
|
+
def _poly_add(a, b, tol):
|
|
38
|
+
|
|
39
|
+
n = max(len(a), len(b))
|
|
40
|
+
out = numpy.zeros(n, dtype=float)
|
|
41
|
+
out[: len(a)] += a
|
|
42
|
+
out[: len(b)] += b
|
|
43
|
+
|
|
44
|
+
return _poly_trim(out, tol)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ========
|
|
48
|
+
# poly sub
|
|
49
|
+
# ========
|
|
50
|
+
|
|
51
|
+
def _poly_sub(a, b, tol):
|
|
52
|
+
|
|
53
|
+
n = max(len(a), len(b))
|
|
54
|
+
out = numpy.zeros(n, dtype=float)
|
|
55
|
+
out[: len(a)] += a
|
|
56
|
+
out[: len(b)] -= b
|
|
57
|
+
|
|
58
|
+
return _poly_trim(out, tol)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ========
|
|
62
|
+
# poly mul
|
|
63
|
+
# ========
|
|
64
|
+
|
|
65
|
+
def _poly_mul(a, b, tol):
|
|
66
|
+
|
|
67
|
+
a = _poly_trim(a, tol)
|
|
68
|
+
b = _poly_trim(b, tol)
|
|
69
|
+
if a.size == 0 or b.size == 0:
|
|
70
|
+
return numpy.zeros(1, dtype=float)
|
|
71
|
+
out = numpy.convolve(a, b)
|
|
72
|
+
return _poly_trim(out, tol)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ===============
|
|
76
|
+
# poly div approx
|
|
77
|
+
# ===============
|
|
78
|
+
|
|
79
|
+
def _poly_div_approx(a, b, tol):
|
|
80
|
+
"""
|
|
81
|
+
Polynomial division q,r = a/b in ascending powers (numpy.polynomial
|
|
82
|
+
convention). Returns q (ascending). Remainder is ignored if it is
|
|
83
|
+
small-ish.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
a = _poly_trim(a, tol)
|
|
87
|
+
b = _poly_trim(b, tol)
|
|
88
|
+
if b.size == 0 or (b.size == 1 and abs(b[0]) <= tol):
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
"division by (near) zero polynomial in branch point resultant")
|
|
91
|
+
# numpy.polydiv uses descending powers, so flip.
|
|
92
|
+
qd, rd = numpy.polydiv(a[::-1], b[::-1])
|
|
93
|
+
q = qd[::-1]
|
|
94
|
+
r = rd[::-1]
|
|
95
|
+
# Accept small remainder (Bareiss should be exact in exact arithmetic).
|
|
96
|
+
# If not small, we still proceed with the quotient (robustness over
|
|
97
|
+
# exactness).
|
|
98
|
+
scale = max(1.0, numpy.linalg.norm(a))
|
|
99
|
+
if numpy.linalg.norm(_poly_trim(r, tol)) > 1e6 * tol * scale:
|
|
100
|
+
pass
|
|
101
|
+
return _poly_trim(q, tol)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# =================
|
|
105
|
+
# det baresiss poly
|
|
106
|
+
# =================
|
|
107
|
+
|
|
108
|
+
def _det_bareiss_poly(M, tol):
|
|
109
|
+
"""
|
|
110
|
+
Fraction-free determinant for a matrix with polynomial entries in z.
|
|
111
|
+
Polynomials are stored as 1D arrays of ascending coefficients.
|
|
112
|
+
Returns det as ascending coefficients.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
n = len(M)
|
|
116
|
+
A = [[_poly_trim(M[i][j], tol) for j in range(n)] for i in range(n)]
|
|
117
|
+
denom = numpy.array([1.0], dtype=float)
|
|
118
|
+
|
|
119
|
+
for k in range(n - 1):
|
|
120
|
+
pivot = A[k][k]
|
|
121
|
+
if pivot.size == 1 and abs(pivot[0]) <= tol:
|
|
122
|
+
swap = None
|
|
123
|
+
for i in range(k + 1, n):
|
|
124
|
+
if not (A[i][k].size == 1 and abs(A[i][k][0]) <= tol):
|
|
125
|
+
swap = i
|
|
126
|
+
break
|
|
127
|
+
if swap is None:
|
|
128
|
+
return numpy.zeros(1, dtype=float)
|
|
129
|
+
A[k], A[swap] = A[swap], A[k]
|
|
130
|
+
pivot = A[k][k]
|
|
131
|
+
|
|
132
|
+
for i in range(k + 1, n):
|
|
133
|
+
for j in range(k + 1, n):
|
|
134
|
+
num = _poly_sub(
|
|
135
|
+
_poly_mul(A[i][j], pivot, tol),
|
|
136
|
+
_poly_mul(A[i][k], A[k][j], tol),
|
|
137
|
+
tol,
|
|
138
|
+
)
|
|
139
|
+
if k > 0:
|
|
140
|
+
A[i][j] = _poly_div_approx(num, denom, tol)
|
|
141
|
+
else:
|
|
142
|
+
A[i][j] = _poly_trim(num, tol)
|
|
143
|
+
|
|
144
|
+
denom = pivot
|
|
145
|
+
|
|
146
|
+
for i in range(k + 1, n):
|
|
147
|
+
A[i][k] = numpy.array([0.0], dtype=float)
|
|
148
|
+
A[k][i] = numpy.array([0.0], dtype=float)
|
|
149
|
+
|
|
150
|
+
return _poly_trim(A[n - 1][n - 1], tol)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ======================
|
|
154
|
+
# resultant discriminant
|
|
155
|
+
# ======================
|
|
156
|
+
|
|
157
|
+
def _resultant_discriminant(a_coeffs, tol):
|
|
158
|
+
"""
|
|
159
|
+
Numerically compute Disc_m(P)(z) as a polynomial in z (ascending coeffs),
|
|
160
|
+
via Sylvester determinant evaluation on a circle + interpolation.
|
|
161
|
+
|
|
162
|
+
a_coeffs[i,j] is coeff of z^i m^j, shape (deg_z+1, s+1).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
import numpy
|
|
166
|
+
|
|
167
|
+
a_coeffs = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
168
|
+
deg_z = a_coeffs.shape[0] - 1
|
|
169
|
+
s = a_coeffs.shape[1] - 1
|
|
170
|
+
if s < 1 or deg_z < 0:
|
|
171
|
+
return numpy.zeros(1, dtype=numpy.complex128)
|
|
172
|
+
|
|
173
|
+
# Degree bound: deg_z(Disc) <= (2s-1)*deg_z
|
|
174
|
+
D = (2 * s - 1) * deg_z
|
|
175
|
+
if D <= 0:
|
|
176
|
+
return numpy.zeros(1, dtype=numpy.complex128)
|
|
177
|
+
|
|
178
|
+
def eval_disc(z):
|
|
179
|
+
# Build P(m) coeffs in descending powers of m: p_desc[k] = coeff of
|
|
180
|
+
# m^(s-k)
|
|
181
|
+
p_asc = numpy.zeros(s + 1, dtype=numpy.complex128)
|
|
182
|
+
for j in range(s + 1):
|
|
183
|
+
p_asc[j] = numpy.polyval(a_coeffs[:, j][::-1], z) # a_j(z)
|
|
184
|
+
p_desc = p_asc[::-1]
|
|
185
|
+
|
|
186
|
+
# Q(m) = dP/dm, descending
|
|
187
|
+
q_asc = numpy.zeros(s, dtype=numpy.complex128)
|
|
188
|
+
for j in range(1, s + 1):
|
|
189
|
+
q_asc[j - 1] = j * p_asc[j]
|
|
190
|
+
q_desc = q_asc[::-1]
|
|
191
|
+
|
|
192
|
+
# Sylvester matrix of P (deg s) and Q (deg s-1): size (2s-1)x(2s-1)
|
|
193
|
+
n = 2 * s - 1
|
|
194
|
+
S = numpy.zeros((n, n), dtype=numpy.complex128)
|
|
195
|
+
|
|
196
|
+
# First (s-1) rows: shifts of P
|
|
197
|
+
for r in range(s - 1):
|
|
198
|
+
S[r, r:r + (s + 1)] = p_desc
|
|
199
|
+
|
|
200
|
+
# Next s rows: shifts of Q
|
|
201
|
+
for r in range(s):
|
|
202
|
+
rr = (s - 1) + r
|
|
203
|
+
S[rr, r:r + s] = q_desc
|
|
204
|
+
|
|
205
|
+
return numpy.linalg.det(S)
|
|
206
|
+
|
|
207
|
+
# Sample points on a circle; scale radius using coefficient magnitude
|
|
208
|
+
# (simple heuristic) (This only affects conditioning of interpolation, not
|
|
209
|
+
# correctness.)
|
|
210
|
+
scale = float(numpy.max(numpy.abs(a_coeffs))) \
|
|
211
|
+
if numpy.max(numpy.abs(a_coeffs)) > 0 else 1.0
|
|
212
|
+
R = 1.0 + 0.1 * scale
|
|
213
|
+
|
|
214
|
+
N = D + 1
|
|
215
|
+
k = numpy.arange(N, dtype=float)
|
|
216
|
+
z_samp = R * numpy.exp(2.0j * numpy.pi * k / float(N))
|
|
217
|
+
d_samp = numpy.array([eval_disc(z) for z in z_samp],
|
|
218
|
+
dtype=numpy.complex128)
|
|
219
|
+
|
|
220
|
+
# Interpolate disc(z) = sum_{j=0}^D c[j] z^j (ascending)
|
|
221
|
+
V = (z_samp[:, None] ** numpy.arange(D + 1)[None, :]).astype(
|
|
222
|
+
numpy.complex128)
|
|
223
|
+
c, _, _, _ = numpy.linalg.lstsq(V, d_samp, rcond=None)
|
|
224
|
+
|
|
225
|
+
# Trim tiny coefficients
|
|
226
|
+
c = _poly_trim(c, tol)
|
|
227
|
+
if c.size == 0:
|
|
228
|
+
c = numpy.zeros(1, dtype=numpy.complex128)
|
|
229
|
+
|
|
230
|
+
# If numerics leave small imag, kill it (disc should be real-coeff if
|
|
231
|
+
# a_coeffs real)
|
|
232
|
+
if numpy.linalg.norm(c.imag) <= \
|
|
233
|
+
1e3 * tol * max(1.0, numpy.linalg.norm(c.real)):
|
|
234
|
+
c = c.real.astype(numpy.float64)
|
|
235
|
+
|
|
236
|
+
return c
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# =====================
|
|
240
|
+
# compute branch points
|
|
241
|
+
# =====================
|
|
242
|
+
|
|
243
|
+
def compute_branch_points(a_coeffs, tol=1e-12, real_tol=None):
|
|
244
|
+
"""
|
|
245
|
+
Compute global branch points of the affine curve P(z,m)=0 by
|
|
246
|
+
z-roots of Disc_m(P)(z) = Res_m(P, dP/dm).
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
z_bp : complex ndarray
|
|
251
|
+
a_s_zero : complex ndarray
|
|
252
|
+
info : dict
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
a_coeffs = numpy.asarray(a_coeffs, dtype=float)
|
|
256
|
+
s = a_coeffs.shape[1] - 1
|
|
257
|
+
if s < 1:
|
|
258
|
+
if real_tol is None:
|
|
259
|
+
real_tol = 1e3 * tol
|
|
260
|
+
return \
|
|
261
|
+
numpy.array([], dtype=complex), \
|
|
262
|
+
numpy.array([], dtype=complex), \
|
|
263
|
+
{
|
|
264
|
+
"disc": numpy.zeros(1, dtype=float),
|
|
265
|
+
"tol": float(tol),
|
|
266
|
+
"real_tol": float(real_tol),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if real_tol is None:
|
|
270
|
+
real_tol = 1e3 * tol
|
|
271
|
+
|
|
272
|
+
a_s = _poly_trim(a_coeffs[:, s], tol)
|
|
273
|
+
a_s_zero = numpy.roots(a_s[::-1]) if a_s.size > 1 else \
|
|
274
|
+
numpy.array([], dtype=complex)
|
|
275
|
+
|
|
276
|
+
disc = _resultant_discriminant(a_coeffs, tol)
|
|
277
|
+
if disc.size <= 1:
|
|
278
|
+
z_bp = numpy.array([], dtype=complex)
|
|
279
|
+
else:
|
|
280
|
+
z_bp = numpy.roots(disc[::-1])
|
|
281
|
+
|
|
282
|
+
info = {
|
|
283
|
+
"disc": disc,
|
|
284
|
+
"tol": float(tol),
|
|
285
|
+
"real_tol": float(real_tol),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return z_bp, a_s_zero, info
|
|
@@ -289,7 +289,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
289
289
|
|
|
290
290
|
# Diagnostic metrics
|
|
291
291
|
fit_metrics = {
|
|
292
|
-
's_min': svals[-1],
|
|
292
|
+
's_min': float(svals[-1]),
|
|
293
293
|
'gap_ratio': float(svals[-2] / svals[-1]),
|
|
294
294
|
'n_small': float(int(numpy.sum(svals <= svals[0] * 1e-12))),
|
|
295
295
|
}
|
|
@@ -14,7 +14,50 @@
|
|
|
14
14
|
import numpy
|
|
15
15
|
from ._continuation_algebraic import powers
|
|
16
16
|
|
|
17
|
-
__all__ = ['decompress_newton_old', 'decompress_newton']
|
|
17
|
+
__all__ = ['build_time_grid', 'decompress_newton_old', 'decompress_newton']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ===============
|
|
21
|
+
# build time grid
|
|
22
|
+
# ===============
|
|
23
|
+
|
|
24
|
+
def build_time_grid(sizes, n0, min_n_time=0):
|
|
25
|
+
"""
|
|
26
|
+
sizes: list/array of requested matrix sizes (e.g. [2000,3000,4000,8000])
|
|
27
|
+
n0: initial size (self.n)
|
|
28
|
+
min_n_time: minimum number of time points to run Newton sweep on
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
t_all: sorted time grid to run solver on
|
|
33
|
+
idx_req: indices of requested times inside t_all (same order as sizes)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
sizes = numpy.asarray(sizes, dtype=float)
|
|
37
|
+
alpha = sizes / float(n0)
|
|
38
|
+
t_req = numpy.log(alpha)
|
|
39
|
+
|
|
40
|
+
# Always include t=0 and T=max(t_req)
|
|
41
|
+
T = float(numpy.max(t_req)) if t_req.size else 0.0
|
|
42
|
+
base = numpy.unique(numpy.r_[0.0, t_req, T])
|
|
43
|
+
t_all = numpy.sort(base)
|
|
44
|
+
|
|
45
|
+
# Add points only if needed: split largest gaps
|
|
46
|
+
N = int(min_n_time) if min_n_time is not None else 0
|
|
47
|
+
while t_all.size < N and t_all.size >= 2:
|
|
48
|
+
gaps = numpy.diff(t_all)
|
|
49
|
+
k = int(numpy.argmax(gaps))
|
|
50
|
+
mid = 0.5 * (t_all[k] + t_all[k+1])
|
|
51
|
+
t_all = numpy.sort(numpy.unique(numpy.r_[t_all, mid]))
|
|
52
|
+
|
|
53
|
+
# Map each requested time to an index in t_all (stable, no float drama)
|
|
54
|
+
# (t_req values came from same construction, so they should match exactly;
|
|
55
|
+
# still: use searchsorted + assert)
|
|
56
|
+
idx_req = numpy.searchsorted(t_all, t_req)
|
|
57
|
+
# optional sanity:
|
|
58
|
+
# assert numpy.allclose(t_all[idx_req], t_req, rtol=0, atol=0)
|
|
59
|
+
|
|
60
|
+
return t_all, idx_req
|
|
18
61
|
|
|
19
62
|
|
|
20
63
|
# ===============
|
|
@@ -0,0 +1,309 @@
|
|
|
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
|
+
import numpy.polynomial.polynomial as poly
|
|
16
|
+
|
|
17
|
+
__all__ = ['compute_support']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ======================
|
|
21
|
+
# poly coeffs in m and z
|
|
22
|
+
# ======================
|
|
23
|
+
|
|
24
|
+
def _poly_coeffs_in_m_at_z(a_coeffs, z):
|
|
25
|
+
|
|
26
|
+
s = a_coeffs.shape[1] - 1
|
|
27
|
+
a = numpy.empty(s + 1, dtype=numpy.complex128)
|
|
28
|
+
for j in range(s + 1):
|
|
29
|
+
a[j] = poly.polyval(z, a_coeffs[:, j])
|
|
30
|
+
return a
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ===============
|
|
34
|
+
# roots poly in m
|
|
35
|
+
# ===============
|
|
36
|
+
|
|
37
|
+
def _roots_poly_in_m(c_asc, tol=0.0):
|
|
38
|
+
|
|
39
|
+
c = numpy.asarray(c_asc, dtype=numpy.complex128).ravel()
|
|
40
|
+
if c.size <= 1:
|
|
41
|
+
return numpy.array([], dtype=numpy.complex128)
|
|
42
|
+
|
|
43
|
+
k = c.size - 1
|
|
44
|
+
while k > 0 and abs(c[k]) <= tol:
|
|
45
|
+
k -= 1
|
|
46
|
+
c = c[:k + 1]
|
|
47
|
+
if c.size <= 1:
|
|
48
|
+
return numpy.array([], dtype=numpy.complex128)
|
|
49
|
+
|
|
50
|
+
return numpy.roots(c[::-1])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ================
|
|
54
|
+
# dPdm coeffs at z
|
|
55
|
+
# ================
|
|
56
|
+
|
|
57
|
+
def _dPdm_coeffs_at_z(a_coeffs, z):
|
|
58
|
+
|
|
59
|
+
a = _poly_coeffs_in_m_at_z(a_coeffs, z)
|
|
60
|
+
s = a.size - 1
|
|
61
|
+
if s <= 0:
|
|
62
|
+
return numpy.array([0.0 + 0.0j], dtype=numpy.complex128)
|
|
63
|
+
d = numpy.empty(s, dtype=numpy.complex128)
|
|
64
|
+
for j in range(1, s + 1):
|
|
65
|
+
d[j - 1] = j * a[j]
|
|
66
|
+
return d
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ==============
|
|
70
|
+
# P and partials
|
|
71
|
+
# ==============
|
|
72
|
+
|
|
73
|
+
def _P_and_partials(a_coeffs, z, m):
|
|
74
|
+
|
|
75
|
+
s = a_coeffs.shape[1] - 1
|
|
76
|
+
|
|
77
|
+
a = numpy.empty(s + 1, dtype=numpy.complex128)
|
|
78
|
+
da = numpy.empty(s + 1, dtype=numpy.complex128)
|
|
79
|
+
for j in range(s + 1):
|
|
80
|
+
a[j] = poly.polyval(z, a_coeffs[:, j])
|
|
81
|
+
da[j] = poly.polyval(z, poly.polyder(a_coeffs[:, j]))
|
|
82
|
+
|
|
83
|
+
mpow = 1.0 + 0.0j
|
|
84
|
+
P = 0.0 + 0.0j
|
|
85
|
+
Pz = 0.0 + 0.0j
|
|
86
|
+
for j in range(s + 1):
|
|
87
|
+
P += a[j] * mpow
|
|
88
|
+
Pz += da[j] * mpow
|
|
89
|
+
mpow *= m
|
|
90
|
+
|
|
91
|
+
Pm = 0.0 + 0.0j
|
|
92
|
+
Pmm = 0.0 + 0.0j
|
|
93
|
+
Pzm = 0.0 + 0.0j
|
|
94
|
+
for j in range(1, s + 1):
|
|
95
|
+
Pm += j * a[j] * (m ** (j - 1))
|
|
96
|
+
Pzm += j * da[j] * (m ** (j - 1))
|
|
97
|
+
for j in range(2, s + 1):
|
|
98
|
+
Pmm += j * (j - 1) * a[j] * (m ** (j - 2))
|
|
99
|
+
|
|
100
|
+
return P, Pz, Pm, Pzm, Pmm, a
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ===========
|
|
104
|
+
# newton edge
|
|
105
|
+
# ===========
|
|
106
|
+
|
|
107
|
+
def _newton_edge(a_coeffs, x0, m0, tol=1e-12, max_iter=50):
|
|
108
|
+
|
|
109
|
+
x = float(x0)
|
|
110
|
+
m = float(m0)
|
|
111
|
+
|
|
112
|
+
for _ in range(max_iter):
|
|
113
|
+
z = x + 0.0j
|
|
114
|
+
P, Pz, Pm, Pzm, Pmm, _ = _P_and_partials(a_coeffs, z, m)
|
|
115
|
+
|
|
116
|
+
f0 = float(numpy.real(P))
|
|
117
|
+
f1 = float(numpy.real(Pm))
|
|
118
|
+
|
|
119
|
+
j00 = float(numpy.real(Pz))
|
|
120
|
+
j01 = float(numpy.real(Pm))
|
|
121
|
+
j10 = float(numpy.real(Pzm))
|
|
122
|
+
j11 = float(numpy.real(Pmm))
|
|
123
|
+
|
|
124
|
+
det = j00 * j11 - j01 * j10
|
|
125
|
+
if det == 0.0 or (not numpy.isfinite(det)):
|
|
126
|
+
return x, m, False
|
|
127
|
+
|
|
128
|
+
dx = (-f0 * j11 + f1 * j01) / det
|
|
129
|
+
dm = (-j00 * f1 + j10 * f0) / det
|
|
130
|
+
|
|
131
|
+
x += dx
|
|
132
|
+
m += dm
|
|
133
|
+
|
|
134
|
+
if abs(dx) + abs(dm) < tol:
|
|
135
|
+
return x, m, True
|
|
136
|
+
|
|
137
|
+
return x, m, False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# =============
|
|
141
|
+
# cluster edges
|
|
142
|
+
# =============
|
|
143
|
+
|
|
144
|
+
def _cluster_edges(edges, x_tol):
|
|
145
|
+
|
|
146
|
+
if len(edges) == 0:
|
|
147
|
+
return numpy.array([], dtype=float)
|
|
148
|
+
|
|
149
|
+
edges = numpy.array(sorted(edges), dtype=float)
|
|
150
|
+
out = [edges[0]]
|
|
151
|
+
for e in edges[1:]:
|
|
152
|
+
if abs(e - out[-1]) > x_tol:
|
|
153
|
+
out.append(e)
|
|
154
|
+
return numpy.array(out, dtype=float)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# =======================
|
|
158
|
+
# pick physical root at z
|
|
159
|
+
# =======================
|
|
160
|
+
|
|
161
|
+
def _pick_physical_root_at_z(a_coeffs, z, im_sign=+1):
|
|
162
|
+
|
|
163
|
+
a = _poly_coeffs_in_m_at_z(a_coeffs, z)
|
|
164
|
+
r = _roots_poly_in_m(a)
|
|
165
|
+
if r.size == 0:
|
|
166
|
+
return numpy.nan + 1j * numpy.nan
|
|
167
|
+
|
|
168
|
+
w_ref = -1.0 / z
|
|
169
|
+
idx = int(numpy.argmin(numpy.abs(r - w_ref)))
|
|
170
|
+
w = r[idx]
|
|
171
|
+
|
|
172
|
+
# optional strictness: if it violates Herglotz, declare failure
|
|
173
|
+
if not numpy.isfinite(w.real) or not numpy.isfinite(w.imag):
|
|
174
|
+
return w
|
|
175
|
+
if (im_sign * w.imag) <= 0.0:
|
|
176
|
+
return w
|
|
177
|
+
|
|
178
|
+
return w
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ===============
|
|
182
|
+
# compute support
|
|
183
|
+
# ===============
|
|
184
|
+
|
|
185
|
+
def compute_support(a_coeffs,
|
|
186
|
+
x_min,
|
|
187
|
+
x_max,
|
|
188
|
+
n_scan=4000,
|
|
189
|
+
y_eps=1e-3,
|
|
190
|
+
im_sign=+1,
|
|
191
|
+
root_tol=0.0,
|
|
192
|
+
edge_rel_tol=1e-6,
|
|
193
|
+
edge_x_cluster_tol=1e-3,
|
|
194
|
+
newton_tol=1e-12):
|
|
195
|
+
"""
|
|
196
|
+
Fast support from fitted polynomial using branch-point system P=0, Pm=0.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
support : list of (a,b)
|
|
201
|
+
info : dict (edges, rel_res_curve, etc.)
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
a_coeffs = numpy.asarray(a_coeffs)
|
|
205
|
+
x_grid = numpy.linspace(float(x_min), float(x_max), int(n_scan))
|
|
206
|
+
|
|
207
|
+
# For each x, find best real critical point m (Pm=0) minimizing rel
|
|
208
|
+
# residual.
|
|
209
|
+
rel = numpy.full(x_grid.size, numpy.inf, dtype=float)
|
|
210
|
+
m_star = numpy.full(x_grid.size, numpy.nan, dtype=float)
|
|
211
|
+
|
|
212
|
+
for i, x in enumerate(x_grid):
|
|
213
|
+
z = x + 0.0j
|
|
214
|
+
dcoef = _dPdm_coeffs_at_z(a_coeffs, z)
|
|
215
|
+
mr = _roots_poly_in_m(dcoef, tol=root_tol)
|
|
216
|
+
|
|
217
|
+
best = numpy.inf
|
|
218
|
+
best_m = numpy.nan
|
|
219
|
+
|
|
220
|
+
for w in mr:
|
|
221
|
+
# accept nearly-real roots; numerical roots can have small imag
|
|
222
|
+
# part
|
|
223
|
+
if abs(w.imag) > 1e-6 * (1.0 + abs(w.real)):
|
|
224
|
+
continue
|
|
225
|
+
m = float(w.real)
|
|
226
|
+
P, _, _, _, _, a = _P_and_partials(a_coeffs, z, m)
|
|
227
|
+
|
|
228
|
+
denom = 1.0
|
|
229
|
+
am = 1.0
|
|
230
|
+
for j in range(a.size):
|
|
231
|
+
denom += abs(a[j]) * abs(am)
|
|
232
|
+
am *= m
|
|
233
|
+
|
|
234
|
+
r = abs(numpy.real(P)) / denom
|
|
235
|
+
if numpy.isfinite(r) and r < best:
|
|
236
|
+
best = float(r)
|
|
237
|
+
best_m = m
|
|
238
|
+
|
|
239
|
+
rel[i] = best
|
|
240
|
+
m_star[i] = best_m
|
|
241
|
+
|
|
242
|
+
# Pick candidate edges as local minima of rel(x), below an automatic scale.
|
|
243
|
+
rel_f = rel[numpy.isfinite(rel)]
|
|
244
|
+
if rel_f.size == 0:
|
|
245
|
+
return [], {"edges": numpy.array([], dtype=float), "n_edges": 0}
|
|
246
|
+
|
|
247
|
+
med = float(numpy.median(rel_f))
|
|
248
|
+
min_rel = float(numpy.min(rel_f))
|
|
249
|
+
|
|
250
|
+
# accept local minima up to a factor above the best one, but never abov
|
|
251
|
+
# background scale
|
|
252
|
+
thr = min(0.1 * med, max(float(edge_rel_tol), 1e4 * min_rel))
|
|
253
|
+
|
|
254
|
+
edges0 = []
|
|
255
|
+
seeds = []
|
|
256
|
+
|
|
257
|
+
for i in range(1, x_grid.size - 1):
|
|
258
|
+
if not numpy.isfinite(rel[i]):
|
|
259
|
+
continue
|
|
260
|
+
if rel[i] <= rel[i - 1] and rel[i] <= rel[i + 1] and rel[i] < thr and \
|
|
261
|
+
numpy.isfinite(m_star[i]):
|
|
262
|
+
edges0.append(float(x_grid[i]))
|
|
263
|
+
seeds.append((float(x_grid[i]), float(m_star[i])))
|
|
264
|
+
|
|
265
|
+
# Refine each seed by 2D Newton (x,m)
|
|
266
|
+
edges = []
|
|
267
|
+
for x0, m0 in seeds:
|
|
268
|
+
xe, me, ok = _newton_edge(a_coeffs, x0, m0, tol=newton_tol)
|
|
269
|
+
if ok and numpy.isfinite(xe) and numpy.isfinite(me):
|
|
270
|
+
edges.append(float(xe))
|
|
271
|
+
|
|
272
|
+
edges = _cluster_edges(edges, edge_x_cluster_tol)
|
|
273
|
+
edges.sort()
|
|
274
|
+
|
|
275
|
+
# Build support by testing midpoints between consecutive real edges
|
|
276
|
+
support = []
|
|
277
|
+
m_im_tol = 1e-10
|
|
278
|
+
|
|
279
|
+
for i in range(edges.size - 1):
|
|
280
|
+
a = float(edges[i])
|
|
281
|
+
b = float(edges[i + 1])
|
|
282
|
+
if b <= a:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
xmid = 0.5 * (a + b)
|
|
286
|
+
|
|
287
|
+
# roots of P(xmid, m) with real coefficients
|
|
288
|
+
a_m = _poly_coeffs_in_m_at_z(a_coeffs, xmid + 0.0j)
|
|
289
|
+
r = _roots_poly_in_m(a_m, tol=root_tol)
|
|
290
|
+
|
|
291
|
+
# interval is support iff there exists a non-real root (complex pair)
|
|
292
|
+
if numpy.any(numpy.abs(numpy.imag(r)) > m_im_tol):
|
|
293
|
+
support.append((a, b))
|
|
294
|
+
|
|
295
|
+
info = {
|
|
296
|
+
"edges": edges,
|
|
297
|
+
"n_edges": int(edges.size),
|
|
298
|
+
"support": support,
|
|
299
|
+
"n_support": int(len(support)),
|
|
300
|
+
"x_grid": x_grid,
|
|
301
|
+
"rel": rel,
|
|
302
|
+
"thr": float(thr),
|
|
303
|
+
"x_min": float(x_min),
|
|
304
|
+
"x_max": float(x_max),
|
|
305
|
+
"n_scan": int(n_scan),
|
|
306
|
+
"y_eps": float(y_eps),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return support, info
|
|
@@ -18,10 +18,11 @@ from ._continuation_algebraic import sample_z_joukowski, \
|
|
|
18
18
|
filter_z_away_from_cuts, fit_polynomial_relation, \
|
|
19
19
|
sanity_check_stieltjes_branch, eval_P
|
|
20
20
|
from ._edge import evolve_edges, merge_edges
|
|
21
|
-
from ._decompress import decompress_newton
|
|
21
|
+
from ._decompress import build_time_grid, decompress_newton
|
|
22
22
|
from ._decompress2 import decompress_coeffs
|
|
23
23
|
from ._homotopy import StieltjesPoly
|
|
24
|
-
from .
|
|
24
|
+
from ._branch_points import compute_branch_points
|
|
25
|
+
from ._support import compute_support
|
|
25
26
|
from ._moments import MomentsESD
|
|
26
27
|
from .._free_form._support import supp
|
|
27
28
|
from .._free_form._plot_util import plot_density, plot_hilbert, plot_stieltjes
|
|
@@ -133,9 +134,6 @@ class AlgebraicForm(object):
|
|
|
133
134
|
# init
|
|
134
135
|
# ====
|
|
135
136
|
|
|
136
|
-
# def __init__(self, A, support=None, delta=1e-6, dtype='complex128',
|
|
137
|
-
# **kwargs):
|
|
138
|
-
|
|
139
137
|
def __init__(self, A, support=None, delta=1e-5, dtype='complex128',
|
|
140
138
|
**kwargs):
|
|
141
139
|
"""
|
|
@@ -147,6 +145,7 @@ class AlgebraicForm(object):
|
|
|
147
145
|
self._stieltjes = None
|
|
148
146
|
self._moments = None
|
|
149
147
|
self.support = support
|
|
148
|
+
self.est_support = None # Estimated from polynmial after fitting
|
|
150
149
|
self.delta = delta # Offset above real axis to apply Plemelj formula
|
|
151
150
|
|
|
152
151
|
# Data type for complex arrays
|
|
@@ -155,6 +154,7 @@ class AlgebraicForm(object):
|
|
|
155
154
|
if hasattr(A, 'stieltjes') and callable(getattr(A, 'stieltjes', None)):
|
|
156
155
|
# This is one of the distribution objects, like MarchenkoPastur
|
|
157
156
|
self._stieltjes = A.stieltjes
|
|
157
|
+
self.support = A.support()
|
|
158
158
|
self.n = 1
|
|
159
159
|
|
|
160
160
|
elif callable(A):
|
|
@@ -182,16 +182,15 @@ class AlgebraicForm(object):
|
|
|
182
182
|
numpy.mean(1.0/(self.eig-z[:, numpy.newaxis]), axis=-1)
|
|
183
183
|
self._moments = MomentsESD(self.eig) # NOTE (never used)
|
|
184
184
|
|
|
185
|
-
#
|
|
186
|
-
if support is None:
|
|
185
|
+
# broad support
|
|
186
|
+
if self.support is None:
|
|
187
187
|
if self.eig is None:
|
|
188
188
|
raise RuntimeError("Support must be provided without data")
|
|
189
|
+
|
|
189
190
|
# Detect support
|
|
190
191
|
self.lam_m, self.lam_p = supp(self.eig, **kwargs)
|
|
191
|
-
self.
|
|
192
|
-
self.broad_support = self.support[0]
|
|
192
|
+
self.broad_support = (self.lam_m, self.lam_p)
|
|
193
193
|
else:
|
|
194
|
-
self.support = support
|
|
195
194
|
self.lam_m = min([s[0] for s in self.support])
|
|
196
195
|
self.lam_p = max([s[1] for s in self.support])
|
|
197
196
|
self.broad_support = (self.lam_m, self.lam_p)
|
|
@@ -251,7 +250,16 @@ class AlgebraicForm(object):
|
|
|
251
250
|
# self.cache.clear()
|
|
252
251
|
|
|
253
252
|
z_fits = []
|
|
254
|
-
|
|
253
|
+
|
|
254
|
+
# Sampling around support, or broad_support. This is only needed to
|
|
255
|
+
# ensure sampled points are not hiting the support itself is not used
|
|
256
|
+
# in any computation. If support is not known, use broad support.
|
|
257
|
+
if self.support is not None:
|
|
258
|
+
possible_support = self.support
|
|
259
|
+
else:
|
|
260
|
+
possible_support = self.broad_support
|
|
261
|
+
|
|
262
|
+
for sup in possible_support:
|
|
255
263
|
a, b = sup
|
|
256
264
|
|
|
257
265
|
for i in range(len(r)):
|
|
@@ -261,7 +269,7 @@ class AlgebraicForm(object):
|
|
|
261
269
|
z_fit = numpy.concatenate(z_fits)
|
|
262
270
|
|
|
263
271
|
# Remove points too close to any cut
|
|
264
|
-
z_fit = filter_z_away_from_cuts(z_fit,
|
|
272
|
+
z_fit = filter_z_away_from_cuts(z_fit, possible_support, y_eps=y_eps,
|
|
265
273
|
x_pad=x_pad)
|
|
266
274
|
|
|
267
275
|
# Fitting (w_inf = None means adaptive weight selection)
|
|
@@ -271,11 +279,11 @@ class AlgebraicForm(object):
|
|
|
271
279
|
triangular=triangular, normalize=normalize, mu=mu,
|
|
272
280
|
mu_reg=mu_reg)
|
|
273
281
|
|
|
274
|
-
# Compute global branch points, zeros of leading a_j, and support
|
|
275
|
-
branch_points, a_s_zero, support = compute_singular_points(a_coeffs)
|
|
276
|
-
|
|
277
282
|
self.a_coeffs = a_coeffs
|
|
278
283
|
|
|
284
|
+
# Estimate support from the fitted polynomial
|
|
285
|
+
self.est_support, _ = self.estimate_support(a_coeffs)
|
|
286
|
+
|
|
279
287
|
# Reporting error
|
|
280
288
|
P_res = numpy.abs(eval_P(z_fit, m1_fit, a_coeffs))
|
|
281
289
|
res_max = numpy.max(P_res[numpy.isfinite(P_res)])
|
|
@@ -288,8 +296,6 @@ class AlgebraicForm(object):
|
|
|
288
296
|
eta=max(y_eps, 1e-2), n_x=128,
|
|
289
297
|
max_bad_frac=0.05)
|
|
290
298
|
|
|
291
|
-
status['branch_points'] = branch_points
|
|
292
|
-
status['a_s_zero'] = a_s_zero
|
|
293
299
|
status['res_max'] = float(res_max)
|
|
294
300
|
status['res_99_9'] = float(res_99_9)
|
|
295
301
|
status['fit_metrics'] = fit_metrics
|
|
@@ -319,7 +325,64 @@ class AlgebraicForm(object):
|
|
|
319
325
|
else:
|
|
320
326
|
print('\nStieltjes sanity check: OK')
|
|
321
327
|
|
|
322
|
-
return a_coeffs,
|
|
328
|
+
return a_coeffs, self.est_support, status
|
|
329
|
+
|
|
330
|
+
# =====================
|
|
331
|
+
# inflate broad support
|
|
332
|
+
# =====================
|
|
333
|
+
|
|
334
|
+
def _inflate_broad_support(self, inflate=0.0):
|
|
335
|
+
"""
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
min_supp, max_supp = self.broad_support
|
|
339
|
+
|
|
340
|
+
c_supp = 0.5 * (max_supp + min_supp)
|
|
341
|
+
r_supp = 0.5 * (max_supp - min_supp)
|
|
342
|
+
|
|
343
|
+
x_min = c_supp - r_supp * (1.0 + inflate)
|
|
344
|
+
x_max = c_supp + r_supp * (1.0 + inflate)
|
|
345
|
+
|
|
346
|
+
return x_min, x_max
|
|
347
|
+
|
|
348
|
+
# ================
|
|
349
|
+
# estimate support
|
|
350
|
+
# ================
|
|
351
|
+
|
|
352
|
+
def estimate_support(self, a_coeffs=None, n_scan=4000):
|
|
353
|
+
"""
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
if a_coeffs is None:
|
|
357
|
+
if self.a_coeffs is None:
|
|
358
|
+
raise RuntimeError('Call "fit" first.')
|
|
359
|
+
else:
|
|
360
|
+
a_coeffs = self.a_coeffs
|
|
361
|
+
|
|
362
|
+
# Inflate a bit to make sure all points are searched
|
|
363
|
+
x_min, x_max = self._inflate_broad_support(inflate=0.2)
|
|
364
|
+
|
|
365
|
+
est_support, info = compute_support(a_coeffs, x_min=x_min, x_max=x_max,
|
|
366
|
+
n_scan=n_scan)
|
|
367
|
+
|
|
368
|
+
return est_support, info
|
|
369
|
+
|
|
370
|
+
# ======================
|
|
371
|
+
# estimate branch points
|
|
372
|
+
# ======================
|
|
373
|
+
|
|
374
|
+
def estimate_branch_points(self):
|
|
375
|
+
"""
|
|
376
|
+
Compute global branch points and zeros of leading a_j
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
if self.a_coeffs is None:
|
|
380
|
+
raise RuntimeError('Call "fit" first.')
|
|
381
|
+
|
|
382
|
+
bp, leading_zeros, info = compute_branch_points(
|
|
383
|
+
self.a_coeffs, tol=1e-12, real_tol=None)
|
|
384
|
+
|
|
385
|
+
return bp, leading_zeros, info
|
|
323
386
|
|
|
324
387
|
# =============
|
|
325
388
|
# generate grid
|
|
@@ -464,7 +527,7 @@ class AlgebraicForm(object):
|
|
|
464
527
|
hilb = -self._stieltjes(x).real / numpy.pi
|
|
465
528
|
|
|
466
529
|
if plot:
|
|
467
|
-
plot_hilbert(x, hilb, support=self.
|
|
530
|
+
plot_hilbert(x, hilb, support=self.broad_support, latex=latex,
|
|
468
531
|
save=save)
|
|
469
532
|
|
|
470
533
|
return hilb
|
|
@@ -592,8 +655,9 @@ class AlgebraicForm(object):
|
|
|
592
655
|
|
|
593
656
|
def decompress(self, size, x=None, method='one', plot=False, latex=False,
|
|
594
657
|
save=False, verbose=False, newton_opt={
|
|
595
|
-
'
|
|
596
|
-
'
|
|
658
|
+
'min_n_times': 10, 'max_iter': 50, 'tol': 1e-12,
|
|
659
|
+
'armijo': 1e-4, 'min_lam': 1e-6, 'w_min': 1e-14,
|
|
660
|
+
'sweep': True}):
|
|
597
661
|
"""
|
|
598
662
|
Free decompression of spectral density.
|
|
599
663
|
"""
|
|
@@ -634,25 +698,23 @@ class AlgebraicForm(object):
|
|
|
634
698
|
# Query grid on the real axis + a small imaginary buffer
|
|
635
699
|
z_query = x + 1j * self.delta
|
|
636
700
|
|
|
637
|
-
# Initial condition at t=0 (physical branch)
|
|
701
|
+
# Initial condition at t = 0 (physical branch)
|
|
638
702
|
w0_list = self._stieltjes(z_query)
|
|
639
703
|
|
|
640
|
-
#
|
|
641
|
-
t =
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if t[0] > 1.0:
|
|
645
|
-
t = numpy.concatenate([numpy.zeros(1), t])
|
|
704
|
+
# Ensure there are at least min_n_times time t, including requested
|
|
705
|
+
# times, and especially time t = 0
|
|
706
|
+
t_all, idx_req = build_time_grid(
|
|
707
|
+
size, self.n, min_n_time=newton_opt.get("min_n_time", 0))
|
|
646
708
|
|
|
647
709
|
# Evolve
|
|
648
710
|
W, ok = decompress_newton(
|
|
649
|
-
z_query,
|
|
711
|
+
z_query, t_all, self.a_coeffs,
|
|
650
712
|
w0_list=w0_list, **newton_opt)
|
|
651
713
|
|
|
652
|
-
|
|
714
|
+
rho_all = W.imag / numpy.pi
|
|
653
715
|
|
|
654
|
-
#
|
|
655
|
-
rho =
|
|
716
|
+
# return only the user-requested ones
|
|
717
|
+
rho = rho_all[idx_req]
|
|
656
718
|
|
|
657
719
|
if verbose:
|
|
658
720
|
print("success rate per t:", ok.mean(axis=1))
|
|
@@ -699,10 +761,19 @@ class AlgebraicForm(object):
|
|
|
699
761
|
Evolves spectral edges.
|
|
700
762
|
"""
|
|
701
763
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
764
|
+
if self.support is not None:
|
|
765
|
+
known_support = self.support
|
|
766
|
+
elif self.est_support is not None:
|
|
767
|
+
known_support = self.est_support
|
|
768
|
+
else:
|
|
769
|
+
raise RuntimeError('Call "fit" first.')
|
|
770
|
+
|
|
771
|
+
edges, ok_edges = evolve_edges(t, self.a_coeffs,
|
|
772
|
+
support=known_support, eta=eta,
|
|
773
|
+
dt_max=dt_max, max_iter=max_iter,
|
|
774
|
+
tol=tol)
|
|
705
775
|
|
|
776
|
+
# Remove spurious edges, where two edge cross and are no longer valid.
|
|
706
777
|
edges2, active_k = merge_edges(edges, tol=1e-4)
|
|
707
778
|
|
|
708
779
|
if verbose:
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
freealg/__init__.py,sha256=SjcYb6HWmaclnnM-m1eC1honZRyfNBWYDYBx23kSdjo,833
|
|
2
|
-
freealg/__version__.py,sha256
|
|
2
|
+
freealg/__version__.py,sha256=wcKNUm4uZK839U_xaeQFOEaUVqbjHp-EU_6MMaml4qk,23
|
|
3
3
|
freealg/_util.py,sha256=RzccUCORgzrI9NdNqwMVugiHU0uDKkJFcIyjFMUOnv8,2518
|
|
4
4
|
freealg/_algebraic_form/__init__.py,sha256=MIB_jVgw2qI-JW_ypqaFSeNAB6c4GvpjNySnap_a6hg,398
|
|
5
|
+
freealg/_algebraic_form/_branch_points.py,sha256=jzvHszw7xFe9B15a5RZV3pGfCGtndvrKJ4GIX6F3qhc,7814
|
|
5
6
|
freealg/_algebraic_form/_constraints.py,sha256=37U7nvtCTocuS7l_nfUznkPi195PY7eXFzeiikrv3B0,2448
|
|
6
|
-
freealg/_algebraic_form/_continuation_algebraic.py,sha256=
|
|
7
|
-
freealg/_algebraic_form/_decompress.py,sha256=
|
|
7
|
+
freealg/_algebraic_form/_continuation_algebraic.py,sha256=vVHFlMJYeXm97pgwEceJB2rGJeGOVhk_Ywg6mjoIA-g,19390
|
|
8
|
+
freealg/_algebraic_form/_decompress.py,sha256=uKiq5jlwmOvGriptIDz97fQiKs_F10uH6eMX1Ix43PQ,22538
|
|
8
9
|
freealg/_algebraic_form/_decompress2.py,sha256=Ng9w9xmGe9M-DApp35IeNeQlvszfzT4NZx5BQn0lQ3I,2459
|
|
9
|
-
freealg/_algebraic_form/_discriminant.py,sha256=755pproom6-xThFARaH20m4GuBwwZS2rc0Y80Yg6NzY,5331
|
|
10
10
|
freealg/_algebraic_form/_edge.py,sha256=7l9QyLJDxaEY4WB6MCUFtfEZSf04wyHwH7YPHFJXSbM,10690
|
|
11
11
|
freealg/_algebraic_form/_homotopy.py,sha256=q5z8YmrT_8m7L3qw_4FD1Sd5eELIvAiAHr2ucOLW258,9508
|
|
12
12
|
freealg/_algebraic_form/_moments.py,sha256=u55RpvQhIMJFGsq8LZ3IlnTKxNgQPhwnPuYUS34YEyw,12400
|
|
13
13
|
freealg/_algebraic_form/_sheets_util.py,sha256=6OLzWQKu-gN8rxM2rbpbN8TjNZFmD8UJ-8t9kcZdkCo,4174
|
|
14
|
-
freealg/_algebraic_form/
|
|
14
|
+
freealg/_algebraic_form/_support.py,sha256=9go_3NjmesSW1e08CiDu8oflpGmAbsh9iZRidMvlARI,7951
|
|
15
|
+
freealg/_algebraic_form/algebraic_form.py,sha256=L5qJFYOX5Qm8LlrBv6YwyuTxROUdn6cCjfXoDklmrlQ,32962
|
|
15
16
|
freealg/_free_form/__init__.py,sha256=5cnSX7kHci3wKx6-BEFhmVY_NjjmQAq1JjWPTEqETTg,611
|
|
16
17
|
freealg/_free_form/_chebyshev.py,sha256=zkyVA8NLf7uUKlJdLz4ijd_SurdsqUgkA5nHGWSybaE,6916
|
|
17
18
|
freealg/_free_form/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
|
|
@@ -44,9 +45,9 @@ freealg/distributions/_wigner.py,sha256=epgx6ne6R_7to5j6-QsWIAVFJQFquWMmYgnZYMN4
|
|
|
44
45
|
freealg/visualization/__init__.py,sha256=NLq_zwueF7ytZ8sl8zLPqm-AODxxXNvfMozHGmmklcE,435
|
|
45
46
|
freealg/visualization/_glue_util.py,sha256=2oKnEYjUOS4OZfivmciVLauVr53kyHMwi6c2zRKilTQ,693
|
|
46
47
|
freealg/visualization/_rgb_hsv.py,sha256=rEskxXxSlKKxIrHRslVkgxHtD010L3ge9YtcVsOPl8E,3650
|
|
47
|
-
freealg-0.7.
|
|
48
|
-
freealg-0.7.
|
|
49
|
-
freealg-0.7.
|
|
50
|
-
freealg-0.7.
|
|
51
|
-
freealg-0.7.
|
|
52
|
-
freealg-0.7.
|
|
48
|
+
freealg-0.7.11.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
|
|
49
|
+
freealg-0.7.11.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
|
|
50
|
+
freealg-0.7.11.dist-info/METADATA,sha256=Cinwx4ei_4R4aY743wVBq_DreuX8KPlwARHCWT--AWo,5517
|
|
51
|
+
freealg-0.7.11.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
52
|
+
freealg-0.7.11.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
|
|
53
|
+
freealg-0.7.11.dist-info/RECORD,,
|
|
@@ -1,226 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|