freealg 0.1.0__tar.gz → 0.1.2__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 (32) hide show
  1. {freealg-0.1.0/freealg.egg-info → freealg-0.1.2}/PKG-INFO +2 -1
  2. freealg-0.1.2/freealg/__version__.py +1 -0
  3. {freealg-0.1.0 → freealg-0.1.2}/freealg/_chebyshev.py +39 -5
  4. {freealg-0.1.0 → freealg-0.1.2}/freealg/_jacobi.py +37 -5
  5. {freealg-0.1.0 → freealg-0.1.2}/freealg/_pade.py +41 -17
  6. {freealg-0.1.0 → freealg-0.1.2}/freealg/_util.py +11 -3
  7. {freealg-0.1.0 → freealg-0.1.2}/freealg/freeform.py +69 -10
  8. {freealg-0.1.0 → freealg-0.1.2/freealg.egg-info}/PKG-INFO +2 -1
  9. {freealg-0.1.0 → freealg-0.1.2}/freealg.egg-info/requires.txt +1 -0
  10. {freealg-0.1.0 → freealg-0.1.2}/requirements.txt +1 -0
  11. freealg-0.1.0/freealg/__version__.py +0 -1
  12. {freealg-0.1.0 → freealg-0.1.2}/CHANGELOG.rst +0 -0
  13. {freealg-0.1.0 → freealg-0.1.2}/LICENSE.txt +0 -0
  14. {freealg-0.1.0 → freealg-0.1.2}/MANIFEST.in +0 -0
  15. {freealg-0.1.0 → freealg-0.1.2}/README.rst +0 -0
  16. {freealg-0.1.0 → freealg-0.1.2}/freealg/__init__.py +0 -0
  17. {freealg-0.1.0 → freealg-0.1.2}/freealg/_damp.py +0 -0
  18. {freealg-0.1.0 → freealg-0.1.2}/freealg/_decompress.py +0 -0
  19. {freealg-0.1.0 → freealg-0.1.2}/freealg/_plot_util.py +0 -0
  20. {freealg-0.1.0 → freealg-0.1.2}/freealg/_sample.py +0 -0
  21. {freealg-0.1.0 → freealg-0.1.2}/freealg/distributions/__init__.py +0 -0
  22. {freealg-0.1.0 → freealg-0.1.2}/freealg/distributions/kesten_mckay.py +0 -0
  23. {freealg-0.1.0 → freealg-0.1.2}/freealg/distributions/marchenko_pastur.py +0 -0
  24. {freealg-0.1.0 → freealg-0.1.2}/freealg/distributions/wachter.py +0 -0
  25. {freealg-0.1.0 → freealg-0.1.2}/freealg/distributions/wigner.py +0 -0
  26. {freealg-0.1.0 → freealg-0.1.2}/freealg.egg-info/SOURCES.txt +0 -0
  27. {freealg-0.1.0 → freealg-0.1.2}/freealg.egg-info/dependency_links.txt +0 -0
  28. {freealg-0.1.0 → freealg-0.1.2}/freealg.egg-info/not-zip-safe +0 -0
  29. {freealg-0.1.0 → freealg-0.1.2}/freealg.egg-info/top_level.txt +0 -0
  30. {freealg-0.1.0 → freealg-0.1.2}/pyproject.toml +0 -0
  31. {freealg-0.1.0 → freealg-0.1.2}/setup.cfg +0 -0
  32. {freealg-0.1.0 → freealg-0.1.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Free probability for large matrices
5
5
  Keywords: leaderboard bot chat
6
6
  Platform: Linux
@@ -30,6 +30,7 @@ Requires-Dist: texplot
30
30
  Requires-Dist: matplotlib
31
31
  Requires-Dist: colorcet
32
32
  Requires-Dist: networkx
33
+ Requires-Dist: statsmodels
33
34
  Provides-Extra: test
34
35
  Requires-Dist: tox; extra == "test"
35
36
  Requires-Dist: pytest-cov; extra == "test"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -14,14 +14,15 @@
14
14
  import numpy
15
15
  from scipy.special import eval_chebyu
16
16
 
17
- __all__ = ['chebyshev_proj', 'chebyshev_approx', 'chebyshev_stieltjes']
17
+ __all__ = ['chebyshev_sample_proj', 'chebyshev_kernel_proj',
18
+ 'chebyshev_approx', 'chebyshev_stieltjes']
18
19
 
19
20
 
20
- # ==============
21
- # chebyshev proj
22
- # ==============
21
+ # =====================
22
+ # chebyshev sample proj
23
+ # =====================
23
24
 
24
- def chebyshev_proj(eig, support, K=10, reg=0.0):
25
+ def chebyshev_sample_proj(eig, support, K=10, reg=0.0):
25
26
  """
26
27
  Estimate the coefficients \\psi_k in
27
28
 
@@ -81,6 +82,39 @@ def chebyshev_proj(eig, support, K=10, reg=0.0):
81
82
  return psi
82
83
 
83
84
 
85
+ # =====================
86
+ # chebyshev kernel proj
87
+ # =====================
88
+
89
+ def chebyshev_kernel_proj(xs, pdf, support, K=10, reg=0.0):
90
+ """
91
+ Projection of a *continuous* density given on a grid (xs, pdf)
92
+ onto the Chebyshev-II basis.
93
+
94
+ xs : 1-D numpy array (original x–axis, not the t-variable)
95
+ pdf : same shape as xs, integrates to 1 on xs
96
+ """
97
+
98
+ lam_m, lam_p = support
99
+ t = (2.0 * xs - (lam_m + lam_p)) / (lam_p - lam_m) # map to [−1,1]
100
+
101
+ norm = numpy.pi / 2.0
102
+ psi = numpy.empty(K + 1)
103
+
104
+ for k in range(K + 1):
105
+ 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
+
108
+ if k == 0:
109
+ penalty = 0
110
+ else:
111
+ penalty = reg * (k / (K + 1))**2
112
+
113
+ psi[k] = moment / (norm + penalty)
114
+
115
+ return psi
116
+
117
+
84
118
  # ================
85
119
  # chebyshev approx
86
120
  # ================
@@ -15,7 +15,8 @@ import numpy
15
15
  from scipy.special import eval_jacobi, roots_jacobi
16
16
  from scipy.special import gammaln, beta as Beta
17
17
 
18
- __all__ = ['jacobi_proj', 'jacobi_approx', 'jacobi_stieltjes']
18
+ __all__ = ['jacobi_sample_proj', 'jacobi_kernel_proj', 'jacobi_approx',
19
+ 'jacobi_stieltjes']
19
20
 
20
21
 
21
22
  # ==============
@@ -43,11 +44,11 @@ def jacobi_sq_norm(k, alpha, beta):
43
44
  return numpy.exp(lg_num - lg_den)
44
45
 
45
46
 
46
- # ===========
47
- # jacobi pro
48
- # ===========
47
+ # ==================
48
+ # jacobi sample proj
49
+ # ==================
49
50
 
50
- def jacobi_proj(eig, support, K=10, alpha=0.0, beta=0.0, reg=0.0):
51
+ def jacobi_sample_proj(eig, support, K=10, alpha=0.0, beta=0.0, reg=0.0):
51
52
  """
52
53
  """
53
54
 
@@ -76,6 +77,37 @@ def jacobi_proj(eig, support, K=10, alpha=0.0, beta=0.0, reg=0.0):
76
77
  return psi
77
78
 
78
79
 
80
+ # ==================
81
+ # jacobi kernel proj
82
+ # ==================
83
+
84
+ def jacobi_kernel_proj(xs, pdf, support, K=10, alpha=0.0, beta=0.0, reg=0.0):
85
+ """
86
+ Same moments as `jacobi_proj`, but the target is a *continuous* density
87
+ given on a grid (xs, pdf).
88
+ """
89
+
90
+ lam_m, lam_p = support
91
+ t = (2.0 * xs - (lam_p + lam_m)) / (lam_p - lam_m) # map to [-1,1]
92
+ psi = numpy.empty(K + 1)
93
+
94
+ for k in range(K + 1):
95
+ Pk = eval_jacobi(k, alpha, beta, t)
96
+ N_k = jacobi_sq_norm(k, alpha, beta)
97
+
98
+ # \int P_k(t) w(t) \rho(t) dt. w(t) cancels with pdf already being rho
99
+ moment = numpy.trapz(Pk * pdf, xs)
100
+
101
+ if k == 0:
102
+ penalty = 0
103
+ else:
104
+ penalty = reg * (k / (K + 1))**2
105
+
106
+ psi[k] = moment / (N_k + penalty)
107
+
108
+ return psi
109
+
110
+
79
111
  # =============
80
112
  # jacobi approx
81
113
  # =============
@@ -108,8 +108,7 @@ def _decode_poles(s, lam_m, lam_p):
108
108
  # inner ls
109
109
  # ========
110
110
 
111
- # def _inner_ls(x, f, poles): # TEST
112
- def _inner_ls(x, f, poles, p=1):
111
+ def _inner_ls(x, f, poles, p=1, pade_reg=0.0):
113
112
  """
114
113
  This is the inner least square (blazing fast).
115
114
  """
@@ -169,7 +168,35 @@ def _inner_ls(x, f, poles, p=1):
169
168
  cols.append(phi)
170
169
 
171
170
  A = numpy.column_stack(cols)
172
- theta, *_ = lstsq(A, f, rcond=None)
171
+
172
+ # theta, *_ = lstsq(A, f, rcond=None) # TEST
173
+ if pade_reg > 0:
174
+ ATA = A.T.dot(A)
175
+
176
+ # # add pade_reg * I
177
+ # ATA.flat[:: ATA.shape[1]+1] += pade_reg
178
+ # ATf = A.T.dot(f)
179
+ # theta = numpy.linalg.solve(ATA, ATf)
180
+
181
+ # figure out how many elements to skip
182
+ if p == 1:
183
+ skip = 2 # skip c and D
184
+ elif p == 0:
185
+ skip = 1 # skip c only
186
+ else:
187
+ skip = 0 # all entries are residues
188
+
189
+ # add λ only for the residue positions
190
+ n = ATA.shape[0]
191
+ for i in range(skip, n):
192
+ ATA[i, i] += pade_reg
193
+
194
+ # then solve
195
+ ATf = A.T.dot(f)
196
+ theta = numpy.linalg.solve(ATA, ATf)
197
+
198
+ else:
199
+ theta, *_ = lstsq(A, f, rcond=None)
173
200
 
174
201
  if p == -1:
175
202
  c, D, resid = 0.0, 0.0, theta
@@ -213,12 +240,11 @@ def _eval_rational(z, c, D, poles, resid):
213
240
  # fit pade
214
241
  # ========
215
242
 
216
- def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', safety=1.0,
217
- max_outer=40, xtol=1e-12, ftol=1e-12, optimizer='ls', verbose=0):
243
+ def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
244
+ safety=1.0, max_outer=40, xtol=1e-12, ftol=1e-12, optimizer='ls',
245
+ verbose=0):
218
246
  """
219
247
  This is the outer optimiser.
220
-
221
- Fits G(x) = (p>=1 ? c : 0) + (p==1 ? D x : 0) + sum r_j/(x - a_j) # TEST
222
248
  """
223
249
 
224
250
  # Checks
@@ -232,10 +258,9 @@ def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', safety=1.0,
232
258
  f = numpy.asarray(f, float)
233
259
 
234
260
  poles0 = _default_poles(q, lam_m, lam_p, safety=safety, odd_side=odd_side)
235
- # if q == 0: # nothing to optimise
236
261
  if q == 0 and p <= 0:
237
- # c, D, resid = _inner_ls(x, f, poles0) # TEST
238
- c, D, resid = _inner_ls(x, f, poles0, p)
262
+ # c, D, resid = _inner_ls(x, f, poles0, pade_reg=pade_reg) # TEST
263
+ c, D, resid = _inner_ls(x, f, poles0, p, pade_reg=pade_reg)
239
264
  pade_sol = {
240
265
  'c': c, 'D': D, 'poles': poles0, 'resid': resid,
241
266
  'outer_iters': 0
@@ -249,11 +274,10 @@ def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', safety=1.0,
249
274
  # residual
250
275
  # --------
251
276
 
252
- # def residual(s): # TEST
253
277
  def residual(s, p=p):
254
278
  poles = _decode_poles(s, lam_m, lam_p)
255
- # c, D, resid = _inner_ls(x, f, poles) # TEST
256
- c, D, resid = _inner_ls(x, f, poles, p)
279
+ # c, D, resid = _inner_ls(x, f, poles, pade_reg=pade_reg) # TEST
280
+ c, D, resid = _inner_ls(x, f, poles, p, pade_reg=pade_reg)
257
281
  return _eval_rational(x, c, D, poles, resid) - f
258
282
 
259
283
  # ----------------
@@ -299,8 +323,8 @@ def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', safety=1.0,
299
323
  raise RuntimeError('"optimizer" is invalid.')
300
324
 
301
325
  poles = _decode_poles(res.x, lam_m, lam_p)
302
- # c, D, resid = _inner_ls(x, f, poles) # TEST
303
- c, D, resid = _inner_ls(x, f, poles, p)
326
+ # c, D, resid = _inner_ls(x, f, poles, pade_reg=pade_reg) # TEST
327
+ c, D, resid = _inner_ls(x, f, poles, p, pade_reg=pade_reg)
304
328
 
305
329
  pade_sol = {
306
330
  'c': c, 'D': D, 'poles': poles, 'resid': resid,
@@ -354,8 +378,8 @@ def fit_pade_old(x, f, lam_m, lam_p, p, q, delta=1e-8, B=numpy.inf,
354
378
  Q(x) = prod_{j=0..q-1}(x - b_j)
355
379
 
356
380
  Constraints:
357
- a_i [lam_m, lam_p]
358
- b_j (-infty, lam_m - delta] cup [lam_p + delta, infty)
381
+ a_i in [lam_m, lam_p]
382
+ b_j in (-infty, lam_m - delta] cup [lam_p + delta, infty)
359
383
 
360
384
  Approach:
361
385
  - Brute‐force all 2^q left/right assignments for denominator roots
@@ -72,14 +72,14 @@ def force_density(psi0, support, approx, grid, alpha=0.0, beta=0.0):
72
72
  if beta <= 0.0 and beta > -0.5:
73
73
  constraints.append({
74
74
  'type': 'eq',
75
- 'fun': lambda psi: approx(numpy.array([lam_m], psi))[0]
75
+ 'fun': lambda psi: approx(numpy.array([lam_m]), psi)[0]
76
76
  })
77
77
 
78
78
  # Enforce zero at right edge
79
79
  if alpha <= 0.0 and alpha > -0.5:
80
80
  constraints.append({
81
81
  'type': 'eq',
82
- 'fun': lambda psi: approx(numpy.array([lam_p], psi))[0]
82
+ 'fun': lambda psi: approx(numpy.array([lam_p]), psi)[0]
83
83
  })
84
84
 
85
85
  # Solve a small quadratic programming
@@ -89,4 +89,12 @@ def force_density(psi0, support, approx, grid, alpha=0.0, beta=0.0):
89
89
  method='SLSQP',
90
90
  options={'maxiter': 1000, 'ftol': 1e-9, 'eps': 1e-8})
91
91
 
92
- return res.x
92
+ psi = res.x
93
+
94
+ # Normalize first mode to unit mass
95
+ x = numpy.linspace(lam_m, lam_p, 1000)
96
+ rho = approx(x, psi)
97
+ mass = numpy.trapz(rho, x)
98
+ psi[0] = psi[0] / mass
99
+
100
+ return psi
@@ -13,10 +13,13 @@
13
13
 
14
14
  import numpy
15
15
  from scipy.stats import gaussian_kde
16
+ # from statsmodels.nonparametric.kde import KDEUnivariate
16
17
  from functools import partial
17
18
  from ._util import compute_eig, force_density
18
- from ._jacobi import jacobi_proj, jacobi_approx, jacobi_stieltjes
19
- from ._chebyshev import chebyshev_proj, chebyshev_approx, chebyshev_stieltjes
19
+ from ._jacobi import jacobi_sample_proj, jacobi_kernel_proj, jacobi_approx, \
20
+ jacobi_stieltjes
21
+ from ._chebyshev import chebyshev_sample_proj, chebyshev_kernel_proj, \
22
+ chebyshev_approx, chebyshev_stieltjes
20
23
  from ._damp import jackson_damping, lanczos_damping, fejer_damping, \
21
24
  exponential_damping, parzen_damping
22
25
  from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
@@ -174,8 +177,9 @@ class FreeForm(object):
174
177
  # ===
175
178
 
176
179
  def fit(self, method='jacobi', K=10, alpha=0.0, beta=0.0, reg=0.0,
177
- damp=None, force=False, pade_p=0, pade_q=1, odd_side='left',
178
- optimizer='ls', plot=False, latex=False, save=False):
180
+ projection='kernel', kernel_bw=None, damp=None, force=False,
181
+ pade_p=0, pade_q=1, odd_side='left', pade_reg=0.0, optimizer='ls',
182
+ plot=False, latex=False, save=False):
179
183
  """
180
184
  Fit model to eigenvalues.
181
185
 
@@ -202,6 +206,19 @@ class FreeForm(object):
202
206
  reg : float, default=0.0
203
207
  Tikhonov regularization coefficient.
204
208
 
209
+ projection : {``'sample'``, ``'kernel'``}, default= ``'kernel'``
210
+ The method of Galerkin projection:
211
+
212
+ * ``'sample'``: directly project samples (eigenvalues) to the
213
+ orthogonal polynomials. This method is highly unstable as it
214
+ treats each sample as a delta Dirac function.
215
+ * ``'kernel'``: computes KDE from the samples and project a
216
+ smooth KDE to the orthogonal polynomials. This method is stable.
217
+
218
+ kernel_bw : float, default=None
219
+ Kernel band-wdth. See scipy.stats.gaussian_kde. This argument is
220
+ relevant if ``projection='kernel'`` is set.
221
+
205
222
  damp : {``'jackson'``, ``'lanczos'``, ``'fejer``, ``'exponential'``,\
206
223
  ``'parzen'``}, default=None
207
224
  Damping method to eliminate Gibbs oscillation.
@@ -225,6 +242,9 @@ class FreeForm(object):
225
242
  optimizer will decide best location by moving them to the left or
226
243
  right of the support.
227
244
 
245
+ pade_reg : float, default=0.0
246
+ Regularization for Pade approximation.
247
+
228
248
  optimizer : {``'ls'``, ``'de'``}, default= ``'ls'``
229
249
  Optimizer for Pade approximation, including:
230
250
 
@@ -278,12 +298,50 @@ class FreeForm(object):
278
298
  if beta <= -1:
279
299
  raise ValueError('"beta" should be greater then "-1".')
280
300
 
301
+ if not (method in ['jacobi', 'chebyshev']):
302
+ raise ValueError('"method" is invalid.')
303
+
304
+ if not (projection in ['sample', 'kernel']):
305
+ raise ValueError('"projection" is invalid.')
306
+
281
307
  # Project eigenvalues to Jacobi polynomials basis
282
308
  if method == 'jacobi':
283
- psi = jacobi_proj(self.eig, support=self.support, K=K, alpha=alpha,
284
- beta=beta, reg=reg)
309
+
310
+ if projection == 'sample':
311
+ psi = jacobi_sample_proj(self.eig, support=self.support, K=K,
312
+ alpha=alpha, beta=beta, reg=reg)
313
+ else:
314
+ # smooth KDE on a fixed grid
315
+ xs = numpy.linspace(self.lam_m, self.lam_p, 2000)
316
+ pdf = gaussian_kde(self.eig, bw_method=kernel_bw)(xs)
317
+
318
+ # Adaptive KDE
319
+ # k = KDEUnivariate(self.eig)
320
+ # k.fit(bw="silverman", fft=False, weights=None, gridsize=1024,
321
+ # adaptive=True)
322
+ # pdf = k.evaluate(xs)
323
+
324
+ psi = jacobi_kernel_proj(xs, pdf, support=self.support, K=K,
325
+ alpha=alpha, beta=beta, reg=reg)
326
+
285
327
  elif method == 'chebyshev':
286
- psi = chebyshev_proj(self.eig, support=self.support, K=K, reg=reg)
328
+
329
+ if projection == 'sample':
330
+ psi = chebyshev_sample_proj(self.eig, support=self.support,
331
+ K=K, reg=reg)
332
+ else:
333
+ # smooth KDE on a fixed grid
334
+ xs = numpy.linspace(self.lam_m, self.lam_p, 2000)
335
+ pdf = gaussian_kde(self.eig, bw_method=kernel_bw)(xs)
336
+
337
+ # Adaptive KDE
338
+ # k = KDEUnivariate(self.eig)
339
+ # k.fit(bw="silverman", fft=False, weights=None, gridsize=1024,
340
+ # adaptive=True)
341
+ # pdf = k.evaluate(xs)
342
+
343
+ psi = chebyshev_kernel_proj(xs, pdf, support=self.support,
344
+ K=K, reg=reg)
287
345
  else:
288
346
  raise ValueError('"method" is invalid.')
289
347
 
@@ -334,8 +392,9 @@ class FreeForm(object):
334
392
  # B=numpy.inf, S=numpy.inf)
335
393
  self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m, self.lam_p,
336
394
  p=pade_p, q=pade_q, odd_side=odd_side,
337
- safety=1.0, max_outer=40, xtol=1e-12,
338
- ftol=1e-12, optimizer=optimizer, verbose=0)
395
+ pade_reg=pade_reg, safety=1.0, max_outer=40,
396
+ xtol=1e-12, ftol=1e-12, optimizer=optimizer,
397
+ verbose=0)
339
398
 
340
399
  if plot:
341
400
  g_supp_approx = eval_pade(x_supp[None, :], self._pade_sol)[0, :]
@@ -418,7 +477,7 @@ class FreeForm(object):
418
477
 
419
478
  # Check density is unit mass
420
479
  mass = numpy.trapz(rho, x)
421
- if not numpy.isclose(mass, 1.0, atol=1e-3):
480
+ if not numpy.isclose(mass, 1.0, atol=1e-2):
422
481
  # raise RuntimeWarning(f'"rho" is not unit mass. mass: {mass}. ' +
423
482
  # r'Set "force=True".')
424
483
  print(f'"rho" is not unit mass. mass: {mass}. Set "force=True".')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Free probability for large matrices
5
5
  Keywords: leaderboard bot chat
6
6
  Platform: Linux
@@ -30,6 +30,7 @@ Requires-Dist: texplot
30
30
  Requires-Dist: matplotlib
31
31
  Requires-Dist: colorcet
32
32
  Requires-Dist: networkx
33
+ Requires-Dist: statsmodels
33
34
  Provides-Extra: test
34
35
  Requires-Dist: tox; extra == "test"
35
36
  Requires-Dist: pytest-cov; extra == "test"
@@ -4,6 +4,7 @@ texplot
4
4
  matplotlib
5
5
  colorcet
6
6
  networkx
7
+ statsmodels
7
8
 
8
9
  [docs]
9
10
  sphinx
@@ -4,3 +4,4 @@ texplot
4
4
  matplotlib
5
5
  colorcet
6
6
  networkx
7
+ statsmodels
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes