freealg 0.1.10__tar.gz → 0.1.12__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.10 → freealg-0.1.12}/PKG-INFO +21 -7
- {freealg-0.1.10 → freealg-0.1.12}/README.rst +19 -6
- {freealg-0.1.10 → freealg-0.1.12}/freealg/__init__.py +2 -2
- freealg-0.1.12/freealg/__version__.py +1 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_chebyshev.py +12 -8
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_decompress.py +15 -15
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_pade.py +50 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_sample.py +6 -4
- freealg-0.1.12/freealg/_support.py +85 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_util.py +1 -1
- {freealg-0.1.10 → freealg-0.1.12}/freealg/freeform.py +178 -139
- {freealg-0.1.10 → freealg-0.1.12}/freealg.egg-info/PKG-INFO +21 -7
- {freealg-0.1.10 → freealg-0.1.12}/freealg.egg-info/SOURCES.txt +1 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg.egg-info/requires.txt +1 -0
- {freealg-0.1.10 → freealg-0.1.12}/requirements.txt +1 -0
- freealg-0.1.10/freealg/__version__.py +0 -1
- {freealg-0.1.10 → freealg-0.1.12}/AUTHORS.txt +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/CHANGELOG.rst +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/LICENSE.txt +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/MANIFEST.in +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_damp.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_jacobi.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/_plot_util.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/distributions/__init__.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/distributions/_kesten_mckay.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/distributions/_marchenko_pastur.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/distributions/_meixner.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/distributions/_wachter.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg/distributions/_wigner.py +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg.egg-info/dependency_links.txt +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg.egg-info/not-zip-safe +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/freealg.egg-info/top_level.txt +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/pyproject.toml +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/setup.cfg +0 -0
- {freealg-0.1.10 → freealg-0.1.12}/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.12
|
|
4
4
|
Summary: Free probability for large matrices
|
|
5
5
|
Keywords: leaderboard bot chat
|
|
6
6
|
Platform: Linux
|
|
@@ -31,6 +31,7 @@ Requires-Dist: texplot
|
|
|
31
31
|
Requires-Dist: matplotlib
|
|
32
32
|
Requires-Dist: colorcet
|
|
33
33
|
Requires-Dist: statsmodels
|
|
34
|
+
Requires-Dist: numba
|
|
34
35
|
Provides-Extra: test
|
|
35
36
|
Requires-Dist: tox; extra == "test"
|
|
36
37
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -69,7 +70,10 @@ Dynamic: summary
|
|
|
69
70
|
:width: 240
|
|
70
71
|
:class: custom-dark
|
|
71
72
|
|
|
72
|
-
*freealg* is a
|
|
73
|
+
*freealg* is a Python package that employs **free** probability to evaluate the spectral
|
|
74
|
+
densities of large matrix **form**\ s. The fundamental algorithm employed by *freealg* is
|
|
75
|
+
**free decompression**, which extrapolates from the empirical spectral densities of small
|
|
76
|
+
submatrices to infer the eigenspectrum of extremely large matrices.
|
|
73
77
|
|
|
74
78
|
Install
|
|
75
79
|
=======
|
|
@@ -95,12 +99,18 @@ Documentation is available at `ameli.github.io/freealg <https://ameli.github.io/
|
|
|
95
99
|
Quick Usage
|
|
96
100
|
===========
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
The following code estimates the eigenvalues of a very large Wishart matrix using a much
|
|
103
|
+
smaller Wishart matrix.
|
|
100
104
|
|
|
101
105
|
.. code-block:: python
|
|
102
106
|
|
|
103
107
|
>>> import freealg as fa
|
|
108
|
+
>>> mp = fa.distributions.MarchenkoPastur(1/50) # Wishart matrices with aspect ratio 1/50
|
|
109
|
+
>>> A = mp.matrix(1000) # Sample a 1000 x 1000 Wishart matrix
|
|
110
|
+
>>> eigs = fa.eigfree(A, 100_000) # Estimate the eigenvalues of 100000 x 100000
|
|
111
|
+
|
|
112
|
+
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>`__.
|
|
113
|
+
|
|
104
114
|
|
|
105
115
|
Test
|
|
106
116
|
====
|
|
@@ -130,14 +140,18 @@ requests and bug reports.
|
|
|
130
140
|
How to Cite
|
|
131
141
|
===========
|
|
132
142
|
|
|
133
|
-
|
|
143
|
+
If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994>`.
|
|
134
144
|
|
|
135
145
|
.. code::
|
|
136
146
|
|
|
137
|
-
@
|
|
138
|
-
|
|
147
|
+
@article{ameli2025spectral,
|
|
148
|
+
title={Spectral Estimation with Free Decompression},
|
|
149
|
+
author={Siavash Ameli and Chris van der Heide and Liam Hodgkinson and Michael W. Mahoney},
|
|
150
|
+
journal={arXiv preprint arXiv:2506.11994},
|
|
151
|
+
year={2025}
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
|
|
141
155
|
License
|
|
142
156
|
=======
|
|
143
157
|
|
|
@@ -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,9 +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
10
|
from . import distributions
|
|
11
11
|
|
|
12
|
-
__all__ = ['FreeForm', 'distributions']
|
|
12
|
+
__all__ = ['FreeForm', 'distributions', 'eigfree']
|
|
13
13
|
|
|
14
14
|
from .__version__ import __version__ # noqa: F401 E402
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.12"
|
|
@@ -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-
|
|
23
|
+
def decompress(freeform, size, x=None, delta=1e-6, iterations=500,
|
|
24
|
+
step_size=0.1, tolerance=1e-9):
|
|
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
|
|
@@ -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
|
# ==========
|
|
@@ -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
|
|
@@ -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,7 +475,7 @@ 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
480
|
print(f'"rho" is not unit mass. mass: {mass:>0.3f}. Set ' +
|
|
503
481
|
r'"force=True".')
|
|
@@ -565,7 +543,7 @@ class FreeForm(object):
|
|
|
565
543
|
"""
|
|
566
544
|
|
|
567
545
|
if self.psi is None:
|
|
568
|
-
raise RuntimeError('
|
|
546
|
+
raise RuntimeError('The spectral density needs to be fit using the .fit() function.')
|
|
569
547
|
|
|
570
548
|
# Create x if not given
|
|
571
549
|
if x is None:
|
|
@@ -599,7 +577,7 @@ class FreeForm(object):
|
|
|
599
577
|
|
|
600
578
|
# Integrate each row over t using trapezoid rule on x_s
|
|
601
579
|
# Namely, hilb[i] = int rho_s(t)/(t - x[i]) dt
|
|
602
|
-
hilb = numpy.
|
|
580
|
+
hilb = numpy.trapezoid(D, x_s, axis=1) / numpy.pi
|
|
603
581
|
|
|
604
582
|
# We use negative sign convention
|
|
605
583
|
hilb = -hilb
|
|
@@ -615,12 +593,10 @@ class FreeForm(object):
|
|
|
615
593
|
# ====
|
|
616
594
|
|
|
617
595
|
def _glue(self, z):
|
|
618
|
-
"""
|
|
619
|
-
"""
|
|
620
|
-
|
|
621
596
|
# Glue function
|
|
597
|
+
if self._pade_sol is None:
|
|
598
|
+
return numpy.zeros_like(z)
|
|
622
599
|
g = eval_pade(z, self._pade_sol)
|
|
623
|
-
|
|
624
600
|
return g
|
|
625
601
|
|
|
626
602
|
# =========
|
|
@@ -629,8 +605,8 @@ class FreeForm(object):
|
|
|
629
605
|
|
|
630
606
|
def stieltjes(self, x=None, y=None, plot=False, latex=False, save=False):
|
|
631
607
|
"""
|
|
632
|
-
Compute Stieltjes transform of the spectral density
|
|
633
|
-
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.
|
|
634
610
|
|
|
635
611
|
Parameters
|
|
636
612
|
----------
|
|
@@ -689,7 +665,12 @@ class FreeForm(object):
|
|
|
689
665
|
"""
|
|
690
666
|
|
|
691
667
|
if self.psi is None:
|
|
692
|
-
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)
|
|
693
674
|
|
|
694
675
|
# Create x if not given
|
|
695
676
|
if x is None:
|
|
@@ -699,43 +680,21 @@ class FreeForm(object):
|
|
|
699
680
|
x_min = numpy.floor(2.0 * (center - 2.0 * radius * scale)) / 2.0
|
|
700
681
|
x_max = numpy.ceil(2.0 * (center + 2.0 * radius * scale)) / 2.0
|
|
701
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
|
|
702
687
|
|
|
703
688
|
# Create y if not given
|
|
704
|
-
if
|
|
705
|
-
y
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
# Stieltjes function
|
|
715
|
-
if self.method == 'jacobi':
|
|
716
|
-
stieltjes = partial(jacobi_stieltjes, psi=self.psi,
|
|
717
|
-
support=self.support, alpha=self.alpha,
|
|
718
|
-
beta=self.beta, n_base=n_base)
|
|
719
|
-
elif self.method == 'chebyshev':
|
|
720
|
-
stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
|
|
721
|
-
support=self.support)
|
|
722
|
-
|
|
723
|
-
mask_p = y >= 0.0
|
|
724
|
-
mask_m = y < 0.0
|
|
725
|
-
|
|
726
|
-
m1 = numpy.zeros_like(z)
|
|
727
|
-
m2 = numpy.zeros_like(z)
|
|
728
|
-
|
|
729
|
-
# Upper half-plane
|
|
730
|
-
m1[mask_p, :] = stieltjes(z[mask_p, :])
|
|
731
|
-
|
|
732
|
-
# Lower half-plane, use Schwarz reflection
|
|
733
|
-
m1[mask_m, :] = numpy.conjugate(
|
|
734
|
-
stieltjes(numpy.conjugate(z[mask_m, :])))
|
|
735
|
-
|
|
736
|
-
# Second Riemann sheet
|
|
737
|
-
m2[mask_p, :] = m1[mask_p, :]
|
|
738
|
-
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)
|
|
739
698
|
|
|
740
699
|
if plot:
|
|
741
700
|
plot_stieltjes(x, y, m1, m2, self.support, latex=latex, save=save)
|
|
@@ -766,44 +725,26 @@ class FreeForm(object):
|
|
|
766
725
|
|
|
767
726
|
m_m : numpy.ndarray
|
|
768
727
|
The Stieltjes transform continued to the secondary branch.
|
|
769
|
-
|
|
770
|
-
See Also
|
|
771
|
-
--------
|
|
772
|
-
density
|
|
773
|
-
hilbert
|
|
774
|
-
|
|
775
|
-
Notes
|
|
776
|
-
-----
|
|
777
|
-
|
|
778
|
-
Notes.
|
|
779
|
-
|
|
780
|
-
References
|
|
781
|
-
----------
|
|
782
|
-
|
|
783
|
-
.. [1] tbd
|
|
784
|
-
|
|
785
|
-
Examples
|
|
786
|
-
--------
|
|
787
|
-
|
|
788
|
-
.. code-block:: python
|
|
789
|
-
|
|
790
|
-
>>> from freealg import FreeForm
|
|
791
728
|
"""
|
|
792
729
|
|
|
793
|
-
|
|
794
|
-
raise RuntimeError('"fit" the model first.')
|
|
730
|
+
assert self.psi is not None, "The fit function has not been called."
|
|
795
731
|
|
|
732
|
+
# Allow for arbitrary input shapes
|
|
796
733
|
z = numpy.asarray(z)
|
|
797
734
|
shape = z.shape
|
|
798
735
|
if len(shape) == 0:
|
|
799
736
|
shape = (1,)
|
|
800
737
|
z = z.reshape(-1, 1)
|
|
801
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
|
+
|
|
802
743
|
# Stieltjes function
|
|
803
744
|
if self.method == 'jacobi':
|
|
804
745
|
stieltjes = partial(jacobi_stieltjes, psi=self.psi,
|
|
805
746
|
support=self.support, alpha=self.alpha,
|
|
806
|
-
beta=self.beta)
|
|
747
|
+
beta=self.beta) # n_base = n_base
|
|
807
748
|
elif self.method == 'chebyshev':
|
|
808
749
|
stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
|
|
809
750
|
support=self.support)
|
|
@@ -814,17 +755,25 @@ class FreeForm(object):
|
|
|
814
755
|
m1 = numpy.zeros_like(z)
|
|
815
756
|
m2 = numpy.zeros_like(z)
|
|
816
757
|
|
|
817
|
-
|
|
818
|
-
|
|
758
|
+
if self._pade_sol is not None:
|
|
759
|
+
# Upper half-plane
|
|
760
|
+
m1[mask_p] = stieltjes(z[mask_p].reshape(-1, 1)).ravel()
|
|
819
761
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
762
|
+
# Lower half-plane, use Schwarz reflection
|
|
763
|
+
m1[mask_m] = numpy.conjugate(
|
|
764
|
+
stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))).ravel()
|
|
823
765
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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()
|
|
828
777
|
|
|
829
778
|
m1, m2 = m1.reshape(*shape), m2.reshape(*shape)
|
|
830
779
|
|
|
@@ -834,8 +783,8 @@ class FreeForm(object):
|
|
|
834
783
|
# decompress
|
|
835
784
|
# ==========
|
|
836
785
|
|
|
837
|
-
def decompress(self, size, x=None,
|
|
838
|
-
step_size=0.1, tolerance=1e-
|
|
786
|
+
def decompress(self, size, x=None, iterations=500, eigvals=True,
|
|
787
|
+
step_size=0.1, tolerance=1e-9, seed=None, plot=False,
|
|
839
788
|
latex=False, save=False):
|
|
840
789
|
"""
|
|
841
790
|
Free decompression of spectral density.
|
|
@@ -850,17 +799,16 @@ class FreeForm(object):
|
|
|
850
799
|
Positions where density to be evaluated at. If `None`, an interval
|
|
851
800
|
slightly larger than the support interval will be used.
|
|
852
801
|
|
|
853
|
-
delta: float, default=1e-4
|
|
854
|
-
Size of the perturbation into the upper half plane for Plemelj's
|
|
855
|
-
formula.
|
|
856
|
-
|
|
857
802
|
iterations: int, default=500
|
|
858
803
|
Maximum number of Newton iterations.
|
|
859
804
|
|
|
805
|
+
eigvals: bool, default=True
|
|
806
|
+
Return estimated (sampled) eigenvalues as well as the density.
|
|
807
|
+
|
|
860
808
|
step_size: float, default=0.1
|
|
861
809
|
Step size for Newton iterations.
|
|
862
810
|
|
|
863
|
-
tolerance: float, default=1e-
|
|
811
|
+
tolerance: float, default=1e-9
|
|
864
812
|
Tolerance for the solution obtained by the Newton solver. Also
|
|
865
813
|
used for the finite difference approximation to the derivative.
|
|
866
814
|
|
|
@@ -882,12 +830,15 @@ class FreeForm(object):
|
|
|
882
830
|
Returns
|
|
883
831
|
-------
|
|
884
832
|
|
|
833
|
+
x : numpy.array
|
|
834
|
+
Locations where the spectral density is estimated
|
|
835
|
+
|
|
885
836
|
rho : numpy.array
|
|
886
|
-
|
|
837
|
+
Estimated spectral density at locations x
|
|
887
838
|
|
|
888
839
|
eigs : numpy.array
|
|
889
840
|
Estimated eigenvalues as low-discrepancy samples of the estimated
|
|
890
|
-
spectral density.
|
|
841
|
+
spectral density. Only returns if ``eigvals=True``.
|
|
891
842
|
|
|
892
843
|
See Also
|
|
893
844
|
--------
|
|
@@ -915,7 +866,7 @@ class FreeForm(object):
|
|
|
915
866
|
|
|
916
867
|
size = int(size)
|
|
917
868
|
|
|
918
|
-
rho, x, (lb, ub) = decompress(self, size, x=x, delta=delta,
|
|
869
|
+
rho, x, (lb, ub) = decompress(self, size, x=x, delta=self.delta,
|
|
919
870
|
iterations=iterations,
|
|
920
871
|
step_size=step_size, tolerance=tolerance)
|
|
921
872
|
x, rho = x.ravel(), rho.ravel()
|
|
@@ -924,6 +875,94 @@ class FreeForm(object):
|
|
|
924
875
|
plot_density(x, rho, support=(lb, ub),
|
|
925
876
|
label='Decompression', latex=latex, save=save)
|
|
926
877
|
|
|
927
|
-
|
|
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.
|
|
928
893
|
|
|
929
|
-
|
|
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)**.2)
|
|
960
|
+
ff.fit(method='chebyshev', K=order, projection='sample', damp='jackson',
|
|
961
|
+
force=True, plot=False, latex=False, save=False, reg=0.05)
|
|
962
|
+
_, _, eigs = ff.decompress(N)
|
|
963
|
+
|
|
964
|
+
if psd:
|
|
965
|
+
eigs = numpy.abs(eigs)
|
|
966
|
+
eigs.sort()
|
|
967
|
+
|
|
968
|
+
return eigs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freealg
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
4
4
|
Summary: Free probability for large matrices
|
|
5
5
|
Keywords: leaderboard bot chat
|
|
6
6
|
Platform: Linux
|
|
@@ -31,6 +31,7 @@ Requires-Dist: texplot
|
|
|
31
31
|
Requires-Dist: matplotlib
|
|
32
32
|
Requires-Dist: colorcet
|
|
33
33
|
Requires-Dist: statsmodels
|
|
34
|
+
Requires-Dist: numba
|
|
34
35
|
Provides-Extra: test
|
|
35
36
|
Requires-Dist: tox; extra == "test"
|
|
36
37
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -69,7 +70,10 @@ Dynamic: summary
|
|
|
69
70
|
:width: 240
|
|
70
71
|
:class: custom-dark
|
|
71
72
|
|
|
72
|
-
*freealg* is a
|
|
73
|
+
*freealg* is a Python package that employs **free** probability to evaluate the spectral
|
|
74
|
+
densities of large matrix **form**\ s. The fundamental algorithm employed by *freealg* is
|
|
75
|
+
**free decompression**, which extrapolates from the empirical spectral densities of small
|
|
76
|
+
submatrices to infer the eigenspectrum of extremely large matrices.
|
|
73
77
|
|
|
74
78
|
Install
|
|
75
79
|
=======
|
|
@@ -95,12 +99,18 @@ Documentation is available at `ameli.github.io/freealg <https://ameli.github.io/
|
|
|
95
99
|
Quick Usage
|
|
96
100
|
===========
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
The following code estimates the eigenvalues of a very large Wishart matrix using a much
|
|
103
|
+
smaller Wishart matrix.
|
|
100
104
|
|
|
101
105
|
.. code-block:: python
|
|
102
106
|
|
|
103
107
|
>>> import freealg as fa
|
|
108
|
+
>>> mp = fa.distributions.MarchenkoPastur(1/50) # Wishart matrices with aspect ratio 1/50
|
|
109
|
+
>>> A = mp.matrix(1000) # Sample a 1000 x 1000 Wishart matrix
|
|
110
|
+
>>> eigs = fa.eigfree(A, 100_000) # Estimate the eigenvalues of 100000 x 100000
|
|
111
|
+
|
|
112
|
+
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>`__.
|
|
113
|
+
|
|
104
114
|
|
|
105
115
|
Test
|
|
106
116
|
====
|
|
@@ -130,14 +140,18 @@ requests and bug reports.
|
|
|
130
140
|
How to Cite
|
|
131
141
|
===========
|
|
132
142
|
|
|
133
|
-
|
|
143
|
+
If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994>`.
|
|
134
144
|
|
|
135
145
|
.. code::
|
|
136
146
|
|
|
137
|
-
@
|
|
138
|
-
|
|
147
|
+
@article{ameli2025spectral,
|
|
148
|
+
title={Spectral Estimation with Free Decompression},
|
|
149
|
+
author={Siavash Ameli and Chris van der Heide and Liam Hodgkinson and Michael W. Mahoney},
|
|
150
|
+
journal={arXiv preprint arXiv:2506.11994},
|
|
151
|
+
year={2025}
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
|
|
141
155
|
License
|
|
142
156
|
=======
|
|
143
157
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.10"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|