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
@@ -0,0 +1,1232 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, 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 resolve_complex_dtype, compute_eig
16
+ # from .._util import compute_eig
17
+ from ._continuation_algebraic import sample_z_joukowski, \
18
+ filter_z_away_from_cuts, fit_polynomial_relation, \
19
+ sanity_check_stieltjes_branch, eval_P
20
+ from ._edge import evolve_edges, merge_edges
21
+ from ._cusp_wrap import cusp_wrap
22
+
23
+ # Decompress with Newton
24
+ # from ._decompress import build_time_grid, decompress_newton
25
+ from ._decompress_util import build_time_grid
26
+ # from ._decompress4 import decompress_newton # WORKS (mass issue)
27
+ # from ._decompress5 import build_time_grid, decompress_newton
28
+ # from ._decompress6 import build_time_grid, decompress_newton
29
+ # from ._decompress4_2 import build_time_grid, decompress_newton
30
+ # from ._decompress_new_2 import build_time_grid, decompress_newton
31
+ # from ._decompress_new import build_time_grid, decompress_newton
32
+ # from ._decompress6 import decompress_newton
33
+ # from ._decompress7 import decompress_newton
34
+ # from ._decompress8 import decompress_newton
35
+ from ._decompress9 import decompress_newton # With Predictor/Corrector
36
+
37
+ # Decompress with coefficients
38
+ from ._decompress2 import decompress_coeffs, plot_candidates
39
+
40
+ # Homotopy
41
+ # from ._homotopy import StieltjesPoly
42
+ # from ._homotopy2 import StieltjesPoly
43
+ # from ._homotopy3 import StieltjesPoly # Viterbi
44
+ # from ._homotopy4 import StieltjesPoly
45
+ from ._homotopy5 import StieltjesPoly
46
+
47
+ from ._branch_points import compute_branch_points
48
+ from ._support import compute_support
49
+ from ._moments import Moments, AlgebraicStieltjesMoments
50
+ from .._free_form._support import supp
51
+ from .._free_form._plot_util import plot_density, plot_hilbert, plot_stieltjes
52
+
53
+ # Fallback to previous numpy API
54
+ if not hasattr(numpy, 'trapezoid'):
55
+ numpy.trapezoid = numpy.trapz
56
+
57
+ __all__ = ['AlgebraicForm']
58
+
59
+
60
+ # ==============
61
+ # Algebraic Form
62
+ # ==============
63
+
64
+ class AlgebraicForm(object):
65
+ """
66
+ Algebraic surrogate for ensemble models.
67
+
68
+ Parameters
69
+ ----------
70
+
71
+ A : numpy.ndarray
72
+ The 2D symmetric :math:`\\mathbf{A}`. The eigenvalues of this will be
73
+ computed upon calling this class. If a 1D array provided, it is
74
+ assumed to be the eigenvalues of :math:`\\mathbf{A}`.
75
+
76
+ support : tuple, default=None
77
+ The support of the density of :math:`\\mathbf{A}`. If `None`, it is
78
+ estimated from the minimum and maximum of the eigenvalues.
79
+
80
+ delta: float, default=1e-6
81
+ Size of perturbations into the upper half plane for Plemelj's
82
+ formula.
83
+
84
+ dtype : {``'complex128'``, ``'complex256'``}, default = ``'complex128'``
85
+ Data type for inner computations of complex variables:
86
+
87
+ * ``'complex128'``: 128-bit complex numbers, equivalent of two double
88
+ precision floating point.
89
+ * ``'complex256'``: 256-bit complex numbers, equivalent of two long
90
+ double precision floating point. This option is only available on
91
+ Linux machines.
92
+
93
+ When using series acceleration methods (such as setting
94
+ ``continuation`` in :func:`fit` function to ``wynn-eps``), setting a
95
+ higher precision floating point arithmetics might improve conference.
96
+
97
+ **kwargs : dict, optional
98
+ Parameters for the :func:`supp` function can also be prescribed
99
+ here when ``support=None``.
100
+
101
+ Attributes
102
+ ----------
103
+
104
+ eig : numpy.array
105
+ Eigenvalues of the matrix
106
+
107
+ support: tuple
108
+ The predicted (or given) support :math:`(\\lambda_{\\min},
109
+ \\lambda_{\\max})` of the eigenvalue density.
110
+
111
+ n : int
112
+ Initial array size (assuming a square matrix when :math:`\\mathbf{A}` is
113
+ 2D).
114
+
115
+ Methods
116
+ -------
117
+
118
+ fit
119
+ Fit the Jacobi polynomials to the empirical density.
120
+
121
+ density
122
+ Compute the spectral density of the matrix.
123
+
124
+ hilbert
125
+ Compute Hilbert transform of the spectral density
126
+
127
+ stieltjes
128
+ Compute Stieltjes transform of the spectral density
129
+
130
+ decompress
131
+ Free decompression of spectral density
132
+
133
+ eigvalsh
134
+ Estimate the eigenvalues
135
+
136
+ cond
137
+ Estimate the condition number
138
+
139
+ trace
140
+ Estimate the trace of a matrix power
141
+
142
+ slogdet
143
+ Estimate the sign and logarithm of the determinant
144
+
145
+ norm
146
+ Estimate the Schatten norm
147
+
148
+ Examples
149
+ --------
150
+
151
+ .. code-block:: python
152
+
153
+ >>> from freealg import FreeForm
154
+ """
155
+
156
+ # ====
157
+ # init
158
+ # ====
159
+
160
+ def __init__(self, A, support=None, delta=1e-5, dtype='complex128',
161
+ **kwargs):
162
+ """
163
+ Initialization.
164
+ """
165
+
166
+ self.A = None
167
+ self.eig = None
168
+ self._stieltjes = None
169
+ self._moments = None
170
+ self.support = support
171
+ self.est_support = None # Estimated from polynmial after fitting
172
+ self.delta = delta # Offset above real axis to apply Plemelj formula
173
+
174
+ # Data type for complex arrays
175
+ self.dtype = resolve_complex_dtype(dtype)
176
+
177
+ if hasattr(A, 'stieltjes') and callable(getattr(A, 'stieltjes', None)):
178
+ # This is one of the distribution objects, like MarchenkoPastur
179
+ self._stieltjes = A.stieltjes
180
+ self.support = A.support()
181
+ self.n = 1
182
+
183
+ elif callable(A):
184
+ # This is a custom function
185
+ self._stieltjes = A
186
+ self.n = 1
187
+
188
+ else:
189
+ # Eigenvalues
190
+ if A.ndim == 1:
191
+ # If A is a 1D array, it is assumed A is the eigenvalues array.
192
+ self.eig = A
193
+ self.n = len(A)
194
+ elif A.ndim == 2:
195
+ # When A is a 2D array, it is assumed A is the actual array,
196
+ # and its eigenvalues will be computed.
197
+ self.A = A
198
+ self.n = A.shape[0]
199
+ assert A.shape[0] == A.shape[1], \
200
+ 'Only square matrices are permitted.'
201
+ self.eig = compute_eig(A)
202
+
203
+ # Use empirical Stieltjes function
204
+ self._stieltjes = lambda z: \
205
+ numpy.mean(1.0/(self.eig-z[:, numpy.newaxis]), axis=-1)
206
+ self._moments = Moments(self.eig) # NOTE (never used)
207
+
208
+ # broad support
209
+ if self.support is None:
210
+ if self.eig is None:
211
+ raise RuntimeError("Support must be provided without data")
212
+
213
+ # Detect support
214
+ self.lam_m, self.lam_p = supp(self.eig, **kwargs)
215
+ self.broad_support = (float(self.lam_m), float(self.lam_p))
216
+ else:
217
+ self.lam_m = float(min([s[0] for s in self.support]))
218
+ self.lam_p = float(max([s[1] for s in self.support]))
219
+ self.broad_support = (self.lam_m, self.lam_p)
220
+
221
+ # Initialize
222
+ self.a_coeffs = None # Polynomial coefficients
223
+ self.status = None # Fitting status
224
+ self.cache = {} # Cache inner-computations
225
+
226
+ # ===
227
+ # fit
228
+ # ===
229
+
230
+ def fit(self, deg_m, deg_z, reg=0.0,
231
+ r=[1.25, 6.0, 20.0],
232
+ n_r=[3, 2, 1],
233
+ n_samples=4096,
234
+ y_eps=2e-2,
235
+ x_pad=0.0,
236
+ triangular=None,
237
+ mu=None,
238
+ mu_reg=None,
239
+ normalize=False,
240
+ verbose=False):
241
+ """
242
+ Fit polynomial.
243
+
244
+ Parameters
245
+ ----------
246
+
247
+ deg_m : int
248
+ Degree :math:`\\deg_m(P)`
249
+
250
+ deg_z : int
251
+ Degree :math:`\\deg_z(P)`
252
+
253
+ mu : array_like, default=None
254
+ If an array :math:`[\\mu_0, \\mu_`, \\dots, \\mu_r]` is given,
255
+ it enforces the first :math:`r+1` moments. Note that :math:`\\mu_0`
256
+ should be :math:`1` to ensure unit mass. See also ``mu_reg`.
257
+
258
+ mu_reg: float, default=None
259
+ If `None`, the constraints ``mu`` are applied as hard constraint.
260
+ If a positive number, the constraints are applied as a soft
261
+ constraints with regularisation ``mu_reg``.
262
+
263
+ Notes
264
+ -----
265
+
266
+ When the input data are from an exact model, hard moment constraint is
267
+ preferred over soft constraint as the latter can hurt an already a good
268
+ fit.
269
+ """
270
+
271
+ # Very important: reset cache whenever this function is called. This
272
+ # also empties all references holding a cache copy.
273
+ # self.cache.clear()
274
+
275
+ z_fits = []
276
+
277
+ # Sampling around support, or broad_support. This is only needed to
278
+ # ensure sampled points are not hiting the support itself is not used
279
+ # in any computation. If support is not known, use broad support.
280
+ if self.support is not None:
281
+ possible_support = self.support
282
+ else:
283
+ possible_support = [self.broad_support]
284
+
285
+ for sup in possible_support:
286
+ a, b = sup
287
+
288
+ for i in range(len(r)):
289
+ z_fits.append(sample_z_joukowski(a, b, n_samples=n_samples,
290
+ r=r[i], n_r=n_r[i]))
291
+
292
+ z_fit = numpy.concatenate(z_fits)
293
+
294
+ # Remove points too close to any cut
295
+ z_fit = filter_z_away_from_cuts(z_fit, possible_support, y_eps=y_eps,
296
+ x_pad=x_pad)
297
+
298
+ # Fitting (w_inf = None means adaptive weight selection)
299
+ m1_fit = self._stieltjes(z_fit)
300
+ a_coeffs, fit_metrics = fit_polynomial_relation(
301
+ z_fit, m1_fit, s=deg_m, deg_z=deg_z, ridge_lambda=reg,
302
+ triangular=triangular, normalize=normalize, mu=mu,
303
+ mu_reg=mu_reg)
304
+
305
+ self.a_coeffs = a_coeffs
306
+
307
+ # Estimate support from the fitted polynomial
308
+ self.est_support, _ = self.estimate_support(a_coeffs)
309
+
310
+ # Reporting error
311
+ P_res = numpy.abs(eval_P(z_fit, m1_fit, a_coeffs))
312
+ res_max = numpy.max(P_res[numpy.isfinite(P_res)])
313
+ res_99_9 = numpy.quantile(P_res[numpy.isfinite(P_res)], 0.999)
314
+
315
+ # Check polynomial has Stieltjes root
316
+ x_min = self.lam_m - 1.0
317
+ x_max = self.lam_p + 1.0
318
+ status = sanity_check_stieltjes_branch(a_coeffs, x_min, x_max,
319
+ eta=max(y_eps, 1e-2), n_x=128,
320
+ max_bad_frac=0.05)
321
+
322
+ status['res_max'] = float(res_max)
323
+ status['res_99_9'] = float(res_99_9)
324
+ status['fit_metrics'] = fit_metrics
325
+ self.status = status
326
+ self._stieltjes = StieltjesPoly(self.a_coeffs) # NOTE overwrite init
327
+ self._moments_base = AlgebraicStieltjesMoments(a_coeffs)
328
+ self.moments = Moments(self._moments_base)
329
+
330
+ if verbose:
331
+ print(f'fit residual max : {res_max:>0.4e}')
332
+ print(f'fit residual 99.9%: {res_99_9:>0.4e}')
333
+
334
+ print('\nCoefficients (real)')
335
+ with numpy.printoptions(precision=8, suppress=True):
336
+ for i in range(a_coeffs.shape[0]):
337
+ for j in range(a_coeffs.shape[1]):
338
+ v = a_coeffs[i, j]
339
+ print(f'{v.real:>+0.8f}', end=' ')
340
+ print('')
341
+
342
+ a_coeffs_img_norm = numpy.linalg.norm(a_coeffs.imag, ord='fro')
343
+ print(f'\nCoefficients (imag) norm: {a_coeffs_img_norm:>0.4e}')
344
+
345
+ if not status['ok']:
346
+ print("\nWARNING: sanity check failed:\n" +
347
+ f"\tfrac_bad: {status['frac_bad']:>0.3f}\n" +
348
+ f"\tn_bad : {status['n_bad']}\n" +
349
+ f"\tn_test : {status['n_test']}")
350
+ else:
351
+ print('\nStieltjes sanity check: OK')
352
+
353
+ return a_coeffs, self.est_support, status
354
+
355
+ # =====================
356
+ # inflate broad support
357
+ # =====================
358
+
359
+ def _inflate_broad_support(self, inflate=0.0):
360
+ """
361
+ """
362
+
363
+ min_supp, max_supp = self.broad_support
364
+
365
+ c_supp = 0.5 * (max_supp + min_supp)
366
+ r_supp = 0.5 * (max_supp - min_supp)
367
+
368
+ x_min = c_supp - r_supp * (1.0 + inflate)
369
+ x_max = c_supp + r_supp * (1.0 + inflate)
370
+
371
+ return x_min, x_max
372
+
373
+ # ================
374
+ # estimate support
375
+ # ================
376
+
377
+ def estimate_support(self, a_coeffs=None, scan_range=None, n_scan=4000):
378
+ """
379
+ """
380
+
381
+ if a_coeffs is None:
382
+ if self.a_coeffs is None:
383
+ raise RuntimeError('Call "fit" first.')
384
+ else:
385
+ a_coeffs = self.a_coeffs
386
+
387
+ # Inflate a bit to make sure all points are searched
388
+ if scan_range is not None:
389
+ x_min, x_max = scan_range
390
+ else:
391
+ x_min, x_max = self._inflate_broad_support(inflate=0.2)
392
+
393
+ est_support, info = compute_support(a_coeffs, x_min=x_min, x_max=x_max,
394
+ n_scan=n_scan)
395
+
396
+ return est_support, info
397
+
398
+ # ======================
399
+ # estimate branch points
400
+ # ======================
401
+
402
+ def estimate_branch_points(self, tol=1e-15, real_tol=None):
403
+ """
404
+ Compute global branch points and zeros of leading a_j
405
+ """
406
+
407
+ if self.a_coeffs is None:
408
+ raise RuntimeError('Call "fit" first.')
409
+
410
+ bp, leading_zeros, info = compute_branch_points(
411
+ self.a_coeffs, tol=tol, real_tol=real_tol)
412
+
413
+ return bp, leading_zeros, info
414
+
415
+ # =============
416
+ # generate grid
417
+ # =============
418
+
419
+ def _generate_grid(self, scale, extend=1.0, N=500):
420
+ """
421
+ Generate a grid of points to evaluate density / Hilbert / Stieltjes
422
+ transforms.
423
+ """
424
+
425
+ radius = 0.5 * (self.lam_p - self.lam_m)
426
+ center = 0.5 * (self.lam_p + self.lam_m)
427
+
428
+ x_min = numpy.floor(extend * (center - extend * radius * scale))
429
+ x_max = numpy.ceil(extend * (center + extend * radius * scale))
430
+
431
+ x_min /= extend
432
+ x_max /= extend
433
+
434
+ return numpy.linspace(x_min, x_max, N)
435
+
436
+ # =======
437
+ # density
438
+ # =======
439
+
440
+ def density(self, x=None, plot=False, latex=False, save=False):
441
+ """
442
+ Evaluate spectral density.
443
+
444
+ Parameters
445
+ ----------
446
+
447
+ x : numpy.array, default=None
448
+ Positions where density to be evaluated at. If `None`, an interval
449
+ slightly larger than the support interval will be used.
450
+
451
+ plot : bool, default=False
452
+ If `True`, density is plotted.
453
+
454
+ latex : bool, default=False
455
+ If `True`, the plot is rendered using LaTeX. This option is
456
+ relevant only if ``plot=True``.
457
+
458
+ save : bool, default=False
459
+ If not `False`, the plot is saved. If a string is given, it is
460
+ assumed to the save filename (with the file extension). This option
461
+ is relevant only if ``plot=True``.
462
+
463
+ Returns
464
+ -------
465
+
466
+ rho : numpy.array
467
+ Density at locations x.
468
+
469
+ See Also
470
+ --------
471
+ hilbert
472
+ stieltjes
473
+
474
+ Examples
475
+ --------
476
+
477
+ .. code-block:: python
478
+
479
+ >>> from freealg import FreeForm
480
+ """
481
+
482
+ if self.a_coeffs is None:
483
+ raise RuntimeError('The model needs to be fit using the .fit() ' +
484
+ 'function.')
485
+
486
+ # Create x if not given
487
+ if x is None:
488
+ x = self._generate_grid(1.25)
489
+
490
+ # Preallocate density to zero
491
+ z = x.astype(complex) + 1j * self.delta
492
+ rho = self._stieltjes(z).imag / numpy.pi
493
+
494
+ if plot:
495
+ plot_density(x, rho, eig=self.eig, support=self.broad_support,
496
+ label='Estimate', latex=latex, save=save)
497
+
498
+ return rho
499
+
500
+ # =======
501
+ # hilbert
502
+ # =======
503
+
504
+ def hilbert(self, x=None, plot=False, latex=False, save=False):
505
+ """
506
+ Compute Hilbert transform of the spectral density.
507
+
508
+ Parameters
509
+ ----------
510
+
511
+ x : numpy.array, default=None
512
+ The locations where Hilbert transform is evaluated at. If `None`,
513
+ an interval slightly larger than the support interval of the
514
+ spectral density is used.
515
+
516
+ plot : bool, default=False
517
+ If `True`, density is plotted.
518
+
519
+ latex : bool, default=False
520
+ If `True`, the plot is rendered using LaTeX. This option is
521
+ relevant only if ``plot=True``.
522
+
523
+ save : bool, default=False
524
+ If not `False`, the plot is saved. If a string is given, it is
525
+ assumed to the save filename (with the file extension). This option
526
+ is relevant only if ``plot=True``.
527
+
528
+ Returns
529
+ -------
530
+
531
+ hilb : numpy.array
532
+ The Hilbert transform on the locations `x`.
533
+
534
+ See Also
535
+ --------
536
+ density
537
+ stieltjes
538
+
539
+ Examples
540
+ --------
541
+
542
+ .. code-block:: python
543
+
544
+ >>> from freealg import FreeForm
545
+ """
546
+
547
+ if self.a_coeffs is None:
548
+ raise RuntimeError('The model needs to be fit using the .fit() ' +
549
+ 'function.')
550
+
551
+ # Create x if not given
552
+ if x is None:
553
+ x = self._generate_grid(1.25)
554
+
555
+ # Preallocate density to zero
556
+ hilb = -self._stieltjes(x).real / numpy.pi
557
+
558
+ if plot:
559
+ plot_hilbert(x, hilb, support=self.broad_support, latex=latex,
560
+ save=save)
561
+
562
+ return hilb
563
+
564
+ # =========
565
+ # stieltjes
566
+ # =========
567
+
568
+ def stieltjes(self, x=None, y=None, plot=False, latex=False, save=False):
569
+ """
570
+ Compute Stieltjes transform of the spectral density on a grid.
571
+
572
+ This function evaluates Stieltjes transform on an array of points, or
573
+ over a 2D Cartesian grid on the complex plane.
574
+
575
+ Parameters
576
+ ----------
577
+
578
+ x : numpy.array, default=None
579
+ The x axis of the grid where the Stieltjes transform is evaluated.
580
+ If `None`, an interval slightly larger than the support interval of
581
+ the spectral density is used.
582
+
583
+ y : numpy.array, default=None
584
+ The y axis of the grid where the Stieltjes transform is evaluated.
585
+ If `None`, a grid on the interval ``[-1, 1]`` is used.
586
+
587
+ plot : bool, default=False
588
+ If `True`, density is plotted.
589
+
590
+ latex : bool, default=False
591
+ If `True`, the plot is rendered using LaTeX. This option is
592
+ relevant only if ``plot=True``.
593
+
594
+ save : bool, default=False
595
+ If not `False`, the plot is saved. If a string is given, it is
596
+ assumed to the save filename (with the file extension). This option
597
+ is relevant only if ``plot=True``.
598
+
599
+ Returns
600
+ -------
601
+
602
+ m : numpy.ndarray
603
+ The Stieltjes transform on the principal branch.
604
+
605
+ See Also
606
+ --------
607
+
608
+ density
609
+ hilbert
610
+
611
+ Examples
612
+ --------
613
+
614
+ .. code-block:: python
615
+
616
+ >>> from freealg import FreeForm
617
+ """
618
+
619
+ if self.a_coeffs is None:
620
+ raise RuntimeError('The model needs to be fit using the .fit() ' +
621
+ 'function.')
622
+
623
+ # Create x if not given
624
+ if x is None:
625
+ x = self._generate_grid(2.0, extend=2.0)[::2]
626
+
627
+ # Create y if not given
628
+ if (plot is False) and (y is None):
629
+ # Do not use a Cartesian grid. Create a 1D array z slightly above
630
+ # the real line.
631
+ y = self.delta * 1j
632
+ z = x.astype(complex) + y # shape (Nx,)
633
+ else:
634
+ # Use a Cartesian grid
635
+ if y is None:
636
+ y = numpy.linspace(-1, 1, 200)
637
+ x_grid, y_grid = numpy.meshgrid(x.real, y.real)
638
+ z = x_grid + 1j * y_grid # shape (Ny, Nx)
639
+
640
+ m = self._stieltjes(z, progress=True)
641
+
642
+ if plot:
643
+ plot_stieltjes(x, y, m, m, self.broad_support, latex=latex,
644
+ save=save)
645
+
646
+ return m
647
+
648
+ # ==============
649
+ # eval stieltjes
650
+ # ==============
651
+
652
+ def _eval_stieltjes(self, z, branches=False):
653
+ """
654
+ Compute Stieltjes transform of the spectral density.
655
+
656
+ Parameters
657
+ ----------
658
+
659
+ z : numpy.array
660
+ The z values in the complex plan where the Stieltjes transform is
661
+ evaluated.
662
+
663
+ branches : bool, default = False
664
+ Return both the principal and secondary branches of the Stieltjes
665
+ transform. The default ``branches=False`` will return only
666
+ the secondary branch.
667
+
668
+ Returns
669
+ -------
670
+
671
+ m_p : numpy.ndarray
672
+ The Stieltjes transform on the principal branch if
673
+ ``branches=True``.
674
+
675
+ m_m : numpy.ndarray
676
+ The Stieltjes transform continued to the secondary branch.
677
+ """
678
+
679
+ pass
680
+
681
+ # ==========
682
+ # decompress
683
+ # ==========
684
+
685
+ def decompress(self, size, x=None, method='one', plot=False, latex=False,
686
+ save=False, verbose=False, min_n_times=10,
687
+ newton_opt={'max_iter': 50, 'tol': 1e-12, 'armijo': 1e-4,
688
+ 'min_lam': 1e-6, 'w_min': 1e-14,
689
+ 'sweep': True}):
690
+ """
691
+ Free decompression of spectral density.
692
+ """
693
+
694
+ # Check size argument
695
+ if numpy.isscalar(size):
696
+ size = int(size)
697
+ else:
698
+ # Check monotonic increment (either all increasing or decreasing)
699
+ diff = numpy.diff(size)
700
+ if not (numpy.all(diff >= 0) or numpy.all(diff <= 0)):
701
+ raise ValueError('"size" increment should be monotonic.')
702
+
703
+ # Decompression ratio equal to e^{t}.
704
+ alpha = numpy.atleast_1d(size) / self.n
705
+
706
+ # Lower and upper bound on new support
707
+ hilb_lb = \
708
+ (1.0 / self._stieltjes(self.lam_m + self.delta * 1j).item()).real
709
+ hilb_ub = \
710
+ (1.0 / self._stieltjes(self.lam_p + self.delta * 1j).item()).real
711
+ lb = self.lam_m - (numpy.max(alpha) - 1) * hilb_lb
712
+ ub = self.lam_p - (numpy.max(alpha) - 1) * hilb_ub
713
+
714
+ # Create x if not given
715
+ if x is None:
716
+ radius = 0.5 * (ub - lb)
717
+ center = 0.5 * (ub + lb)
718
+ scale = 1.25
719
+ x_min = numpy.floor(center - radius * scale)
720
+ x_max = numpy.ceil(center + radius * scale)
721
+ x = numpy.linspace(x_min, x_max, 200)
722
+ else:
723
+ x = numpy.asarray(x)
724
+
725
+ if method == 'one':
726
+
727
+ # Query grid on the real axis + a small imaginary buffer
728
+ z_query = x + 1j * self.delta
729
+
730
+ # Initial condition at t = 0 (physical branch)
731
+ w0_list = self._stieltjes(z_query)
732
+
733
+ # Ensure there are at least min_n_times time t, including requested
734
+ # times, and especially time t = 0
735
+ t_all, idx_req = build_time_grid(
736
+ size, self.n, min_n_times=min_n_times)
737
+
738
+ # Evolve
739
+ W, ok = decompress_newton(
740
+ z_query, t_all, self.a_coeffs,
741
+ w0_list=w0_list, **newton_opt)
742
+
743
+ rho_all = W.imag / numpy.pi
744
+
745
+ # return only the user-requested ones
746
+ rho = rho_all[idx_req]
747
+
748
+ if verbose:
749
+ print("success rate per t:", ok.mean(axis=1))
750
+
751
+ elif method == 'two':
752
+
753
+ # Preallocate density to zero
754
+ rho = numpy.zeros((alpha.size, x.size), dtype=float)
755
+
756
+ # Decompress to each alpha
757
+ for i in range(alpha.size):
758
+ t_i = numpy.log(alpha[i])
759
+ coeffs_i = decompress_coeffs(self.a_coeffs, t_i)
760
+
761
+ def mom(k):
762
+ return self.moments(k, t_i)
763
+
764
+ stieltjes_i = StieltjesPoly(coeffs_i, mom)
765
+ rho[i, :] = stieltjes_i(x).imag
766
+
767
+ rho = rho / numpy.pi
768
+
769
+ else:
770
+ raise ValueError('"method" is invalid.')
771
+
772
+ # If the input size was only a scalar, return a 1D rho, otherwise 2D.
773
+ if numpy.isscalar(size):
774
+ rho = numpy.squeeze(rho)
775
+
776
+ # Plot only the last size
777
+ if plot:
778
+ if numpy.isscalar(size):
779
+ rho_last = rho
780
+ else:
781
+ rho_last = rho[-1, :]
782
+ plot_density(x, rho_last, support=(lb, ub),
783
+ label='Decompression', latex=latex, save=save)
784
+
785
+ return rho, x
786
+
787
+ # ==========
788
+ # candidates
789
+ # ==========
790
+
791
+ def candidates(self, size, x=None, verbose=False):
792
+
793
+ # Check size argument
794
+ if numpy.isscalar(size):
795
+ size = int(size)
796
+ else:
797
+ # Check monotonic increment (either all increasing or decreasing)
798
+ diff = numpy.diff(size)
799
+ if not (numpy.all(diff >= 0) or numpy.all(diff <= 0)):
800
+ raise ValueError('"size" increment should be monotonic.')
801
+
802
+ # Decompression ratio equal to e^{t}.
803
+ alpha = numpy.atleast_1d(size) / self.n
804
+
805
+ # Lower and upper bound on new support
806
+ hilb_lb = \
807
+ (1.0 / self._stieltjes(self.lam_m + self.delta * 1j).item()).real
808
+ hilb_ub = \
809
+ (1.0 / self._stieltjes(self.lam_p + self.delta * 1j).item()).real
810
+ lb = self.lam_m - (numpy.max(alpha) - 1) * hilb_lb
811
+ ub = self.lam_p - (numpy.max(alpha) - 1) * hilb_ub
812
+
813
+ # Create x if not given
814
+ if x is None:
815
+ radius = 0.5 * (ub - lb)
816
+ center = 0.5 * (ub + lb)
817
+ scale = 1.25
818
+ x_min = numpy.floor(center - radius * scale)
819
+ x_max = numpy.ceil(center + radius * scale)
820
+ x = numpy.linspace(x_min, x_max, 2000)
821
+ else:
822
+ x = numpy.asarray(x)
823
+
824
+ for i in range(alpha.size):
825
+ t_i = numpy.log(alpha[i])
826
+ coeffs_i = decompress_coeffs(self.a_coeffs, t_i)
827
+ plot_candidates(coeffs_i, x, size=int(alpha[i]*self.n),
828
+ verbose=verbose)
829
+
830
+ # ====
831
+ # edge
832
+ # ====
833
+
834
+ def edge(self, t, eta=1e-3, dt_max=0.1, max_iter=30, tol=1e-12,
835
+ verbose=False):
836
+ """
837
+ Evolves spectral edges.
838
+
839
+ Fix: if t is a scalar or length-1 array, we prepend t=0 internally so
840
+ evolve_edges actually advances from the initialization at t=0.
841
+ """
842
+
843
+ if self.support is not None:
844
+ known_support = self.support
845
+ elif self.est_support is not None:
846
+ known_support = self.est_support
847
+ else:
848
+ raise RuntimeError('Call "fit" first.')
849
+
850
+ t = numpy.asarray(t, dtype=float).ravel()
851
+
852
+ if t.size == 1:
853
+ t1 = float(t[0])
854
+ if t1 == 0.0:
855
+ t_grid = numpy.array([0.0], dtype=float)
856
+ complex_edges, ok_edges = evolve_edges(
857
+ t_grid, self.a_coeffs, support=known_support, eta=eta,
858
+ dt_max=dt_max, max_iter=max_iter, tol=tol
859
+ )
860
+ else:
861
+ # prepend 0 and drop it after evolution
862
+ t_grid = numpy.array([0.0, t1], dtype=float)
863
+ complex_edges2, ok_edges2 = evolve_edges(
864
+ t_grid, self.a_coeffs, support=known_support, eta=eta,
865
+ dt_max=dt_max, max_iter=max_iter, tol=tol
866
+ )
867
+ complex_edges = complex_edges2[-1:, :]
868
+ ok_edges = ok_edges2[-1:, :]
869
+ else:
870
+ # For vector t, require it starts at 0 for correct initialization
871
+ # (you can relax this if you want by prepending 0 similarly).
872
+ complex_edges, ok_edges = evolve_edges(
873
+ t, self.a_coeffs, support=known_support, eta=eta,
874
+ dt_max=dt_max, max_iter=max_iter, tol=tol
875
+ )
876
+
877
+ real_edges = complex_edges.real
878
+
879
+ # Remove spurious edges / merges for plotting
880
+ real_merged_edges, active_k = merge_edges(real_edges, tol=1e-4)
881
+
882
+ if verbose:
883
+ print("edge success rate:", ok_edges.mean())
884
+
885
+ return complex_edges, real_merged_edges, active_k
886
+
887
+ # ====
888
+ # cusp
889
+ # ====
890
+
891
+ def cusp(self, t_grid):
892
+ """
893
+ """
894
+
895
+ return cusp_wrap(self, t_grid, edge_kwargs=None, max_iter=50,
896
+ tol=1.0e-12)
897
+
898
+ # ========
899
+ # eigvalsh
900
+ # ========
901
+
902
+ def eigvalsh(self, size=None, seed=None, **kwargs):
903
+ """
904
+ Estimate the eigenvalues.
905
+
906
+ This function estimates the eigenvalues of the freeform matrix
907
+ or a larger matrix containing it using free decompression.
908
+
909
+ Parameters
910
+ ----------
911
+
912
+ size : int, default=None
913
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
914
+ eigenvalues of. If None, returns estimates of the eigenvalues of
915
+ :math:`\\mathbf{A}` itself.
916
+
917
+ seed : int, default=None
918
+ The seed for the Quasi-Monte Carlo sampler.
919
+
920
+ **kwargs : dict, optional
921
+ Pass additional options to the underlying
922
+ :func:`FreeForm.decompress` function.
923
+
924
+ Returns
925
+ -------
926
+
927
+ eigs : numpy.array
928
+ Eigenvalues of decompressed matrix
929
+
930
+ See Also
931
+ --------
932
+
933
+ FreeForm.decompress
934
+ FreeForm.cond
935
+
936
+ Notes
937
+ -----
938
+
939
+ All arguments to the `.decompress()` procedure can be provided.
940
+
941
+ Examples
942
+ --------
943
+
944
+ .. code-block:: python
945
+ :emphasize-lines: 1
946
+
947
+ >>> from freealg import FreeForm
948
+ """
949
+
950
+ # if size is None:
951
+ # size = self.n
952
+ #
953
+ # rho, x = self.decompress(size, **kwargs)
954
+ # eigs = numpy.sort(sample(x, rho, size, method='qmc', seed=seed))
955
+ #
956
+ # return eigs
957
+ pass
958
+
959
+ # ====
960
+ # cond
961
+ # ====
962
+
963
+ def cond(self, size=None, seed=None, **kwargs):
964
+ """
965
+ Estimate the condition number.
966
+
967
+ This function estimates the condition number of the matrix
968
+ :math:`\\mathbf{A}` or a larger matrix containing :math:`\\mathbf{A}`
969
+ using free decompression.
970
+
971
+ Parameters
972
+ ----------
973
+
974
+ size : int, default=None
975
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
976
+ eigenvalues of. If None, returns estimates of the eigenvalues of
977
+ :math:`\\mathbf{A}` itself.
978
+
979
+ **kwargs : dict, optional
980
+ Pass additional options to the underlying
981
+ :func:`FreeForm.decompress` function.
982
+
983
+ Returns
984
+ -------
985
+
986
+ c : float
987
+ Condition number
988
+
989
+ See Also
990
+ --------
991
+
992
+ FreeForm.eigvalsh
993
+ FreeForm.norm
994
+ FreeForm.slogdet
995
+ FreeForm.trace
996
+
997
+ Examples
998
+ --------
999
+
1000
+ .. code-block:: python
1001
+ :emphasize-lines: 1
1002
+
1003
+ >>> from freealg import FreeForm
1004
+ """
1005
+
1006
+ eigs = self.eigvalsh(size=size, **kwargs)
1007
+ return eigs.max() / eigs.min()
1008
+
1009
+ # =====
1010
+ # trace
1011
+ # =====
1012
+
1013
+ def trace(self, size=None, p=1.0, seed=None, **kwargs):
1014
+ """
1015
+ Estimate the trace of a power.
1016
+
1017
+ This function estimates the trace of the matrix power
1018
+ :math:`\\mathbf{A}^p` of the freeform or that of a larger matrix
1019
+ containing it.
1020
+
1021
+ Parameters
1022
+ ----------
1023
+
1024
+ size : int, default=None
1025
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1026
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1027
+ :math:`\\mathbf{A}` itself.
1028
+
1029
+ p : float, default=1.0
1030
+ The exponent :math:`p` in :math:`\\mathbf{A}^p`.
1031
+
1032
+ seed : int, default=None
1033
+ The seed for the Quasi-Monte Carlo sampler.
1034
+
1035
+ **kwargs : dict, optional
1036
+ Pass additional options to the underlying
1037
+ :func:`FreeForm.decompress` function.
1038
+
1039
+ Returns
1040
+ -------
1041
+
1042
+ trace : float
1043
+ matrix trace
1044
+
1045
+ See Also
1046
+ --------
1047
+
1048
+ FreeForm.eigvalsh
1049
+ FreeForm.cond
1050
+ FreeForm.slogdet
1051
+ FreeForm.norm
1052
+
1053
+ Notes
1054
+ -----
1055
+
1056
+ The trace is highly amenable to subsampling: under free decompression
1057
+ the average eigenvalue is assumed constant, so the trace increases
1058
+ linearly. Traces of powers fall back to :func:`eigvalsh`.
1059
+ All arguments to the `.decompress()` procedure can be provided.
1060
+
1061
+ Examples
1062
+ --------
1063
+
1064
+ .. code-block:: python
1065
+ :emphasize-lines: 1
1066
+
1067
+ >>> from freealg import FreeForm
1068
+ """
1069
+
1070
+ if numpy.isclose(p, 1.0):
1071
+ return numpy.mean(self.eig) * (size / self.n)
1072
+
1073
+ eig = self.eigvalsh(size=size, seed=seed, **kwargs)
1074
+ return numpy.sum(eig ** p)
1075
+
1076
+ # =======
1077
+ # slogdet
1078
+ # =======
1079
+
1080
+ def slogdet(self, size=None, seed=None, **kwargs):
1081
+ """
1082
+ Estimate the sign and logarithm of the determinant.
1083
+
1084
+ This function estimates the *slogdet* of the freeform or that of
1085
+ a larger matrix containing it using free decompression.
1086
+
1087
+ Parameters
1088
+ ----------
1089
+
1090
+ size : int, default=None
1091
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1092
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1093
+ :math:`\\mathbf{A}` itself.
1094
+
1095
+ seed : int, default=None
1096
+ The seed for the Quasi-Monte Carlo sampler.
1097
+
1098
+ Returns
1099
+ -------
1100
+
1101
+ sign : float
1102
+ Sign of determinant
1103
+
1104
+ ld : float
1105
+ natural logarithm of the absolute value of the determinant
1106
+
1107
+ See Also
1108
+ --------
1109
+
1110
+ FreeForm.eigvalsh
1111
+ FreeForm.cond
1112
+ FreeForm.trace
1113
+ FreeForm.norm
1114
+
1115
+ Notes
1116
+ -----
1117
+
1118
+ All arguments to the `.decompress()` procedure can be provided.
1119
+
1120
+ Examples
1121
+ --------
1122
+
1123
+ .. code-block:: python
1124
+ :emphasize-lines: 1
1125
+
1126
+ >>> from freealg import FreeForm
1127
+ """
1128
+
1129
+ eigs = self.eigvalsh(size=size, seed=seed, **kwargs)
1130
+ sign = numpy.prod(numpy.sign(eigs))
1131
+ ld = numpy.sum(numpy.log(numpy.abs(eigs)))
1132
+ return sign, ld
1133
+
1134
+ # ====
1135
+ # norm
1136
+ # ====
1137
+
1138
+ def norm(self, size=None, order=2, seed=None, **kwargs):
1139
+ """
1140
+ Estimate the Schatten norm.
1141
+
1142
+ This function estimates the norm of the freeform or a larger
1143
+ matrix containing it using free decompression.
1144
+
1145
+ Parameters
1146
+ ----------
1147
+
1148
+ size : int, default=None
1149
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1150
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1151
+ :math:`\\mathbf{A}` itself.
1152
+
1153
+ order : {float, ``''inf``, ``'-inf'``, ``'fro'``, ``'nuc'``}, default=2
1154
+ Order of the norm.
1155
+
1156
+ * float :math:`p`: Schatten p-norm.
1157
+ * ``'inf'``: Largest absolute eigenvalue
1158
+ :math:`\\max \\vert \\lambda_i \\vert)`
1159
+ * ``'-inf'``: Smallest absolute eigenvalue
1160
+ :math:`\\min \\vert \\lambda_i \\vert)`
1161
+ * ``'fro'``: Frobenius norm corresponding to :math:`p=2`
1162
+ * ``'nuc'``: Nuclear (or trace) norm corresponding to :math:`p=1`
1163
+
1164
+ seed : int, default=None
1165
+ The seed for the Quasi-Monte Carlo sampler.
1166
+
1167
+ **kwargs : dict, optional
1168
+ Pass additional options to the underlying
1169
+ :func:`FreeForm.decompress` function.
1170
+
1171
+ Returns
1172
+ -------
1173
+
1174
+ norm : float
1175
+ matrix norm
1176
+
1177
+ See Also
1178
+ --------
1179
+
1180
+ FreeForm.eigvalsh
1181
+ FreeForm.cond
1182
+ FreeForm.slogdet
1183
+ FreeForm.trace
1184
+
1185
+ Notes
1186
+ -----
1187
+
1188
+ Thes Schatten :math:`p`-norm is defined by
1189
+
1190
+ .. math::
1191
+
1192
+ \\Vert \\mathbf{A} \\Vert_p = \\left(
1193
+ \\sum_{i=1}^N \\vert \\lambda_i \\vert^p \\right)^{1/p}.
1194
+
1195
+ Examples
1196
+ --------
1197
+
1198
+ .. code-block:: python
1199
+ :emphasize-lines: 1
1200
+
1201
+ >>> from freealg import FreeForm
1202
+ """
1203
+
1204
+ eigs = self.eigvalsh(size, seed=seed, **kwargs)
1205
+
1206
+ # Check order type and convert to float
1207
+ if order == 'nuc':
1208
+ order = 1
1209
+ elif order == 'fro':
1210
+ order = 2
1211
+ elif order == 'inf':
1212
+ order = float('inf')
1213
+ elif order == '-inf':
1214
+ order = -float('inf')
1215
+ elif not isinstance(order,
1216
+ (int, float, numpy.integer, numpy.floating)) \
1217
+ and not isinstance(order, (bool, numpy.bool_)):
1218
+ raise ValueError('"order" is invalid.')
1219
+
1220
+ # Compute norm
1221
+ if numpy.isinf(order) and not numpy.isneginf(order):
1222
+ norm_ = max(numpy.abs(eigs))
1223
+
1224
+ elif numpy.isneginf(order):
1225
+ norm_ = min(numpy.abs(eigs))
1226
+
1227
+ elif isinstance(order, (int, float, numpy.integer, numpy.floating)) \
1228
+ and not isinstance(order, (bool, numpy.bool_)):
1229
+ norm_q = numpy.sum(numpy.abs(eigs)**order)
1230
+ norm_ = norm_q**(1.0 / order)
1231
+
1232
+ return norm_