freealg 0.1.12__py3-none-any.whl → 0.1.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freealg/__init__.py +2 -1
- freealg/__version__.py +1 -1
- freealg/_chebyshev.py +4 -5
- freealg/_decompress.py +218 -42
- freealg/_pade.py +23 -13
- freealg/_plot_util.py +6 -3
- freealg/_support.py +125 -24
- freealg/distributions/_kesten_mckay.py +13 -5
- freealg/distributions/_marchenko_pastur.py +10 -2
- freealg/distributions/_meixner.py +10 -2
- freealg/distributions/_wachter.py +10 -2
- freealg/distributions/_wigner.py +10 -2
- freealg/eigfree.py +120 -0
- freealg/freeform.py +84 -60
- {freealg-0.1.12.dist-info → freealg-0.1.14.dist-info}/METADATA +17 -7
- freealg-0.1.14.dist-info/RECORD +25 -0
- freealg-0.1.12.dist-info/RECORD +0 -24
- {freealg-0.1.12.dist-info → freealg-0.1.14.dist-info}/WHEEL +0 -0
- {freealg-0.1.12.dist-info → freealg-0.1.14.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.1.12.dist-info → freealg-0.1.14.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.1.12.dist-info → freealg-0.1.14.dist-info}/top_level.txt +0 -0
freealg/__init__.py
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
# under the terms of the license found in the LICENSE.txt file in the root
|
|
7
7
|
# directory of this source tree.
|
|
8
8
|
|
|
9
|
-
from .freeform import FreeForm
|
|
9
|
+
from .freeform import FreeForm
|
|
10
|
+
from .eigfree import eigfree
|
|
10
11
|
from . import distributions
|
|
11
12
|
|
|
12
13
|
__all__ = ['FreeForm', 'distributions', 'eigfree']
|
freealg/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.14"
|
freealg/_chebyshev.py
CHANGED
|
@@ -58,7 +58,6 @@ def chebyshev_sample_proj(eig, support, K=10, reg=0.0):
|
|
|
58
58
|
|
|
59
59
|
# Map to [–1,1] interval
|
|
60
60
|
t = (2 * eig - (lam_m + lam_p)) / (lam_p - lam_m)
|
|
61
|
-
N = eig.size
|
|
62
61
|
|
|
63
62
|
# Inner‐product norm of each U_k under w(t) = sqrt{1–t^2} is \\pi/2
|
|
64
63
|
norm = numpy.pi / 2
|
|
@@ -104,7 +103,7 @@ def chebyshev_kernel_proj(xs, pdf, support, K=10, reg=0.0):
|
|
|
104
103
|
|
|
105
104
|
for k in range(K + 1):
|
|
106
105
|
Pk = eval_chebyu(k, t) # U_k(t) on the grid
|
|
107
|
-
moment = numpy.trapezoid(Pk * pdf, xs)
|
|
106
|
+
moment = numpy.trapezoid(Pk * pdf, xs) # \int U_k(t) \rho(x) dx
|
|
108
107
|
|
|
109
108
|
if k == 0:
|
|
110
109
|
penalty = 0
|
|
@@ -226,12 +225,12 @@ def chebyshev_stieltjes(z, psi, support):
|
|
|
226
225
|
S = wynn_pade(psi_zero, J)
|
|
227
226
|
|
|
228
227
|
# build powers J^(k+1) for k=0..K
|
|
229
|
-
#K = len(psi) - 1
|
|
228
|
+
# K = len(psi) - 1
|
|
230
229
|
# shape: (..., K+1)
|
|
231
|
-
#Jpow = J[..., None] ** numpy.arange(1, K+2)
|
|
230
|
+
# Jpow = J[..., None] ** numpy.arange(1, K+2)
|
|
232
231
|
|
|
233
232
|
# sum psi_k * J^(k+1)
|
|
234
|
-
#S = numpy.sum(psi * Jpow, axis=-1)
|
|
233
|
+
# S = numpy.sum(psi * Jpow, axis=-1)
|
|
235
234
|
|
|
236
235
|
# assemble m(z)
|
|
237
236
|
m_z = -2 / span * numpy.pi * S
|
freealg/_decompress.py
CHANGED
|
@@ -11,17 +11,198 @@
|
|
|
11
11
|
# =======
|
|
12
12
|
|
|
13
13
|
import numpy
|
|
14
|
-
# from scipy.integrate import solve_ivp
|
|
15
14
|
|
|
16
15
|
__all__ = ['decompress', 'reverse_characteristics']
|
|
17
16
|
|
|
18
17
|
|
|
18
|
+
# =============
|
|
19
|
+
# secant method
|
|
20
|
+
# =============
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
|
|
24
|
+
alpha=0.5, max_bt=2, eps=1e-30, step_factor=5.0,
|
|
25
|
+
post_smooth=True, jump_tol=10.0, verbose=False):
|
|
26
|
+
"""
|
|
27
|
+
Solves :math:``f(z) = a`` for many starting points simultaneously
|
|
28
|
+
using the secant method in the complex plane.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
f : callable
|
|
33
|
+
Function that accepts and returns complex `ndarray`s.
|
|
34
|
+
|
|
35
|
+
z0, z1 : array_like
|
|
36
|
+
Two initial guesses. ``z1`` may be broadcast to ``z0``.
|
|
37
|
+
|
|
38
|
+
a : complex or array_like, optional
|
|
39
|
+
Right‑hand‑side targets (broadcasted to ``z0``). Defaults to ``0+0j``.
|
|
40
|
+
|
|
41
|
+
tol : float, optional
|
|
42
|
+
Convergence criterion on ``|f(z) - a|``. Defaults to ``1e-12``.
|
|
43
|
+
|
|
44
|
+
max_iter : int, optional
|
|
45
|
+
Maximum number of secant iterations. Defaults to ``100``.
|
|
46
|
+
|
|
47
|
+
alpha : float, optional
|
|
48
|
+
Back‑tracking shrink factor (``0 < alpha < 1``). Defaults to ``0.5``.
|
|
49
|
+
|
|
50
|
+
max_bt : int, optional
|
|
51
|
+
Maximum back‑tracking trials per iteration. Defaults to ``0``.
|
|
52
|
+
|
|
53
|
+
eps : float, optional
|
|
54
|
+
Safeguard added to tiny denominators. Defaults to ``1e-30``.
|
|
55
|
+
|
|
56
|
+
post_smooth : bool, optional
|
|
57
|
+
If True (default) run a single vectorised clean-up pass that
|
|
58
|
+
re-solves points whose final root differs from the *nearest*
|
|
59
|
+
neighbour by more than ``jump_tol`` times the local median jump.
|
|
60
|
+
|
|
61
|
+
jump_tol : float, optional
|
|
62
|
+
Sensitivity of the clean-up pass; larger tolerance implies fewer
|
|
63
|
+
re-solves.
|
|
64
|
+
|
|
65
|
+
verbose : bool, optional
|
|
66
|
+
If *True*, prints progress every 10 iterations.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
roots : ndarray
|
|
71
|
+
Estimated roots, shaped like the broadcast inputs.
|
|
72
|
+
residuals : ndarray
|
|
73
|
+
Final residuals ``|f(root) - a|``.
|
|
74
|
+
iterations : ndarray
|
|
75
|
+
Iteration count for each point.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# Broadcast inputs
|
|
79
|
+
z0, z1, a = numpy.broadcast_arrays(
|
|
80
|
+
numpy.asarray(z0, numpy.complex128),
|
|
81
|
+
numpy.asarray(z1, numpy.complex128),
|
|
82
|
+
numpy.asarray(a, numpy.complex128),
|
|
83
|
+
)
|
|
84
|
+
orig_shape = z0.shape
|
|
85
|
+
z0, z1, a = (x.ravel() for x in (z0, z1, a))
|
|
86
|
+
|
|
87
|
+
n_points = z0.size
|
|
88
|
+
roots = z1.copy()
|
|
89
|
+
iterations = numpy.zeros(n_points, dtype=int)
|
|
90
|
+
|
|
91
|
+
f0 = f(z0) - a
|
|
92
|
+
f1 = f(z1) - a
|
|
93
|
+
residuals = numpy.abs(f1)
|
|
94
|
+
converged = residuals < tol
|
|
95
|
+
|
|
96
|
+
# Entering main loop
|
|
97
|
+
for k in range(max_iter):
|
|
98
|
+
active = ~converged
|
|
99
|
+
if not active.any():
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
# Secant step
|
|
103
|
+
denom = f1 - f0
|
|
104
|
+
denom = numpy.where(numpy.abs(denom) < eps, denom + eps, denom)
|
|
105
|
+
dz = (z1 - z0) * f1 / denom
|
|
106
|
+
|
|
107
|
+
# Step-size limiter
|
|
108
|
+
prev_step = numpy.maximum(numpy.abs(z1 - z0), eps)
|
|
109
|
+
max_step = step_factor * prev_step
|
|
110
|
+
big = numpy.abs(dz) > max_step
|
|
111
|
+
dz[big] *= max_step[big] / numpy.abs(dz[big])
|
|
112
|
+
|
|
113
|
+
z2 = z1 - dz
|
|
114
|
+
f2 = f(z2) - a
|
|
115
|
+
|
|
116
|
+
# Line search by backtracking
|
|
117
|
+
worse = (numpy.abs(f2) >= numpy.abs(f1)) & active
|
|
118
|
+
if worse.any():
|
|
119
|
+
shrink = numpy.ones_like(dz)
|
|
120
|
+
for _ in range(max_bt):
|
|
121
|
+
shrink[worse] *= alpha
|
|
122
|
+
z_try = z1[worse] - shrink[worse] * dz[worse]
|
|
123
|
+
f_try = f(z_try) - a[worse]
|
|
124
|
+
|
|
125
|
+
improved = numpy.abs(f_try) < numpy.abs(f1[worse])
|
|
126
|
+
if not improved.any():
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
idx = numpy.flatnonzero(worse)[improved]
|
|
130
|
+
z2[idx], f2[idx] = z_try[improved], f_try[improved]
|
|
131
|
+
worse[idx] = False
|
|
132
|
+
if not worse.any():
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# Book‑keeping
|
|
136
|
+
newly_conv = (numpy.abs(f2) < tol) & active
|
|
137
|
+
converged[newly_conv] = True
|
|
138
|
+
iterations[newly_conv] = k + 1
|
|
139
|
+
roots[newly_conv] = z2[newly_conv]
|
|
140
|
+
residuals[newly_conv] = numpy.abs(f2[newly_conv])
|
|
141
|
+
|
|
142
|
+
still = active & ~newly_conv
|
|
143
|
+
z0[still], z1[still] = z1[still], z2[still]
|
|
144
|
+
f0[still], f1[still] = f1[still], f2[still]
|
|
145
|
+
|
|
146
|
+
if verbose and k % 10 == 0:
|
|
147
|
+
print(f"Iter {k}: {converged.sum()} / {n_points} converged")
|
|
148
|
+
|
|
149
|
+
# Non‑converged points
|
|
150
|
+
remaining = ~converged
|
|
151
|
+
roots[remaining] = z1[remaining]
|
|
152
|
+
residuals[remaining] = numpy.abs(f1[remaining])
|
|
153
|
+
iterations[remaining] = max_iter
|
|
154
|
+
|
|
155
|
+
# Optional clean-up pass
|
|
156
|
+
if post_smooth and n_points > 2:
|
|
157
|
+
# absolute jump to *nearest* neighbour (left or right)
|
|
158
|
+
diff_left = numpy.empty_like(roots)
|
|
159
|
+
diff_right = numpy.empty_like(roots)
|
|
160
|
+
diff_left[1:] = numpy.abs(roots[1:] - roots[:-1])
|
|
161
|
+
diff_right[:-1] = numpy.abs(roots[:-1] - roots[1:])
|
|
162
|
+
jump = numpy.minimum(diff_left, diff_right)
|
|
163
|
+
|
|
164
|
+
# ignore unconverged points
|
|
165
|
+
median_jump = numpy.median(jump[~remaining])
|
|
166
|
+
bad = (jump > jump_tol * median_jump) & ~remaining
|
|
167
|
+
|
|
168
|
+
if bad.any():
|
|
169
|
+
z_first_all = numpy.where(bad & (diff_left <= diff_right),
|
|
170
|
+
roots - diff_left,
|
|
171
|
+
roots + diff_right)
|
|
172
|
+
|
|
173
|
+
# keep only the offending indices
|
|
174
|
+
z_first = z_first_all[bad]
|
|
175
|
+
z_second = z_first + (roots[bad] - z_first) * 1e-2
|
|
176
|
+
|
|
177
|
+
# re-solve just the outliers in one vector call
|
|
178
|
+
new_root, new_res, new_iter = secant_complex(
|
|
179
|
+
f, z_first, z_second, a[bad],
|
|
180
|
+
tol=tol, max_iter=max_iter,
|
|
181
|
+
alpha=alpha, max_bt=max_bt,
|
|
182
|
+
eps=eps, step_factor=step_factor,
|
|
183
|
+
post_smooth=False, # avoid recursion
|
|
184
|
+
)
|
|
185
|
+
roots[bad] = new_root
|
|
186
|
+
residuals[bad] = new_res
|
|
187
|
+
iterations[bad] = iterations[bad] + new_iter
|
|
188
|
+
|
|
189
|
+
if verbose:
|
|
190
|
+
print(f"Clean-up: re-solved {bad.sum()} outliers")
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
roots.reshape(orig_shape),
|
|
194
|
+
residuals.reshape(orig_shape),
|
|
195
|
+
iterations.reshape(orig_shape),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
19
199
|
# ==========
|
|
20
200
|
# decompress
|
|
21
201
|
# ==========
|
|
22
202
|
|
|
23
|
-
|
|
24
|
-
|
|
203
|
+
|
|
204
|
+
def decompress(freeform, size, x=None, delta=1e-4, max_iter=500,
|
|
205
|
+
tolerance=1e-8):
|
|
25
206
|
"""
|
|
26
207
|
Free decompression of spectral density.
|
|
27
208
|
|
|
@@ -42,15 +223,11 @@ def decompress(freeform, size, x=None, delta=1e-6, iterations=500,
|
|
|
42
223
|
Size of the perturbation into the upper half plane for Plemelj's
|
|
43
224
|
formula.
|
|
44
225
|
|
|
45
|
-
|
|
46
|
-
Maximum number of
|
|
226
|
+
max_iter: int, default=500
|
|
227
|
+
Maximum number of secant method iterations.
|
|
47
228
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
tolerance: float, default=1e-4
|
|
52
|
-
Tolerance for the solution obtained by the Newton solver. Also
|
|
53
|
-
used for the finite difference approximation to the derivative.
|
|
229
|
+
tolerance: float, default=1e-12
|
|
230
|
+
Tolerance for the solution obtained by the secant method solver.
|
|
54
231
|
|
|
55
232
|
Returns
|
|
56
233
|
-------
|
|
@@ -85,56 +262,55 @@ def decompress(freeform, size, x=None, delta=1e-6, iterations=500,
|
|
|
85
262
|
alpha = size / freeform.n
|
|
86
263
|
m = freeform._eval_stieltjes
|
|
87
264
|
# Lower and upper bound on new support
|
|
88
|
-
hilb_lb = (1 / m(freeform.lam_m + delta * 1j)
|
|
89
|
-
hilb_ub = (1 / m(freeform.lam_p + delta * 1j)
|
|
265
|
+
hilb_lb = (1 / m(freeform.lam_m + delta * 1j)).real
|
|
266
|
+
hilb_ub = (1 / m(freeform.lam_p + delta * 1j)).real
|
|
90
267
|
lb = freeform.lam_m - (alpha - 1) * hilb_lb
|
|
91
268
|
ub = freeform.lam_p - (alpha - 1) * hilb_ub
|
|
92
269
|
|
|
93
270
|
# Create x if not given
|
|
94
|
-
|
|
271
|
+
on_grid = (x is None)
|
|
272
|
+
if on_grid:
|
|
95
273
|
radius = 0.5 * (ub - lb)
|
|
96
274
|
center = 0.5 * (ub + lb)
|
|
97
275
|
scale = 1.25
|
|
98
276
|
x_min = numpy.floor(center - radius * scale)
|
|
99
277
|
x_max = numpy.ceil(center + radius * scale)
|
|
100
278
|
x = numpy.linspace(x_min, x_max, 500)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return z + (1 / m(z)[1]) * (1 - alpha)
|
|
104
|
-
|
|
105
|
-
# Ensure that input is an array
|
|
106
|
-
x = numpy.asarray(x)
|
|
279
|
+
else:
|
|
280
|
+
x = numpy.asarray(x)
|
|
107
281
|
|
|
108
282
|
target = x + delta * 1j
|
|
283
|
+
if numpy.isclose(alpha, 1.0):
|
|
284
|
+
return freeform.density(x), x, freeform.support
|
|
109
285
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Broken Newton steps can produce a lot of warnings. Removing them
|
|
114
|
-
# for now.
|
|
115
|
-
with numpy.errstate(all='ignore'):
|
|
116
|
-
for _ in range(iterations):
|
|
117
|
-
objective = _char_z(z) - target
|
|
118
|
-
mask = numpy.abs(objective) >= tolerance
|
|
119
|
-
if not numpy.any(mask):
|
|
120
|
-
break
|
|
121
|
-
z_m = z[mask]
|
|
286
|
+
# Characteristic curve map
|
|
287
|
+
def _char_z(z):
|
|
288
|
+
return z + (1 / m(z)) * (1 - alpha)
|
|
122
289
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
dfdz[dfdz == 0] = 1.0
|
|
290
|
+
z0 = numpy.full(target.shape, numpy.mean(freeform.support) + .1j,
|
|
291
|
+
dtype=numpy.complex128)
|
|
292
|
+
z1 = z0 - .2j
|
|
127
293
|
|
|
128
|
-
|
|
129
|
-
|
|
294
|
+
roots, _, _ = secant_complex(
|
|
295
|
+
_char_z, z0, z1,
|
|
296
|
+
a=target,
|
|
297
|
+
tol=tolerance,
|
|
298
|
+
max_iter=max_iter
|
|
299
|
+
)
|
|
130
300
|
|
|
131
301
|
# Plemelj's formula
|
|
132
|
-
|
|
302
|
+
z = roots
|
|
303
|
+
char_s = m(z) / alpha
|
|
133
304
|
rho = numpy.maximum(0, char_s.imag / numpy.pi)
|
|
134
305
|
rho[numpy.isnan(rho) | numpy.isinf(rho)] = 0
|
|
135
|
-
|
|
306
|
+
if on_grid:
|
|
307
|
+
x, rho = x.ravel(), rho.ravel()
|
|
308
|
+
# dx = x[1] - x[0]
|
|
309
|
+
# left_idx, right_idx = support_from_density(dx, rho)
|
|
310
|
+
# x, rho = x[left_idx-1:right_idx+1], rho[left_idx-1:right_idx+1]
|
|
311
|
+
rho = rho / numpy.trapezoid(rho, x)
|
|
136
312
|
|
|
137
|
-
return rho, x, (lb, ub)
|
|
313
|
+
return rho.reshape(*x.shape), x, (lb, ub)
|
|
138
314
|
|
|
139
315
|
|
|
140
316
|
# =======================
|
|
@@ -152,7 +328,7 @@ def reverse_characteristics(freeform, z_inits, T, iterations=500,
|
|
|
152
328
|
m = freeform._eval_stieltjes
|
|
153
329
|
|
|
154
330
|
def _char_z(z, t):
|
|
155
|
-
return z + (1 / m(z)
|
|
331
|
+
return z + (1 / m(z)) * (1 - numpy.exp(t))
|
|
156
332
|
|
|
157
333
|
target_z, target_t = numpy.meshgrid(z_inits, t_eval)
|
|
158
334
|
|
freealg/_pade.py
CHANGED
|
@@ -236,9 +236,10 @@ def _eval_rational(z, c, D, poles, resid):
|
|
|
236
236
|
|
|
237
237
|
return c + D * z + term
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
#
|
|
241
|
-
#
|
|
239
|
+
|
|
240
|
+
# =========
|
|
241
|
+
# Wynn pade
|
|
242
|
+
# =========
|
|
242
243
|
|
|
243
244
|
@numba.jit(nopython=True, parallel=True)
|
|
244
245
|
def wynn_pade(coeffs, x):
|
|
@@ -248,48 +249,57 @@ def wynn_pade(coeffs, x):
|
|
|
248
249
|
returns a function handle that computes the Pade approximant at any x
|
|
249
250
|
using Wynn's epsilon algorithm.
|
|
250
251
|
|
|
251
|
-
Parameters
|
|
252
|
-
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
|
|
255
|
+
coeffs (list or array):
|
|
256
|
+
Coefficients [a0, a1, a2, ...] of the power series.
|
|
253
257
|
|
|
254
|
-
Returns
|
|
255
|
-
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
|
|
261
|
+
function:
|
|
262
|
+
A function approximant(x) that returns the approximated value f(x).
|
|
256
263
|
"""
|
|
264
|
+
|
|
257
265
|
# Number of coefficients
|
|
258
266
|
xn = x.ravel()
|
|
259
267
|
d = len(xn)
|
|
260
268
|
N = len(coeffs)
|
|
261
|
-
|
|
269
|
+
|
|
262
270
|
# Compute the partial sums s_n = sum_{i=0}^n a_i * x^i for n=0,...,N-1
|
|
263
271
|
eps = numpy.zeros((N+1, N, d), dtype=numpy.complex128)
|
|
264
272
|
for i in numba.prange(d):
|
|
265
273
|
partial_sum = 0.0
|
|
266
274
|
for n in range(N):
|
|
267
275
|
partial_sum += coeffs[n] * (xn[i] ** n)
|
|
268
|
-
eps[0,n,i] = partial_sum
|
|
276
|
+
eps[0, n, i] = partial_sum
|
|
269
277
|
|
|
270
278
|
for i in numba.prange(d):
|
|
271
279
|
for k in range(1, N+1):
|
|
272
280
|
for j in range(N - k):
|
|
273
|
-
delta = eps[k-1, j+1,i] - eps[k-1, j,i]
|
|
281
|
+
delta = eps[k-1, j+1, i] - eps[k-1, j, i]
|
|
274
282
|
if delta == 0:
|
|
275
283
|
rec_delta = numpy.inf
|
|
276
284
|
elif numpy.isinf(delta) or numpy.isnan(delta):
|
|
277
285
|
rec_delta = 0.0
|
|
278
286
|
else:
|
|
279
287
|
rec_delta = 1.0 / delta
|
|
280
|
-
eps[k,j,i] = rec_delta
|
|
288
|
+
eps[k, j, i] = rec_delta
|
|
281
289
|
if k > 1:
|
|
282
|
-
eps[k,j,i] += eps[k-2,j+1,i]
|
|
290
|
+
eps[k, j, i] += eps[k-2, j+1, i]
|
|
283
291
|
|
|
284
292
|
if (N % 2) == 0:
|
|
285
293
|
N -= 1
|
|
286
|
-
|
|
294
|
+
|
|
287
295
|
return eps[N-1, 0, :].reshape(x.shape)
|
|
288
296
|
|
|
297
|
+
|
|
289
298
|
# ========
|
|
290
299
|
# fit pade
|
|
291
300
|
# ========
|
|
292
301
|
|
|
302
|
+
|
|
293
303
|
def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
|
|
294
304
|
safety=1.0, max_outer=40, xtol=1e-12, ftol=1e-12, optimizer='ls',
|
|
295
305
|
verbose=0):
|
freealg/_plot_util.py
CHANGED
|
@@ -139,7 +139,7 @@ def _auto_bins(array, method='scott', factor=5):
|
|
|
139
139
|
# ============
|
|
140
140
|
|
|
141
141
|
def plot_density(x, rho, eig=None, support=None, label='',
|
|
142
|
-
title='Spectral
|
|
142
|
+
title='Spectral Density', latex=False, save=False):
|
|
143
143
|
"""
|
|
144
144
|
"""
|
|
145
145
|
|
|
@@ -147,8 +147,11 @@ def plot_density(x, rho, eig=None, support=None, label='',
|
|
|
147
147
|
|
|
148
148
|
fig, ax = plt.subplots(figsize=(6, 2.7))
|
|
149
149
|
|
|
150
|
-
if
|
|
151
|
-
|
|
150
|
+
if eig is not None:
|
|
151
|
+
if support is not None:
|
|
152
|
+
lam_m, lam_p = support
|
|
153
|
+
else:
|
|
154
|
+
lam_m, lam_p = min(eig), max(eig)
|
|
152
155
|
bins = numpy.linspace(lam_m, lam_p, _auto_bins(eig))
|
|
153
156
|
_ = ax.hist(eig, bins, density=True, color='silver',
|
|
154
157
|
edgecolor='none', label='Histogram')
|
freealg/_support.py
CHANGED
|
@@ -1,31 +1,126 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# SPDX-FileType: SOURCE
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
# the terms of the license found in the LICENSE.txt file in the root directory
|
|
6
|
+
# of this source tree.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# =======
|
|
10
|
+
# Imports
|
|
11
|
+
# =======
|
|
12
|
+
|
|
1
13
|
import numpy
|
|
14
|
+
import numba
|
|
2
15
|
from scipy.stats import gaussian_kde
|
|
3
16
|
|
|
4
|
-
|
|
17
|
+
|
|
18
|
+
@numba.njit(numba.types.UniTuple(numba.types.int64, 2)(
|
|
19
|
+
numba.types.float64,
|
|
20
|
+
numba.types.float64[::1]
|
|
21
|
+
))
|
|
22
|
+
def support_from_density(dx, density):
|
|
23
|
+
"""
|
|
24
|
+
Estimates the support from a collection of noisy observations of a
|
|
25
|
+
density over a grid of x-values with mesh spacing dx.
|
|
26
|
+
"""
|
|
27
|
+
n = density.shape[0]
|
|
28
|
+
target = 1.0 / dx
|
|
29
|
+
|
|
30
|
+
# 1) compute total_sum once
|
|
31
|
+
total_sum = 0.0
|
|
32
|
+
for t in range(n):
|
|
33
|
+
total_sum += density[t]
|
|
34
|
+
|
|
35
|
+
# 2) set up our “best‐so‐far” trackers
|
|
36
|
+
large = 1e300
|
|
37
|
+
best_nonneg_sum = large
|
|
38
|
+
best_nonneg_idx = -1
|
|
39
|
+
best_nonpos_sum = -large
|
|
40
|
+
best_nonpos_idx = -1
|
|
41
|
+
|
|
42
|
+
# 3) seed with first element (i.e. prefix_sum for k=1)
|
|
43
|
+
prefix_sum = density[0]
|
|
44
|
+
if prefix_sum >= 0.0:
|
|
45
|
+
best_nonneg_sum, best_nonneg_idx = prefix_sum, 1
|
|
46
|
+
else:
|
|
47
|
+
best_nonpos_sum, best_nonpos_idx = prefix_sum, 1
|
|
48
|
+
|
|
49
|
+
# 4) sweep j from 2...n–1, updating prefix_sum on the fly
|
|
50
|
+
optimal_i, optimal_j = 1, 2
|
|
51
|
+
minimal_cost = large
|
|
52
|
+
|
|
53
|
+
for j in range(2, n):
|
|
54
|
+
# extend prefix_sum to cover density[0]...density[j-1]
|
|
55
|
+
prefix_sum += density[j-1]
|
|
56
|
+
|
|
57
|
+
# cost for [0...i], [i...j]
|
|
58
|
+
diff_mid = prefix_sum - target
|
|
59
|
+
if diff_mid >= 0.0 and best_nonneg_sum <= diff_mid:
|
|
60
|
+
cost12 = diff_mid
|
|
61
|
+
i_cand = best_nonneg_idx
|
|
62
|
+
elif diff_mid < 0.0 and best_nonpos_sum >= diff_mid:
|
|
63
|
+
cost12 = -diff_mid
|
|
64
|
+
i_cand = best_nonpos_idx
|
|
65
|
+
else:
|
|
66
|
+
cost_using_nonpos = diff_mid - 2.0 * best_nonpos_sum
|
|
67
|
+
cost_using_nonneg = 2.0 * best_nonneg_sum - diff_mid
|
|
68
|
+
if cost_using_nonpos < cost_using_nonneg:
|
|
69
|
+
cost12, i_cand = cost_using_nonpos, best_nonpos_idx
|
|
70
|
+
else:
|
|
71
|
+
cost12, i_cand = cost_using_nonneg, best_nonneg_idx
|
|
72
|
+
|
|
73
|
+
# cost for [j...n]
|
|
74
|
+
cost3 = total_sum - prefix_sum
|
|
75
|
+
if cost3 < 0.0:
|
|
76
|
+
cost3 = -cost3
|
|
77
|
+
|
|
78
|
+
# total and maybe update best split
|
|
79
|
+
total_cost = cost12 + cost3
|
|
80
|
+
if total_cost < minimal_cost:
|
|
81
|
+
minimal_cost = total_cost
|
|
82
|
+
optimal_i, optimal_j = i_cand, j
|
|
83
|
+
|
|
84
|
+
# update our prefix‐sum trackers
|
|
85
|
+
if prefix_sum >= 0.0:
|
|
86
|
+
if prefix_sum < best_nonneg_sum:
|
|
87
|
+
best_nonneg_sum, best_nonneg_idx = prefix_sum, j
|
|
88
|
+
else:
|
|
89
|
+
if prefix_sum > best_nonpos_sum:
|
|
90
|
+
best_nonpos_sum, best_nonpos_idx = prefix_sum, j
|
|
91
|
+
|
|
92
|
+
return optimal_i, optimal_j
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def detect_support(eigs, method='asymp', k=None, p=0.001, **kwargs):
|
|
5
96
|
"""
|
|
6
97
|
Estimates the support of the eigenvalue density.
|
|
7
98
|
|
|
8
99
|
Parameters
|
|
9
100
|
----------
|
|
10
|
-
method : {``'range'``, ``'
|
|
11
|
-
``'interior_smooth'``}, \
|
|
12
|
-
default= ``'
|
|
101
|
+
method : {``'range'``, ``'asymp'``, ``'jackknife'``, ``'regression'``,
|
|
102
|
+
``'interior'``, ``'interior_smooth'``}, \
|
|
103
|
+
default= ``'asymp'``
|
|
13
104
|
The method of support estimation:
|
|
14
105
|
|
|
15
|
-
* ``'range'``: no estimation; the support is the range of the
|
|
106
|
+
* ``'range'``: no estimation; the support is the range of the
|
|
107
|
+
eigenvalues.
|
|
108
|
+
* ``'asymp'``: assume the relative error in the min/max estimator is
|
|
109
|
+
1/n.
|
|
16
110
|
* ``'jackknife'``: estimates the support using Quenouille's [1]
|
|
17
|
-
jackknife estimator. Fast and simple, more accurate than the
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
111
|
+
jackknife estimator. Fast and simple, more accurate than the
|
|
112
|
+
range.
|
|
113
|
+
* ``'regression'``: estimates the support by performing a regression
|
|
114
|
+
under the assumption that the edge behavior is of square-root
|
|
115
|
+
type. Often most accurate.
|
|
21
116
|
* ``'interior'``: estimates a support assuming the range overestimates;
|
|
22
117
|
uses quantiles (p, 1-p).
|
|
23
|
-
* ``'interior_smooth'``: same as ``'interior'`` but using kernel
|
|
24
|
-
estimation.
|
|
118
|
+
* ``'interior_smooth'``: same as ``'interior'`` but using kernel
|
|
119
|
+
density estimation.
|
|
25
120
|
|
|
26
121
|
k : int, default = None
|
|
27
|
-
Number of extreme order statistics to use for ``method='regression'``.
|
|
28
|
-
|
|
122
|
+
Number of extreme order statistics to use for ``method='regression'``.
|
|
123
|
+
|
|
29
124
|
p : float, default=0.001
|
|
30
125
|
The edges of the support of the distribution is detected by the
|
|
31
126
|
:math:`p`-quantile on the left and :math:`(1-p)`-quantile on the right
|
|
@@ -36,28 +131,33 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
|
|
|
36
131
|
References
|
|
37
132
|
----------
|
|
38
133
|
|
|
39
|
-
.. [1] Quenouille, M. H. (1949, July). Approximate tests of correlation in
|
|
40
|
-
|
|
41
|
-
|
|
134
|
+
.. [1] Quenouille, M. H. (1949, July). Approximate tests of correlation in
|
|
135
|
+
time-series. In Mathematical Proceedings of the Cambridge
|
|
136
|
+
Philosophical Society (Vol. 45, No. 3, pp. 483-484). Cambridge
|
|
137
|
+
University Press.
|
|
42
138
|
"""
|
|
43
139
|
|
|
44
|
-
if method=='range':
|
|
140
|
+
if method == 'range':
|
|
45
141
|
lam_m = eigs.min()
|
|
46
142
|
lam_p = eigs.max()
|
|
47
143
|
|
|
48
|
-
elif method=='
|
|
144
|
+
elif method == 'asymp':
|
|
145
|
+
lam_m = eigs.min() - abs(eigs.min()) / len(eigs)
|
|
146
|
+
lam_p = eigs.max() + abs(eigs.max()) / len(eigs)
|
|
147
|
+
|
|
148
|
+
elif method == 'jackknife':
|
|
49
149
|
x, n = numpy.sort(eigs), len(eigs)
|
|
50
|
-
lam_m = x[0]
|
|
150
|
+
lam_m = x[0] - (n - 1)/n * (x[1] - x[0])
|
|
51
151
|
lam_p = x[-1] + (n - 1)/n * (x[-1] - x[-2])
|
|
52
152
|
|
|
53
|
-
elif method=='regression':
|
|
153
|
+
elif method == 'regression':
|
|
54
154
|
x, n = numpy.sort(eigs), len(eigs)
|
|
55
155
|
if k is None:
|
|
56
156
|
k = int(round(n ** (2/3)))
|
|
57
157
|
k = max(5, min(k, n // 2))
|
|
58
158
|
|
|
59
159
|
# The theoretical cdf near the edge behaves like const*(x - a)^{3/2},
|
|
60
|
-
# so (i/n)
|
|
160
|
+
# so (i/n) ~ (x - a)^{3/2} -> x ~ a + const*(i/n)^{2/3}.
|
|
61
161
|
y = ((numpy.arange(1, k + 1) - 0.5) / n) ** (2 / 3)
|
|
62
162
|
|
|
63
163
|
# Left edge: regress x_{(i)} on y
|
|
@@ -66,10 +166,10 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
|
|
|
66
166
|
# Right edge: regress x_{(n-i+1)} on y
|
|
67
167
|
_, lam_p = numpy.polyfit(y, x[-k:][::-1], 1)
|
|
68
168
|
|
|
69
|
-
elif method=='interior':
|
|
169
|
+
elif method == 'interior':
|
|
70
170
|
lam_m, lam_p = numpy.quantile(eigs, [p, 1-p])
|
|
71
|
-
|
|
72
|
-
elif method=='interior_smooth':
|
|
171
|
+
|
|
172
|
+
elif method == 'interior_smooth':
|
|
73
173
|
kde = gaussian_kde(eigs)
|
|
74
174
|
xs = numpy.linspace(eigs.min(), eigs.max(), 1000)
|
|
75
175
|
fs = kde(xs)
|
|
@@ -79,6 +179,7 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
|
|
|
79
179
|
|
|
80
180
|
lam_m = numpy.interp(p, cdf, xs)
|
|
81
181
|
lam_p = numpy.interp(1-p, cdf, xs)
|
|
182
|
+
|
|
82
183
|
else:
|
|
83
184
|
raise NotImplementedError("Unknown method")
|
|
84
185
|
|