freealg 0.1.9__tar.gz → 0.1.11__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.1.9 → freealg-0.1.11}/PKG-INFO +20 -7
- {freealg-0.1.9 → freealg-0.1.11}/README.rst +19 -6
- {freealg-0.1.9 → freealg-0.1.11}/freealg/__init__.py +3 -2
- freealg-0.1.11/freealg/__version__.py +1 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_chebyshev.py +12 -8
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_decompress.py +15 -15
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_pade.py +50 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_plot_util.py +55 -2
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_sample.py +8 -5
- freealg-0.1.11/freealg/_support.py +85 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_util.py +1 -1
- {freealg-0.1.9 → freealg-0.1.11}/freealg/distributions/__init__.py +5 -5
- freealg-0.1.9/freealg/distributions/kesten_mckay.py → freealg-0.1.11/freealg/distributions/_kesten_mckay.py +2 -1
- freealg-0.1.9/freealg/distributions/marchenko_pastur.py → freealg-0.1.11/freealg/distributions/_marchenko_pastur.py +4 -2
- freealg-0.1.9/freealg/distributions/meixner.py → freealg-0.1.11/freealg/distributions/_meixner.py +2 -1
- freealg-0.1.9/freealg/distributions/wachter.py → freealg-0.1.11/freealg/distributions/_wachter.py +4 -2
- freealg-0.1.9/freealg/distributions/wigner.py → freealg-0.1.11/freealg/distributions/_wigner.py +4 -2
- {freealg-0.1.9 → freealg-0.1.11}/freealg/freeform.py +181 -147
- {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/PKG-INFO +20 -7
- {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/SOURCES.txt +6 -5
- freealg-0.1.9/freealg/__version__.py +0 -1
- {freealg-0.1.9 → freealg-0.1.11}/AUTHORS.txt +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/CHANGELOG.rst +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/LICENSE.txt +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/MANIFEST.in +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_damp.py +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg/_jacobi.py +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/dependency_links.txt +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/not-zip-safe +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/requires.txt +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/top_level.txt +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/pyproject.toml +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/requirements.txt +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/setup.cfg +0 -0
- {freealg-0.1.9 → freealg-0.1.11}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freealg
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Free probability for large matrices
|
|
5
5
|
Keywords: leaderboard bot chat
|
|
6
6
|
Platform: Linux
|
|
@@ -69,7 +69,10 @@ Dynamic: summary
|
|
|
69
69
|
:width: 240
|
|
70
70
|
:class: custom-dark
|
|
71
71
|
|
|
72
|
-
*freealg* is a
|
|
72
|
+
*freealg* is a Python package that employs **free** probability to evaluate the spectral
|
|
73
|
+
densities of large matrix **form**\ s. The fundamental algorithm employed by *freealg* is
|
|
74
|
+
**free decompression**, which extrapolates from the empirical spectral densities of small
|
|
75
|
+
submatrices to infer the eigenspectrum of extremely large matrices.
|
|
73
76
|
|
|
74
77
|
Install
|
|
75
78
|
=======
|
|
@@ -95,12 +98,18 @@ Documentation is available at `ameli.github.io/freealg <https://ameli.github.io/
|
|
|
95
98
|
Quick Usage
|
|
96
99
|
===========
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
The following code estimates the eigenvalues of a very large Wishart matrix using a much
|
|
102
|
+
smaller Wishart matrix.
|
|
100
103
|
|
|
101
104
|
.. code-block:: python
|
|
102
105
|
|
|
103
106
|
>>> import freealg as fa
|
|
107
|
+
>>> mp = fa.distributions.MarchenkoPastur(1/50) # Wishart matrices with aspect ratio 1/50
|
|
108
|
+
>>> A = mp.matrix(1000) # Sample a 1000 x 1000 Wishart matrix
|
|
109
|
+
>>> eigs = fa.eigfree(A, 100_000) # Estimate the eigenvalues of 100000 x 100000
|
|
110
|
+
|
|
111
|
+
For more details on how to interface with *freealg* check out the `Quick Start Guide <https://github.com/ameli/freealg/blob/main/notebooks/quick_start.ipynb>`.
|
|
112
|
+
|
|
104
113
|
|
|
105
114
|
Test
|
|
106
115
|
====
|
|
@@ -130,14 +139,18 @@ requests and bug reports.
|
|
|
130
139
|
How to Cite
|
|
131
140
|
===========
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994>`.
|
|
134
143
|
|
|
135
144
|
.. code::
|
|
136
145
|
|
|
137
|
-
@
|
|
138
|
-
|
|
146
|
+
@article{ameli2025spectral,
|
|
147
|
+
title={Spectral Estimation with Free Decompression},
|
|
148
|
+
author={Siavash Ameli and Chris van der Heide and Liam Hodgkinson and Michael W. Mahoney},
|
|
149
|
+
journal={arXiv preprint arXiv:2506.11994},
|
|
150
|
+
year={2025}
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
|
|
141
154
|
License
|
|
142
155
|
=======
|
|
143
156
|
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
:width: 240
|
|
4
4
|
:class: custom-dark
|
|
5
5
|
|
|
6
|
-
*freealg* is a
|
|
6
|
+
*freealg* is a Python package that employs **free** probability to evaluate the spectral
|
|
7
|
+
densities of large matrix **form**\ s. The fundamental algorithm employed by *freealg* is
|
|
8
|
+
**free decompression**, which extrapolates from the empirical spectral densities of small
|
|
9
|
+
submatrices to infer the eigenspectrum of extremely large matrices.
|
|
7
10
|
|
|
8
11
|
Install
|
|
9
12
|
=======
|
|
@@ -29,12 +32,18 @@ Documentation is available at `ameli.github.io/freealg <https://ameli.github.io/
|
|
|
29
32
|
Quick Usage
|
|
30
33
|
===========
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
The following code estimates the eigenvalues of a very large Wishart matrix using a much
|
|
36
|
+
smaller Wishart matrix.
|
|
34
37
|
|
|
35
38
|
.. code-block:: python
|
|
36
39
|
|
|
37
40
|
>>> import freealg as fa
|
|
41
|
+
>>> mp = fa.distributions.MarchenkoPastur(1/50) # Wishart matrices with aspect ratio 1/50
|
|
42
|
+
>>> A = mp.matrix(1000) # Sample a 1000 x 1000 Wishart matrix
|
|
43
|
+
>>> eigs = fa.eigfree(A, 100_000) # Estimate the eigenvalues of 100000 x 100000
|
|
44
|
+
|
|
45
|
+
For more details on how to interface with *freealg* check out the `Quick Start Guide <https://github.com/ameli/freealg/blob/main/notebooks/quick_start.ipynb>`.
|
|
46
|
+
|
|
38
47
|
|
|
39
48
|
Test
|
|
40
49
|
====
|
|
@@ -64,14 +73,18 @@ requests and bug reports.
|
|
|
64
73
|
How to Cite
|
|
65
74
|
===========
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994>`.
|
|
68
77
|
|
|
69
78
|
.. code::
|
|
70
79
|
|
|
71
|
-
@
|
|
72
|
-
|
|
80
|
+
@article{ameli2025spectral,
|
|
81
|
+
title={Spectral Estimation with Free Decompression},
|
|
82
|
+
author={Siavash Ameli and Chris van der Heide and Liam Hodgkinson and Michael W. Mahoney},
|
|
83
|
+
journal={arXiv preprint arXiv:2506.11994},
|
|
84
|
+
year={2025}
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
|
|
75
88
|
License
|
|
76
89
|
=======
|
|
77
90
|
|
|
@@ -6,8 +6,9 @@
|
|
|
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, eigfree
|
|
10
|
+
from . import distributions
|
|
10
11
|
|
|
11
|
-
__all__ = ['FreeForm']
|
|
12
|
+
__all__ = ['FreeForm', 'distributions', 'eigfree']
|
|
12
13
|
|
|
13
14
|
from .__version__ import __version__ # noqa: F401 E402
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.11"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import numpy
|
|
15
15
|
from scipy.special import eval_chebyu
|
|
16
|
+
from ._pade import wynn_pade
|
|
16
17
|
|
|
17
18
|
__all__ = ['chebyshev_sample_proj', 'chebyshev_kernel_proj',
|
|
18
19
|
'chebyshev_approx', 'chebyshev_stieltjes']
|
|
@@ -66,7 +67,7 @@ def chebyshev_sample_proj(eig, support, K=10, reg=0.0):
|
|
|
66
67
|
for k in range(K+1):
|
|
67
68
|
|
|
68
69
|
# empirical moment M_k = (1/N) \\sum U_k(t_i)
|
|
69
|
-
M_k = numpy.
|
|
70
|
+
M_k = numpy.mean(eval_chebyu(k, t))
|
|
70
71
|
|
|
71
72
|
# Regularization
|
|
72
73
|
if k == 0:
|
|
@@ -103,7 +104,7 @@ def chebyshev_kernel_proj(xs, pdf, support, K=10, reg=0.0):
|
|
|
103
104
|
|
|
104
105
|
for k in range(K + 1):
|
|
105
106
|
Pk = eval_chebyu(k, t) # U_k(t) on the grid
|
|
106
|
-
moment = numpy.
|
|
107
|
+
moment = numpy.trapezoid(Pk * pdf, xs) # \int U_k(t) \rho(x) dx
|
|
107
108
|
|
|
108
109
|
if k == 0:
|
|
109
110
|
penalty = 0
|
|
@@ -218,18 +219,21 @@ def chebyshev_stieltjes(z, psi, support):
|
|
|
218
219
|
Jp = u + root
|
|
219
220
|
|
|
220
221
|
# Make sure J is Herglotz
|
|
221
|
-
J = numpy.zeros_like(
|
|
222
|
-
J = numpy.where(
|
|
222
|
+
J = numpy.zeros_like(Jm)
|
|
223
|
+
J = numpy.where(root.imag < 0, Jp, Jm)
|
|
224
|
+
|
|
225
|
+
psi_zero = numpy.concatenate([[0], psi])
|
|
226
|
+
S = wynn_pade(psi_zero, J)
|
|
223
227
|
|
|
224
228
|
# build powers J^(k+1) for k=0..K
|
|
225
|
-
K = len(psi) - 1
|
|
229
|
+
#K = len(psi) - 1
|
|
226
230
|
# shape: (..., K+1)
|
|
227
|
-
Jpow = J[..., None] ** numpy.arange(1, K+2)
|
|
231
|
+
#Jpow = J[..., None] ** numpy.arange(1, K+2)
|
|
228
232
|
|
|
229
233
|
# sum psi_k * J^(k+1)
|
|
230
|
-
S = numpy.sum(psi * Jpow, axis=-1)
|
|
234
|
+
#S = numpy.sum(psi * Jpow, axis=-1)
|
|
231
235
|
|
|
232
236
|
# assemble m(z)
|
|
233
|
-
m_z = -
|
|
237
|
+
m_z = -2 / span * numpy.pi * S
|
|
234
238
|
|
|
235
239
|
return m_z
|
|
@@ -20,16 +20,16 @@ __all__ = ['decompress', 'reverse_characteristics']
|
|
|
20
20
|
# decompress
|
|
21
21
|
# ==========
|
|
22
22
|
|
|
23
|
-
def decompress(
|
|
24
|
-
tolerance=1e-4):
|
|
23
|
+
def decompress(freeform, size, x=None, delta=1e-4, iterations=500,
|
|
24
|
+
step_size=0.1, tolerance=1e-4):
|
|
25
25
|
"""
|
|
26
26
|
Free decompression of spectral density.
|
|
27
27
|
|
|
28
28
|
Parameters
|
|
29
29
|
----------
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
The initial matrix to be decompressed
|
|
31
|
+
freeform : FreeForm
|
|
32
|
+
The initial freeform object of matrix to be decompressed
|
|
33
33
|
|
|
34
34
|
size : int
|
|
35
35
|
Size of the decompressed matrix.
|
|
@@ -82,13 +82,13 @@ def decompress(matrix, size, x=None, delta=1e-4, iterations=500, step_size=0.1,
|
|
|
82
82
|
>>> from freealg import FreeForm
|
|
83
83
|
"""
|
|
84
84
|
|
|
85
|
-
alpha = size /
|
|
86
|
-
m =
|
|
85
|
+
alpha = size / freeform.n
|
|
86
|
+
m = freeform._eval_stieltjes
|
|
87
87
|
# Lower and upper bound on new support
|
|
88
|
-
hilb_lb = (1 / m(
|
|
89
|
-
hilb_ub = (1 / m(
|
|
90
|
-
lb =
|
|
91
|
-
ub =
|
|
88
|
+
hilb_lb = (1 / m(freeform.lam_m + delta * 1j)[1]).real
|
|
89
|
+
hilb_ub = (1 / m(freeform.lam_p + delta * 1j)[1]).real
|
|
90
|
+
lb = freeform.lam_m - (alpha - 1) * hilb_lb
|
|
91
|
+
ub = freeform.lam_p - (alpha - 1) * hilb_ub
|
|
92
92
|
|
|
93
93
|
# Create x if not given
|
|
94
94
|
if x is None:
|
|
@@ -107,7 +107,7 @@ def decompress(matrix, size, x=None, delta=1e-4, iterations=500, step_size=0.1,
|
|
|
107
107
|
|
|
108
108
|
target = x + delta * 1j
|
|
109
109
|
|
|
110
|
-
z = numpy.full(target.shape, numpy.mean(
|
|
110
|
+
z = numpy.full(target.shape, numpy.mean(freeform.support) - .1j,
|
|
111
111
|
dtype=numpy.complex128)
|
|
112
112
|
|
|
113
113
|
# Broken Newton steps can produce a lot of warnings. Removing them
|
|
@@ -141,22 +141,22 @@ def decompress(matrix, size, x=None, delta=1e-4, iterations=500, step_size=0.1,
|
|
|
141
141
|
# reverse characteristics
|
|
142
142
|
# =======================
|
|
143
143
|
|
|
144
|
-
def reverse_characteristics(
|
|
145
|
-
tolerance=1e-8):
|
|
144
|
+
def reverse_characteristics(freeform, z_inits, T, iterations=500,
|
|
145
|
+
step_size=0.1, tolerance=1e-8):
|
|
146
146
|
"""
|
|
147
147
|
"""
|
|
148
148
|
|
|
149
149
|
t_span = (0, T)
|
|
150
150
|
t_eval = numpy.linspace(t_span[0], t_span[1], 50)
|
|
151
151
|
|
|
152
|
-
m =
|
|
152
|
+
m = freeform._eval_stieltjes
|
|
153
153
|
|
|
154
154
|
def _char_z(z, t):
|
|
155
155
|
return z + (1 / m(z)[1]) * (1 - numpy.exp(t))
|
|
156
156
|
|
|
157
157
|
target_z, target_t = numpy.meshgrid(z_inits, t_eval)
|
|
158
158
|
|
|
159
|
-
z = numpy.full(target_z.shape, numpy.mean(
|
|
159
|
+
z = numpy.full(target_z.shape, numpy.mean(freeform.support) - .1j,
|
|
160
160
|
dtype=numpy.complex128)
|
|
161
161
|
|
|
162
162
|
# Broken Newton steps can produce a lot of warnings. Removing them for now.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# =======
|
|
13
13
|
|
|
14
14
|
import numpy
|
|
15
|
+
import numba
|
|
15
16
|
from numpy.linalg import lstsq
|
|
16
17
|
from itertools import product
|
|
17
18
|
from scipy.optimize import least_squares, differential_evolution
|
|
@@ -235,6 +236,55 @@ def _eval_rational(z, c, D, poles, resid):
|
|
|
235
236
|
|
|
236
237
|
return c + D * z + term
|
|
237
238
|
|
|
239
|
+
# ========
|
|
240
|
+
# Wynn epsilon algorithm for Pade
|
|
241
|
+
# ========
|
|
242
|
+
|
|
243
|
+
@numba.jit(nopython=True, parallel=True)
|
|
244
|
+
def wynn_pade(coeffs, x):
|
|
245
|
+
"""
|
|
246
|
+
Given the coefficients of a power series
|
|
247
|
+
f(x) = sum_{n=0}^∞ coeffs[n] * x^n,
|
|
248
|
+
returns a function handle that computes the Pade approximant at any x
|
|
249
|
+
using Wynn's epsilon algorithm.
|
|
250
|
+
|
|
251
|
+
Parameters:
|
|
252
|
+
coeffs (list or array): Coefficients [a0, a1, a2, ...] of the power series.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
function: A function approximant(x) that returns the approximated value f(x).
|
|
256
|
+
"""
|
|
257
|
+
# Number of coefficients
|
|
258
|
+
xn = x.ravel()
|
|
259
|
+
d = len(xn)
|
|
260
|
+
N = len(coeffs)
|
|
261
|
+
|
|
262
|
+
# Compute the partial sums s_n = sum_{i=0}^n a_i * x^i for n=0,...,N-1
|
|
263
|
+
eps = numpy.zeros((N+1, N, d), dtype=numpy.complex128)
|
|
264
|
+
for i in numba.prange(d):
|
|
265
|
+
partial_sum = 0.0
|
|
266
|
+
for n in range(N):
|
|
267
|
+
partial_sum += coeffs[n] * (xn[i] ** n)
|
|
268
|
+
eps[0,n,i] = partial_sum
|
|
269
|
+
|
|
270
|
+
for i in numba.prange(d):
|
|
271
|
+
for k in range(1, N+1):
|
|
272
|
+
for j in range(N - k):
|
|
273
|
+
delta = eps[k-1, j+1,i] - eps[k-1, j,i]
|
|
274
|
+
if delta == 0:
|
|
275
|
+
rec_delta = numpy.inf
|
|
276
|
+
elif numpy.isinf(delta) or numpy.isnan(delta):
|
|
277
|
+
rec_delta = 0.0
|
|
278
|
+
else:
|
|
279
|
+
rec_delta = 1.0 / delta
|
|
280
|
+
eps[k,j,i] = rec_delta
|
|
281
|
+
if k > 1:
|
|
282
|
+
eps[k,j,i] += eps[k-2,j+1,i]
|
|
283
|
+
|
|
284
|
+
if (N % 2) == 0:
|
|
285
|
+
N -= 1
|
|
286
|
+
|
|
287
|
+
return eps[N-1, 0, :].reshape(x.shape)
|
|
238
288
|
|
|
239
289
|
# ========
|
|
240
290
|
# fit pade
|
|
@@ -81,6 +81,59 @@ def plot_fit(psi, x_supp, g_supp, g_supp_approx, support, latex=False,
|
|
|
81
81
|
show_and_save=save_status, verbose=True)
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
# =========
|
|
85
|
+
# auto bins
|
|
86
|
+
# =========
|
|
87
|
+
|
|
88
|
+
def _auto_bins(array, method='scott', factor=5):
|
|
89
|
+
"""
|
|
90
|
+
Automatic choice for the number of bins for the histogram of an array.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
|
|
95
|
+
array : numpy.array
|
|
96
|
+
An array for histogram.
|
|
97
|
+
|
|
98
|
+
method : {``'freedman'``, ``'scott'``, ``'sturges'``}, default= ``'scott'``
|
|
99
|
+
Method of choosing number of bins.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
|
|
104
|
+
num_bins : int
|
|
105
|
+
Number of bins for histogram.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
if method == 'freedman':
|
|
109
|
+
|
|
110
|
+
q75, q25 = numpy.percentile(array, [75, 25])
|
|
111
|
+
iqr = q75 - q25
|
|
112
|
+
bin_width = 2 * iqr / (len(array) ** (1/3))
|
|
113
|
+
|
|
114
|
+
if bin_width == 0:
|
|
115
|
+
# Fallback default
|
|
116
|
+
return
|
|
117
|
+
num_bins = 100
|
|
118
|
+
else:
|
|
119
|
+
num_bins = int(numpy.ceil((array.max() - array.min()) / bin_width))
|
|
120
|
+
|
|
121
|
+
elif method == 'scott':
|
|
122
|
+
|
|
123
|
+
std = numpy.std(array)
|
|
124
|
+
bin_width = 3.5 * std / (len(array) ** (1/3))
|
|
125
|
+
num_bins = int(numpy.ceil((array.max() - array.min()) / bin_width))
|
|
126
|
+
|
|
127
|
+
elif method == 'sturges':
|
|
128
|
+
|
|
129
|
+
num_bins = int(numpy.ceil(numpy.log2(len(array)) + 1))
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError('"method" is invalid.')
|
|
133
|
+
|
|
134
|
+
return num_bins * factor
|
|
135
|
+
|
|
136
|
+
|
|
84
137
|
# ============
|
|
85
138
|
# plot density
|
|
86
139
|
# ============
|
|
@@ -96,7 +149,7 @@ def plot_density(x, rho, eig=None, support=None, label='',
|
|
|
96
149
|
|
|
97
150
|
if (support is not None) and (eig is not None):
|
|
98
151
|
lam_m, lam_p = support
|
|
99
|
-
bins = numpy.linspace(lam_m, lam_p,
|
|
152
|
+
bins = numpy.linspace(lam_m, lam_p, _auto_bins(eig))
|
|
100
153
|
_ = ax.hist(eig, bins, density=True, color='silver',
|
|
101
154
|
edgecolor='none', label='Histogram')
|
|
102
155
|
else:
|
|
@@ -503,7 +556,7 @@ def plot_samples(x, rho, x_min, x_max, samples, latex=False, save=False):
|
|
|
503
556
|
|
|
504
557
|
fig, ax = plt.subplots(figsize=(6, 3))
|
|
505
558
|
|
|
506
|
-
bins = numpy.linspace(x_min, x_max, samples
|
|
559
|
+
bins = numpy.linspace(x_min, x_max, _auto_bins(samples))
|
|
507
560
|
_ = ax.hist(samples, bins, density=True, color='silver',
|
|
508
561
|
edgecolor='none', label='Samples histogram')
|
|
509
562
|
ax.plot(x, rho, color='black', label='Exact density')
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import numpy
|
|
14
14
|
from scipy.integrate import cumulative_trapezoid
|
|
15
|
-
from scipy.interpolate import
|
|
15
|
+
from scipy.interpolate import PchipInterpolator
|
|
16
16
|
from scipy.stats import qmc
|
|
17
17
|
|
|
18
18
|
__all__ = ['qmc_sample']
|
|
@@ -22,14 +22,16 @@ __all__ = ['qmc_sample']
|
|
|
22
22
|
# quantile func
|
|
23
23
|
# =============
|
|
24
24
|
|
|
25
|
-
def _quantile_func(x, rho):
|
|
25
|
+
def _quantile_func(x, rho, clamp=1e-4, eps=1e-8):
|
|
26
26
|
"""
|
|
27
27
|
Construct a quantile function from evaluations of an estimated density
|
|
28
28
|
on a grid (x, rho(x)).
|
|
29
29
|
"""
|
|
30
|
-
|
|
30
|
+
rho_clamp = rho.copy()
|
|
31
|
+
rho_clamp[rho < clamp] = eps
|
|
32
|
+
cdf = cumulative_trapezoid(rho_clamp, x, initial=0)
|
|
31
33
|
cdf /= cdf[-1]
|
|
32
|
-
return
|
|
34
|
+
return PchipInterpolator(cdf, x, extrapolate=False)
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
# ==========
|
|
@@ -82,7 +84,8 @@ def qmc_sample(x, rho, num_pts, seed=None):
|
|
|
82
84
|
>>> numpy.allclose(samples.mean(), 0.75, atol=0.02)
|
|
83
85
|
"""
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
if seed is not None:
|
|
88
|
+
numpy.random.rand(seed)
|
|
86
89
|
|
|
87
90
|
quantile = _quantile_func(x, rho)
|
|
88
91
|
engine = qmc.Halton(d=1)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
from scipy.stats import gaussian_kde
|
|
3
|
+
|
|
4
|
+
def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs):
|
|
5
|
+
"""
|
|
6
|
+
Estimates the support of the eigenvalue density.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
method : {``'range'``, ``'jackknife'``, ``'regression'``, ``'interior'``,
|
|
11
|
+
``'interior_smooth'``}, \
|
|
12
|
+
default= ``'jackknife'``
|
|
13
|
+
The method of support estimation:
|
|
14
|
+
|
|
15
|
+
* ``'range'``: no estimation; the support is the range of the eigenvalues
|
|
16
|
+
* ``'jackknife'``: estimates the support using Quenouille's [1]
|
|
17
|
+
jackknife estimator. Fast and simple, more accurate than the range.
|
|
18
|
+
* ``'regression'``: estimates the support by performing a regression under
|
|
19
|
+
the assumption that the edge behavior is of square-root type. Often
|
|
20
|
+
most accurate.
|
|
21
|
+
* ``'interior'``: estimates a support assuming the range overestimates;
|
|
22
|
+
uses quantiles (p, 1-p).
|
|
23
|
+
* ``'interior_smooth'``: same as ``'interior'`` but using kernel density
|
|
24
|
+
estimation.
|
|
25
|
+
|
|
26
|
+
k : int, default = None
|
|
27
|
+
Number of extreme order statistics to use for ``method='regression'``.
|
|
28
|
+
|
|
29
|
+
p : float, default=0.001
|
|
30
|
+
The edges of the support of the distribution is detected by the
|
|
31
|
+
:math:`p`-quantile on the left and :math:`(1-p)`-quantile on the right
|
|
32
|
+
where ``method='interior'`` or ``method='interior_smooth'``.
|
|
33
|
+
This value should be between 0 and 1, ideally a small number close to
|
|
34
|
+
zero.
|
|
35
|
+
|
|
36
|
+
References
|
|
37
|
+
----------
|
|
38
|
+
|
|
39
|
+
.. [1] Quenouille, M. H. (1949, July). Approximate tests of correlation in time-series.
|
|
40
|
+
In Mathematical Proceedings of the Cambridge Philosophical Society (Vol. 45, No. 3,
|
|
41
|
+
pp. 483-484). Cambridge University Press.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
if method=='range':
|
|
45
|
+
lam_m = eigs.min()
|
|
46
|
+
lam_p = eigs.max()
|
|
47
|
+
|
|
48
|
+
elif method=='jackknife':
|
|
49
|
+
x, n = numpy.sort(eigs), len(eigs)
|
|
50
|
+
lam_m = x[0] - (n - 1)/n * (x[1] - x[0])
|
|
51
|
+
lam_p = x[-1] + (n - 1)/n * (x[-1] - x[-2])
|
|
52
|
+
|
|
53
|
+
elif method=='regression':
|
|
54
|
+
x, n = numpy.sort(eigs), len(eigs)
|
|
55
|
+
if k is None:
|
|
56
|
+
k = int(round(n ** (2/3)))
|
|
57
|
+
k = max(5, min(k, n // 2))
|
|
58
|
+
|
|
59
|
+
# The theoretical cdf near the edge behaves like const*(x - a)^{3/2},
|
|
60
|
+
# so (i/n) ≈ (x - a)^{3/2} ⇒ x ≈ a + const*(i/n)^{2/3}.
|
|
61
|
+
y = ((numpy.arange(1, k + 1) - 0.5) / n) ** (2 / 3)
|
|
62
|
+
|
|
63
|
+
# Left edge: regress x_{(i)} on y
|
|
64
|
+
_, lam_m = numpy.polyfit(y, x[:k], 1)
|
|
65
|
+
|
|
66
|
+
# Right edge: regress x_{(n-i+1)} on y
|
|
67
|
+
_, lam_p = numpy.polyfit(y, x[-k:][::-1], 1)
|
|
68
|
+
|
|
69
|
+
elif method=='interior':
|
|
70
|
+
lam_m, lam_p = numpy.quantile(eigs, [p, 1-p])
|
|
71
|
+
|
|
72
|
+
elif method=='interior_smooth':
|
|
73
|
+
kde = gaussian_kde(eigs)
|
|
74
|
+
xs = numpy.linspace(eigs.min(), eigs.max(), 1000)
|
|
75
|
+
fs = kde(xs)
|
|
76
|
+
|
|
77
|
+
cdf = numpy.cumsum(fs)
|
|
78
|
+
cdf /= cdf[-1]
|
|
79
|
+
|
|
80
|
+
lam_m = numpy.interp(p, cdf, xs)
|
|
81
|
+
lam_p = numpy.interp(1-p, cdf, xs)
|
|
82
|
+
else:
|
|
83
|
+
raise NotImplementedError("Unknown method")
|
|
84
|
+
|
|
85
|
+
return lam_m, lam_p
|
|
@@ -143,7 +143,7 @@ def force_density(psi0, support, approx, grid, alpha=0.0, beta=0.0):
|
|
|
143
143
|
# Normalize first mode to unit mass
|
|
144
144
|
x = numpy.linspace(lam_m, lam_p, 1000)
|
|
145
145
|
rho = approx(x, psi)
|
|
146
|
-
mass = numpy.
|
|
146
|
+
mass = numpy.trapezoid(rho, x)
|
|
147
147
|
psi[0] = psi[0] / mass
|
|
148
148
|
|
|
149
149
|
return psi
|
|
@@ -6,10 +6,10 @@
|
|
|
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 .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
9
|
+
from ._marchenko_pastur import MarchenkoPastur
|
|
10
|
+
from ._wigner import Wigner
|
|
11
|
+
from ._kesten_mckay import KestenMcKay
|
|
12
|
+
from ._wachter import Wachter
|
|
13
|
+
from ._meixner import Meixner
|
|
14
14
|
|
|
15
15
|
__all__ = ['MarchenkoPastur', 'Wigner', 'KestenMcKay', 'Wachter', 'Meixner']
|
|
@@ -501,7 +501,8 @@ class MarchenkoPastur(object):
|
|
|
501
501
|
:class: custom-dark
|
|
502
502
|
"""
|
|
503
503
|
|
|
504
|
-
|
|
504
|
+
if seed is not None:
|
|
505
|
+
numpy.random.seed(seed)
|
|
505
506
|
|
|
506
507
|
if x_min is None:
|
|
507
508
|
x_min = self.lam_m
|
|
@@ -578,7 +579,8 @@ class MarchenkoPastur(object):
|
|
|
578
579
|
>>> A = mp.matrix(2000)
|
|
579
580
|
"""
|
|
580
581
|
|
|
581
|
-
|
|
582
|
+
if seed is not None:
|
|
583
|
+
numpy.random.seed(seed)
|
|
582
584
|
|
|
583
585
|
# Parameters
|
|
584
586
|
m = int(size / self.lam)
|
freealg-0.1.9/freealg/distributions/wachter.py → freealg-0.1.11/freealg/distributions/_wachter.py
RENAMED
|
@@ -501,7 +501,8 @@ class Wachter(object):
|
|
|
501
501
|
:class: custom-dark
|
|
502
502
|
"""
|
|
503
503
|
|
|
504
|
-
|
|
504
|
+
if seed is not None:
|
|
505
|
+
numpy.random.seed(seed)
|
|
505
506
|
|
|
506
507
|
if x_min is None:
|
|
507
508
|
x_min = self.lam_m
|
|
@@ -581,7 +582,8 @@ class Wachter(object):
|
|
|
581
582
|
>>> A = wa.matrix(2000)
|
|
582
583
|
"""
|
|
583
584
|
|
|
584
|
-
|
|
585
|
+
if seed is not None:
|
|
586
|
+
numpy.random.seed(seed)
|
|
585
587
|
|
|
586
588
|
n = size
|
|
587
589
|
m1 = int(self.a * n)
|
freealg-0.1.9/freealg/distributions/wigner.py → freealg-0.1.11/freealg/distributions/_wigner.py
RENAMED
|
@@ -478,7 +478,8 @@ class Wigner(object):
|
|
|
478
478
|
:class: custom-dark
|
|
479
479
|
"""
|
|
480
480
|
|
|
481
|
-
|
|
481
|
+
if seed is not None:
|
|
482
|
+
numpy.random.seed(seed)
|
|
482
483
|
|
|
483
484
|
if x_min is None:
|
|
484
485
|
x_min = self.lam_m
|
|
@@ -555,7 +556,8 @@ class Wigner(object):
|
|
|
555
556
|
>>> A = wg.matrix(2000)
|
|
556
557
|
"""
|
|
557
558
|
|
|
558
|
-
|
|
559
|
+
if seed is not None:
|
|
560
|
+
numpy.random.seed(seed)
|
|
559
561
|
|
|
560
562
|
# Parameters
|
|
561
563
|
n = size
|
|
@@ -26,8 +26,9 @@ from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
|
|
|
26
26
|
from ._pade import fit_pade, eval_pade
|
|
27
27
|
from ._decompress import decompress
|
|
28
28
|
from ._sample import qmc_sample
|
|
29
|
+
from ._support import detect_support
|
|
29
30
|
|
|
30
|
-
__all__ = ['FreeForm']
|
|
31
|
+
__all__ = ['FreeForm', 'eigfree']
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
# =========
|
|
@@ -50,12 +51,12 @@ class FreeForm(object):
|
|
|
50
51
|
The support of the density of :math:`\\mathbf{A}`. If `None`, it is
|
|
51
52
|
estimated from the minimum and maximum of the eigenvalues.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
delta: float, default=1e-6
|
|
55
|
+
Size of perturbations into the upper half plane for Plemelj's
|
|
56
|
+
formula.
|
|
57
|
+
|
|
58
|
+
Parameters for the ``detect_support`` function can also be prescribed here
|
|
59
|
+
when ``support=None``.
|
|
59
60
|
|
|
60
61
|
Notes
|
|
61
62
|
-----
|
|
@@ -73,12 +74,16 @@ class FreeForm(object):
|
|
|
73
74
|
eig : numpy.array
|
|
74
75
|
Eigenvalues of the matrix
|
|
75
76
|
|
|
77
|
+
support: tuple
|
|
78
|
+
The predicted (or given) support :math:`(\lambda_\min, \lambda_\max)` of the
|
|
79
|
+
eigenvalue density.
|
|
80
|
+
|
|
76
81
|
psi : numpy.array
|
|
77
82
|
Jacobi coefficients.
|
|
78
83
|
|
|
79
84
|
n : int
|
|
80
85
|
Initial array size (assuming a square matrix when :math:`\\mathbf{A}`
|
|
81
|
-
is 2D)
|
|
86
|
+
is 2D).
|
|
82
87
|
|
|
83
88
|
Methods
|
|
84
89
|
-------
|
|
@@ -110,13 +115,14 @@ class FreeForm(object):
|
|
|
110
115
|
# init
|
|
111
116
|
# ====
|
|
112
117
|
|
|
113
|
-
def __init__(self, A, support=None,
|
|
118
|
+
def __init__(self, A, support=None, delta=1e-6, **kwargs):
|
|
114
119
|
"""
|
|
115
120
|
Initialization.
|
|
116
121
|
"""
|
|
117
122
|
|
|
118
123
|
self.A = None
|
|
119
124
|
self.eig = None
|
|
125
|
+
self.delta = delta
|
|
120
126
|
|
|
121
127
|
# Eigenvalues
|
|
122
128
|
if A.ndim == 1:
|
|
@@ -134,8 +140,7 @@ class FreeForm(object):
|
|
|
134
140
|
|
|
135
141
|
# Support
|
|
136
142
|
if support is None:
|
|
137
|
-
self.lam_m, self.lam_p =
|
|
138
|
-
smoothen=True)
|
|
143
|
+
self.lam_m, self.lam_p = detect_support(self.eig, **kwargs)
|
|
139
144
|
else:
|
|
140
145
|
self.lam_m = support[0]
|
|
141
146
|
self.lam_p = support[1]
|
|
@@ -148,30 +153,6 @@ class FreeForm(object):
|
|
|
148
153
|
self.beta = None
|
|
149
154
|
self._pade_sol = None
|
|
150
155
|
|
|
151
|
-
# ==============
|
|
152
|
-
# detect support
|
|
153
|
-
# ==============
|
|
154
|
-
|
|
155
|
-
def _detect_support(self, eig, p, smoothen=True):
|
|
156
|
-
"""
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
# Using quantile directly.
|
|
160
|
-
if smoothen:
|
|
161
|
-
kde = gaussian_kde(eig)
|
|
162
|
-
xs = numpy.linspace(eig.min(), eig.max(), 1000)
|
|
163
|
-
fs = kde(xs)
|
|
164
|
-
|
|
165
|
-
cdf = numpy.cumsum(fs)
|
|
166
|
-
cdf /= cdf[-1]
|
|
167
|
-
|
|
168
|
-
lam_m = numpy.interp(p, cdf, xs)
|
|
169
|
-
lam_p = numpy.interp(1-p, cdf, xs)
|
|
170
|
-
else:
|
|
171
|
-
lam_m, lam_p = numpy.quantile(eig, [p, 1-p])
|
|
172
|
-
|
|
173
|
-
return lam_m, lam_p
|
|
174
|
-
|
|
175
156
|
# ===
|
|
176
157
|
# fit
|
|
177
158
|
# ===
|
|
@@ -403,19 +384,16 @@ class FreeForm(object):
|
|
|
403
384
|
self.alpha = alpha
|
|
404
385
|
self.beta = beta
|
|
405
386
|
|
|
406
|
-
# For holomorphic continuation for the lower half-plane
|
|
407
|
-
x_supp = numpy.linspace(self.lam_m, self.lam_p, 1000)
|
|
408
|
-
g_supp = 2.0 * numpy.pi * self.hilbert(x_supp)
|
|
409
|
-
|
|
410
387
|
# Fit a pade approximation
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
388
|
+
if method != 'chebyshev' or projection != 'sample':
|
|
389
|
+
# For holomorphic continuation for the lower half-plane
|
|
390
|
+
x_supp = numpy.linspace(self.lam_m, self.lam_p, 1000)
|
|
391
|
+
g_supp = 2.0 * numpy.pi * self.hilbert(x_supp)
|
|
392
|
+
self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m, self.lam_p,
|
|
393
|
+
p=pade_p, q=pade_q, odd_side=odd_side,
|
|
394
|
+
pade_reg=pade_reg, safety=1.0, max_outer=40,
|
|
395
|
+
xtol=1e-12, ftol=1e-12, optimizer=optimizer,
|
|
396
|
+
verbose=0)
|
|
419
397
|
|
|
420
398
|
if plot:
|
|
421
399
|
g_supp_approx = eval_pade(x_supp[None, :], self._pade_sol)[0, :]
|
|
@@ -471,7 +449,7 @@ class FreeForm(object):
|
|
|
471
449
|
"""
|
|
472
450
|
|
|
473
451
|
if self.psi is None:
|
|
474
|
-
raise RuntimeError('
|
|
452
|
+
raise RuntimeError('The spectral density needs to be fit using the .fit() function.')
|
|
475
453
|
|
|
476
454
|
# Create x if not given
|
|
477
455
|
if x is None:
|
|
@@ -497,19 +475,15 @@ class FreeForm(object):
|
|
|
497
475
|
raise RuntimeError('"method" is invalid.')
|
|
498
476
|
|
|
499
477
|
# Check density is unit mass
|
|
500
|
-
mass = numpy.
|
|
478
|
+
mass = numpy.trapezoid(rho, x)
|
|
501
479
|
if not numpy.isclose(mass, 1.0, atol=1e-2):
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
print(f'"rho" is not unit mass. mass: {mass}. Set "force=True".')
|
|
480
|
+
print(f'"rho" is not unit mass. mass: {mass:>0.3f}. Set ' +
|
|
481
|
+
r'"force=True".')
|
|
505
482
|
|
|
506
483
|
# Check density is positive
|
|
507
484
|
min_rho = numpy.min(rho)
|
|
508
485
|
if min_rho < 0.0 - 1e-3:
|
|
509
|
-
|
|
510
|
-
# f'"rho" is not positive. min_rho: {min_rho}. Set ' +
|
|
511
|
-
# r'"force=True".')
|
|
512
|
-
print(f'"rho" is not positive. min_rho: {min_rho}. Set ' +
|
|
486
|
+
print(f'"rho" is not positive. min_rho: {min_rho:>0.3f}. Set ' +
|
|
513
487
|
r'"force=True".')
|
|
514
488
|
|
|
515
489
|
if plot:
|
|
@@ -569,7 +543,7 @@ class FreeForm(object):
|
|
|
569
543
|
"""
|
|
570
544
|
|
|
571
545
|
if self.psi is None:
|
|
572
|
-
raise RuntimeError('
|
|
546
|
+
raise RuntimeError('The spectral density needs to be fit using the .fit() function.')
|
|
573
547
|
|
|
574
548
|
# Create x if not given
|
|
575
549
|
if x is None:
|
|
@@ -603,7 +577,7 @@ class FreeForm(object):
|
|
|
603
577
|
|
|
604
578
|
# Integrate each row over t using trapezoid rule on x_s
|
|
605
579
|
# Namely, hilb[i] = int rho_s(t)/(t - x[i]) dt
|
|
606
|
-
hilb = numpy.
|
|
580
|
+
hilb = numpy.trapezoid(D, x_s, axis=1) / numpy.pi
|
|
607
581
|
|
|
608
582
|
# We use negative sign convention
|
|
609
583
|
hilb = -hilb
|
|
@@ -619,22 +593,20 @@ class FreeForm(object):
|
|
|
619
593
|
# ====
|
|
620
594
|
|
|
621
595
|
def _glue(self, z):
|
|
622
|
-
"""
|
|
623
|
-
"""
|
|
624
|
-
|
|
625
596
|
# Glue function
|
|
597
|
+
if self._pade_sol is None:
|
|
598
|
+
return numpy.zeros_like(z)
|
|
626
599
|
g = eval_pade(z, self._pade_sol)
|
|
627
|
-
|
|
628
600
|
return g
|
|
629
601
|
|
|
630
602
|
# =========
|
|
631
603
|
# stieltjes
|
|
632
604
|
# =========
|
|
633
605
|
|
|
634
|
-
def stieltjes(self, x, y, plot=False, latex=False, save=False):
|
|
606
|
+
def stieltjes(self, x=None, y=None, plot=False, latex=False, save=False):
|
|
635
607
|
"""
|
|
636
|
-
Compute Stieltjes transform of the spectral density
|
|
637
|
-
grid on the complex plane.
|
|
608
|
+
Compute Stieltjes transform of the spectral density, evaluated on an array
|
|
609
|
+
of points, or over a 2D Cartesian grid on the complex plane.
|
|
638
610
|
|
|
639
611
|
Parameters
|
|
640
612
|
----------
|
|
@@ -693,7 +665,12 @@ class FreeForm(object):
|
|
|
693
665
|
"""
|
|
694
666
|
|
|
695
667
|
if self.psi is None:
|
|
696
|
-
raise RuntimeError('
|
|
668
|
+
raise RuntimeError('The spectral density needs to be fit using the .fit() function.')
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
# Determine whether the Stieltjes transform is to be computed on
|
|
672
|
+
# a Cartesian grid
|
|
673
|
+
cartesian = plot | (y is not None)
|
|
697
674
|
|
|
698
675
|
# Create x if not given
|
|
699
676
|
if x is None:
|
|
@@ -703,43 +680,21 @@ class FreeForm(object):
|
|
|
703
680
|
x_min = numpy.floor(2.0 * (center - 2.0 * radius * scale)) / 2.0
|
|
704
681
|
x_max = numpy.ceil(2.0 * (center + 2.0 * radius * scale)) / 2.0
|
|
705
682
|
x = numpy.linspace(x_min, x_max, 500)
|
|
683
|
+
if not cartesian:
|
|
684
|
+
# Evaluate slightly above the real line
|
|
685
|
+
x = x.astype(complex)
|
|
686
|
+
x += self.delta * 1j
|
|
706
687
|
|
|
707
688
|
# Create y if not given
|
|
708
|
-
if
|
|
709
|
-
y
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
# Stieltjes function
|
|
719
|
-
if self.method == 'jacobi':
|
|
720
|
-
stieltjes = partial(jacobi_stieltjes, psi=self.psi,
|
|
721
|
-
support=self.support, alpha=self.alpha,
|
|
722
|
-
beta=self.beta, n_base=n_base)
|
|
723
|
-
elif self.method == 'chebyshev':
|
|
724
|
-
stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
|
|
725
|
-
support=self.support)
|
|
726
|
-
|
|
727
|
-
mask_p = y >= 0.0
|
|
728
|
-
mask_m = y < 0.0
|
|
729
|
-
|
|
730
|
-
m1 = numpy.zeros_like(z)
|
|
731
|
-
m2 = numpy.zeros_like(z)
|
|
732
|
-
|
|
733
|
-
# Upper half-plane
|
|
734
|
-
m1[mask_p, :] = stieltjes(z[mask_p, :])
|
|
735
|
-
|
|
736
|
-
# Lower half-plane, use Schwarz reflection
|
|
737
|
-
m1[mask_m, :] = numpy.conjugate(
|
|
738
|
-
stieltjes(numpy.conjugate(z[mask_m, :])))
|
|
739
|
-
|
|
740
|
-
# Second Riemann sheet
|
|
741
|
-
m2[mask_p, :] = m1[mask_p, :]
|
|
742
|
-
m2[mask_m, :] = -m1[mask_m, :] + self._glue(z[mask_m, :])
|
|
689
|
+
if cartesian:
|
|
690
|
+
if y is None:
|
|
691
|
+
y = numpy.linspace(-1, 1, 400)
|
|
692
|
+
x_grid, y_grid = numpy.meshgrid(x.real, y.real)
|
|
693
|
+
z = x_grid + 1j * y_grid # shape (Ny, Nx)
|
|
694
|
+
else:
|
|
695
|
+
z = x
|
|
696
|
+
|
|
697
|
+
m1, m2 = self._eval_stieltjes(z)
|
|
743
698
|
|
|
744
699
|
if plot:
|
|
745
700
|
plot_stieltjes(x, y, m1, m2, self.support, latex=latex, save=save)
|
|
@@ -770,44 +725,26 @@ class FreeForm(object):
|
|
|
770
725
|
|
|
771
726
|
m_m : numpy.ndarray
|
|
772
727
|
The Stieltjes transform continued to the secondary branch.
|
|
773
|
-
|
|
774
|
-
See Also
|
|
775
|
-
--------
|
|
776
|
-
density
|
|
777
|
-
hilbert
|
|
778
|
-
|
|
779
|
-
Notes
|
|
780
|
-
-----
|
|
781
|
-
|
|
782
|
-
Notes.
|
|
783
|
-
|
|
784
|
-
References
|
|
785
|
-
----------
|
|
786
|
-
|
|
787
|
-
.. [1] tbd
|
|
788
|
-
|
|
789
|
-
Examples
|
|
790
|
-
--------
|
|
791
|
-
|
|
792
|
-
.. code-block:: python
|
|
793
|
-
|
|
794
|
-
>>> from freealg import FreeForm
|
|
795
728
|
"""
|
|
796
729
|
|
|
797
|
-
|
|
798
|
-
raise RuntimeError('"fit" the model first.')
|
|
730
|
+
assert self.psi is not None, "The fit function has not been called."
|
|
799
731
|
|
|
732
|
+
# Allow for arbitrary input shapes
|
|
800
733
|
z = numpy.asarray(z)
|
|
801
734
|
shape = z.shape
|
|
802
735
|
if len(shape) == 0:
|
|
803
736
|
shape = (1,)
|
|
804
737
|
z = z.reshape(-1, 1)
|
|
805
738
|
|
|
739
|
+
# # Set the number of bases as the number of x points insides support
|
|
740
|
+
# mask_sup = numpy.logical_and(z.real >= self.lam_m, z.real <= self.lam_p)
|
|
741
|
+
# n_base = 2 * numpy.sum(mask_sup)
|
|
742
|
+
|
|
806
743
|
# Stieltjes function
|
|
807
744
|
if self.method == 'jacobi':
|
|
808
745
|
stieltjes = partial(jacobi_stieltjes, psi=self.psi,
|
|
809
746
|
support=self.support, alpha=self.alpha,
|
|
810
|
-
beta=self.beta)
|
|
747
|
+
beta=self.beta) # n_base = n_base
|
|
811
748
|
elif self.method == 'chebyshev':
|
|
812
749
|
stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
|
|
813
750
|
support=self.support)
|
|
@@ -818,17 +755,25 @@ class FreeForm(object):
|
|
|
818
755
|
m1 = numpy.zeros_like(z)
|
|
819
756
|
m2 = numpy.zeros_like(z)
|
|
820
757
|
|
|
821
|
-
|
|
822
|
-
|
|
758
|
+
if self._pade_sol is not None:
|
|
759
|
+
# Upper half-plane
|
|
760
|
+
m1[mask_p] = stieltjes(z[mask_p].reshape(-1, 1)).ravel()
|
|
823
761
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
762
|
+
# Lower half-plane, use Schwarz reflection
|
|
763
|
+
m1[mask_m] = numpy.conjugate(
|
|
764
|
+
stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))).ravel()
|
|
827
765
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
766
|
+
# Second Riemann sheet
|
|
767
|
+
m2[mask_p] = m1[mask_p]
|
|
768
|
+
m2[mask_m] = -m1[mask_m] + self._glue(
|
|
769
|
+
z[mask_m].reshape(-1, 1)).ravel()
|
|
770
|
+
|
|
771
|
+
else:
|
|
772
|
+
m2[:] = stieltjes(z.reshape(-1,1)).reshape(*m2.shape)
|
|
773
|
+
m1[mask_p] = m2[mask_p]
|
|
774
|
+
m1[mask_m] = numpy.conjugate(
|
|
775
|
+
stieltjes(numpy.conjugate(z[mask_m].reshape(-1,1)))
|
|
776
|
+
).ravel()
|
|
832
777
|
|
|
833
778
|
m1, m2 = m1.reshape(*shape), m2.reshape(*shape)
|
|
834
779
|
|
|
@@ -838,8 +783,8 @@ class FreeForm(object):
|
|
|
838
783
|
# decompress
|
|
839
784
|
# ==========
|
|
840
785
|
|
|
841
|
-
def decompress(self, size, x=None,
|
|
842
|
-
step_size=0.1, tolerance=1e-
|
|
786
|
+
def decompress(self, size, x=None, iterations=500, eigvals=True,
|
|
787
|
+
step_size=0.1, tolerance=1e-6, seed=None, plot=False,
|
|
843
788
|
latex=False, save=False):
|
|
844
789
|
"""
|
|
845
790
|
Free decompression of spectral density.
|
|
@@ -854,17 +799,16 @@ class FreeForm(object):
|
|
|
854
799
|
Positions where density to be evaluated at. If `None`, an interval
|
|
855
800
|
slightly larger than the support interval will be used.
|
|
856
801
|
|
|
857
|
-
delta: float, default=1e-4
|
|
858
|
-
Size of the perturbation into the upper half plane for Plemelj's
|
|
859
|
-
formula.
|
|
860
|
-
|
|
861
802
|
iterations: int, default=500
|
|
862
803
|
Maximum number of Newton iterations.
|
|
863
804
|
|
|
805
|
+
eigvals: bool, default=True
|
|
806
|
+
Return estimated (sampled) eigenvalues as well as the density.
|
|
807
|
+
|
|
864
808
|
step_size: float, default=0.1
|
|
865
809
|
Step size for Newton iterations.
|
|
866
810
|
|
|
867
|
-
tolerance: float, default=1e-
|
|
811
|
+
tolerance: float, default=1e-6
|
|
868
812
|
Tolerance for the solution obtained by the Newton solver. Also
|
|
869
813
|
used for the finite difference approximation to the derivative.
|
|
870
814
|
|
|
@@ -886,12 +830,15 @@ class FreeForm(object):
|
|
|
886
830
|
Returns
|
|
887
831
|
-------
|
|
888
832
|
|
|
833
|
+
x : numpy.array
|
|
834
|
+
Locations where the spectral density is estimated
|
|
835
|
+
|
|
889
836
|
rho : numpy.array
|
|
890
|
-
|
|
837
|
+
Estimated spectral density at locations x
|
|
891
838
|
|
|
892
839
|
eigs : numpy.array
|
|
893
840
|
Estimated eigenvalues as low-discrepancy samples of the estimated
|
|
894
|
-
spectral density.
|
|
841
|
+
spectral density. Only returns if ``eigvals=True``.
|
|
895
842
|
|
|
896
843
|
See Also
|
|
897
844
|
--------
|
|
@@ -919,7 +866,7 @@ class FreeForm(object):
|
|
|
919
866
|
|
|
920
867
|
size = int(size)
|
|
921
868
|
|
|
922
|
-
rho, x, (lb, ub) = decompress(self, size, x=x, delta=delta,
|
|
869
|
+
rho, x, (lb, ub) = decompress(self, size, x=x, delta=self.delta,
|
|
923
870
|
iterations=iterations,
|
|
924
871
|
step_size=step_size, tolerance=tolerance)
|
|
925
872
|
x, rho = x.ravel(), rho.ravel()
|
|
@@ -928,6 +875,93 @@ class FreeForm(object):
|
|
|
928
875
|
plot_density(x, rho, support=(lb, ub),
|
|
929
876
|
label='Decompression', latex=latex, save=save)
|
|
930
877
|
|
|
931
|
-
|
|
878
|
+
if eigvals:
|
|
879
|
+
eigs = numpy.sort(qmc_sample(x, rho, size, seed=seed))
|
|
880
|
+
return x, rho, eigs
|
|
881
|
+
else:
|
|
882
|
+
return x, rho
|
|
883
|
+
|
|
884
|
+
def eigfree(A, N = None, psd = None):
|
|
885
|
+
"""
|
|
886
|
+
Estimate the eigenvalues of a matrix :math:`\\mathbf{A}` or a larger matrix
|
|
887
|
+
containing :math:`\\mathbf{A}` using free decompression.
|
|
888
|
+
|
|
889
|
+
This is a convenience function for the FreeForm class with some effective
|
|
890
|
+
defaults that work well for common random matrix ensembles. For improved
|
|
891
|
+
performance and plotting utilites, consider finetuning parameters using
|
|
892
|
+
the FreeForm class.
|
|
932
893
|
|
|
933
|
-
|
|
894
|
+
Parameters
|
|
895
|
+
----------
|
|
896
|
+
|
|
897
|
+
A : numpy.ndarray
|
|
898
|
+
The symmetric real-valued matrix :math:`\\mathbf{A}` whose eigenvalues
|
|
899
|
+
(or those of a matrix containing :math:`\\mathbf{A}`) are to be computed.
|
|
900
|
+
|
|
901
|
+
N : int, default=None
|
|
902
|
+
The size of the matrix containing :math:`\\mathbf{A}` to estimate
|
|
903
|
+
eigenvalues of. If None, returns estimates of the eigenvalues of
|
|
904
|
+
:math:`\\mathbf{A}` itself.
|
|
905
|
+
|
|
906
|
+
psd: bool, default=None
|
|
907
|
+
Determines whether the matrix is positive-semidefinite (PSD; all
|
|
908
|
+
eigenvalues are non-negative). If None, the matrix is considered PSD if
|
|
909
|
+
all sampled eigenvalues are positive.
|
|
910
|
+
|
|
911
|
+
Notes
|
|
912
|
+
-----
|
|
913
|
+
|
|
914
|
+
Notes.
|
|
915
|
+
|
|
916
|
+
References
|
|
917
|
+
----------
|
|
918
|
+
|
|
919
|
+
.. [1] Reference.
|
|
920
|
+
|
|
921
|
+
Examples
|
|
922
|
+
--------
|
|
923
|
+
|
|
924
|
+
.. code-block:: python
|
|
925
|
+
|
|
926
|
+
>>> from freealg import FreeForm
|
|
927
|
+
"""
|
|
928
|
+
n = A.shape[0]
|
|
929
|
+
|
|
930
|
+
# Size of sample matrix
|
|
931
|
+
n_s = int(80*(1 + numpy.log(n)))
|
|
932
|
+
|
|
933
|
+
# If matrix is not large enough, return eigenvalues
|
|
934
|
+
if n < n_s:
|
|
935
|
+
return compute_eig(A)
|
|
936
|
+
|
|
937
|
+
if N is None:
|
|
938
|
+
N = n
|
|
939
|
+
|
|
940
|
+
# Number of samples
|
|
941
|
+
num_samples = int(10 * (n / n_s)**0.5)
|
|
942
|
+
|
|
943
|
+
# Collect eigenvalue samples
|
|
944
|
+
samples = []
|
|
945
|
+
for _ in range(num_samples):
|
|
946
|
+
indices = numpy.random.choice(n, n_s, replace=False)
|
|
947
|
+
samples.append(compute_eig(A[numpy.ix_(indices, indices)]))
|
|
948
|
+
samples = numpy.concatenate(samples).ravel()
|
|
949
|
+
|
|
950
|
+
# If all eigenvalues are positive, set PSD flag
|
|
951
|
+
if psd is None:
|
|
952
|
+
psd = samples.min() > 0
|
|
953
|
+
|
|
954
|
+
ff = FreeForm(samples)
|
|
955
|
+
# Since we are resampling, we need to provide the correct matrix size
|
|
956
|
+
ff.n = n_s
|
|
957
|
+
|
|
958
|
+
# Perform fit and estimate eigenvalues
|
|
959
|
+
order = 1 + int(len(samples)**.25)
|
|
960
|
+
ff.fit(method='chebyshev', K=order, projection='sample', damp='jackson',
|
|
961
|
+
force=True, plot=False, latex=False, save=False, reg=0.01)
|
|
962
|
+
_, _, eigs = ff.decompress(N)
|
|
963
|
+
|
|
964
|
+
if psd:
|
|
965
|
+
eigs = numpy.abs(eigs)
|
|
966
|
+
|
|
967
|
+
return eigs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freealg
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Free probability for large matrices
|
|
5
5
|
Keywords: leaderboard bot chat
|
|
6
6
|
Platform: Linux
|
|
@@ -69,7 +69,10 @@ Dynamic: summary
|
|
|
69
69
|
:width: 240
|
|
70
70
|
:class: custom-dark
|
|
71
71
|
|
|
72
|
-
*freealg* is a
|
|
72
|
+
*freealg* is a Python package that employs **free** probability to evaluate the spectral
|
|
73
|
+
densities of large matrix **form**\ s. The fundamental algorithm employed by *freealg* is
|
|
74
|
+
**free decompression**, which extrapolates from the empirical spectral densities of small
|
|
75
|
+
submatrices to infer the eigenspectrum of extremely large matrices.
|
|
73
76
|
|
|
74
77
|
Install
|
|
75
78
|
=======
|
|
@@ -95,12 +98,18 @@ Documentation is available at `ameli.github.io/freealg <https://ameli.github.io/
|
|
|
95
98
|
Quick Usage
|
|
96
99
|
===========
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
The following code estimates the eigenvalues of a very large Wishart matrix using a much
|
|
102
|
+
smaller Wishart matrix.
|
|
100
103
|
|
|
101
104
|
.. code-block:: python
|
|
102
105
|
|
|
103
106
|
>>> import freealg as fa
|
|
107
|
+
>>> mp = fa.distributions.MarchenkoPastur(1/50) # Wishart matrices with aspect ratio 1/50
|
|
108
|
+
>>> A = mp.matrix(1000) # Sample a 1000 x 1000 Wishart matrix
|
|
109
|
+
>>> eigs = fa.eigfree(A, 100_000) # Estimate the eigenvalues of 100000 x 100000
|
|
110
|
+
|
|
111
|
+
For more details on how to interface with *freealg* check out the `Quick Start Guide <https://github.com/ameli/freealg/blob/main/notebooks/quick_start.ipynb>`.
|
|
112
|
+
|
|
104
113
|
|
|
105
114
|
Test
|
|
106
115
|
====
|
|
@@ -130,14 +139,18 @@ requests and bug reports.
|
|
|
130
139
|
How to Cite
|
|
131
140
|
===========
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994>`.
|
|
134
143
|
|
|
135
144
|
.. code::
|
|
136
145
|
|
|
137
|
-
@
|
|
138
|
-
|
|
146
|
+
@article{ameli2025spectral,
|
|
147
|
+
title={Spectral Estimation with Free Decompression},
|
|
148
|
+
author={Siavash Ameli and Chris van der Heide and Liam Hodgkinson and Michael W. Mahoney},
|
|
149
|
+
journal={arXiv preprint arXiv:2506.11994},
|
|
150
|
+
year={2025}
|
|
139
151
|
}
|
|
140
152
|
|
|
153
|
+
|
|
141
154
|
License
|
|
142
155
|
=======
|
|
143
156
|
|
|
@@ -16,6 +16,7 @@ freealg/_jacobi.py
|
|
|
16
16
|
freealg/_pade.py
|
|
17
17
|
freealg/_plot_util.py
|
|
18
18
|
freealg/_sample.py
|
|
19
|
+
freealg/_support.py
|
|
19
20
|
freealg/_util.py
|
|
20
21
|
freealg/freeform.py
|
|
21
22
|
freealg.egg-info/PKG-INFO
|
|
@@ -25,8 +26,8 @@ freealg.egg-info/not-zip-safe
|
|
|
25
26
|
freealg.egg-info/requires.txt
|
|
26
27
|
freealg.egg-info/top_level.txt
|
|
27
28
|
freealg/distributions/__init__.py
|
|
28
|
-
freealg/distributions/
|
|
29
|
-
freealg/distributions/
|
|
30
|
-
freealg/distributions/
|
|
31
|
-
freealg/distributions/
|
|
32
|
-
freealg/distributions/
|
|
29
|
+
freealg/distributions/_kesten_mckay.py
|
|
30
|
+
freealg/distributions/_marchenko_pastur.py
|
|
31
|
+
freealg/distributions/_meixner.py
|
|
32
|
+
freealg/distributions/_wachter.py
|
|
33
|
+
freealg/distributions/_wigner.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|