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.
Files changed (35) hide show
  1. {freealg-0.1.9 → freealg-0.1.11}/PKG-INFO +20 -7
  2. {freealg-0.1.9 → freealg-0.1.11}/README.rst +19 -6
  3. {freealg-0.1.9 → freealg-0.1.11}/freealg/__init__.py +3 -2
  4. freealg-0.1.11/freealg/__version__.py +1 -0
  5. {freealg-0.1.9 → freealg-0.1.11}/freealg/_chebyshev.py +12 -8
  6. {freealg-0.1.9 → freealg-0.1.11}/freealg/_decompress.py +15 -15
  7. {freealg-0.1.9 → freealg-0.1.11}/freealg/_pade.py +50 -0
  8. {freealg-0.1.9 → freealg-0.1.11}/freealg/_plot_util.py +55 -2
  9. {freealg-0.1.9 → freealg-0.1.11}/freealg/_sample.py +8 -5
  10. freealg-0.1.11/freealg/_support.py +85 -0
  11. {freealg-0.1.9 → freealg-0.1.11}/freealg/_util.py +1 -1
  12. {freealg-0.1.9 → freealg-0.1.11}/freealg/distributions/__init__.py +5 -5
  13. freealg-0.1.9/freealg/distributions/kesten_mckay.py → freealg-0.1.11/freealg/distributions/_kesten_mckay.py +2 -1
  14. freealg-0.1.9/freealg/distributions/marchenko_pastur.py → freealg-0.1.11/freealg/distributions/_marchenko_pastur.py +4 -2
  15. freealg-0.1.9/freealg/distributions/meixner.py → freealg-0.1.11/freealg/distributions/_meixner.py +2 -1
  16. freealg-0.1.9/freealg/distributions/wachter.py → freealg-0.1.11/freealg/distributions/_wachter.py +4 -2
  17. freealg-0.1.9/freealg/distributions/wigner.py → freealg-0.1.11/freealg/distributions/_wigner.py +4 -2
  18. {freealg-0.1.9 → freealg-0.1.11}/freealg/freeform.py +181 -147
  19. {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/PKG-INFO +20 -7
  20. {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/SOURCES.txt +6 -5
  21. freealg-0.1.9/freealg/__version__.py +0 -1
  22. {freealg-0.1.9 → freealg-0.1.11}/AUTHORS.txt +0 -0
  23. {freealg-0.1.9 → freealg-0.1.11}/CHANGELOG.rst +0 -0
  24. {freealg-0.1.9 → freealg-0.1.11}/LICENSE.txt +0 -0
  25. {freealg-0.1.9 → freealg-0.1.11}/MANIFEST.in +0 -0
  26. {freealg-0.1.9 → freealg-0.1.11}/freealg/_damp.py +0 -0
  27. {freealg-0.1.9 → freealg-0.1.11}/freealg/_jacobi.py +0 -0
  28. {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/dependency_links.txt +0 -0
  29. {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/not-zip-safe +0 -0
  30. {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/requires.txt +0 -0
  31. {freealg-0.1.9 → freealg-0.1.11}/freealg.egg-info/top_level.txt +0 -0
  32. {freealg-0.1.9 → freealg-0.1.11}/pyproject.toml +0 -0
  33. {freealg-0.1.9 → freealg-0.1.11}/requirements.txt +0 -0
  34. {freealg-0.1.9 → freealg-0.1.11}/setup.cfg +0 -0
  35. {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.9
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 python package that employs **free** probability for large matrix **form**\ s.
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
- Create and Train a Model
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
- * TBD
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
- @inproceedings{
138
- TBD
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 python package that employs **free** probability for large matrix **form**\ s.
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
- Create and Train a Model
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
- * TBD
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
- @inproceedings{
72
- TBD
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.sum(eval_chebyu(k, t)) / N
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.trapz(Pk * pdf, xs) # \int U_k(t) \rho(x) dx
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(Jp)
222
- J = numpy.where(Jp.imag > 0, Jm, Jp)
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 = - (2.0 / span) * numpy.pi * S
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(matrix, size, x=None, delta=1e-4, iterations=500, step_size=0.1,
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
- matrix : FreeForm
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 / matrix.n
86
- m = matrix._eval_stieltjes
85
+ alpha = size / freeform.n
86
+ m = freeform._eval_stieltjes
87
87
  # Lower and upper bound on new support
88
- hilb_lb = (1 / m(matrix.lam_m + delta * 1j)[1]).real
89
- hilb_ub = (1 / m(matrix.lam_p + delta * 1j)[1]).real
90
- lb = matrix.lam_m - (alpha - 1) * hilb_lb
91
- ub = matrix.lam_p - (alpha - 1) * hilb_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(matrix.support) - .1j,
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(matrix, z_inits, T, iterations=500, step_size=0.1,
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 = matrix._eval_stieltjes
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(matrix.support) - .1j,
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, 250)
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.size // 15)
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 interp1d
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
- cdf = cumulative_trapezoid(rho, x, initial=0)
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 interp1d(cdf, x, bounds_error=False, assume_sorted=True)
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
- numpy.random.rand(seed)
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.trapz(rho, x)
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 .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
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']
@@ -494,7 +494,8 @@ class KestenMcKay(object):
494
494
  :class: custom-dark
495
495
  """
496
496
 
497
- numpy.random.seed(seed)
497
+ if seed is not None:
498
+ numpy.random.seed(seed)
498
499
 
499
500
  if x_min is None:
500
501
  x_min = self.lam_m
@@ -501,7 +501,8 @@ class MarchenkoPastur(object):
501
501
  :class: custom-dark
502
502
  """
503
503
 
504
- numpy.random.seed(seed)
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
- numpy.random.seed(seed)
582
+ if seed is not None:
583
+ numpy.random.seed(seed)
582
584
 
583
585
  # Parameters
584
586
  m = int(size / self.lam)
@@ -526,7 +526,8 @@ class Meixner(object):
526
526
  :class: custom-dark
527
527
  """
528
528
 
529
- numpy.random.seed(seed)
529
+ if seed is not None:
530
+ numpy.random.seed(seed)
530
531
 
531
532
  if x_min is None:
532
533
  x_min = self.lam_m
@@ -501,7 +501,8 @@ class Wachter(object):
501
501
  :class: custom-dark
502
502
  """
503
503
 
504
- numpy.random.seed(seed)
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
- numpy.random.seed(seed)
585
+ if seed is not None:
586
+ numpy.random.seed(seed)
585
587
 
586
588
  n = size
587
589
  m1 = int(self.a * n)
@@ -478,7 +478,8 @@ class Wigner(object):
478
478
  :class: custom-dark
479
479
  """
480
480
 
481
- numpy.random.seed(seed)
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
- numpy.random.seed(seed)
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
- p : float, default=0.001
54
- The edges of the support of the distribution is detected by the
55
- :math:`p`-quantile on the left and :math:`(1-p)`-quantile on the right.
56
- If the argument ``support`` is directly provided, this option is
57
- ignored. This value should be between 0 and 1, ideally a small
58
- number close to zero.
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, p=0.001):
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 = self._detect_support(self.eig, 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
- # self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m,
412
- # self.lam_p, pade_p, pade_q, delta=1e-8,
413
- # B=numpy.inf, S=numpy.inf)
414
- self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m, self.lam_p,
415
- p=pade_p, q=pade_q, odd_side=odd_side,
416
- pade_reg=pade_reg, safety=1.0, max_outer=40,
417
- xtol=1e-12, ftol=1e-12, optimizer=optimizer,
418
- verbose=0)
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('"fit" the model first.')
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.trapz(rho, x)
478
+ mass = numpy.trapezoid(rho, x)
501
479
  if not numpy.isclose(mass, 1.0, atol=1e-2):
502
- # raise RuntimeWarning(f'"rho" is not unit mass. mass: {mass}. ' +
503
- # r'Set "force=True".')
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
- # raise RuntimeWarning(
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('"fit" the model first.')
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.trapz(D, x_s, axis=1) / numpy.pi
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 over a 2D Cartesian
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('"fit" the model first.')
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 y is None:
709
- y = numpy.linspace(-1, 1, 400)
710
-
711
- x_grid, y_grid = numpy.meshgrid(x, y)
712
- z = x_grid + 1j * y_grid # shape (Ny, Nx)
713
-
714
- # Set the number of bases as the number of x points insides support
715
- mask_sup = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
716
- n_base = 2 * numpy.sum(mask_sup)
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
- if self.psi is None:
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
- # Upper half-plane
822
- m1[mask_p] = stieltjes(z[mask_p].reshape(-1, 1)).reshape(-1)
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
- # Lower half-plane, use Schwarz reflection
825
- m1[mask_m] = numpy.conjugate(
826
- stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))).reshape(-1)
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
- # Second Riemann sheet
829
- m2[mask_p] = m1[mask_p]
830
- m2[mask_m] = -m1[mask_m] + self._glue(
831
- z[mask_m].reshape(-1, 1)).reshape(-1)
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, delta=1e-6, iterations=500,
842
- step_size=0.1, tolerance=1e-4, seed=None, plot=False,
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-4
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
- Spectral density
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
- eigs = numpy.sort(qmc_sample(x, rho, size, seed=seed))
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
- return rho, eigs
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.9
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 python package that employs **free** probability for large matrix **form**\ s.
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
- Create and Train a Model
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
- * TBD
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
- @inproceedings{
138
- TBD
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/kesten_mckay.py
29
- freealg/distributions/marchenko_pastur.py
30
- freealg/distributions/meixner.py
31
- freealg/distributions/wachter.py
32
- freealg/distributions/wigner.py
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