freealg 0.1.10__py3-none-any.whl → 0.1.12__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 CHANGED
@@ -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
freealg/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.10"
1
+ __version__ = "0.1.12"
freealg/_chebyshev.py CHANGED
@@ -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
freealg/_decompress.py CHANGED
@@ -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-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
- 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.
freealg/_pade.py CHANGED
@@ -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
freealg/_sample.py CHANGED
@@ -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
  # ==========
freealg/_support.py ADDED
@@ -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
freealg/_util.py CHANGED
@@ -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
freealg/freeform.py CHANGED
@@ -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,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.trapz(rho, x)
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('"fit" the model first.')
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.trapz(D, x_s, axis=1) / numpy.pi
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 over a 2D Cartesian
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('"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)
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 y is None:
705
- y = numpy.linspace(-1, 1, 400)
706
-
707
- x_grid, y_grid = numpy.meshgrid(x, y)
708
- z = x_grid + 1j * y_grid # shape (Ny, Nx)
709
-
710
- # Set the number of bases as the number of x points insides support
711
- mask_sup = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
712
- n_base = 2 * numpy.sum(mask_sup)
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
- if self.psi is None:
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
- # Upper half-plane
818
- 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()
819
761
 
820
- # Lower half-plane, use Schwarz reflection
821
- m1[mask_m] = numpy.conjugate(
822
- 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()
823
765
 
824
- # Second Riemann sheet
825
- m2[mask_p] = m1[mask_p]
826
- m2[mask_m] = -m1[mask_m] + self._glue(
827
- 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()
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, delta=1e-6, iterations=500,
838
- 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-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-4
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
- Spectral density
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
- 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.
928
893
 
929
- 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)**.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.10
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
  Provides-Extra: docs
36
37
  Dynamic: classifier
@@ -49,7 +50,10 @@ Dynamic: summary
49
50
  :width: 240
50
51
  :class: custom-dark
51
52
 
52
- *freealg* is a python package that employs **free** probability for large matrix **form**\ s.
53
+ *freealg* is a Python package that employs **free** probability to evaluate the spectral
54
+ densities of large matrix **form**\ s. The fundamental algorithm employed by *freealg* is
55
+ **free decompression**, which extrapolates from the empirical spectral densities of small
56
+ submatrices to infer the eigenspectrum of extremely large matrices.
53
57
 
54
58
  Install
55
59
  =======
@@ -75,12 +79,18 @@ Documentation is available at `ameli.github.io/freealg <https://ameli.github.io/
75
79
  Quick Usage
76
80
  ===========
77
81
 
78
- Create and Train a Model
79
- ------------------------
82
+ The following code estimates the eigenvalues of a very large Wishart matrix using a much
83
+ smaller Wishart matrix.
80
84
 
81
85
  .. code-block:: python
82
86
 
83
87
  >>> import freealg as fa
88
+ >>> mp = fa.distributions.MarchenkoPastur(1/50) # Wishart matrices with aspect ratio 1/50
89
+ >>> A = mp.matrix(1000) # Sample a 1000 x 1000 Wishart matrix
90
+ >>> eigs = fa.eigfree(A, 100_000) # Estimate the eigenvalues of 100000 x 100000
91
+
92
+ 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>`__.
93
+
84
94
 
85
95
  Test
86
96
  ====
@@ -110,14 +120,18 @@ requests and bug reports.
110
120
  How to Cite
111
121
  ===========
112
122
 
113
- * TBD
123
+ If you use this work, please cite the `arXiv paper <https://arxiv.org/abs/2506.11994>`.
114
124
 
115
125
  .. code::
116
126
 
117
- @inproceedings{
118
- TBD
127
+ @article{ameli2025spectral,
128
+ title={Spectral Estimation with Free Decompression},
129
+ author={Siavash Ameli and Chris van der Heide and Liam Hodgkinson and Michael W. Mahoney},
130
+ journal={arXiv preprint arXiv:2506.11994},
131
+ year={2025}
119
132
  }
120
133
 
134
+
121
135
  License
122
136
  =======
123
137
 
@@ -0,0 +1,24 @@
1
+ freealg/__init__.py,sha256=YqewBd3fq4nm-L3oGcExhEDR2wtVcrtggkSGzfpDqr4,528
2
+ freealg/__version__.py,sha256=LcIlFjHZFfiF9Rd4UHoakmombOFkxIYk00I181frGBM,23
3
+ freealg/_chebyshev.py,sha256=oDJtRCwKEHazitzVBDbfdQz1jkyfsJOJfcAfo-PNkNo,5745
4
+ freealg/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
5
+ freealg/_decompress.py,sha256=WdKXkZ9cbrzIHEACEZyVmLNR9kMK7OQLaBFsZKUjIKQ,4723
6
+ freealg/_jacobi.py,sha256=AT4ONSHGGDxVKE3MGMLyMR8uDFiO-e9u3x5udYfdJJk,5635
7
+ freealg/_pade.py,sha256=yREJYSmnWaVUNRBNxjuQUqeLe_XSaGa9_VzV6HG5RkA,15164
8
+ freealg/_plot_util.py,sha256=BOYre8FPhrxmW1VRj3I40dCjWTFqUBTInmXc3wFunKQ,19648
9
+ freealg/_sample.py,sha256=ckC75eqv-mRP1F5BnhvsjfLTaoAzHK8bebl9bCRZYDo,2561
10
+ freealg/_support.py,sha256=A8hUjfKnSkHm09KLcEkeEXeTieKjhH-sVPd7I3_p4VE,3100
11
+ freealg/_util.py,sha256=PWLXcsTb0-FinGWvNiY12D-f4CHQB5bP_W3ThqfY4FY,3681
12
+ freealg/freeform.py,sha256=OtFiLeaViEUUzxI4Ivp5twf6Pkr7hpqERFppa6C6kCA,30435
13
+ freealg/distributions/__init__.py,sha256=t_yZyEkW_W_tSV9IvgYXtVASxD2BEdiNVXcV2ebMy8M,579
14
+ freealg/distributions/_kesten_mckay.py,sha256=HDMjbM1AcNxlwrpYeGmRqcbP10QsLI5RCeKvjVK3tOk,19566
15
+ freealg/distributions/_marchenko_pastur.py,sha256=th921hlEEtTbnHnRyBgT54a_e-9ZzAl9rB78O9FjorY,16688
16
+ freealg/distributions/_meixner.py,sha256=ItE0zYG2vhyUkObxbx4bDZaJ0BHVQWPzAJGLdMz10l4,17206
17
+ freealg/distributions/_wachter.py,sha256=lw70PT3TZlCf7mHU8IqoygXFUWB4IL57obkng0_ZGeI,16591
18
+ freealg/distributions/_wigner.py,sha256=2ZSPjgmDr9q9qiz6jO6yhXFo4ALHfxK1f0EzolzhRNE,15565
19
+ freealg-0.1.12.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
20
+ freealg-0.1.12.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
21
+ freealg-0.1.12.dist-info/METADATA,sha256=Xwep_keCRRaY_GBIsLUnZ8Opx5-I20dhXJ8us53_wAI,4029
22
+ freealg-0.1.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ freealg-0.1.12.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
24
+ freealg-0.1.12.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,23 +0,0 @@
1
- freealg/__init__.py,sha256=rVrM-mp_I54TEMFKb0sBhX1MIRGp4BMi7tHYqFwlQnM,508
2
- freealg/__version__.py,sha256=z0zCHFTcKSR0tJ6h5qrpNmRVP21QIPP8N0p7quCnnm0,23
3
- freealg/_chebyshev.py,sha256=X6u5pKjR1HPZ-KbCfr7zT6HRwB6pZMADvVS3sT5LTkA,5638
4
- freealg/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
5
- freealg/_decompress.py,sha256=7U2lL8F5z76aFuZJBsPj70jEVRuzvJHnIh5FSw-aLME,4680
6
- freealg/_jacobi.py,sha256=AT4ONSHGGDxVKE3MGMLyMR8uDFiO-e9u3x5udYfdJJk,5635
7
- freealg/_pade.py,sha256=mP96wEPfIzHLZ6PDB5OyhmSA8N1uVPVUkmJa3ebXXiU,13623
8
- freealg/_plot_util.py,sha256=BOYre8FPhrxmW1VRj3I40dCjWTFqUBTInmXc3wFunKQ,19648
9
- freealg/_sample.py,sha256=FAi8drpHcChrJxIbkmw-lWc8UUkLK-fCM34H9CqO7Po,2476
10
- freealg/_util.py,sha256=alJ9s1U_sHL7dXq7hI10fa8CF_AZ6Xmy_QsoyDYPSDQ,3677
11
- freealg/freeform.py,sha256=U-JTb3264lGCqX5DgH7eUeLzXr2jeOJ6kNFxoEXzQwc,28634
12
- freealg/distributions/__init__.py,sha256=t_yZyEkW_W_tSV9IvgYXtVASxD2BEdiNVXcV2ebMy8M,579
13
- freealg/distributions/_kesten_mckay.py,sha256=HDMjbM1AcNxlwrpYeGmRqcbP10QsLI5RCeKvjVK3tOk,19566
14
- freealg/distributions/_marchenko_pastur.py,sha256=th921hlEEtTbnHnRyBgT54a_e-9ZzAl9rB78O9FjorY,16688
15
- freealg/distributions/_meixner.py,sha256=ItE0zYG2vhyUkObxbx4bDZaJ0BHVQWPzAJGLdMz10l4,17206
16
- freealg/distributions/_wachter.py,sha256=lw70PT3TZlCf7mHU8IqoygXFUWB4IL57obkng0_ZGeI,16591
17
- freealg/distributions/_wigner.py,sha256=2ZSPjgmDr9q9qiz6jO6yhXFo4ALHfxK1f0EzolzhRNE,15565
18
- freealg-0.1.10.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
19
- freealg-0.1.10.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
20
- freealg-0.1.10.dist-info/METADATA,sha256=ili3JNECNyFGSK3vGQA7zpvOYf5_7uDX36kVvCnG5ss,2942
21
- freealg-0.1.10.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
22
- freealg-0.1.10.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
23
- freealg-0.1.10.dist-info/RECORD,,