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
|
@@ -110,7 +110,7 @@ class KestenMcKay(object):
|
|
|
110
110
|
# density
|
|
111
111
|
# =======
|
|
112
112
|
|
|
113
|
-
def density(self, x=None, plot=False, latex=False, save=False):
|
|
113
|
+
def density(self, x=None, plot=False, latex=False, save=False, eig=None):
|
|
114
114
|
"""
|
|
115
115
|
Density of distribution.
|
|
116
116
|
|
|
@@ -137,6 +137,10 @@ class KestenMcKay(object):
|
|
|
137
137
|
assumed to the save filename (with the file extension). This option
|
|
138
138
|
is relevant only if ``plot=True``.
|
|
139
139
|
|
|
140
|
+
eig : numpy.array, default=None
|
|
141
|
+
A collection of eigenvalues to compare to via histogram. This
|
|
142
|
+
option is relevant only if ``plot=True``.
|
|
143
|
+
|
|
140
144
|
Returns
|
|
141
145
|
-------
|
|
142
146
|
|
|
@@ -173,7 +177,11 @@ class KestenMcKay(object):
|
|
|
173
177
|
numpy.sqrt(4.0 * (self.d - 1.0) - x[mask]**2)
|
|
174
178
|
|
|
175
179
|
if plot:
|
|
176
|
-
|
|
180
|
+
if eig is not None:
|
|
181
|
+
label = 'Estimate'
|
|
182
|
+
else:
|
|
183
|
+
label = ''
|
|
184
|
+
plot_density(x, rho, label=label, latex=latex, save=save, eig=eig)
|
|
177
185
|
|
|
178
186
|
return rho
|
|
179
187
|
|
|
@@ -539,9 +547,9 @@ class KestenMcKay(object):
|
|
|
539
547
|
|
|
540
548
|
return samples
|
|
541
549
|
|
|
542
|
-
#
|
|
543
|
-
#
|
|
544
|
-
#
|
|
550
|
+
# ===============
|
|
551
|
+
# haar orthogonal
|
|
552
|
+
# ===============
|
|
545
553
|
|
|
546
554
|
def _haar_orthogonal(self, n, k, seed=None):
|
|
547
555
|
"""
|
|
@@ -108,7 +108,7 @@ class MarchenkoPastur(object):
|
|
|
108
108
|
# density
|
|
109
109
|
# =======
|
|
110
110
|
|
|
111
|
-
def density(self, x=None, plot=False, latex=False, save=False):
|
|
111
|
+
def density(self, x=None, plot=False, latex=False, save=False, eig=None):
|
|
112
112
|
"""
|
|
113
113
|
Density of distribution.
|
|
114
114
|
|
|
@@ -135,6 +135,10 @@ class MarchenkoPastur(object):
|
|
|
135
135
|
assumed to the save filename (with the file extension). This option
|
|
136
136
|
is relevant only if ``plot=True``.
|
|
137
137
|
|
|
138
|
+
eig : numpy.array, default=None
|
|
139
|
+
A collection of eigenvalues to compare to via histogram. This
|
|
140
|
+
option is relevant only if ``plot=True``.
|
|
141
|
+
|
|
138
142
|
Returns
|
|
139
143
|
-------
|
|
140
144
|
|
|
@@ -171,7 +175,11 @@ class MarchenkoPastur(object):
|
|
|
171
175
|
numpy.sqrt((self.lam_p - x[mask]) * (x[mask] - self.lam_m))
|
|
172
176
|
|
|
173
177
|
if plot:
|
|
174
|
-
|
|
178
|
+
if eig is not None:
|
|
179
|
+
label = 'Estimate'
|
|
180
|
+
else:
|
|
181
|
+
label = ''
|
|
182
|
+
plot_density(x, rho, label=label, latex=latex, save=save, eig=eig)
|
|
175
183
|
|
|
176
184
|
return rho
|
|
177
185
|
|
|
@@ -114,7 +114,7 @@ class Meixner(object):
|
|
|
114
114
|
# density
|
|
115
115
|
# =======
|
|
116
116
|
|
|
117
|
-
def density(self, x=None, plot=False, latex=False, save=False):
|
|
117
|
+
def density(self, x=None, plot=False, latex=False, save=False, eig=None):
|
|
118
118
|
"""
|
|
119
119
|
Density of distribution.
|
|
120
120
|
|
|
@@ -141,6 +141,10 @@ class Meixner(object):
|
|
|
141
141
|
assumed to the save filename (with the file extension). This option
|
|
142
142
|
is relevant only if ``plot=True``.
|
|
143
143
|
|
|
144
|
+
eig : numpy.array, default=None
|
|
145
|
+
A collection of eigenvalues to compare to via histogram. This
|
|
146
|
+
option is relevant only if ``plot=True``.
|
|
147
|
+
|
|
144
148
|
Returns
|
|
145
149
|
-------
|
|
146
150
|
|
|
@@ -188,7 +192,11 @@ class Meixner(object):
|
|
|
188
192
|
rho[mask] = numer[mask] / denom[mask]
|
|
189
193
|
|
|
190
194
|
if plot:
|
|
191
|
-
|
|
195
|
+
if eig is not None:
|
|
196
|
+
label = 'Estimate'
|
|
197
|
+
else:
|
|
198
|
+
label = ''
|
|
199
|
+
plot_density(x, rho, label=label, latex=latex, save=save, eig=eig)
|
|
192
200
|
|
|
193
201
|
return rho
|
|
194
202
|
|
|
@@ -115,7 +115,7 @@ class Wachter(object):
|
|
|
115
115
|
# density
|
|
116
116
|
# =======
|
|
117
117
|
|
|
118
|
-
def density(self, x=None, plot=False, latex=False, save=False):
|
|
118
|
+
def density(self, x=None, plot=False, latex=False, save=False, eig=None):
|
|
119
119
|
"""
|
|
120
120
|
Density of distribution.
|
|
121
121
|
|
|
@@ -142,6 +142,10 @@ class Wachter(object):
|
|
|
142
142
|
assumed to the save filename (with the file extension). This option
|
|
143
143
|
is relevant only if ``plot=True``.
|
|
144
144
|
|
|
145
|
+
eig : numpy.array, default=None
|
|
146
|
+
A collection of eigenvalues to compare to via histogram. This
|
|
147
|
+
option is relevant only if ``plot=True``.
|
|
148
|
+
|
|
145
149
|
Returns
|
|
146
150
|
-------
|
|
147
151
|
|
|
@@ -179,7 +183,11 @@ class Wachter(object):
|
|
|
179
183
|
numpy.sqrt((self.lam_p - x[mask]) * (x[mask] - self.lam_m))
|
|
180
184
|
|
|
181
185
|
if plot:
|
|
182
|
-
|
|
186
|
+
if eig is not None:
|
|
187
|
+
label = 'Estimate'
|
|
188
|
+
else:
|
|
189
|
+
label = ''
|
|
190
|
+
plot_density(x, rho, label=label, latex=latex, save=save, eig=eig)
|
|
183
191
|
|
|
184
192
|
return rho
|
|
185
193
|
|
freealg/distributions/_wigner.py
CHANGED
|
@@ -96,7 +96,7 @@ class Wigner(object):
|
|
|
96
96
|
# density
|
|
97
97
|
# =======
|
|
98
98
|
|
|
99
|
-
def density(self, x=None, plot=False, latex=False, save=False):
|
|
99
|
+
def density(self, x=None, plot=False, latex=False, save=False, eig=None):
|
|
100
100
|
"""
|
|
101
101
|
Density of distribution.
|
|
102
102
|
|
|
@@ -123,6 +123,10 @@ class Wigner(object):
|
|
|
123
123
|
assumed to the save filename (with the file extension). This option
|
|
124
124
|
is relevant only if ``plot=True``.
|
|
125
125
|
|
|
126
|
+
eig : numpy.array, default=None
|
|
127
|
+
A collection of eigenvalues to compare to via histogram. This
|
|
128
|
+
option is relevant only if ``plot=True``.
|
|
129
|
+
|
|
126
130
|
Returns
|
|
127
131
|
-------
|
|
128
132
|
|
|
@@ -159,7 +163,11 @@ class Wigner(object):
|
|
|
159
163
|
numpy.sqrt(self.r**2 - x[mask]**2)
|
|
160
164
|
|
|
161
165
|
if plot:
|
|
162
|
-
|
|
166
|
+
if eig is not None:
|
|
167
|
+
label = 'Estimate'
|
|
168
|
+
else:
|
|
169
|
+
label = ''
|
|
170
|
+
plot_density(x, rho, label=label, latex=latex, save=save, eig=eig)
|
|
163
171
|
|
|
164
172
|
return rho
|
|
165
173
|
|
freealg/eigfree.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2025, Siavash Ameli <sameli@berkeley.edu>
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileType: SOURCE
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
# the terms of the license found in the LICENSE.txt file in the root directory
|
|
7
|
+
# of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
from ._util import compute_eig
|
|
16
|
+
from .freeform import FreeForm
|
|
17
|
+
|
|
18
|
+
__all__ = ['eigfree']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ========
|
|
22
|
+
# eig free
|
|
23
|
+
# ========
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def eigfree(A, N=None, psd=None, plots=False):
|
|
27
|
+
"""
|
|
28
|
+
Estimate the eigenvalues of a matrix.
|
|
29
|
+
|
|
30
|
+
This function estimates the eigenvalues of the matrix :math:`\\mathbf{A}`
|
|
31
|
+
or a larger matrix containing :math:`\\mathbf{A}` using free decompression.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
|
|
36
|
+
A : numpy.ndarray
|
|
37
|
+
The symmetric real-valued matrix :math:`\\mathbf{A}` whose eigenvalues
|
|
38
|
+
(or those of a matrix containing :math:`\\mathbf{A}`) are to be
|
|
39
|
+
computed.
|
|
40
|
+
|
|
41
|
+
N : int, default=None
|
|
42
|
+
The size of the matrix containing :math:`\\mathbf{A}` to estimate
|
|
43
|
+
eigenvalues of. If None, returns estimates of the eigenvalues of
|
|
44
|
+
:math:`\\mathbf{A}` itself.
|
|
45
|
+
|
|
46
|
+
psd: bool, default=None
|
|
47
|
+
Determines whether the matrix is positive-semidefinite (PSD; all
|
|
48
|
+
eigenvalues are non-negative). If None, the matrix is considered PSD if
|
|
49
|
+
all sampled eigenvalues are positive.
|
|
50
|
+
|
|
51
|
+
plots : bool, default=False
|
|
52
|
+
Print out all relevant plots for diagnosing eigenvalue accuracy.
|
|
53
|
+
|
|
54
|
+
Notes
|
|
55
|
+
-----
|
|
56
|
+
|
|
57
|
+
This is a convenience function for the FreeForm class with some effective
|
|
58
|
+
defaults that work well for common random matrix ensembles. For improved
|
|
59
|
+
performance and plotting utilites, consider finetuning parameters using
|
|
60
|
+
the FreeForm class.
|
|
61
|
+
|
|
62
|
+
References
|
|
63
|
+
----------
|
|
64
|
+
|
|
65
|
+
.. [1] Reference.
|
|
66
|
+
|
|
67
|
+
Examples
|
|
68
|
+
--------
|
|
69
|
+
|
|
70
|
+
.. code-block:: python
|
|
71
|
+
|
|
72
|
+
>>> from freealg import FreeForm
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
|
76
|
+
raise RuntimeError("Only square matrices are permitted.")
|
|
77
|
+
n = A.shape[0]
|
|
78
|
+
|
|
79
|
+
if N is None:
|
|
80
|
+
N = n
|
|
81
|
+
|
|
82
|
+
# Size of sample matrix
|
|
83
|
+
n_s = int(80*(1 + numpy.log(n)))
|
|
84
|
+
# If matrix is not large enough, return eigenvalues
|
|
85
|
+
if n < n_s:
|
|
86
|
+
return compute_eig(A)
|
|
87
|
+
# Number of samples
|
|
88
|
+
num_samples = int(10 * (n / n_s)**0.5)
|
|
89
|
+
|
|
90
|
+
# Collect eigenvalue samples
|
|
91
|
+
samples = []
|
|
92
|
+
for _ in range(num_samples):
|
|
93
|
+
indices = numpy.random.choice(n, n_s, replace=False)
|
|
94
|
+
samples.append(compute_eig(A[numpy.ix_(indices, indices)]))
|
|
95
|
+
samples = numpy.concatenate(samples).ravel()
|
|
96
|
+
|
|
97
|
+
# If all eigenvalues are positive, set PSD flag
|
|
98
|
+
if psd is None:
|
|
99
|
+
psd = samples.min() > 0
|
|
100
|
+
|
|
101
|
+
ff = FreeForm(samples)
|
|
102
|
+
# Since we are resampling, we need to provide the correct matrix size
|
|
103
|
+
ff.n = n_s
|
|
104
|
+
|
|
105
|
+
# Perform fit and estimate eigenvalues
|
|
106
|
+
order = 1 + int(len(samples)**.2)
|
|
107
|
+
ff.fit(method='chebyshev', K=order, projection='sample',
|
|
108
|
+
force=True, plot=False, latex=False, save=False)
|
|
109
|
+
|
|
110
|
+
if plots:
|
|
111
|
+
ff.density(plot=True)
|
|
112
|
+
ff.stieltjes(plot=True)
|
|
113
|
+
|
|
114
|
+
_, _, eigs = ff.decompress(N, plot=plots)
|
|
115
|
+
|
|
116
|
+
if psd:
|
|
117
|
+
eigs = numpy.abs(eigs)
|
|
118
|
+
eigs.sort()
|
|
119
|
+
|
|
120
|
+
return eigs
|
freealg/freeform.py
CHANGED
|
@@ -28,7 +28,7 @@ from ._decompress import decompress
|
|
|
28
28
|
from ._sample import qmc_sample
|
|
29
29
|
from ._support import detect_support
|
|
30
30
|
|
|
31
|
-
__all__ = ['FreeForm'
|
|
31
|
+
__all__ = ['FreeForm']
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
# =========
|
|
@@ -75,15 +75,15 @@ class FreeForm(object):
|
|
|
75
75
|
Eigenvalues of the matrix
|
|
76
76
|
|
|
77
77
|
support: tuple
|
|
78
|
-
The predicted (or given) support :math:`(
|
|
79
|
-
eigenvalue density.
|
|
78
|
+
The predicted (or given) support :math:`(\\lambda_{\\min},
|
|
79
|
+
\\lambda_{\\max})` of the eigenvalue density.
|
|
80
80
|
|
|
81
81
|
psi : numpy.array
|
|
82
82
|
Jacobi coefficients.
|
|
83
83
|
|
|
84
|
-
n
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
n : int
|
|
85
|
+
Initial array size (assuming a square matrix when :math:`\\mathbf{A}` is
|
|
86
|
+
2D).
|
|
87
87
|
|
|
88
88
|
Methods
|
|
89
89
|
-------
|
|
@@ -390,10 +390,10 @@ class FreeForm(object):
|
|
|
390
390
|
x_supp = numpy.linspace(self.lam_m, self.lam_p, 1000)
|
|
391
391
|
g_supp = 2.0 * numpy.pi * self.hilbert(x_supp)
|
|
392
392
|
self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m, self.lam_p,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
393
|
+
p=pade_p, q=pade_q, odd_side=odd_side,
|
|
394
|
+
pade_reg=pade_reg, safety=1.0,
|
|
395
|
+
max_outer=40, xtol=1e-12, ftol=1e-12,
|
|
396
|
+
optimizer=optimizer, verbose=0)
|
|
397
397
|
|
|
398
398
|
if plot:
|
|
399
399
|
g_supp_approx = eval_pade(x_supp[None, :], self._pade_sol)[0, :]
|
|
@@ -449,7 +449,8 @@ class FreeForm(object):
|
|
|
449
449
|
"""
|
|
450
450
|
|
|
451
451
|
if self.psi is None:
|
|
452
|
-
raise RuntimeError('The spectral density needs to be fit using
|
|
452
|
+
raise RuntimeError('The spectral density needs to be fit using ' +
|
|
453
|
+
'the .fit() function.')
|
|
453
454
|
|
|
454
455
|
# Create x if not given
|
|
455
456
|
if x is None:
|
|
@@ -543,7 +544,8 @@ class FreeForm(object):
|
|
|
543
544
|
"""
|
|
544
545
|
|
|
545
546
|
if self.psi is None:
|
|
546
|
-
raise RuntimeError('The spectral density needs to be fit using
|
|
547
|
+
raise RuntimeError('The spectral density needs to be fit using ' +
|
|
548
|
+
'the .fit() function.')
|
|
547
549
|
|
|
548
550
|
# Create x if not given
|
|
549
551
|
if x is None:
|
|
@@ -605,8 +607,10 @@ class FreeForm(object):
|
|
|
605
607
|
|
|
606
608
|
def stieltjes(self, x=None, y=None, plot=False, latex=False, save=False):
|
|
607
609
|
"""
|
|
608
|
-
Compute Stieltjes transform of the spectral density
|
|
609
|
-
|
|
610
|
+
Compute Stieltjes transform of the spectral density on a grid.
|
|
611
|
+
|
|
612
|
+
This function evaluates Stieltjes transform on an array of points, or
|
|
613
|
+
over a 2D Cartesian grid on the complex plane.
|
|
610
614
|
|
|
611
615
|
Parameters
|
|
612
616
|
----------
|
|
@@ -665,11 +669,11 @@ class FreeForm(object):
|
|
|
665
669
|
"""
|
|
666
670
|
|
|
667
671
|
if self.psi is None:
|
|
668
|
-
raise RuntimeError('The spectral density needs to be fit using
|
|
669
|
-
|
|
672
|
+
raise RuntimeError('The spectral density needs to be fit using ' +
|
|
673
|
+
'the .fit() function.')
|
|
670
674
|
|
|
671
|
-
# Determine whether the Stieltjes transform is to be computed on
|
|
672
|
-
#
|
|
675
|
+
# Determine whether the Stieltjes transform is to be computed on a
|
|
676
|
+
# Cartesian grid
|
|
673
677
|
cartesian = plot | (y is not None)
|
|
674
678
|
|
|
675
679
|
# Create x if not given
|
|
@@ -693,8 +697,8 @@ class FreeForm(object):
|
|
|
693
697
|
z = x_grid + 1j * y_grid # shape (Ny, Nx)
|
|
694
698
|
else:
|
|
695
699
|
z = x
|
|
696
|
-
|
|
697
|
-
m1, m2 = self._eval_stieltjes(z)
|
|
700
|
+
|
|
701
|
+
m1, m2 = self._eval_stieltjes(z, branches=True)
|
|
698
702
|
|
|
699
703
|
if plot:
|
|
700
704
|
plot_stieltjes(x, y, m1, m2, self.support, latex=latex, save=save)
|
|
@@ -705,7 +709,7 @@ class FreeForm(object):
|
|
|
705
709
|
# eval stieltjes
|
|
706
710
|
# ==============
|
|
707
711
|
|
|
708
|
-
def _eval_stieltjes(self, z):
|
|
712
|
+
def _eval_stieltjes(self, z, branches=False):
|
|
709
713
|
"""
|
|
710
714
|
Compute Stieltjes transform of the spectral density.
|
|
711
715
|
|
|
@@ -716,12 +720,18 @@ class FreeForm(object):
|
|
|
716
720
|
The z values in the complex plan where the Stieltjes transform is
|
|
717
721
|
evaluated.
|
|
718
722
|
|
|
723
|
+
branches : bool, default = False
|
|
724
|
+
Return both the principal and secondary branches of the Stieltjes
|
|
725
|
+
transform. The default ``branches=False`` will return only
|
|
726
|
+
the secondary branch.
|
|
727
|
+
|
|
719
728
|
|
|
720
729
|
Returns
|
|
721
730
|
-------
|
|
722
731
|
|
|
723
732
|
m_p : numpy.ndarray
|
|
724
|
-
The Stieltjes transform on the principal branch
|
|
733
|
+
The Stieltjes transform on the principal branch if
|
|
734
|
+
``branches=True``.
|
|
725
735
|
|
|
726
736
|
m_m : numpy.ndarray
|
|
727
737
|
The Stieltjes transform continued to the secondary branch.
|
|
@@ -737,14 +747,15 @@ class FreeForm(object):
|
|
|
737
747
|
z = z.reshape(-1, 1)
|
|
738
748
|
|
|
739
749
|
# # Set the number of bases as the number of x points insides support
|
|
740
|
-
# mask_sup = numpy.logical_and(z.real >= self.lam_m,
|
|
750
|
+
# mask_sup = numpy.logical_and(z.real >= self.lam_m,
|
|
751
|
+
# z.real <= self.lam_p)
|
|
741
752
|
# n_base = 2 * numpy.sum(mask_sup)
|
|
742
753
|
|
|
743
754
|
# Stieltjes function
|
|
744
755
|
if self.method == 'jacobi':
|
|
745
756
|
stieltjes = partial(jacobi_stieltjes, psi=self.psi,
|
|
746
757
|
support=self.support, alpha=self.alpha,
|
|
747
|
-
beta=self.beta)
|
|
758
|
+
beta=self.beta) # n_base = n_base
|
|
748
759
|
elif self.method == 'chebyshev':
|
|
749
760
|
stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
|
|
750
761
|
support=self.support)
|
|
@@ -760,31 +771,34 @@ class FreeForm(object):
|
|
|
760
771
|
m1[mask_p] = stieltjes(z[mask_p].reshape(-1, 1)).ravel()
|
|
761
772
|
|
|
762
773
|
# Lower half-plane, use Schwarz reflection
|
|
763
|
-
|
|
764
|
-
|
|
774
|
+
z_conj = numpy.conjugate(z[mask_m].reshape(-1, 1))
|
|
775
|
+
m1[mask_m] = numpy.conjugate(stieltjes(z_conj)).ravel()
|
|
765
776
|
|
|
766
777
|
# Second Riemann sheet
|
|
767
778
|
m2[mask_p] = m1[mask_p]
|
|
768
779
|
m2[mask_m] = -m1[mask_m] + self._glue(
|
|
769
780
|
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()
|
|
777
781
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
782
|
+
else:
|
|
783
|
+
m2[:] = stieltjes(z.reshape(-1, 1)).reshape(*m2.shape)
|
|
784
|
+
if branches:
|
|
785
|
+
m1[mask_p] = m2[mask_p]
|
|
786
|
+
m1[mask_m] = numpy.conjugate(
|
|
787
|
+
stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))
|
|
788
|
+
).ravel()
|
|
789
|
+
|
|
790
|
+
if not branches:
|
|
791
|
+
return m2.reshape(*shape)
|
|
792
|
+
else:
|
|
793
|
+
m1, m2 = m1.reshape(*shape), m2.reshape(*shape)
|
|
794
|
+
return m1, m2
|
|
781
795
|
|
|
782
796
|
# ==========
|
|
783
797
|
# decompress
|
|
784
798
|
# ==========
|
|
785
799
|
|
|
786
|
-
def decompress(self, size, x=None,
|
|
787
|
-
|
|
800
|
+
def decompress(self, size, x=None, max_iter=500, eigvals=True,
|
|
801
|
+
tolerance=1e-9, seed=None, plot=False,
|
|
788
802
|
latex=False, save=False):
|
|
789
803
|
"""
|
|
790
804
|
Free decompression of spectral density.
|
|
@@ -799,15 +813,12 @@ class FreeForm(object):
|
|
|
799
813
|
Positions where density to be evaluated at. If `None`, an interval
|
|
800
814
|
slightly larger than the support interval will be used.
|
|
801
815
|
|
|
802
|
-
|
|
803
|
-
Maximum number of
|
|
816
|
+
max_iter: int, default=500
|
|
817
|
+
Maximum number of secant method iterations.
|
|
804
818
|
|
|
805
819
|
eigvals: bool, default=True
|
|
806
820
|
Return estimated (sampled) eigenvalues as well as the density.
|
|
807
821
|
|
|
808
|
-
step_size: float, default=0.1
|
|
809
|
-
Step size for Newton iterations.
|
|
810
|
-
|
|
811
822
|
tolerance: float, default=1e-9
|
|
812
823
|
Tolerance for the solution obtained by the Newton solver. Also
|
|
813
824
|
used for the finite difference approximation to the derivative.
|
|
@@ -867,8 +878,7 @@ class FreeForm(object):
|
|
|
867
878
|
size = int(size)
|
|
868
879
|
|
|
869
880
|
rho, x, (lb, ub) = decompress(self, size, x=x, delta=self.delta,
|
|
870
|
-
|
|
871
|
-
step_size=step_size, tolerance=tolerance)
|
|
881
|
+
max_iter=max_iter, tolerance=tolerance)
|
|
872
882
|
x, rho = x.ravel(), rho.ravel()
|
|
873
883
|
|
|
874
884
|
if plot:
|
|
@@ -881,14 +891,15 @@ class FreeForm(object):
|
|
|
881
891
|
else:
|
|
882
892
|
return x, rho
|
|
883
893
|
|
|
884
|
-
|
|
894
|
+
|
|
895
|
+
def eigfree(A, N=None, psd=None, plots=False):
|
|
885
896
|
"""
|
|
886
897
|
Estimate the eigenvalues of a matrix :math:`\\mathbf{A}` or a larger matrix
|
|
887
898
|
containing :math:`\\mathbf{A}` using free decompression.
|
|
888
899
|
|
|
889
900
|
This is a convenience function for the FreeForm class with some effective
|
|
890
901
|
defaults that work well for common random matrix ensembles. For improved
|
|
891
|
-
performance and plotting utilites, consider finetuning parameters using
|
|
902
|
+
performance and plotting utilites, consider finetuning parameters using
|
|
892
903
|
the FreeForm class.
|
|
893
904
|
|
|
894
905
|
Parameters
|
|
@@ -896,18 +907,22 @@ def eigfree(A, N = None, psd = None):
|
|
|
896
907
|
|
|
897
908
|
A : numpy.ndarray
|
|
898
909
|
The symmetric real-valued matrix :math:`\\mathbf{A}` whose eigenvalues
|
|
899
|
-
(or those of a matrix containing :math:`\\mathbf{A}`) are to be
|
|
910
|
+
(or those of a matrix containing :math:`\\mathbf{A}`) are to be
|
|
911
|
+
computed.
|
|
900
912
|
|
|
901
913
|
N : int, default=None
|
|
902
914
|
The size of the matrix containing :math:`\\mathbf{A}` to estimate
|
|
903
915
|
eigenvalues of. If None, returns estimates of the eigenvalues of
|
|
904
916
|
:math:`\\mathbf{A}` itself.
|
|
905
917
|
|
|
906
|
-
psd: bool, default=None
|
|
907
|
-
Determines whether the matrix is positive-semidefinite (PSD; all
|
|
918
|
+
psd : bool, default=None
|
|
919
|
+
Determines whether the matrix is positive-semidefinite (PSD; all
|
|
908
920
|
eigenvalues are non-negative). If None, the matrix is considered PSD if
|
|
909
921
|
all sampled eigenvalues are positive.
|
|
910
922
|
|
|
923
|
+
plots : bool, default=False
|
|
924
|
+
Print out all relevant plots for diagnosing eigenvalue accuracy.
|
|
925
|
+
|
|
911
926
|
Notes
|
|
912
927
|
-----
|
|
913
928
|
|
|
@@ -925,20 +940,24 @@ def eigfree(A, N = None, psd = None):
|
|
|
925
940
|
|
|
926
941
|
>>> from freealg import FreeForm
|
|
927
942
|
"""
|
|
943
|
+
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
|
944
|
+
raise RuntimeError("Only square matrices are permitted.")
|
|
928
945
|
n = A.shape[0]
|
|
929
|
-
|
|
946
|
+
|
|
947
|
+
if N is None:
|
|
948
|
+
N = n
|
|
949
|
+
|
|
930
950
|
# Size of sample matrix
|
|
931
951
|
n_s = int(80*(1 + numpy.log(n)))
|
|
932
|
-
|
|
933
952
|
# If matrix is not large enough, return eigenvalues
|
|
934
953
|
if n < n_s:
|
|
935
954
|
return compute_eig(A)
|
|
936
|
-
|
|
937
|
-
if N is None:
|
|
938
|
-
N = n
|
|
939
|
-
|
|
940
955
|
# Number of samples
|
|
941
956
|
num_samples = int(10 * (n / n_s)**0.5)
|
|
957
|
+
# else:
|
|
958
|
+
# # Use the entire matrix given
|
|
959
|
+
# n_s = n
|
|
960
|
+
# num_samples = 1
|
|
942
961
|
|
|
943
962
|
# Collect eigenvalue samples
|
|
944
963
|
samples = []
|
|
@@ -957,12 +976,17 @@ def eigfree(A, N = None, psd = None):
|
|
|
957
976
|
|
|
958
977
|
# Perform fit and estimate eigenvalues
|
|
959
978
|
order = 1 + int(len(samples)**.2)
|
|
960
|
-
ff.fit(method='chebyshev', K=order, projection='sample',
|
|
961
|
-
force=True, plot=False, latex=False, save=False
|
|
962
|
-
|
|
979
|
+
ff.fit(method='chebyshev', K=order, projection='sample',
|
|
980
|
+
force=True, plot=False, latex=False, save=False)
|
|
981
|
+
|
|
982
|
+
if plots:
|
|
983
|
+
ff.density(plot=True)
|
|
984
|
+
ff.stieltjes(plot=True)
|
|
985
|
+
|
|
986
|
+
_, _, eigs = ff.decompress(N, plot=plots)
|
|
963
987
|
|
|
964
988
|
if psd:
|
|
965
989
|
eigs = numpy.abs(eigs)
|
|
966
990
|
eigs.sort()
|
|
967
991
|
|
|
968
|
-
return eigs
|
|
992
|
+
return eigs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freealg
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
4
4
|
Summary: Free probability for large matrices
|
|
5
5
|
Keywords: leaderboard bot chat
|
|
6
6
|
Platform: Linux
|
|
@@ -50,6 +50,12 @@ Dynamic: summary
|
|
|
50
50
|
:width: 240
|
|
51
51
|
:class: custom-dark
|
|
52
52
|
|
|
53
|
+
`Paper <https://arxiv.org/abs/2506.11994>`__ |
|
|
54
|
+
`Slides <https://www.dropbox.com/scl/fi/03gjuyz17k9yhsqy0isoz/free_decomporession_slides.pdf?rlkey=8f82mhciyl2ju02l7hv1md5li&st=26xmhjga&dl=0>`__ |
|
|
55
|
+
`Docs <https://ameli.github.io/freealg>`__
|
|
56
|
+
|
|
57
|
+
.. `Slides <https://ameli.github.io/freealg/_static/data/slides.pdf>`__ |
|
|
58
|
+
|
|
53
59
|
*freealg* is a Python package that employs **free** probability to evaluate the spectral
|
|
54
60
|
densities of large matrix **form**\ s. The fundamental algorithm employed by *freealg* is
|
|
55
61
|
**free decompression**, which extrapolates from the empirical spectral densities of small
|
|
@@ -120,15 +126,19 @@ requests and bug reports.
|
|
|
120
126
|
How to Cite
|
|
121
127
|
===========
|
|
122
128
|
|
|
123
|
-
If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994
|
|
129
|
+
If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994>`__.
|
|
124
130
|
|
|
125
131
|
.. code::
|
|
126
132
|
|
|
127
|
-
@article{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
@article{spectral2025,
|
|
134
|
+
title={Spectral Estimation with Free Decompression},
|
|
135
|
+
author={Siavash Ameli and Chris van der Heide and Liam Hodgkinson and Michael W. Mahoney},
|
|
136
|
+
year={2025},
|
|
137
|
+
eprint={2506.11994},
|
|
138
|
+
archivePrefix={arXiv},
|
|
139
|
+
primaryClass={stat.ML},
|
|
140
|
+
url={https://arxiv.org/abs/2506.11994},
|
|
141
|
+
journal={arXiv preprint arXiv:2506.11994},
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
|