freealg 0.1.11__py3-none-any.whl → 0.7.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.
Files changed (59) hide show
  1. freealg/__init__.py +8 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +12 -0
  4. freealg/_algebraic_form/_branch_points.py +288 -0
  5. freealg/_algebraic_form/_constraints.py +139 -0
  6. freealg/_algebraic_form/_continuation_algebraic.py +706 -0
  7. freealg/_algebraic_form/_decompress.py +641 -0
  8. freealg/_algebraic_form/_decompress2.py +204 -0
  9. freealg/_algebraic_form/_edge.py +330 -0
  10. freealg/_algebraic_form/_homotopy.py +323 -0
  11. freealg/_algebraic_form/_moments.py +448 -0
  12. freealg/_algebraic_form/_sheets_util.py +145 -0
  13. freealg/_algebraic_form/_support.py +309 -0
  14. freealg/_algebraic_form/algebraic_form.py +1232 -0
  15. freealg/_free_form/__init__.py +16 -0
  16. freealg/{_chebyshev.py → _free_form/_chebyshev.py} +75 -43
  17. freealg/_free_form/_decompress.py +993 -0
  18. freealg/_free_form/_density_util.py +243 -0
  19. freealg/_free_form/_jacobi.py +359 -0
  20. freealg/_free_form/_linalg.py +508 -0
  21. freealg/{_pade.py → _free_form/_pade.py} +42 -208
  22. freealg/{_plot_util.py → _free_form/_plot_util.py} +37 -22
  23. freealg/{_sample.py → _free_form/_sample.py} +58 -22
  24. freealg/_free_form/_series.py +454 -0
  25. freealg/_free_form/_support.py +214 -0
  26. freealg/_free_form/free_form.py +1362 -0
  27. freealg/_geometric_form/__init__.py +13 -0
  28. freealg/_geometric_form/_continuation_genus0.py +175 -0
  29. freealg/_geometric_form/_continuation_genus1.py +275 -0
  30. freealg/_geometric_form/_elliptic_functions.py +174 -0
  31. freealg/_geometric_form/_sphere_maps.py +63 -0
  32. freealg/_geometric_form/_torus_maps.py +118 -0
  33. freealg/_geometric_form/geometric_form.py +1094 -0
  34. freealg/_util.py +56 -110
  35. freealg/distributions/__init__.py +7 -1
  36. freealg/distributions/_chiral_block.py +494 -0
  37. freealg/distributions/_deformed_marchenko_pastur.py +726 -0
  38. freealg/distributions/_deformed_wigner.py +386 -0
  39. freealg/distributions/_kesten_mckay.py +29 -15
  40. freealg/distributions/_marchenko_pastur.py +224 -95
  41. freealg/distributions/_meixner.py +47 -37
  42. freealg/distributions/_wachter.py +29 -17
  43. freealg/distributions/_wigner.py +27 -14
  44. freealg/visualization/__init__.py +12 -0
  45. freealg/visualization/_glue_util.py +32 -0
  46. freealg/visualization/_rgb_hsv.py +125 -0
  47. freealg-0.7.12.dist-info/METADATA +172 -0
  48. freealg-0.7.12.dist-info/RECORD +53 -0
  49. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/WHEEL +1 -1
  50. freealg/_decompress.py +0 -180
  51. freealg/_jacobi.py +0 -218
  52. freealg/_support.py +0 -85
  53. freealg/freeform.py +0 -967
  54. freealg-0.1.11.dist-info/METADATA +0 -140
  55. freealg-0.1.11.dist-info/RECORD +0 -24
  56. /freealg/{_damp.py → _free_form/_damp.py} +0 -0
  57. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/AUTHORS.txt +0 -0
  58. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/LICENSE.txt +0 -0
  59. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/top_level.txt +0 -0
freealg/freeform.py DELETED
@@ -1,967 +0,0 @@
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 scipy.stats import gaussian_kde
16
- # from statsmodels.nonparametric.kde import KDEUnivariate
17
- from functools import partial
18
- from ._util import compute_eig, beta_kde, force_density
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
23
- from ._damp import jackson_damping, lanczos_damping, fejer_damping, \
24
- exponential_damping, parzen_damping
25
- from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
26
- from ._pade import fit_pade, eval_pade
27
- from ._decompress import decompress
28
- from ._sample import qmc_sample
29
- from ._support import detect_support
30
-
31
- __all__ = ['FreeForm', 'eigfree']
32
-
33
-
34
- # =========
35
- # Free Form
36
- # =========
37
-
38
- class FreeForm(object):
39
- """
40
- Free probability for large matrices.
41
-
42
- Parameters
43
- ----------
44
-
45
- A : numpy.ndarray
46
- The 2D symmetric :math:`\\mathbf{A}`. The eigenvalues of this will be
47
- computed upon calling this class. If a 1D array provided, it is
48
- assumed to be the eigenvalues of :math:`\\mathbf{A}`.
49
-
50
- support : tuple, default=None
51
- The support of the density of :math:`\\mathbf{A}`. If `None`, it is
52
- estimated from the minimum and maximum of the eigenvalues.
53
-
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``.
60
-
61
- Notes
62
- -----
63
-
64
- Notes.
65
-
66
- References
67
- ----------
68
-
69
- .. [1] Reference.
70
-
71
- Attributes
72
- ----------
73
-
74
- eig : numpy.array
75
- Eigenvalues of the matrix
76
-
77
- support: tuple
78
- The predicted (or given) support :math:`(\lambda_\min, \lambda_\max)` of the
79
- eigenvalue density.
80
-
81
- psi : numpy.array
82
- Jacobi coefficients.
83
-
84
- n : int
85
- Initial array size (assuming a square matrix when :math:`\\mathbf{A}`
86
- is 2D).
87
-
88
- Methods
89
- -------
90
-
91
- fit
92
- Fit the Jacobi polynomials to the empirical density.
93
-
94
- density
95
- Compute the spectral density of the matrix.
96
-
97
- hilbert
98
- Compute Hilbert transform of the spectral density
99
-
100
- stieltjes
101
- Compute Stieltjes transform of the spectral density
102
-
103
- decompress
104
- Free decompression of spectral density
105
-
106
- Examples
107
- --------
108
-
109
- .. code-block:: python
110
-
111
- >>> from freealg import FreeForm
112
- """
113
-
114
- # ====
115
- # init
116
- # ====
117
-
118
- def __init__(self, A, support=None, delta=1e-6, **kwargs):
119
- """
120
- Initialization.
121
- """
122
-
123
- self.A = None
124
- self.eig = None
125
- self.delta = delta
126
-
127
- # Eigenvalues
128
- if A.ndim == 1:
129
- # When A is a 1D array, it is assumed A is the eigenvalue array.
130
- self.eig = A
131
- self.n = len(A)
132
- elif A.ndim == 2:
133
- # When A is a 2D array, it is assumed A is the actual array,
134
- # and its eigenvalues will be computed.
135
- self.A = A
136
- self.n = A.shape[0]
137
- assert A.shape[0] == A.shape[1], \
138
- 'Only square matrices are permitted.'
139
- self.eig = compute_eig(A)
140
-
141
- # Support
142
- if support is None:
143
- self.lam_m, self.lam_p = detect_support(self.eig, **kwargs)
144
- else:
145
- self.lam_m = support[0]
146
- self.lam_p = support[1]
147
- self.support = (self.lam_m, self.lam_p)
148
-
149
- # Initialize
150
- self.method = None
151
- self.psi = None
152
- self.alpha = None
153
- self.beta = None
154
- self._pade_sol = None
155
-
156
- # ===
157
- # fit
158
- # ===
159
-
160
- def fit(self, method='jacobi', K=10, alpha=0.0, beta=0.0, reg=0.0,
161
- projection='gaussian', kernel_bw=None, damp=None, force=False,
162
- pade_p=0, pade_q=1, odd_side='left', pade_reg=0.0, optimizer='ls',
163
- plot=False, latex=False, save=False):
164
- """
165
- Fit model to eigenvalues.
166
-
167
- Parameters
168
- ----------
169
-
170
- method : {``'jacobi'``, ``'chebyshev'``}, default= ``'jacobi'``
171
- Method of approximation, either by Jacobi polynomials or Chebyshev
172
- polynomials of the second kind.
173
-
174
- K : int, default=10
175
- Highest polynomial degree
176
-
177
- alpha : float, default=0.0
178
- Jacobi parameter :math:`\\alpha`. Determines the slope of the
179
- fitting model on the right side of interval. This should be greater
180
- then -1. This option is only applicable when ``method='jacobi'``.
181
-
182
- beta : float, default=0.0
183
- Jacobi parameter :math:`\\beta`. Determines the slope of the
184
- fitting model on the left side of interval. This should be greater
185
- then -1. This option is only applicable when ``method='jacobi'``.
186
-
187
- reg : float, default=0.0
188
- Tikhonov regularization coefficient.
189
-
190
- projection : {``'sample'``, ``'gaussian'``, ``'beta'``}, \
191
- default= ``'beta'``
192
- The method of Galerkin projection:
193
-
194
- * ``'sample'``: directly project samples (eigenvalues) to the
195
- orthogonal polynomials. This method is highly unstable as it
196
- treats each sample as a delta Dirac function.
197
- * ``'gaussian'``: computes Gaussian-Kernel KDE from the samples and
198
- project a smooth KDE to the orthogonal polynomials. This method
199
- is stable.
200
- * ``'beta'``: computes Beta-Kernel KDE from the samples and
201
- project a smooth KDE to the orthogonal polynomials. This method
202
- is stable.
203
-
204
- kernel_bw : float, default=0.001
205
- Kernel band-wdth. See scipy.stats.gaussian_kde. This argument is
206
- relevant if ``projection='kernel'`` is set.
207
-
208
- damp : {``'jackson'``, ``'lanczos'``, ``'fejer``, ``'exponential'``,\
209
- ``'parzen'``}, default=None
210
- Damping method to eliminate Gibbs oscillation.
211
-
212
- force : bool, default=False
213
- If `True`, it forces the density to have unit mass and to be
214
- strictly positive.
215
-
216
- pade_p : int, default=0
217
- Degree of polynomial :math:`P(z)` is :math:`q+p` where :math:`p`
218
- can only be ``-1``, ``0``, or ``1``. See notes below.
219
-
220
- pade_q : int, default=1
221
- Degree of polynomial :math:`Q(z)` is :math:`q`. See notes below.
222
-
223
- odd_side : {``'left'``, ``'right'``}, default= ``'left'``
224
- In case of odd number of poles (when :math:`q` is odd), the extra
225
- pole is set to the left or right side of the support interval,
226
- while all other poles are split in half to the left and right. Note
227
- that this is only for the initialization of the poles. The
228
- optimizer will decide best location by moving them to the left or
229
- right of the support.
230
-
231
- pade_reg : float, default=0.0
232
- Regularization for Pade approximation.
233
-
234
- optimizer : {``'ls'``, ``'de'``}, default= ``'ls'``
235
- Optimizer for Pade approximation, including:
236
-
237
- * ``'ls'``: least square (local, fast)
238
- * ``'de'``: differential evolution (global, slow)
239
-
240
- plot : bool, default=False
241
- If `True`, the approximation coefficients and Pade approximation to
242
- the Hilbert transform are plotted.
243
-
244
- latex : bool, default=False
245
- If `True`, the plot is rendered using LaTeX. This option is
246
- relevant only if ``plot=True``.
247
-
248
- save : bool, default=False
249
- If not `False`, the plot is saved. If a string is given, it is
250
- assumed to the save filename (with the file extension). This option
251
- is relevant only if ``plot=True``.
252
-
253
- Returns
254
- -------
255
-
256
- psi : (K+1, ) numpy.ndarray
257
- Coefficients of fitting Jacobi polynomials
258
-
259
- Notes
260
- -----
261
-
262
- The Pade approximation for the glue function :math:`G(z)` is
263
-
264
- .. math::
265
-
266
- G(z) = \\frac{P(z)}{Q(z)},
267
-
268
- where :math:`P(z)` and :math:`Q(z)` are polynomials of order
269
- :math:`p+q` and :math:`q` respectively. Note that :math:`p` can only
270
- be -1, 0, or 1, effectively making Pade approximation of order
271
- :math:`q-1:q`, :math:`q:q`, or :math:`q-1:q`.
272
-
273
- Examples
274
- --------
275
-
276
- .. code-block:: python
277
-
278
- >>> from freealg import FreeForm
279
- """
280
-
281
- if alpha <= -1:
282
- raise ValueError('"alpha" should be greater then "-1".')
283
-
284
- if beta <= -1:
285
- raise ValueError('"beta" should be greater then "-1".')
286
-
287
- if not (method in ['jacobi', 'chebyshev']):
288
- raise ValueError('"method" is invalid.')
289
-
290
- if not (projection in ['sample', 'gaussian', 'beta']):
291
- raise ValueError('"projection" is invalid.')
292
-
293
- # Project eigenvalues to Jacobi polynomials basis
294
- if method == 'jacobi':
295
-
296
- if projection == 'sample':
297
- psi = jacobi_sample_proj(self.eig, support=self.support, K=K,
298
- alpha=alpha, beta=beta, reg=reg)
299
- else:
300
- # smooth KDE on a fixed grid
301
- xs = numpy.linspace(self.lam_m, self.lam_p, 2000)
302
-
303
- if projection == 'gaussian':
304
- pdf = gaussian_kde(self.eig, bw_method=kernel_bw)(xs)
305
- else:
306
- pdf = beta_kde(self.eig, xs, self.lam_m, self.lam_p,
307
- kernel_bw)
308
-
309
- # Adaptive KDE
310
- # k = KDEUnivariate(self.eig)
311
- # k.fit(bw="silverman", fft=False, weights=None, gridsize=1024,
312
- # adaptive=True)
313
- # pdf = k.evaluate(xs)
314
-
315
- # TEST
316
- # import matplotlib.pyplot as plt
317
- # plt.plot(xs, pdf)
318
- # plt.grid(True)
319
- # plt.show()
320
-
321
- psi = jacobi_kernel_proj(xs, pdf, support=self.support, K=K,
322
- alpha=alpha, beta=beta, reg=reg)
323
-
324
- elif method == 'chebyshev':
325
-
326
- if projection == 'sample':
327
- psi = chebyshev_sample_proj(self.eig, support=self.support,
328
- K=K, reg=reg)
329
- else:
330
- # smooth KDE on a fixed grid
331
- xs = numpy.linspace(self.lam_m, self.lam_p, 2000)
332
-
333
- if projection == 'gaussian':
334
- pdf = gaussian_kde(self.eig, bw_method=kernel_bw)(xs)
335
- else:
336
- pdf = beta_kde(self.eig, xs, self.lam_m, self.lam_p,
337
- kernel_bw)
338
-
339
- # Adaptive KDE
340
- # k = KDEUnivariate(self.eig)
341
- # k.fit(bw="silverman", fft=False, weights=None, gridsize=1024,
342
- # adaptive=True)
343
- # pdf = k.evaluate(xs)
344
-
345
- psi = chebyshev_kernel_proj(xs, pdf, support=self.support,
346
- K=K, reg=reg)
347
- else:
348
- raise ValueError('"method" is invalid.')
349
-
350
- # Damping
351
- if damp is not None:
352
- if damp == 'jackson':
353
- g = jackson_damping(K+1)
354
- elif damp == 'lanczos':
355
- g = lanczos_damping(K+1)
356
- elif damp == 'fejer':
357
- g = fejer_damping(K+1)
358
- elif damp == 'exponential':
359
- g = exponential_damping(K+1)
360
- elif damp == 'parzen':
361
- g = parzen_damping(K+1)
362
-
363
- psi = psi * g
364
-
365
- if force:
366
- # A grid to check and enforce positivity and unit mass on it
367
- grid = numpy.linspace(self.lam_m, self.lam_p, 500)
368
-
369
- if method == 'jacobi':
370
- approx = partial(jacobi_approx, support=self.support,
371
- alpha=alpha, beta=beta)
372
- elif method == 'chebyshev':
373
- approx = partial(chebyshev_approx, support=self.support)
374
- else:
375
- raise RuntimeError('"method" is invalid.')
376
-
377
- # Enforce positivity, unit mass, and zero at edges
378
- psi = force_density(psi, support=self.support, approx=approx,
379
- grid=grid, alpha=alpha, beta=beta)
380
-
381
- # Update attributes
382
- self.method = method
383
- self.psi = psi
384
- self.alpha = alpha
385
- self.beta = beta
386
-
387
- # Fit a pade approximation
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)
397
-
398
- if plot:
399
- g_supp_approx = eval_pade(x_supp[None, :], self._pade_sol)[0, :]
400
- plot_fit(psi, x_supp, g_supp, g_supp_approx, support=self.support,
401
- latex=latex, save=save)
402
-
403
- return self.psi
404
-
405
- # =======
406
- # density
407
- # =======
408
-
409
- def density(self, x=None, plot=False, latex=False, save=False):
410
- """
411
- Evaluate density.
412
-
413
- Parameters
414
- ----------
415
-
416
- x : numpy.array, default=None
417
- Positions where density to be evaluated at. If `None`, an interval
418
- slightly larger than the support interval will be used.
419
-
420
- plot : bool, default=False
421
- If `True`, density is plotted.
422
-
423
- latex : bool, default=False
424
- If `True`, the plot is rendered using LaTeX. This option is
425
- relevant only if ``plot=True``.
426
-
427
- save : bool, default=False
428
- If not `False`, the plot is saved. If a string is given, it is
429
- assumed to the save filename (with the file extension). This option
430
- is relevant only if ``plot=True``.
431
-
432
- Returns
433
- -------
434
-
435
- rho : numpy.array
436
- Density at locations x.
437
-
438
- See Also
439
- --------
440
- hilbert
441
- stieltjes
442
-
443
- Examples
444
- --------
445
-
446
- .. code-block:: python
447
-
448
- >>> from freealg import FreeForm
449
- """
450
-
451
- if self.psi is None:
452
- raise RuntimeError('The spectral density needs to be fit using the .fit() function.')
453
-
454
- # Create x if not given
455
- if x is None:
456
- radius = 0.5 * (self.lam_p - self.lam_m)
457
- center = 0.5 * (self.lam_p + self.lam_m)
458
- scale = 1.25
459
- x_min = numpy.floor(center - radius * scale)
460
- x_max = numpy.ceil(center + radius * scale)
461
- x = numpy.linspace(x_min, x_max, 500)
462
-
463
- # Preallocate density to zero
464
- rho = numpy.zeros_like(x)
465
-
466
- # Compute density only inside support
467
- mask = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
468
-
469
- if self.method == 'jacobi':
470
- rho[mask] = jacobi_approx(x[mask], self.psi, self.support,
471
- self.alpha, self.beta)
472
- elif self.method == 'chebyshev':
473
- rho[mask] = chebyshev_approx(x[mask], self.psi, self.support)
474
- else:
475
- raise RuntimeError('"method" is invalid.')
476
-
477
- # Check density is unit mass
478
- mass = numpy.trapezoid(rho, x)
479
- if not numpy.isclose(mass, 1.0, atol=1e-2):
480
- print(f'"rho" is not unit mass. mass: {mass:>0.3f}. Set ' +
481
- r'"force=True".')
482
-
483
- # Check density is positive
484
- min_rho = numpy.min(rho)
485
- if min_rho < 0.0 - 1e-3:
486
- print(f'"rho" is not positive. min_rho: {min_rho:>0.3f}. Set ' +
487
- r'"force=True".')
488
-
489
- if plot:
490
- plot_density(x, rho, eig=self.eig, support=self.support,
491
- label='Estimate', latex=latex, save=save)
492
-
493
- return rho
494
-
495
- # =======
496
- # hilbert
497
- # =======
498
-
499
- def hilbert(self, x=None, rho=None, plot=False, latex=False, save=False):
500
- """
501
- Compute Hilbert transform of the spectral density.
502
-
503
- Parameters
504
- ----------
505
-
506
- x : numpy.array, default=None
507
- The locations where Hilbert transform is evaluated at. If `None`,
508
- an interval slightly larger than the support interval of the
509
- spectral density is used.
510
-
511
- rho : numpy.array, default=None
512
- Density. If `None`, it will be computed.
513
-
514
- plot : bool, default=False
515
- If `True`, density is plotted.
516
-
517
- latex : bool, default=False
518
- If `True`, the plot is rendered using LaTeX. This option is
519
- relevant only if ``plot=True``.
520
-
521
- save : bool, default=False
522
- If not `False`, the plot is saved. If a string is given, it is
523
- assumed to the save filename (with the file extension). This option
524
- is relevant only if ``plot=True``.
525
-
526
- Returns
527
- -------
528
-
529
- hilb : numpy.array
530
- The Hilbert transform on the locations `x`.
531
-
532
- See Also
533
- --------
534
- density
535
- stieltjes
536
-
537
- Examples
538
- --------
539
-
540
- .. code-block:: python
541
-
542
- >>> from freealg import FreeForm
543
- """
544
-
545
- if self.psi is None:
546
- raise RuntimeError('The spectral density needs to be fit using the .fit() function.')
547
-
548
- # Create x if not given
549
- if x is None:
550
- radius = 0.5 * (self.lam_p - self.lam_m)
551
- center = 0.5 * (self.lam_p + self.lam_m)
552
- scale = 1.25
553
- x_min = numpy.floor(center - radius * scale)
554
- x_max = numpy.ceil(center + radius * scale)
555
- x = numpy.linspace(x_min, x_max, 500)
556
-
557
- # if (numpy.min(x) > self.lam_m) or (numpy.max(x) < self.lam_p):
558
- # raise ValueError('"x" does not encompass support interval.')
559
-
560
- # Preallocate density to zero
561
- if rho is None:
562
- rho = self.density(x)
563
-
564
- # mask of support [lam_m, lam_p]
565
- mask = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
566
- x_s = x[mask]
567
- rho_s = rho[mask]
568
-
569
- # Form the matrix of integrands: rho_s / (t - x_i)
570
- # Here, we have diff[i,j] = x[i] - x_s[j]
571
- diff = x[:, None] - x_s[None, :]
572
- D = rho_s[None, :] / diff
573
-
574
- # Principal‐value: wherever t == x_i, then diff == 0, zero that entry
575
- # (numpy.isclose handles floating‐point exactly)
576
- D[numpy.isclose(diff, 0.0)] = 0.0
577
-
578
- # Integrate each row over t using trapezoid rule on x_s
579
- # Namely, hilb[i] = int rho_s(t)/(t - x[i]) dt
580
- hilb = numpy.trapezoid(D, x_s, axis=1) / numpy.pi
581
-
582
- # We use negative sign convention
583
- hilb = -hilb
584
-
585
- if plot:
586
- plot_hilbert(x, hilb, support=self.support, latex=latex,
587
- save=save)
588
-
589
- return hilb
590
-
591
- # ====
592
- # glue
593
- # ====
594
-
595
- def _glue(self, z):
596
- # Glue function
597
- if self._pade_sol is None:
598
- return numpy.zeros_like(z)
599
- g = eval_pade(z, self._pade_sol)
600
- return g
601
-
602
- # =========
603
- # stieltjes
604
- # =========
605
-
606
- def stieltjes(self, x=None, y=None, plot=False, latex=False, save=False):
607
- """
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
-
611
- Parameters
612
- ----------
613
-
614
- x : numpy.array, default=None
615
- The x axis of the grid where the Stieltjes transform is evaluated.
616
- If `None`, an interval slightly larger than the support interval of
617
- the spectral density is used.
618
-
619
- y : numpy.array, default=None
620
- The y axis of the grid where the Stieltjes transform is evaluated.
621
- If `None`, a grid on the interval ``[-1, 1]`` is used.
622
-
623
- plot : bool, default=False
624
- If `True`, density is plotted.
625
-
626
- latex : bool, default=False
627
- If `True`, the plot is rendered using LaTeX. This option is
628
- relevant only if ``plot=True``.
629
-
630
- save : bool, default=False
631
- If not `False`, the plot is saved. If a string is given, it is
632
- assumed to the save filename (with the file extension). This option
633
- is relevant only if ``plot=True``.
634
-
635
- Returns
636
- -------
637
-
638
- m_p : numpy.ndarray
639
- The Stieltjes transform on the principal branch.
640
-
641
- m_m : numpy.ndarray
642
- The Stieltjes transform continued to the secondary branch.
643
-
644
- See Also
645
- --------
646
- density
647
- hilbert
648
-
649
- Notes
650
- -----
651
-
652
- Notes.
653
-
654
- References
655
- ----------
656
-
657
- .. [1] tbd
658
-
659
- Examples
660
- --------
661
-
662
- .. code-block:: python
663
-
664
- >>> from freealg import FreeForm
665
- """
666
-
667
- if self.psi is None:
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)
674
-
675
- # Create x if not given
676
- if x is None:
677
- radius = 0.5 * (self.lam_p - self.lam_m)
678
- center = 0.5 * (self.lam_p + self.lam_m)
679
- scale = 2.0
680
- x_min = numpy.floor(2.0 * (center - 2.0 * radius * scale)) / 2.0
681
- x_max = numpy.ceil(2.0 * (center + 2.0 * radius * scale)) / 2.0
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
687
-
688
- # Create y if not given
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)
698
-
699
- if plot:
700
- plot_stieltjes(x, y, m1, m2, self.support, latex=latex, save=save)
701
-
702
- return m1, m2
703
-
704
- # ==============
705
- # eval stieltjes
706
- # ==============
707
-
708
- def _eval_stieltjes(self, z):
709
- """
710
- Compute Stieltjes transform of the spectral density.
711
-
712
- Parameters
713
- ----------
714
-
715
- z : numpy.array
716
- The z values in the complex plan where the Stieltjes transform is
717
- evaluated.
718
-
719
-
720
- Returns
721
- -------
722
-
723
- m_p : numpy.ndarray
724
- The Stieltjes transform on the principal branch.
725
-
726
- m_m : numpy.ndarray
727
- The Stieltjes transform continued to the secondary branch.
728
- """
729
-
730
- assert self.psi is not None, "The fit function has not been called."
731
-
732
- # Allow for arbitrary input shapes
733
- z = numpy.asarray(z)
734
- shape = z.shape
735
- if len(shape) == 0:
736
- shape = (1,)
737
- z = z.reshape(-1, 1)
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
-
743
- # Stieltjes function
744
- if self.method == 'jacobi':
745
- stieltjes = partial(jacobi_stieltjes, psi=self.psi,
746
- support=self.support, alpha=self.alpha,
747
- beta=self.beta) # n_base = n_base
748
- elif self.method == 'chebyshev':
749
- stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
750
- support=self.support)
751
-
752
- mask_p = z.imag >= 0.0
753
- mask_m = z.imag < 0.0
754
-
755
- m1 = numpy.zeros_like(z)
756
- m2 = numpy.zeros_like(z)
757
-
758
- if self._pade_sol is not None:
759
- # Upper half-plane
760
- m1[mask_p] = stieltjes(z[mask_p].reshape(-1, 1)).ravel()
761
-
762
- # Lower half-plane, use Schwarz reflection
763
- m1[mask_m] = numpy.conjugate(
764
- stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))).ravel()
765
-
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()
777
-
778
- m1, m2 = m1.reshape(*shape), m2.reshape(*shape)
779
-
780
- return m1, m2
781
-
782
- # ==========
783
- # decompress
784
- # ==========
785
-
786
- def decompress(self, size, x=None, iterations=500, eigvals=True,
787
- step_size=0.1, tolerance=1e-6, seed=None, plot=False,
788
- latex=False, save=False):
789
- """
790
- Free decompression of spectral density.
791
-
792
- Parameters
793
- ----------
794
-
795
- size : int
796
- Size of the decompressed matrix.
797
-
798
- x : numpy.array, default=None
799
- Positions where density to be evaluated at. If `None`, an interval
800
- slightly larger than the support interval will be used.
801
-
802
- iterations: int, default=500
803
- Maximum number of Newton iterations.
804
-
805
- eigvals: bool, default=True
806
- Return estimated (sampled) eigenvalues as well as the density.
807
-
808
- step_size: float, default=0.1
809
- Step size for Newton iterations.
810
-
811
- tolerance: float, default=1e-6
812
- Tolerance for the solution obtained by the Newton solver. Also
813
- used for the finite difference approximation to the derivative.
814
-
815
- seed : int, default=None
816
- Seed for random number generator. Used for QMC sampling.
817
-
818
- plot : bool, default=False
819
- If `True`, density is plotted.
820
-
821
- latex : bool, default=False
822
- If `True`, the plot is rendered using LaTeX. This option is
823
- relevant only if ``plot=True``.
824
-
825
- save : bool, default=False
826
- If not `False`, the plot is saved. If a string is given, it is
827
- assumed to the save filename (with the file extension). This option
828
- is relevant only if ``plot=True``.
829
-
830
- Returns
831
- -------
832
-
833
- x : numpy.array
834
- Locations where the spectral density is estimated
835
-
836
- rho : numpy.array
837
- Estimated spectral density at locations x
838
-
839
- eigs : numpy.array
840
- Estimated eigenvalues as low-discrepancy samples of the estimated
841
- spectral density. Only returns if ``eigvals=True``.
842
-
843
- See Also
844
- --------
845
-
846
- density
847
- stieltjes
848
-
849
- Notes
850
- -----
851
-
852
- Work in progress.
853
-
854
- References
855
- ----------
856
-
857
- .. [1] tbd
858
-
859
- Examples
860
- --------
861
-
862
- .. code-block:: python
863
-
864
- >>> from freealg import FreeForm
865
- """
866
-
867
- size = int(size)
868
-
869
- rho, x, (lb, ub) = decompress(self, size, x=x, delta=self.delta,
870
- iterations=iterations,
871
- step_size=step_size, tolerance=tolerance)
872
- x, rho = x.ravel(), rho.ravel()
873
-
874
- if plot:
875
- plot_density(x, rho, support=(lb, ub),
876
- label='Decompression', latex=latex, save=save)
877
-
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.
893
-
894
- Parameters
895
- ----------
896
-
897
- A : numpy.ndarray
898
- The symmetric real-valued matrix :math:`\\mathbf{A}` whose eigenvalues
899
- (or those of a matrix containing :math:`\\mathbf{A}`) are to be computed.
900
-
901
- N : int, default=None
902
- The size of the matrix containing :math:`\\mathbf{A}` to estimate
903
- eigenvalues of. If None, returns estimates of the eigenvalues of
904
- :math:`\\mathbf{A}` itself.
905
-
906
- psd: bool, default=None
907
- Determines whether the matrix is positive-semidefinite (PSD; all
908
- eigenvalues are non-negative). If None, the matrix is considered PSD if
909
- all sampled eigenvalues are positive.
910
-
911
- Notes
912
- -----
913
-
914
- Notes.
915
-
916
- References
917
- ----------
918
-
919
- .. [1] Reference.
920
-
921
- Examples
922
- --------
923
-
924
- .. code-block:: python
925
-
926
- >>> from freealg import FreeForm
927
- """
928
- n = A.shape[0]
929
-
930
- # Size of sample matrix
931
- n_s = int(80*(1 + numpy.log(n)))
932
-
933
- # If matrix is not large enough, return eigenvalues
934
- if n < n_s:
935
- return compute_eig(A)
936
-
937
- if N is None:
938
- N = n
939
-
940
- # Number of samples
941
- num_samples = int(10 * (n / n_s)**0.5)
942
-
943
- # Collect eigenvalue samples
944
- samples = []
945
- for _ in range(num_samples):
946
- indices = numpy.random.choice(n, n_s, replace=False)
947
- samples.append(compute_eig(A[numpy.ix_(indices, indices)]))
948
- samples = numpy.concatenate(samples).ravel()
949
-
950
- # If all eigenvalues are positive, set PSD flag
951
- if psd is None:
952
- psd = samples.min() > 0
953
-
954
- ff = FreeForm(samples)
955
- # Since we are resampling, we need to provide the correct matrix size
956
- ff.n = n_s
957
-
958
- # Perform fit and estimate eigenvalues
959
- order = 1 + int(len(samples)**.25)
960
- ff.fit(method='chebyshev', K=order, projection='sample', damp='jackson',
961
- force=True, plot=False, latex=False, save=False, reg=0.01)
962
- _, _, eigs = ff.decompress(N)
963
-
964
- if psd:
965
- eigs = numpy.abs(eigs)
966
-
967
- return eigs