freealg 0.1.13__py3-none-any.whl → 0.1.15__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,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 .freeform import FreeForm, eigfree
9
+ from .freeform import FreeForm
10
+ from .eigfree import eigfree, condfree
10
11
  from . import distributions
11
12
 
12
- __all__ = ['FreeForm', 'distributions', 'eigfree']
13
+ __all__ = ['FreeForm', 'distributions', 'eigfree', 'condfree']
13
14
 
14
15
  from .__version__ import __version__ # noqa: F401 E402
freealg/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.13"
1
+ __version__ = "0.1.15"
freealg/_chebyshev.py CHANGED
@@ -58,7 +58,6 @@ def chebyshev_sample_proj(eig, support, K=10, reg=0.0):
58
58
 
59
59
  # Map to [–1,1] interval
60
60
  t = (2 * eig - (lam_m + lam_p)) / (lam_p - lam_m)
61
- N = eig.size
62
61
 
63
62
  # Inner‐product norm of each U_k under w(t) = sqrt{1–t^2} is \\pi/2
64
63
  norm = numpy.pi / 2
@@ -104,7 +103,7 @@ def chebyshev_kernel_proj(xs, pdf, support, K=10, reg=0.0):
104
103
 
105
104
  for k in range(K + 1):
106
105
  Pk = eval_chebyu(k, t) # U_k(t) on the grid
107
- moment = numpy.trapezoid(Pk * pdf, xs) # \int U_k(t) \rho(x) dx
106
+ moment = numpy.trapezoid(Pk * pdf, xs) # \int U_k(t) \rho(x) dx
108
107
 
109
108
  if k == 0:
110
109
  penalty = 0
@@ -225,14 +224,6 @@ def chebyshev_stieltjes(z, psi, support):
225
224
  psi_zero = numpy.concatenate([[0], psi])
226
225
  S = wynn_pade(psi_zero, J)
227
226
 
228
- # build powers J^(k+1) for k=0..K
229
- #K = len(psi) - 1
230
- # shape: (..., K+1)
231
- #Jpow = J[..., None] ** numpy.arange(1, K+2)
232
-
233
- # sum psi_k * J^(k+1)
234
- #S = numpy.sum(psi * Jpow, axis=-1)
235
-
236
227
  # assemble m(z)
237
228
  m_z = -2 / span * numpy.pi * S
238
229
 
freealg/_decompress.py CHANGED
@@ -11,12 +11,18 @@
11
11
  # =======
12
12
 
13
13
  import numpy
14
- # from scipy.integrate import solve_ivp
15
14
 
16
15
  __all__ = ['decompress', 'reverse_characteristics']
17
16
 
17
+
18
+ # =============
19
+ # secant method
20
+ # =============
21
+
22
+
18
23
  def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
19
- alpha=0.5, max_bt=12, eps=1e-30, verbose=False):
24
+ alpha=0.5, max_bt=1, eps=1e-30, step_factor=5.0,
25
+ post_smooth=True, jump_tol=10.0, verbose=False):
20
26
  """
21
27
  Solves :math:``f(z) = a`` for many starting points simultaneously
22
28
  using the secant method in the complex plane.
@@ -42,11 +48,20 @@ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
42
48
  Back‑tracking shrink factor (``0 < alpha < 1``). Defaults to ``0.5``.
43
49
 
44
50
  max_bt : int, optional
45
- Maximum back‑tracking trials per iteration. Defaults to ``12``.
51
+ Maximum back‑tracking trials per iteration. Defaults to ``0``.
46
52
 
47
53
  eps : float, optional
48
54
  Safeguard added to tiny denominators. Defaults to ``1e-30``.
49
55
 
56
+ post_smooth : bool, optional
57
+ If True (default) run a single vectorised clean-up pass that
58
+ re-solves points whose final root differs from the *nearest*
59
+ neighbour by more than ``jump_tol`` times the local median jump.
60
+
61
+ jump_tol : float, optional
62
+ Sensitivity of the clean-up pass; larger tolerance implies fewer
63
+ re-solves.
64
+
50
65
  verbose : bool, optional
51
66
  If *True*, prints progress every 10 iterations.
52
67
 
@@ -69,8 +84,8 @@ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
69
84
  orig_shape = z0.shape
70
85
  z0, z1, a = (x.ravel() for x in (z0, z1, a))
71
86
 
72
- n_points = z0.size
73
- roots = z1.copy()
87
+ n_points = z0.size
88
+ roots = z1.copy()
74
89
  iterations = numpy.zeros(n_points, dtype=int)
75
90
 
76
91
  f0 = f(z0) - a
@@ -87,9 +102,16 @@ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
87
102
  # Secant step
88
103
  denom = f1 - f0
89
104
  denom = numpy.where(numpy.abs(denom) < eps, denom + eps, denom)
90
- dz = (z1 - z0) * f1 / denom
91
- z2 = z1 - dz
92
- f2 = f(z2) - a
105
+ dz = (z1 - z0) * f1 / denom
106
+
107
+ # Step-size limiter
108
+ prev_step = numpy.maximum(numpy.abs(z1 - z0), eps)
109
+ max_step = step_factor * prev_step
110
+ big = numpy.abs(dz) > max_step
111
+ dz[big] *= max_step[big] / numpy.abs(dz[big])
112
+
113
+ z2 = z1 - dz
114
+ f2 = f(z2) - a
93
115
 
94
116
  # Line search by backtracking
95
117
  worse = (numpy.abs(f2) >= numpy.abs(f1)) & active
@@ -130,18 +152,57 @@ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
130
152
  residuals[remaining] = numpy.abs(f1[remaining])
131
153
  iterations[remaining] = max_iter
132
154
 
155
+ # Optional clean-up pass
156
+ if post_smooth and n_points > 2:
157
+ # absolute jump to *nearest* neighbour (left or right)
158
+ diff_left = numpy.empty_like(roots)
159
+ diff_right = numpy.empty_like(roots)
160
+ diff_left[1:] = numpy.abs(roots[1:] - roots[:-1])
161
+ diff_right[:-1] = numpy.abs(roots[:-1] - roots[1:])
162
+ jump = numpy.minimum(diff_left, diff_right)
163
+
164
+ # ignore unconverged points
165
+ median_jump = numpy.median(jump[~remaining])
166
+ bad = (jump > jump_tol * median_jump) & ~remaining
167
+
168
+ if bad.any():
169
+ z_first_all = numpy.where(bad & (diff_left <= diff_right),
170
+ roots - diff_left,
171
+ roots + diff_right)
172
+
173
+ # keep only the offending indices
174
+ z_first = z_first_all[bad]
175
+ z_second = z_first + (roots[bad] - z_first) * 1e-2
176
+
177
+ # re-solve just the outliers in one vector call
178
+ new_root, new_res, new_iter = secant_complex(
179
+ f, z_first, z_second, a[bad],
180
+ tol=tol, max_iter=max_iter,
181
+ alpha=alpha, max_bt=max_bt,
182
+ eps=eps, step_factor=step_factor,
183
+ post_smooth=False, # avoid recursion
184
+ )
185
+ roots[bad] = new_root
186
+ residuals[bad] = new_res
187
+ iterations[bad] = iterations[bad] + new_iter
188
+
189
+ if verbose:
190
+ print(f"Clean-up: re-solved {bad.sum()} outliers")
191
+
133
192
  return (
134
193
  roots.reshape(orig_shape),
135
194
  residuals.reshape(orig_shape),
136
195
  iterations.reshape(orig_shape),
137
196
  )
138
197
 
198
+
139
199
  # ==========
140
200
  # decompress
141
201
  # ==========
142
202
 
143
- def decompress(freeform, size, x=None, delta=1e-6, max_iter=500,
144
- tolerance=1e-12):
203
+
204
+ def decompress(freeform, size, x=None, delta=1e-4, max_iter=500,
205
+ tolerance=1e-8):
145
206
  """
146
207
  Free decompression of spectral density.
147
208
 
@@ -201,34 +262,35 @@ def decompress(freeform, size, x=None, delta=1e-6, max_iter=500,
201
262
  alpha = size / freeform.n
202
263
  m = freeform._eval_stieltjes
203
264
  # Lower and upper bound on new support
204
- hilb_lb = (1 / m(freeform.lam_m + delta * 1j)[1]).real
205
- hilb_ub = (1 / m(freeform.lam_p + delta * 1j)[1]).real
265
+ hilb_lb = (1 / m(freeform.lam_m + delta * 1j)).real
266
+ hilb_ub = (1 / m(freeform.lam_p + delta * 1j)).real
206
267
  lb = freeform.lam_m - (alpha - 1) * hilb_lb
207
268
  ub = freeform.lam_p - (alpha - 1) * hilb_ub
208
269
 
209
270
  # Create x if not given
210
- if x is None:
271
+ on_grid = (x is None)
272
+ if on_grid:
211
273
  radius = 0.5 * (ub - lb)
212
274
  center = 0.5 * (ub + lb)
213
275
  scale = 1.25
214
276
  x_min = numpy.floor(center - radius * scale)
215
277
  x_max = numpy.ceil(center + radius * scale)
216
278
  x = numpy.linspace(x_min, x_max, 500)
279
+ else:
280
+ x = numpy.asarray(x)
217
281
 
218
- # Ensure that input is an array
219
- x = numpy.asarray(x)
220
282
  target = x + delta * 1j
221
283
  if numpy.isclose(alpha, 1.0):
222
284
  return freeform.density(x), x, freeform.support
223
285
 
224
286
  # Characteristic curve map
225
287
  def _char_z(z):
226
- return z + (1 / m(z)[1]) * (1 - alpha)
288
+ return z + (1 / m(z)) * (1 - alpha)
227
289
 
228
- z0 = numpy.full(target.shape, numpy.mean(freeform.support) + delta*1j,
290
+ z0 = numpy.full(target.shape, numpy.mean(freeform.support) + .1j,
229
291
  dtype=numpy.complex128)
230
- z1 = z0 - numpy.log(alpha) * 1j
231
-
292
+ z1 = z0 - .2j
293
+
232
294
  roots, _, _ = secant_complex(
233
295
  _char_z, z0, z1,
234
296
  a=target,
@@ -238,9 +300,15 @@ def decompress(freeform, size, x=None, delta=1e-6, max_iter=500,
238
300
 
239
301
  # Plemelj's formula
240
302
  z = roots
241
- char_s = m(z)[1] / alpha
303
+ char_s = m(z) / alpha
242
304
  rho = numpy.maximum(0, char_s.imag / numpy.pi)
243
305
  rho[numpy.isnan(rho) | numpy.isinf(rho)] = 0
306
+ if on_grid:
307
+ x, rho = x.ravel(), rho.ravel()
308
+ # dx = x[1] - x[0]
309
+ # left_idx, right_idx = support_from_density(dx, rho)
310
+ # x, rho = x[left_idx-1:right_idx+1], rho[left_idx-1:right_idx+1]
311
+ rho = rho / numpy.trapezoid(rho, x)
244
312
 
245
313
  return rho.reshape(*x.shape), x, (lb, ub)
246
314
 
@@ -260,7 +328,7 @@ def reverse_characteristics(freeform, z_inits, T, iterations=500,
260
328
  m = freeform._eval_stieltjes
261
329
 
262
330
  def _char_z(z, t):
263
- return z + (1 / m(z)[1]) * (1 - numpy.exp(t))
331
+ return z + (1 / m(z)) * (1 - numpy.exp(t))
264
332
 
265
333
  target_z, target_t = numpy.meshgrid(z_inits, t_eval)
266
334
 
freealg/_pade.py CHANGED
@@ -236,9 +236,10 @@ def _eval_rational(z, c, D, poles, resid):
236
236
 
237
237
  return c + D * z + term
238
238
 
239
- # ========
240
- # Wynn epsilon algorithm for Pade
241
- # ========
239
+
240
+ # =========
241
+ # Wynn pade
242
+ # =========
242
243
 
243
244
  @numba.jit(nopython=True, parallel=True)
244
245
  def wynn_pade(coeffs, x):
@@ -248,48 +249,57 @@ def wynn_pade(coeffs, x):
248
249
  returns a function handle that computes the Pade approximant at any x
249
250
  using Wynn's epsilon algorithm.
250
251
 
251
- Parameters:
252
- coeffs (list or array): Coefficients [a0, a1, a2, ...] of the power series.
252
+ Parameters
253
+ ----------
254
+
255
+ coeffs (list or array):
256
+ Coefficients [a0, a1, a2, ...] of the power series.
253
257
 
254
- Returns:
255
- function: A function approximant(x) that returns the approximated value f(x).
258
+ Returns
259
+ -------
260
+
261
+ function:
262
+ A function approximant(x) that returns the approximated value f(x).
256
263
  """
264
+
257
265
  # Number of coefficients
258
266
  xn = x.ravel()
259
267
  d = len(xn)
260
268
  N = len(coeffs)
261
-
269
+
262
270
  # Compute the partial sums s_n = sum_{i=0}^n a_i * x^i for n=0,...,N-1
263
271
  eps = numpy.zeros((N+1, N, d), dtype=numpy.complex128)
264
272
  for i in numba.prange(d):
265
273
  partial_sum = 0.0
266
274
  for n in range(N):
267
275
  partial_sum += coeffs[n] * (xn[i] ** n)
268
- eps[0,n,i] = partial_sum
276
+ eps[0, n, i] = partial_sum
269
277
 
270
278
  for i in numba.prange(d):
271
279
  for k in range(1, N+1):
272
280
  for j in range(N - k):
273
- delta = eps[k-1, j+1,i] - eps[k-1, j,i]
281
+ delta = eps[k-1, j+1, i] - eps[k-1, j, i]
274
282
  if delta == 0:
275
283
  rec_delta = numpy.inf
276
284
  elif numpy.isinf(delta) or numpy.isnan(delta):
277
285
  rec_delta = 0.0
278
286
  else:
279
287
  rec_delta = 1.0 / delta
280
- eps[k,j,i] = rec_delta
288
+ eps[k, j, i] = rec_delta
281
289
  if k > 1:
282
- eps[k,j,i] += eps[k-2,j+1,i]
290
+ eps[k, j, i] += eps[k-2, j+1, i]
283
291
 
284
292
  if (N % 2) == 0:
285
293
  N -= 1
286
-
294
+
287
295
  return eps[N-1, 0, :].reshape(x.shape)
288
296
 
297
+
289
298
  # ========
290
299
  # fit pade
291
300
  # ========
292
301
 
302
+
293
303
  def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
294
304
  safety=1.0, max_outer=40, xtol=1e-12, ftol=1e-12, optimizer='ls',
295
305
  verbose=0):
freealg/_plot_util.py CHANGED
@@ -139,7 +139,7 @@ def _auto_bins(array, method='scott', factor=5):
139
139
  # ============
140
140
 
141
141
  def plot_density(x, rho, eig=None, support=None, label='',
142
- title='Spectral density', latex=False, save=False):
142
+ title='Spectral Density', latex=False, save=False):
143
143
  """
144
144
  """
145
145
 
@@ -147,8 +147,11 @@ def plot_density(x, rho, eig=None, support=None, label='',
147
147
 
148
148
  fig, ax = plt.subplots(figsize=(6, 2.7))
149
149
 
150
- if (support is not None) and (eig is not None):
151
- lam_m, lam_p = support
150
+ if eig is not None:
151
+ if support is not None:
152
+ lam_m, lam_p = support
153
+ else:
154
+ lam_m, lam_p = min(eig), max(eig)
152
155
  bins = numpy.linspace(lam_m, lam_p, _auto_bins(eig))
153
156
  _ = ax.hist(eig, bins, density=True, color='silver',
154
157
  edgecolor='none', label='Histogram')
freealg/_support.py CHANGED
@@ -1,31 +1,126 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # SPDX-FileType: SOURCE
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it under
5
+ # the terms of the license found in the LICENSE.txt file in the root directory
6
+ # of this source tree.
7
+
8
+
9
+ # =======
10
+ # Imports
11
+ # =======
12
+
1
13
  import numpy
14
+ import numba
2
15
  from scipy.stats import gaussian_kde
3
16
 
4
- def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs):
17
+
18
+ @numba.njit(numba.types.UniTuple(numba.types.int64, 2)(
19
+ numba.types.float64,
20
+ numba.types.float64[::1]
21
+ ))
22
+ def support_from_density(dx, density):
23
+ """
24
+ Estimates the support from a collection of noisy observations of a
25
+ density over a grid of x-values with mesh spacing dx.
26
+ """
27
+ n = density.shape[0]
28
+ target = 1.0 / dx
29
+
30
+ # 1) compute total_sum once
31
+ total_sum = 0.0
32
+ for t in range(n):
33
+ total_sum += density[t]
34
+
35
+ # 2) set up our “best‐so‐far” trackers
36
+ large = 1e300
37
+ best_nonneg_sum = large
38
+ best_nonneg_idx = -1
39
+ best_nonpos_sum = -large
40
+ best_nonpos_idx = -1
41
+
42
+ # 3) seed with first element (i.e. prefix_sum for k=1)
43
+ prefix_sum = density[0]
44
+ if prefix_sum >= 0.0:
45
+ best_nonneg_sum, best_nonneg_idx = prefix_sum, 1
46
+ else:
47
+ best_nonpos_sum, best_nonpos_idx = prefix_sum, 1
48
+
49
+ # 4) sweep j from 2...n–1, updating prefix_sum on the fly
50
+ optimal_i, optimal_j = 1, 2
51
+ minimal_cost = large
52
+
53
+ for j in range(2, n):
54
+ # extend prefix_sum to cover density[0]...density[j-1]
55
+ prefix_sum += density[j-1]
56
+
57
+ # cost for [0...i], [i...j]
58
+ diff_mid = prefix_sum - target
59
+ if diff_mid >= 0.0 and best_nonneg_sum <= diff_mid:
60
+ cost12 = diff_mid
61
+ i_cand = best_nonneg_idx
62
+ elif diff_mid < 0.0 and best_nonpos_sum >= diff_mid:
63
+ cost12 = -diff_mid
64
+ i_cand = best_nonpos_idx
65
+ else:
66
+ cost_using_nonpos = diff_mid - 2.0 * best_nonpos_sum
67
+ cost_using_nonneg = 2.0 * best_nonneg_sum - diff_mid
68
+ if cost_using_nonpos < cost_using_nonneg:
69
+ cost12, i_cand = cost_using_nonpos, best_nonpos_idx
70
+ else:
71
+ cost12, i_cand = cost_using_nonneg, best_nonneg_idx
72
+
73
+ # cost for [j...n]
74
+ cost3 = total_sum - prefix_sum
75
+ if cost3 < 0.0:
76
+ cost3 = -cost3
77
+
78
+ # total and maybe update best split
79
+ total_cost = cost12 + cost3
80
+ if total_cost < minimal_cost:
81
+ minimal_cost = total_cost
82
+ optimal_i, optimal_j = i_cand, j
83
+
84
+ # update our prefix‐sum trackers
85
+ if prefix_sum >= 0.0:
86
+ if prefix_sum < best_nonneg_sum:
87
+ best_nonneg_sum, best_nonneg_idx = prefix_sum, j
88
+ else:
89
+ if prefix_sum > best_nonpos_sum:
90
+ best_nonpos_sum, best_nonpos_idx = prefix_sum, j
91
+
92
+ return optimal_i, optimal_j
93
+
94
+
95
+ def detect_support(eigs, method='asymp', k=None, p=0.001, **kwargs):
5
96
  """
6
97
  Estimates the support of the eigenvalue density.
7
98
 
8
99
  Parameters
9
100
  ----------
10
- method : {``'range'``, ``'jackknife'``, ``'regression'``, ``'interior'``,
11
- ``'interior_smooth'``}, \
12
- default= ``'jackknife'``
101
+ method : {``'range'``, ``'asymp'``, ``'jackknife'``, ``'regression'``,
102
+ ``'interior'``, ``'interior_smooth'``}, \
103
+ default= ``'asymp'``
13
104
  The method of support estimation:
14
105
 
15
- * ``'range'``: no estimation; the support is the range of the eigenvalues
106
+ * ``'range'``: no estimation; the support is the range of the
107
+ eigenvalues.
108
+ * ``'asymp'``: assume the relative error in the min/max estimator is
109
+ 1/n.
16
110
  * ``'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.
111
+ jackknife estimator. Fast and simple, more accurate than the
112
+ range.
113
+ * ``'regression'``: estimates the support by performing a regression
114
+ under the assumption that the edge behavior is of square-root
115
+ type. Often most accurate.
21
116
  * ``'interior'``: estimates a support assuming the range overestimates;
22
117
  uses quantiles (p, 1-p).
23
- * ``'interior_smooth'``: same as ``'interior'`` but using kernel density
24
- estimation.
118
+ * ``'interior_smooth'``: same as ``'interior'`` but using kernel
119
+ density estimation.
25
120
 
26
121
  k : int, default = None
27
- Number of extreme order statistics to use for ``method='regression'``.
28
-
122
+ Number of extreme order statistics to use for ``method='regression'``.
123
+
29
124
  p : float, default=0.001
30
125
  The edges of the support of the distribution is detected by the
31
126
  :math:`p`-quantile on the left and :math:`(1-p)`-quantile on the right
@@ -36,28 +131,33 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
36
131
  References
37
132
  ----------
38
133
 
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.
134
+ .. [1] Quenouille, M. H. (1949, July). Approximate tests of correlation in
135
+ time-series. In Mathematical Proceedings of the Cambridge
136
+ Philosophical Society (Vol. 45, No. 3, pp. 483-484). Cambridge
137
+ University Press.
42
138
  """
43
139
 
44
- if method=='range':
140
+ if method == 'range':
45
141
  lam_m = eigs.min()
46
142
  lam_p = eigs.max()
47
143
 
48
- elif method=='jackknife':
144
+ elif method == 'asymp':
145
+ lam_m = eigs.min() - abs(eigs.min()) / len(eigs)
146
+ lam_p = eigs.max() + abs(eigs.max()) / len(eigs)
147
+
148
+ elif method == 'jackknife':
49
149
  x, n = numpy.sort(eigs), len(eigs)
50
- lam_m = x[0] - (n - 1)/n * (x[1] - x[0])
150
+ lam_m = x[0] - (n - 1)/n * (x[1] - x[0])
51
151
  lam_p = x[-1] + (n - 1)/n * (x[-1] - x[-2])
52
152
 
53
- elif method=='regression':
153
+ elif method == 'regression':
54
154
  x, n = numpy.sort(eigs), len(eigs)
55
155
  if k is None:
56
156
  k = int(round(n ** (2/3)))
57
157
  k = max(5, min(k, n // 2))
58
158
 
59
159
  # 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}.
160
+ # so (i/n) ~ (x - a)^{3/2} -> x ~ a + const*(i/n)^{2/3}.
61
161
  y = ((numpy.arange(1, k + 1) - 0.5) / n) ** (2 / 3)
62
162
 
63
163
  # Left edge: regress x_{(i)} on y
@@ -66,10 +166,10 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
66
166
  # Right edge: regress x_{(n-i+1)} on y
67
167
  _, lam_p = numpy.polyfit(y, x[-k:][::-1], 1)
68
168
 
69
- elif method=='interior':
169
+ elif method == 'interior':
70
170
  lam_m, lam_p = numpy.quantile(eigs, [p, 1-p])
71
-
72
- elif method=='interior_smooth':
171
+
172
+ elif method == 'interior_smooth':
73
173
  kde = gaussian_kde(eigs)
74
174
  xs = numpy.linspace(eigs.min(), eigs.max(), 1000)
75
175
  fs = kde(xs)
@@ -79,6 +179,7 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
79
179
 
80
180
  lam_m = numpy.interp(p, cdf, xs)
81
181
  lam_p = numpy.interp(1-p, cdf, xs)
182
+
82
183
  else:
83
184
  raise NotImplementedError("Unknown method")
84
185
 
@@ -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
- plot_density(x, rho, label='', latex=latex, save=save)
180
+ if eig is not None:
181
+ label = 'Theoretical'
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
- # Haar unitary
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
- plot_density(x, rho, label='', latex=latex, save=save)
178
+ if eig is not None:
179
+ label = 'Theoretical'
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
- plot_density(x, rho, label='', latex=latex, save=save)
195
+ if eig is not None:
196
+ label = 'Theoretical'
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
- plot_density(x, rho, label='', latex=latex, save=save)
186
+ if eig is not None:
187
+ label = 'Theoretical'
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
 
@@ -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
- plot_density(x, rho, label='', latex=latex, save=save)
166
+ if eig is not None:
167
+ label = 'Theoretical'
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,170 @@
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', 'condfree']
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 condfree
73
+ >>> from freealg.distributions import MarchenkoPastur
74
+ >>> mp = MarchenkoPastur(1/50)
75
+ >>> A = mp.matrix(3000)
76
+ >>> eigs = eigfree(A)
77
+ """
78
+
79
+ if A.ndim != 2 or A.shape[0] != A.shape[1]:
80
+ raise RuntimeError("Only square matrices are permitted.")
81
+ n = A.shape[0]
82
+
83
+ if N is None:
84
+ N = n
85
+
86
+ # Size of sample matrix
87
+ n_s = int(80*(1 + numpy.log(n)))
88
+ # If matrix is not large enough, return eigenvalues
89
+ if n < n_s:
90
+ return compute_eig(A)
91
+ # Number of samples
92
+ num_samples = int(10 * (n / n_s)**0.5)
93
+
94
+ # Collect eigenvalue samples
95
+ samples = []
96
+ for _ in range(num_samples):
97
+ indices = numpy.random.choice(n, n_s, replace=False)
98
+ samples.append(compute_eig(A[numpy.ix_(indices, indices)]))
99
+ samples = numpy.concatenate(samples).ravel()
100
+
101
+ # If all eigenvalues are positive, set PSD flag
102
+ if psd is None:
103
+ psd = samples.min() > 0
104
+
105
+ ff = FreeForm(samples)
106
+ # Since we are resampling, we need to provide the correct matrix size
107
+ ff.n = n_s
108
+
109
+ # Perform fit and estimate eigenvalues
110
+ order = 1 + int(len(samples)**.2)
111
+ ff.fit(method='chebyshev', K=order, projection='sample',
112
+ force=True, plot=False, latex=False, save=False)
113
+
114
+ if plots:
115
+ ff.density(plot=True)
116
+ ff.stieltjes(plot=True)
117
+
118
+ _, _, eigs = ff.decompress(N, plot=plots)
119
+
120
+ if psd:
121
+ eigs = numpy.abs(eigs)
122
+ eigs.sort()
123
+
124
+ return eigs
125
+
126
+
127
+ # ========
128
+ # cond free
129
+ # ========
130
+
131
+ def condfree(A, N=None):
132
+ """
133
+ Estimate the condition number of a positive-definite matrix.
134
+
135
+ This function estimates the condition number of the matrix
136
+ :math:`\\mathbf{A}` or a larger matrix containing :math:`\\mathbf{A}`
137
+ using free decompression.
138
+
139
+ Parameters
140
+ ----------
141
+
142
+ A : numpy.ndarray
143
+ The symmetric real-valued matrix :math:`\\mathbf{A}` whose condition
144
+ number (or that of a matrix containing :math:`\\mathbf{A}`) are to be
145
+ computed.
146
+
147
+ N : int, default=None
148
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
149
+ eigenvalues of. If None, returns estimates of the eigenvalues of
150
+ :math:`\\mathbf{A}` itself.
151
+
152
+ Notes
153
+ -----
154
+
155
+ This is a convenience function using the eigfree procedure.
156
+
157
+ Examples
158
+ --------
159
+
160
+ .. code-block:: python
161
+
162
+ >>> from freealg import condfree
163
+ >>> from freealg.distributions import MarchenkoPastur
164
+ >>> mp = MarchenkoPastur(1/50)
165
+ >>> A = mp.matrix(3000)
166
+ >>> condfree(A)
167
+ """
168
+
169
+ eigs = eigfree(A, N)
170
+ return eigs.max() / eigs.min()
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', 'eigfree']
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:`(\lambda_\min, \lambda_\max)` of the
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 : int
85
- Initial array size (assuming a square matrix when :math:`\\mathbf{A}`
86
- is 2D).
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
- 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)
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 the .fit() function.')
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 the .fit() function.')
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, evaluated on an array
609
- of points, or over a 2D Cartesian grid on the complex plane.
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 the .fit() function.')
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
- # a Cartesian grid
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, z.real <= self.lam_p)
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) # n_base = n_base
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
- m1[mask_m] = numpy.conjugate(
764
- stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))).ravel()
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
- m1, m2 = m1.reshape(*shape), m2.reshape(*shape)
779
-
780
- return m1, m2
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
800
  def decompress(self, size, x=None, max_iter=500, eigvals=True,
787
- step_size=0.1, tolerance=1e-9, seed=None, plot=False,
801
+ tolerance=1e-9, seed=None, plot=False,
788
802
  latex=False, save=False):
789
803
  """
790
804
  Free decompression of spectral density.
@@ -805,9 +819,6 @@ class FreeForm(object):
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.
@@ -880,14 +891,15 @@ class FreeForm(object):
880
891
  else:
881
892
  return x, rho
882
893
 
883
- def eigfree(A, N = None, psd = None):
894
+
895
+ def eigfree(A, N=None, psd=None, plots=False):
884
896
  """
885
897
  Estimate the eigenvalues of a matrix :math:`\\mathbf{A}` or a larger matrix
886
898
  containing :math:`\\mathbf{A}` using free decompression.
887
899
 
888
900
  This is a convenience function for the FreeForm class with some effective
889
901
  defaults that work well for common random matrix ensembles. For improved
890
- performance and plotting utilites, consider finetuning parameters using
902
+ performance and plotting utilites, consider finetuning parameters using
891
903
  the FreeForm class.
892
904
 
893
905
  Parameters
@@ -895,18 +907,22 @@ def eigfree(A, N = None, psd = None):
895
907
 
896
908
  A : numpy.ndarray
897
909
  The symmetric real-valued matrix :math:`\\mathbf{A}` whose eigenvalues
898
- (or those of a matrix containing :math:`\\mathbf{A}`) are to be computed.
910
+ (or those of a matrix containing :math:`\\mathbf{A}`) are to be
911
+ computed.
899
912
 
900
913
  N : int, default=None
901
914
  The size of the matrix containing :math:`\\mathbf{A}` to estimate
902
915
  eigenvalues of. If None, returns estimates of the eigenvalues of
903
916
  :math:`\\mathbf{A}` itself.
904
917
 
905
- psd: bool, default=None
906
- Determines whether the matrix is positive-semidefinite (PSD; all
918
+ psd : bool, default=None
919
+ Determines whether the matrix is positive-semidefinite (PSD; all
907
920
  eigenvalues are non-negative). If None, the matrix is considered PSD if
908
921
  all sampled eigenvalues are positive.
909
922
 
923
+ plots : bool, default=False
924
+ Print out all relevant plots for diagnosing eigenvalue accuracy.
925
+
910
926
  Notes
911
927
  -----
912
928
 
@@ -924,20 +940,24 @@ def eigfree(A, N = None, psd = None):
924
940
 
925
941
  >>> from freealg import FreeForm
926
942
  """
943
+ if A.ndim != 2 or A.shape[0] != A.shape[1]:
944
+ raise RuntimeError("Only square matrices are permitted.")
927
945
  n = A.shape[0]
928
-
946
+
947
+ if N is None:
948
+ N = n
949
+
929
950
  # Size of sample matrix
930
951
  n_s = int(80*(1 + numpy.log(n)))
931
-
932
952
  # If matrix is not large enough, return eigenvalues
933
953
  if n < n_s:
934
954
  return compute_eig(A)
935
-
936
- if N is None:
937
- N = n
938
-
939
955
  # Number of samples
940
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
941
961
 
942
962
  # Collect eigenvalue samples
943
963
  samples = []
@@ -956,12 +976,17 @@ def eigfree(A, N = None, psd = None):
956
976
 
957
977
  # Perform fit and estimate eigenvalues
958
978
  order = 1 + int(len(samples)**.2)
959
- ff.fit(method='chebyshev', K=order, projection='sample', damp='jackson',
960
- force=True, plot=False, latex=False, save=False, reg=0.05)
961
- _, _, eigs = ff.decompress(N)
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)
962
987
 
963
988
  if psd:
964
989
  eigs = numpy.abs(eigs)
965
990
  eigs.sort()
966
991
 
967
- return eigs
992
+ return eigs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.1.13
3
+ Version: 0.1.15
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{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}
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
 
@@ -0,0 +1,25 @@
1
+ freealg/__init__.py,sha256=oYfXRQgu--OhZAY9cIVdfEHDVz6XQmqzTsIqIFAlPws,570
2
+ freealg/__version__.py,sha256=qb0TalpSt1CbprnFyeLUKqgrqNtmnk9IoQQ7umAoXVY,23
3
+ freealg/_chebyshev.py,sha256=dsAj3YEpmkzB65smluZ0Fi5IZSdpnQXBSIuKMg19grA,5523
4
+ freealg/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
5
+ freealg/_decompress.py,sha256=Q19AmpqxBWcrbFSniDOnh9ethOLm5_7bqDLXUNGCxk4,10372
6
+ freealg/_jacobi.py,sha256=AT4ONSHGGDxVKE3MGMLyMR8uDFiO-e9u3x5udYfdJJk,5635
7
+ freealg/_pade.py,sha256=wer31W6c6lDCag4THKGdW69r5d7uimsH_Y53wJih144,15185
8
+ freealg/_plot_util.py,sha256=U4alp7Pzg315_7jJdu1UB0tIUcxUovQgHDHsUYoa2Z0,19728
9
+ freealg/_sample.py,sha256=ckC75eqv-mRP1F5BnhvsjfLTaoAzHK8bebl9bCRZYDo,2561
10
+ freealg/_support.py,sha256=LIM_VWH8TzLJlp_q5A0ql-xawPUNyH2YI9ZKBlHHuzo,6122
11
+ freealg/_util.py,sha256=PWLXcsTb0-FinGWvNiY12D-f4CHQB5bP_W3ThqfY4FY,3681
12
+ freealg/eigfree.py,sha256=ssS6t6b5wSgZNOo3ydA0MxvnhJivbYH8bwdKNmD81FE,4571
13
+ freealg/freeform.py,sha256=j1oECmOyc-v6o7bIrbW844QZqAG6hJm6NjGvLXRMvEo,31263
14
+ freealg/distributions/__init__.py,sha256=t_yZyEkW_W_tSV9IvgYXtVASxD2BEdiNVXcV2ebMy8M,579
15
+ freealg/distributions/_kesten_mckay.py,sha256=210RF2OQEYLZBeLB6wmbdHnZPs_9ldDNHm_FMlg5tis,19881
16
+ freealg/distributions/_marchenko_pastur.py,sha256=kchFccRMuVF2Cus_99vdEwuRimkHzEUV8xt5kZFg7ZI,16994
17
+ freealg/distributions/_meixner.py,sha256=ws7t_EUa7V0s97dgMQIJLv1b6qMLqf9fLLbTJQudf_8,17512
18
+ freealg/distributions/_wachter.py,sha256=Hna_MXqAPjuRkeilLPMf4Xg_3C6tTu5oZLEQnA-RuE4,16897
19
+ freealg/distributions/_wigner.py,sha256=SxgPLtvIVBi9m4De-oBD0x6-2Je_eBqpDrpDYcoLuis,15871
20
+ freealg-0.1.15.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
21
+ freealg-0.1.15.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
22
+ freealg-0.1.15.dist-info/METADATA,sha256=mvdtAxG-4t3-rThaz_lqpG3B_Ca8Z2LrpekEocrxVRk,4497
23
+ freealg-0.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ freealg-0.1.15.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
25
+ freealg-0.1.15.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- freealg/__init__.py,sha256=YqewBd3fq4nm-L3oGcExhEDR2wtVcrtggkSGzfpDqr4,528
2
- freealg/__version__.py,sha256=khDKUuWafURKVs5EAZkpOMiUHI2-V7axlqrWLPUpuZo,23
3
- freealg/_chebyshev.py,sha256=oDJtRCwKEHazitzVBDbfdQz1jkyfsJOJfcAfo-PNkNo,5745
4
- freealg/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
5
- freealg/_decompress.py,sha256=vTnxV_7XPYUVVnWCnALCrZkQT77IAPm_CzrWMgiTiqg,7986
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=_HDgchJaeryUTgywobSM4Yr8SjXi6pRVG8kQkTKDdMM,30375
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.13.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
20
- freealg-0.1.13.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
21
- freealg-0.1.13.dist-info/METADATA,sha256=83nEmVJt6xwHPu6o0qlYN0JQ_zYuqgIPgQBvHsAdvkE,4029
22
- freealg-0.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- freealg-0.1.13.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
24
- freealg-0.1.13.dist-info/RECORD,,