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,1362 @@
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 functools import partial
16
+ from .._util import resolve_complex_dtype, compute_eig
17
+ from ._density_util import kde, force_density
18
+ from ._jacobi import jacobi_sample_proj, jacobi_kernel_proj, jacobi_density, \
19
+ jacobi_stieltjes
20
+ from ._chebyshev import chebyshev_sample_proj, chebyshev_kernel_proj, \
21
+ chebyshev_density, chebyshev_stieltjes
22
+ from ._damp import jackson_damping, lanczos_damping, fejer_damping, \
23
+ exponential_damping, parzen_damping
24
+ from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
25
+ from ._pade import fit_pade, eval_pade
26
+ from ._decompress import decompress
27
+ from ._sample import sample
28
+ from ._support import supp
29
+
30
+ # Fallback to previous numpy API
31
+ if not hasattr(numpy, 'trapezoid'):
32
+ numpy.trapezoid = numpy.trapz
33
+
34
+ __all__ = ['FreeForm']
35
+
36
+
37
+ # =========
38
+ # Free Form
39
+ # =========
40
+
41
+ class FreeForm(object):
42
+ """
43
+ Free probability for large matrices.
44
+
45
+ Parameters
46
+ ----------
47
+
48
+ A : numpy.ndarray
49
+ The 2D symmetric :math:`\\mathbf{A}`. The eigenvalues of this will be
50
+ computed upon calling this class. If a 1D array provided, it is
51
+ assumed to be the eigenvalues of :math:`\\mathbf{A}`.
52
+
53
+ support : tuple, default=None
54
+ The support of the density of :math:`\\mathbf{A}`. If `None`, it is
55
+ estimated from the minimum and maximum of the eigenvalues.
56
+
57
+ delta: float, default=1e-6
58
+ Size of perturbations into the upper half plane for Plemelj's
59
+ formula.
60
+
61
+ dtype : {``'complex128'``, ``'complex256'``}, default = ``'complex128'``
62
+ Data type for inner computations of complex variables:
63
+
64
+ * ``'complex128'``: 128-bit complex numbers, equivalent of two double
65
+ precision floating point.
66
+ * ``'complex256'``: 256-bit complex numbers, equivalent of two long
67
+ double precision floating point. This optino is only available on
68
+ Linux machines.
69
+
70
+ When using series acceleration methods (such as setting
71
+ ``continuation`` in :func:`fit` function to ``wynn-eps``), setting a
72
+ higher precision floating point arithmetics might improve conference.
73
+
74
+ **kwargs : dict, optional
75
+ Parameters for the :func:`supp` function can also be prescribed
76
+ here when ``support=None``.
77
+
78
+ Attributes
79
+ ----------
80
+
81
+ eig : numpy.array
82
+ Eigenvalues of the matrix
83
+
84
+ support: tuple
85
+ The predicted (or given) support :math:`(\\lambda_{\\min},
86
+ \\lambda_{\\max})` of the eigenvalue density.
87
+
88
+ psi : numpy.array
89
+ Jacobi coefficients.
90
+
91
+ n : int
92
+ Initial array size (assuming a square matrix when :math:`\\mathbf{A}` is
93
+ 2D).
94
+
95
+ Methods
96
+ -------
97
+
98
+ fit
99
+ Fit the Jacobi polynomials to the empirical density.
100
+
101
+ density
102
+ Compute the spectral density of the matrix.
103
+
104
+ hilbert
105
+ Compute Hilbert transform of the spectral density
106
+
107
+ stieltjes
108
+ Compute Stieltjes transform of the spectral density
109
+
110
+ decompress
111
+ Free decompression of spectral density
112
+
113
+ eigvalsh
114
+ Estimate the eigenvalues
115
+
116
+ cond
117
+ Estimate the condition number
118
+
119
+ trace
120
+ Estimate the trace of a matrix power
121
+
122
+ slogdet
123
+ Estimate the sign and logarithm of the determinant
124
+
125
+ norm
126
+ Estimate the Schatten norm
127
+
128
+ Examples
129
+ --------
130
+
131
+ .. code-block:: python
132
+
133
+ >>> from freealg import FreeForm
134
+ """
135
+
136
+ # ====
137
+ # init
138
+ # ====
139
+
140
+ def __init__(self, A, support=None, delta=1e-6, dtype='complex128',
141
+ **kwargs):
142
+ """
143
+ Initialization.
144
+ """
145
+
146
+ self.A = None
147
+ self.eig = None
148
+ self.delta = delta # Offset above real axis to apply Plemelj formula
149
+
150
+ # Data type for complex arrays
151
+ self.dtype = resolve_complex_dtype(dtype)
152
+
153
+ # Eigenvalues
154
+ if A.ndim == 1:
155
+ # When A is a 1D array, it is assumed A is the eigenvalue array.
156
+ self.eig = A
157
+ self.n = len(A)
158
+ elif A.ndim == 2:
159
+ # When A is a 2D array, it is assumed A is the actual array,
160
+ # and its eigenvalues will be computed.
161
+ self.A = A
162
+ self.n = A.shape[0]
163
+ assert A.shape[0] == A.shape[1], \
164
+ 'Only square matrices are permitted.'
165
+ self.eig = compute_eig(A)
166
+
167
+ # Support
168
+ if support is None:
169
+ # Detect support
170
+ self.lam_m, self.lam_p = supp(self.eig, **kwargs)
171
+ else:
172
+ self.lam_m = float(support[0])
173
+ self.lam_p = float(support[1])
174
+ self.support = (self.lam_m, self.lam_p)
175
+
176
+ # Number of quadrature points to evaluate Stieltjes using Gauss-Jacobi
177
+ self.n_quad = None
178
+
179
+ # Initialize
180
+ self.method = None # fitting rho: jacobi, chebyshev
181
+ self.continuation = None # analytic continuation: pade, wynn
182
+ self._pade_sol = None # result of pade approximation
183
+ self.psi = None # coefficients of estimating rho
184
+ self.alpha = None # Jacobi polynomials alpha parameter
185
+ self.beta = None # Jacobi polynomials beta parameter
186
+ self.cache = {} # Cache inner-computations
187
+
188
+ # ===
189
+ # fit
190
+ # ===
191
+
192
+ def fit(self, method='jacobi', K=10, alpha=0.0, beta=0.0, n_quad=60,
193
+ reg=0.0, projection='gaussian', kernel_bw=0.001, damp=None,
194
+ force=False, continuation='pade', pade_p=1, pade_q=1,
195
+ odd_side='left', pade_reg=0.0, optimizer='ls', plot=False,
196
+ latex=False, save=False):
197
+ """
198
+ Fit model to eigenvalues.
199
+
200
+ Parameters
201
+ ----------
202
+
203
+ method : {``'jacobi'``, ``'chebyshev'``}, default= ``'jacobi'``
204
+ Method of approximation, either by Jacobi polynomials or Chebyshev
205
+ polynomials of the second kind.
206
+
207
+ K : int, default=10
208
+ Highest polynomial degree
209
+
210
+ alpha : float, default=0.0
211
+ Jacobi parameter :math:`\\alpha`. Determines the slope of the
212
+ fitting model on the right side of interval. This should be greater
213
+ then -1. This option is only applicable when ``method='jacobi'``.
214
+
215
+ beta : float, default=0.0
216
+ Jacobi parameter :math:`\\beta`. Determines the slope of the
217
+ fitting model on the left side of interval. This should be greater
218
+ then -1. This option is only applicable when ``method='jacobi'``.
219
+
220
+ n_quad : int, default=60
221
+ Number of quadrature points to evaluate Stieltjes transform later
222
+ on (when :func:`decompress` is called) using Gauss-Jacob
223
+ quadrature. This option is relevant only if ``method='jacobi'``.
224
+
225
+ reg : float, default=0.0
226
+ Tikhonov regularization coefficient.
227
+
228
+ projection : {``'sample'``, ``'gaussian'``, ``'beta'``}, \
229
+ default= ``'beta'``
230
+ The method of Galerkin projection:
231
+
232
+ * ``'sample'``: directly project samples (eigenvalues) to the
233
+ orthogonal polynomials. This method is highly unstable as it
234
+ treats each sample as a delta Dirac function.
235
+ * ``'gaussian'``: computes Gaussian-Kernel KDE from the samples and
236
+ project a smooth KDE to the orthogonal polynomials. This method
237
+ is stable.
238
+ * ``'beta'``: computes Beta-Kernel KDE from the samples and
239
+ project a smooth KDE to the orthogonal polynomials. This method
240
+ is stable.
241
+
242
+ kernel_bw : float, default=0.001
243
+ Kernel band-wdth. See scipy.stats.gaussian_kde. This argument is
244
+ relevant if ``projection='kernel'`` is set.
245
+
246
+ damp : {``'jackson'``, ``'lanczos'``, ``'fejer``, ``'exponential'``,\
247
+ ``'parzen'``}, default=None
248
+ Damping method to eliminate Gibbs oscillation.
249
+
250
+ force : bool, default=False
251
+ If `True`, it forces the density to have unit mass and to be
252
+ strictly positive.
253
+
254
+ continuation : {``'pade'``, ``'wynn-eps'``, ``'wynn-rho'``, \
255
+ ``'levin'``, ``'weniger'``, ``'brezinski'``}, default= ``'pade'``
256
+ Method of analytic continuation to construct the second branch of
257
+ Steltjes transform in the lower-half complex plane:
258
+
259
+ * ``'pade'``: using Riemann-Hilbert problem with Pade
260
+ approximation.
261
+ * ``'wynn-eps'``: Wynn's :math:`\\epsilon` algorithm.
262
+ * ``'wynn-rho'``: Wynn's :math:`\\rho` algorithm (`experimental`).
263
+ * ``'levin'``: Levin's :math:`u` transform (`experimental`).
264
+ * ``'weniger'``: Weniger's :math:`\\delta^2` algorithm
265
+ (`experimental`).
266
+ * ``'brezinski'``: Brezinski's :math:`\\theta` algorithm
267
+ (`experimental`).
268
+
269
+ pade_p : int, default=1
270
+ Degree of polynomial :math:`P(z)` is :math:`p` where :math:`p` can
271
+ only be ``q-1``, ``q``, or ``q+1``. See notes below. This option
272
+ is applicable if ``continuation='pade'``.
273
+
274
+ pade_q : int, default=1
275
+ Degree of polynomial :math:`Q(z)` is :math:`q` where :math:`q` can
276
+ only be ``p-1``, ``p``, or ``p+1``. See notes below. This option
277
+ is applicable if ``continuation='pade'``.
278
+
279
+ odd_side : {``'left'``, ``'right'``}, default= ``'left'``
280
+ In case of odd number of poles (when :math:`q` is odd), the extra
281
+ pole is set to the left or right side of the support interval,
282
+ while all other poles are split in half to the left and right. Note
283
+ that this is only for the initialization of the poles. The
284
+ optimizer will decide best location by moving them to the left or
285
+ right of the support. This option is applicable if
286
+ ``continuation='pade'``.
287
+
288
+ pade_reg : float, default=0.0
289
+ Regularization for Pade approximation. This option is applicable if
290
+ ``continuation='pade'``.
291
+
292
+ optimizer : {``'ls'``, ``'de'``}, default= ``'ls'``
293
+ Optimizer for Pade approximation, including:
294
+
295
+ * ``'ls'``: least square (local, fast)
296
+ * ``'de'``: differential evolution (global, slow)
297
+
298
+ This option is applicable if ``continuation='pade'``.
299
+
300
+ plot : bool, default=False
301
+ If `True`, the approximation coefficients and Pade approximation to
302
+ the Hilbert transform (if applicable) are plotted.
303
+
304
+ latex : bool, default=False
305
+ If `True`, the plot is rendered using LaTeX. This option is
306
+ relevant only if ``plot=True``.
307
+
308
+ save : bool, default=False
309
+ If not `False`, the plot is saved. If a string is given, it is
310
+ assumed to the save filename (with the file extension). This option
311
+ is relevant only if ``plot=True``.
312
+
313
+ Returns
314
+ -------
315
+
316
+ psi : (K+1, ) numpy.ndarray
317
+ Coefficients of fitting Jacobi polynomials
318
+
319
+ Notes
320
+ -----
321
+
322
+ The Pade approximation for the glue function :math:`G(z)` is
323
+
324
+ .. math::
325
+
326
+ G(z) = \\frac{P(z)}{Q(z)},
327
+
328
+ where :math:`P(z)` and :math:`Q(z)` are polynomials of order
329
+ :math:`p+q` and :math:`q` respectively. Note that :math:`p` can only
330
+ be -1, 0, or 1, effectively making Pade approximation of order
331
+ :math:`q-1:q`, :math:`q:q`, or :math:`q-1:q`.
332
+
333
+ Examples
334
+ --------
335
+
336
+ .. code-block:: python
337
+
338
+ >>> from freealg import FreeForm
339
+ """
340
+
341
+ # Very important: reset cache whenever this function is called. This
342
+ # also empties all references holdign a cache copy.
343
+ self.cache.clear()
344
+
345
+ if alpha <= -1:
346
+ raise ValueError('"alpha" should be greater then "-1".')
347
+
348
+ if beta <= -1:
349
+ raise ValueError('"beta" should be greater then "-1".')
350
+
351
+ if not (method in ['jacobi', 'chebyshev']):
352
+ raise ValueError('"method" is invalid.')
353
+
354
+ if not (projection in ['sample', 'gaussian', 'beta']):
355
+ raise ValueError('"projection" is invalid.')
356
+
357
+ # Project eigenvalues to Jacobi polynomials basis
358
+ if method == 'jacobi':
359
+
360
+ # Set number of Gauss-Jacobi quadratures. This is not used in this
361
+ # function (used later when decompress is called)
362
+ self.n_quad = n_quad
363
+
364
+ if projection == 'sample':
365
+ psi = jacobi_sample_proj(self.eig, support=self.support, K=K,
366
+ alpha=alpha, beta=beta, reg=reg)
367
+ elif projection in ['gaussian', 'beta']:
368
+ # smooth KDE on a fixed grid
369
+ xs = numpy.linspace(self.lam_m, self.lam_p, 2000)
370
+
371
+ pdf = kde(self.eig, xs, self.lam_m, self.lam_p, kernel_bw,
372
+ kernel=projection)
373
+
374
+ psi = jacobi_kernel_proj(xs, pdf, support=self.support, K=K,
375
+ alpha=alpha, beta=beta, reg=reg)
376
+ else:
377
+ raise NotImplementedError('"projection" is invalid.')
378
+
379
+ elif method == 'chebyshev':
380
+
381
+ if projection == 'sample':
382
+ psi = chebyshev_sample_proj(self.eig, support=self.support,
383
+ K=K, reg=reg)
384
+ elif projection in ['gaussian', 'beta']:
385
+ # smooth KDE on a fixed grid
386
+ xs = numpy.linspace(self.lam_m, self.lam_p, 2000)
387
+
388
+ pdf = kde(self.eig, xs, self.lam_m, self.lam_p, kernel_bw,
389
+ kernel=projection)
390
+
391
+ psi = chebyshev_kernel_proj(xs, pdf, support=self.support,
392
+ K=K, reg=reg)
393
+ else:
394
+ raise NotImplementedError('"projection" is invalid.')
395
+
396
+ else:
397
+ raise NotImplementedError('"method" is invalid.')
398
+
399
+ # Damping
400
+ if damp is not None:
401
+ if damp == 'jackson':
402
+ g = jackson_damping(K+1)
403
+ elif damp == 'lanczos':
404
+ g = lanczos_damping(K+1)
405
+ elif damp == 'fejer':
406
+ g = fejer_damping(K+1)
407
+ elif damp == 'exponential':
408
+ g = exponential_damping(K+1)
409
+ elif damp == 'parzen':
410
+ g = parzen_damping(K+1)
411
+
412
+ psi = psi * g
413
+
414
+ if force:
415
+ # A grid to check and enforce positivity and unit mass on it
416
+ grid = numpy.linspace(self.lam_m, self.lam_p, 500)
417
+
418
+ if method == 'jacobi':
419
+ density = partial(jacobi_density, support=self.support,
420
+ alpha=alpha, beta=beta)
421
+ elif method == 'chebyshev':
422
+ density = partial(chebyshev_density, support=self.support)
423
+ else:
424
+ raise RuntimeError('"method" is invalid.')
425
+
426
+ # Enforce positivity, unit mass, and zero at edges
427
+ psi = force_density(psi, support=self.support, density=density,
428
+ grid=grid, alpha=alpha, beta=beta)
429
+
430
+ # Update attributes
431
+ self.method = method
432
+ self.psi = psi
433
+ self.alpha = alpha
434
+ self.beta = beta
435
+
436
+ # Analytic continuation
437
+ if continuation not in ['pade', 'wynn-eps', 'wynn-rho', 'levin',
438
+ 'weniger', 'brezinski']:
439
+ raise NotImplementedError('"continuation" method is invalid.')
440
+
441
+ self.continuation = continuation
442
+
443
+ if self.continuation == 'pade':
444
+
445
+ # For holomorphic continuation for the lower half-plane
446
+ x_supp = numpy.linspace(self.lam_m, self.lam_p, 1000)
447
+ g_supp = 2.0 * numpy.pi * self.hilbert(x_supp)
448
+ self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m, self.lam_p,
449
+ p=pade_p, q=pade_q, odd_side=odd_side,
450
+ pade_reg=pade_reg, safety=1.0,
451
+ max_outer=40, xtol=1e-12, ftol=1e-12,
452
+ optimizer=optimizer, verbose=0)
453
+ else:
454
+ # Do nothing. Make sure _pade_sol is still None
455
+ self._pade_sol = None
456
+
457
+ if plot:
458
+ if self._pade_sol is not None:
459
+ g_supp_approx = eval_pade(x_supp[None, :],
460
+ self._pade_sol)[0, :]
461
+ else:
462
+ x_supp = None
463
+ g_supp = None
464
+ g_supp_approx = None
465
+ plot_fit(psi, x_supp, g_supp, g_supp_approx, support=self.support,
466
+ latex=latex, save=save)
467
+
468
+ return self.psi
469
+
470
+ # =============
471
+ # generate grid
472
+ # =============
473
+
474
+ def _generate_grid(self, scale, extend=1.0, N=500):
475
+ """
476
+ Generate a grid of points to evaluate density / Hilbert / Stieltjes
477
+ transforms.
478
+ """
479
+
480
+ radius = 0.5 * (self.lam_p - self.lam_m)
481
+ center = 0.5 * (self.lam_p + self.lam_m)
482
+
483
+ x_min = numpy.floor(extend * (center - extend * radius * scale))
484
+ x_max = numpy.ceil(extend * (center + extend * radius * scale))
485
+
486
+ x_min /= extend
487
+ x_max /= extend
488
+
489
+ return numpy.linspace(x_min, x_max, N)
490
+
491
+ # =======
492
+ # density
493
+ # =======
494
+
495
+ def density(self, x=None, plot=False, latex=False, save=False):
496
+ """
497
+ Evaluate spectral density.
498
+
499
+ Parameters
500
+ ----------
501
+
502
+ x : numpy.array, default=None
503
+ Positions where density to be evaluated at. If `None`, an interval
504
+ slightly larger than the support interval will be used.
505
+
506
+ plot : bool, default=False
507
+ If `True`, density is plotted.
508
+
509
+ latex : bool, default=False
510
+ If `True`, the plot is rendered using LaTeX. This option is
511
+ relevant only if ``plot=True``.
512
+
513
+ save : bool, default=False
514
+ If not `False`, the plot is saved. If a string is given, it is
515
+ assumed to the save filename (with the file extension). This option
516
+ is relevant only if ``plot=True``.
517
+
518
+ Returns
519
+ -------
520
+
521
+ rho : numpy.array
522
+ Density at locations x.
523
+
524
+ See Also
525
+ --------
526
+ hilbert
527
+ stieltjes
528
+
529
+ Examples
530
+ --------
531
+
532
+ .. code-block:: python
533
+
534
+ >>> from freealg import FreeForm
535
+ """
536
+
537
+ if self.psi is None:
538
+ raise RuntimeError('The model needs to be fit using the .fit() ' +
539
+ 'function.')
540
+
541
+ # Create x if not given
542
+ if x is None:
543
+ x = self._generate_grid(1.25)
544
+
545
+ # Preallocate density to zero
546
+ rho = numpy.zeros_like(x)
547
+
548
+ # Compute density only inside support
549
+ mask = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
550
+
551
+ if self.method == 'jacobi':
552
+ rho[mask] = jacobi_density(x[mask], self.psi, self.support,
553
+ self.alpha, self.beta)
554
+ elif self.method == 'chebyshev':
555
+ rho[mask] = chebyshev_density(x[mask], self.psi, self.support)
556
+ else:
557
+ raise RuntimeError('"method" is invalid.')
558
+
559
+ # Check density is unit mass
560
+ mass = numpy.trapezoid(rho, x)
561
+ if not numpy.isclose(mass, 1.0, atol=1e-2):
562
+ print(f'"rho" is not unit mass. mass: {mass:>0.3f}. Set ' +
563
+ r'"force=True".')
564
+
565
+ # Check density is positive
566
+ min_rho = numpy.min(rho)
567
+ if min_rho < 0.0 - 1e-3:
568
+ print(f'"rho" is not positive. min_rho: {min_rho:>0.3f}. Set ' +
569
+ r'"force=True".')
570
+
571
+ if plot:
572
+ plot_density(x, rho, eig=self.eig, support=self.support,
573
+ label='Estimate', latex=latex, save=save)
574
+
575
+ return rho
576
+
577
+ # =======
578
+ # hilbert
579
+ # =======
580
+
581
+ def hilbert(self, x=None, rho=None, plot=False, latex=False, save=False):
582
+ """
583
+ Compute Hilbert transform of the spectral density.
584
+
585
+ Parameters
586
+ ----------
587
+
588
+ x : numpy.array, default=None
589
+ The locations where Hilbert transform is evaluated at. If `None`,
590
+ an interval slightly larger than the support interval of the
591
+ spectral density is used.
592
+
593
+ rho : numpy.array, default=None
594
+ Density. If `None`, it will be computed.
595
+
596
+ plot : bool, default=False
597
+ If `True`, density is plotted.
598
+
599
+ latex : bool, default=False
600
+ If `True`, the plot is rendered using LaTeX. This option is
601
+ relevant only if ``plot=True``.
602
+
603
+ save : bool, default=False
604
+ If not `False`, the plot is saved. If a string is given, it is
605
+ assumed to the save filename (with the file extension). This option
606
+ is relevant only if ``plot=True``.
607
+
608
+ Returns
609
+ -------
610
+
611
+ hilb : numpy.array
612
+ The Hilbert transform on the locations `x`.
613
+
614
+ See Also
615
+ --------
616
+ density
617
+ stieltjes
618
+
619
+ Examples
620
+ --------
621
+
622
+ .. code-block:: python
623
+
624
+ >>> from freealg import FreeForm
625
+ """
626
+
627
+ if self.psi is None:
628
+ raise RuntimeError('The model needs to be fit using the .fit() ' +
629
+ 'function.')
630
+
631
+ # Create x if not given
632
+ if x is None:
633
+ x = self._generate_grid(1.25)
634
+
635
+ # if (numpy.min(x) > self.lam_m) or (numpy.max(x) < self.lam_p):
636
+ # raise ValueError('"x" does not encompass support interval.')
637
+
638
+ # Preallocate density to zero
639
+ if rho is None:
640
+ rho = self.density(x)
641
+
642
+ # mask of support [lam_m, lam_p]
643
+ mask = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
644
+ x_s = x[mask]
645
+ rho_s = rho[mask]
646
+
647
+ # Form the matrix of integrands: rho_s / (t - x_i)
648
+ # Here, we have diff[i,j] = x[i] - x_s[j]
649
+ diff = x[:, None] - x_s[None, :]
650
+ D = rho_s[None, :] / diff
651
+
652
+ # Principal-value: wherever t == x_i, then diff == 0, zero that entry
653
+ # (numpy.isclose handles floating-point exactly)
654
+ D[numpy.isclose(diff, 0.0)] = 0.0
655
+
656
+ # Integrate each row over t using trapezoid rule on x_s
657
+ # Namely, hilb[i] = int rho_s(t)/(t - x[i]) dt
658
+ hilb = numpy.trapezoid(D, x_s, axis=1) / numpy.pi
659
+
660
+ # We use negative sign convention
661
+ hilb = -hilb
662
+
663
+ if plot:
664
+ plot_hilbert(x, hilb, support=self.support, latex=latex,
665
+ save=save)
666
+
667
+ return hilb
668
+
669
+ # ====
670
+ # glue
671
+ # ====
672
+
673
+ def _glue(self, z):
674
+ """
675
+ Glue function.
676
+
677
+ Notes
678
+ -----
679
+
680
+ This function needs self._pade_sol to be initialized in .fit()
681
+ function. This only works when continuation method is set to "pade".
682
+ """
683
+
684
+ if self._pade_sol is None:
685
+ raise RuntimeError('"_glue" is called but "_pade_sol" is not' +
686
+ 'initialized. This is likely a ' +
687
+ 'development bug.')
688
+
689
+ g = eval_pade(z, self._pade_sol)
690
+
691
+ return g
692
+
693
+ # =========
694
+ # stieltjes
695
+ # =========
696
+
697
+ def stieltjes(self, x=None, y=None, plot=False, latex=False, save=False):
698
+ """
699
+ Compute Stieltjes transform of the spectral density on a grid.
700
+
701
+ This function evaluates Stieltjes transform on an array of points, or
702
+ over a 2D Cartesian grid on the complex plane.
703
+
704
+ Parameters
705
+ ----------
706
+
707
+ x : numpy.array, default=None
708
+ The x axis of the grid where the Stieltjes transform is evaluated.
709
+ If `None`, an interval slightly larger than the support interval of
710
+ the spectral density is used.
711
+
712
+ y : numpy.array, default=None
713
+ The y axis of the grid where the Stieltjes transform is evaluated.
714
+ If `None`, a grid on the interval ``[-1, 1]`` is used.
715
+
716
+ plot : bool, default=False
717
+ If `True`, density is plotted.
718
+
719
+ latex : bool, default=False
720
+ If `True`, the plot is rendered using LaTeX. This option is
721
+ relevant only if ``plot=True``.
722
+
723
+ save : bool, default=False
724
+ If not `False`, the plot is saved. If a string is given, it is
725
+ assumed to the save filename (with the file extension). This option
726
+ is relevant only if ``plot=True``.
727
+
728
+ Returns
729
+ -------
730
+
731
+ m_p : numpy.ndarray
732
+ The Stieltjes transform on the principal branch.
733
+
734
+ m_m : numpy.ndarray
735
+ The Stieltjes transform continued to the secondary branch.
736
+
737
+ See Also
738
+ --------
739
+
740
+ density
741
+ hilbert
742
+
743
+ Examples
744
+ --------
745
+
746
+ .. code-block:: python
747
+
748
+ >>> from freealg import FreeForm
749
+ """
750
+
751
+ if self.psi is None:
752
+ raise RuntimeError('The model needs to be fit using the .fit() ' +
753
+ 'function.')
754
+
755
+ # Create x if not given
756
+ if x is None:
757
+ x = self._generate_grid(2.0, extend=2.0)
758
+
759
+ # Create y if not given
760
+ if (plot is False) and (y is None):
761
+ # Do not use a Cartesian grid. Create a 1D array z slightly above
762
+ # the real line.
763
+ y = self.delta * 1j
764
+ z = x.astype(complex) + y # shape (Nx,)
765
+ else:
766
+ # Use a Cartesian grid
767
+ if y is None:
768
+ y = numpy.linspace(-1, 1, 400)
769
+ x_grid, y_grid = numpy.meshgrid(x.real, y.real)
770
+ z = x_grid + 1j * y_grid # shape (Ny, Nx)
771
+
772
+ m1, m2 = self._eval_stieltjes(z, branches=True)
773
+
774
+ if plot:
775
+ plot_stieltjes(x, y, m1, m2, self.support, latex=latex, save=save)
776
+
777
+ return m1, m2
778
+
779
+ # ==============
780
+ # eval stieltjes
781
+ # ==============
782
+
783
+ def _eval_stieltjes(self, z, branches=False):
784
+ """
785
+ Compute Stieltjes transform of the spectral density.
786
+
787
+ Parameters
788
+ ----------
789
+
790
+ z : numpy.array
791
+ The z values in the complex plan where the Stieltjes transform is
792
+ evaluated.
793
+
794
+ branches : bool, default = False
795
+ Return both the principal and secondary branches of the Stieltjes
796
+ transform. The default ``branches=False`` will return only
797
+ the secondary branch.
798
+
799
+ Returns
800
+ -------
801
+
802
+ m_p : numpy.ndarray
803
+ The Stieltjes transform on the principal branch if
804
+ ``branches=True``.
805
+
806
+ m_m : numpy.ndarray
807
+ The Stieltjes transform continued to the secondary branch.
808
+ """
809
+
810
+ if self.psi is None:
811
+ raise RuntimeError('"fit" the model first.')
812
+
813
+ z = numpy.asarray(z)
814
+
815
+ # Stieltjes function
816
+ if self.method == 'jacobi':
817
+
818
+ # Number of quadrature points
819
+ if z.ndim == 2:
820
+ # set to twice num x points inside support. This oversampling
821
+ # avoids anti-aliasing when visualizing.
822
+ x = z[0, :].real
823
+ mask_sup = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
824
+ n_quad = 2 * numpy.sum(mask_sup)
825
+ else:
826
+ # If this is None, the calling function will handle it.
827
+ n_quad = self.n_quad
828
+
829
+ stieltjes = partial(jacobi_stieltjes, cache=self.cache,
830
+ psi=self.psi, support=self.support,
831
+ alpha=self.alpha, beta=self.beta,
832
+ continuation=self.continuation,
833
+ dtype=self.dtype, n_quad=n_quad)
834
+
835
+ elif self.method == 'chebyshev':
836
+ stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
837
+ support=self.support,
838
+ continuation=self.continuation,
839
+ dtype=self.dtype)
840
+
841
+ # Allow for arbitrary input shapes
842
+ shape = z.shape
843
+ if len(shape) == 0:
844
+ shape = (1,)
845
+ z = z.reshape(-1, 1)
846
+
847
+ mask_p = z.imag >= 0.0
848
+ mask_m = z.imag < 0.0
849
+
850
+ m1 = numpy.zeros_like(z)
851
+ m2 = numpy.zeros_like(z)
852
+
853
+ if self.continuation == 'pade':
854
+ # Upper half-plane
855
+ m1[mask_p] = stieltjes(z[mask_p].reshape(-1, 1)).ravel()
856
+
857
+ # Lower half-plane, use Schwarz reflection
858
+ z_conj = numpy.conjugate(z[mask_m].reshape(-1, 1))
859
+ m1[mask_m] = numpy.conjugate(stieltjes(z_conj)).ravel()
860
+
861
+ # Second Riemann sheet
862
+ m2[mask_p] = m1[mask_p]
863
+ m2[mask_m] = -m1[mask_m] + self._glue(
864
+ z[mask_m].reshape(-1, 1)).ravel()
865
+
866
+ elif self.continuation in ['wynn-eps', 'wynn-rho', 'levin', 'weniger',
867
+ 'brezinski']:
868
+ m2[:] = stieltjes(z.reshape(-1, 1)).reshape(*m2.shape)
869
+ if branches:
870
+ m1[mask_p] = m2[mask_p]
871
+ z_conj = numpy.conjugate(z[mask_m].reshape(-1, 1))
872
+ m1[mask_m] = numpy.conjugate(stieltjes(z_conj)).ravel()
873
+
874
+ else:
875
+ raise NotImplementedError('Invalid continuation method.')
876
+
877
+ if not branches:
878
+ return m2.reshape(*shape)
879
+ else:
880
+ m1 = m1.reshape(*shape)
881
+ m2 = m2.reshape(*shape)
882
+ return m1, m2
883
+
884
+ # ==========
885
+ # decompress
886
+ # ==========
887
+
888
+ def decompress(self, size, x=None, method='newton', max_iter=500,
889
+ step_size=0.1, tolerance=1e-4, plot=False, latex=False,
890
+ save=False, plot_diagnostics=False):
891
+ """
892
+ Free decompression of spectral density.
893
+
894
+ Parameters
895
+ ----------
896
+
897
+ size : int or array_like
898
+ Size(s) of the decompressed matrix. This can be a scalar or an
899
+ array of sizes. For each matrix size in ``size`` array, a density
900
+ is produced.
901
+
902
+ x : numpy.array, default=None
903
+ Positions where density to be evaluated at. If `None`, an interval
904
+ slightly larger than the support interval will be used.
905
+
906
+ method : {``'newton'``, ``'secant'``}, default= ``'newton'``
907
+ Root-finding method.
908
+
909
+ max_iter: int, default=500
910
+ Maximum number of root-finding method iterations.
911
+
912
+ step_size: float, default=0.1
913
+ Step size for Newton iterations.
914
+
915
+ tolerance: float, default=1e-4
916
+ Tolerance for the solution obtained by the Newton solver. Also
917
+ used for the finite difference approximation to the derivative.
918
+
919
+ plot : bool, default=False
920
+ If `True`, density is plotted.
921
+
922
+ latex : bool, default=False
923
+ If `True`, the plot is rendered using LaTeX. This option is
924
+ relevant only if ``plot=True``.
925
+
926
+ save : bool, default=False
927
+ If not `False`, the plot is saved. If a string is given, it is
928
+ assumed to the save filename (with the file extension). This option
929
+ is relevant only if ``plot=True``.
930
+
931
+ plot_diagnostics : bool, default=False
932
+ Plots diagnostics including convergence and number of iterations
933
+ of root finding method.
934
+
935
+ Returns
936
+ -------
937
+
938
+ rho : numpy.array or numpy.ndarray
939
+ Estimated spectral density at locations x. ``rho`` can be a 1D or
940
+ 2D array output:
941
+
942
+ * If ``size`` is a scalar, ``rho`` is a 1D array of the same size
943
+ as ``x``.
944
+ * If ``size`` is an array of size `n`, ``rho`` is a 2D array with
945
+ `n` rows, where each row corresponds to decompression to a size.
946
+ Number of columns of ``rho`` is the same as the size of ``x``.
947
+
948
+ x : numpy.array
949
+ Locations where the spectral density is estimated
950
+
951
+ See Also
952
+ --------
953
+
954
+ density
955
+ stieltjes
956
+
957
+ Examples
958
+ --------
959
+
960
+ .. code-block:: python
961
+
962
+ >>> from freealg import FreeForm
963
+ """
964
+
965
+ # Check size argument
966
+ if numpy.isscalar(size):
967
+ size = int(size)
968
+ else:
969
+ # Check monotonic increment (either all increasing or decreasing)
970
+ diff = numpy.diff(size)
971
+ if not (numpy.all(diff >= 0) or numpy.all(diff <= 0)):
972
+ raise ValueError('"size" increment should be monotonic.')
973
+
974
+ # Decompression ratio equal to e^{t}.
975
+ alpha = numpy.atleast_1d(size) / self.n
976
+
977
+ # Lower and upper bound on new support
978
+ m = self._eval_stieltjes
979
+ hilb_lb = (1.0 / m(self.lam_m + self.delta * 1j).item()).real
980
+ hilb_ub = (1.0 / m(self.lam_p + self.delta * 1j).item()).real
981
+ lb = self.lam_m - (numpy.max(alpha) - 1) * hilb_lb
982
+ ub = self.lam_p - (numpy.max(alpha) - 1) * hilb_ub
983
+
984
+ # Create x if not given
985
+ if x is None:
986
+ radius = 0.5 * (ub - lb)
987
+ center = 0.5 * (ub + lb)
988
+ scale = 1.25
989
+ x_min = numpy.floor(center - radius * scale)
990
+ x_max = numpy.ceil(center + radius * scale)
991
+ x = numpy.linspace(x_min, x_max, 500)
992
+ else:
993
+ x = numpy.asarray(x)
994
+
995
+ if (alpha.size > 8) and plot_diagnostics:
996
+ raise RuntimeError(
997
+ 'Too many diagnostic plots for %d sizes.' % alpha.size)
998
+
999
+ # Decompress to each alpha
1000
+ rho = numpy.zeros((alpha.size, x.size), dtype=float)
1001
+ for i in range(alpha.size):
1002
+
1003
+ # Initial guess for roots (only for the first iteration)
1004
+ # if i == 0:
1005
+ # roots = numpy.full(x.shape, numpy.mean(self.support) - 0.1j,
1006
+ # dtype=self.dtype)
1007
+ roots = None
1008
+
1009
+ rho[i, :], roots = decompress(
1010
+ self, alpha[i], x, roots_init=roots, method=method,
1011
+ delta=self.delta, max_iter=max_iter, step_size=step_size,
1012
+ tolerance=tolerance, plot_diagnostics=plot_diagnostics)
1013
+
1014
+ # If the input size was only a scalar, return a 1D rho, otherwise 2D.
1015
+ if numpy.isscalar(size):
1016
+ rho = numpy.squeeze(rho)
1017
+
1018
+ # Plot only the last size
1019
+ if plot:
1020
+ if numpy.isscalar(size):
1021
+ rho_last = rho
1022
+ else:
1023
+ rho_last = rho[-1, :]
1024
+ plot_density(x, rho_last, support=(lb, ub),
1025
+ label='Decompression', latex=latex, save=save)
1026
+
1027
+ return rho, x
1028
+
1029
+ # ========
1030
+ # eigvalsh
1031
+ # ========
1032
+
1033
+ def eigvalsh(self, size=None, seed=None, **kwargs):
1034
+ """
1035
+ Estimate the eigenvalues.
1036
+
1037
+ This function estimates the eigenvalues of the freeform matrix
1038
+ or a larger matrix containing it using free decompression.
1039
+
1040
+ Parameters
1041
+ ----------
1042
+
1043
+ size : int, default=None
1044
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1045
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1046
+ :math:`\\mathbf{A}` itself.
1047
+
1048
+ seed : int, default=None
1049
+ The seed for the Quasi-Monte Carlo sampler.
1050
+
1051
+ **kwargs : dict, optional
1052
+ Pass additional options to the underlying
1053
+ :func:`FreeForm.decompress` function.
1054
+
1055
+ Returns
1056
+ -------
1057
+
1058
+ eigs : numpy.array
1059
+ Eigenvalues of decompressed matrix
1060
+
1061
+ See Also
1062
+ --------
1063
+
1064
+ FreeForm.decompress
1065
+ FreeForm.cond
1066
+
1067
+ Notes
1068
+ -----
1069
+
1070
+ All arguments to the `.decompress()` procedure can be provided.
1071
+
1072
+ Examples
1073
+ --------
1074
+
1075
+ .. code-block:: python
1076
+ :emphasize-lines: 1
1077
+
1078
+ >>> from freealg import FreeForm
1079
+ """
1080
+
1081
+ if size is None:
1082
+ size = self.n
1083
+
1084
+ rho, x = self.decompress(size, **kwargs)
1085
+ eigs = numpy.sort(sample(x, rho, size, method='qmc', seed=seed))
1086
+
1087
+ return eigs
1088
+
1089
+ # ====
1090
+ # cond
1091
+ # ====
1092
+
1093
+ def cond(self, size=None, seed=None, **kwargs):
1094
+ """
1095
+ Estimate the condition number.
1096
+
1097
+ This function estimates the condition number of the matrix
1098
+ :math:`\\mathbf{A}` or a larger matrix containing :math:`\\mathbf{A}`
1099
+ using free decompression.
1100
+
1101
+ Parameters
1102
+ ----------
1103
+
1104
+ size : int, default=None
1105
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1106
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1107
+ :math:`\\mathbf{A}` itself.
1108
+
1109
+ **kwargs : dict, optional
1110
+ Pass additional options to the underlying
1111
+ :func:`FreeForm.decompress` function.
1112
+
1113
+ Returns
1114
+ -------
1115
+
1116
+ c : float
1117
+ Condition number
1118
+
1119
+ See Also
1120
+ --------
1121
+
1122
+ FreeForm.eigvalsh
1123
+ FreeForm.norm
1124
+ FreeForm.slogdet
1125
+ FreeForm.trace
1126
+
1127
+ Examples
1128
+ --------
1129
+
1130
+ .. code-block:: python
1131
+ :emphasize-lines: 1
1132
+
1133
+ >>> from freealg import FreeForm
1134
+ """
1135
+
1136
+ eigs = self.eigvalsh(size=size, **kwargs)
1137
+ return eigs.max() / eigs.min()
1138
+
1139
+ # =====
1140
+ # trace
1141
+ # =====
1142
+
1143
+ def trace(self, size=None, p=1.0, seed=None, **kwargs):
1144
+ """
1145
+ Estimate the trace of a power.
1146
+
1147
+ This function estimates the trace of the matrix power
1148
+ :math:`\\mathbf{A}^p` of the freeform or that of a larger matrix
1149
+ containing it.
1150
+
1151
+ Parameters
1152
+ ----------
1153
+
1154
+ size : int, default=None
1155
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1156
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1157
+ :math:`\\mathbf{A}` itself.
1158
+
1159
+ p : float, default=1.0
1160
+ The exponent :math:`p` in :math:`\\mathbf{A}^p`.
1161
+
1162
+ seed : int, default=None
1163
+ The seed for the Quasi-Monte Carlo sampler.
1164
+
1165
+ **kwargs : dict, optional
1166
+ Pass additional options to the underlying
1167
+ :func:`FreeForm.decompress` function.
1168
+
1169
+ Returns
1170
+ -------
1171
+
1172
+ trace : float
1173
+ matrix trace
1174
+
1175
+ See Also
1176
+ --------
1177
+
1178
+ FreeForm.eigvalsh
1179
+ FreeForm.cond
1180
+ FreeForm.slogdet
1181
+ FreeForm.norm
1182
+
1183
+ Notes
1184
+ -----
1185
+
1186
+ The trace is highly amenable to subsampling: under free decompression
1187
+ the average eigenvalue is assumed constant, so the trace increases
1188
+ linearly. Traces of powers fall back to :func:`eigvalsh`.
1189
+ All arguments to the `.decompress()` procedure can be provided.
1190
+
1191
+ Examples
1192
+ --------
1193
+
1194
+ .. code-block:: python
1195
+ :emphasize-lines: 1
1196
+
1197
+ >>> from freealg import FreeForm
1198
+ """
1199
+
1200
+ if numpy.isclose(p, 1.0):
1201
+ return numpy.mean(self.eig) * (size / self.n)
1202
+
1203
+ eig = self.eigvalsh(size=size, seed=seed, **kwargs)
1204
+ return numpy.sum(eig ** p)
1205
+
1206
+ # =======
1207
+ # slogdet
1208
+ # =======
1209
+
1210
+ def slogdet(self, size=None, seed=None, **kwargs):
1211
+ """
1212
+ Estimate the sign and logarithm of the determinant.
1213
+
1214
+ This function estimates the *slogdet* of the freeform or that of
1215
+ a larger matrix containing it using free decompression.
1216
+
1217
+ Parameters
1218
+ ----------
1219
+
1220
+ size : int, default=None
1221
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1222
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1223
+ :math:`\\mathbf{A}` itself.
1224
+
1225
+ seed : int, default=None
1226
+ The seed for the Quasi-Monte Carlo sampler.
1227
+
1228
+ Returns
1229
+ -------
1230
+
1231
+ sign : float
1232
+ Sign of determinant
1233
+
1234
+ ld : float
1235
+ natural logarithm of the absolute value of the determinant
1236
+
1237
+ See Also
1238
+ --------
1239
+
1240
+ FreeForm.eigvalsh
1241
+ FreeForm.cond
1242
+ FreeForm.trace
1243
+ FreeForm.norm
1244
+
1245
+ Notes
1246
+ -----
1247
+
1248
+ All arguments to the `.decompress()` procedure can be provided.
1249
+
1250
+ Examples
1251
+ --------
1252
+
1253
+ .. code-block:: python
1254
+ :emphasize-lines: 1
1255
+
1256
+ >>> from freealg import FreeForm
1257
+ """
1258
+
1259
+ eigs = self.eigvalsh(size=size, seed=seed, **kwargs)
1260
+ sign = numpy.prod(numpy.sign(eigs))
1261
+ ld = numpy.sum(numpy.log(numpy.abs(eigs)))
1262
+ return sign, ld
1263
+
1264
+ # ====
1265
+ # norm
1266
+ # ====
1267
+
1268
+ def norm(self, size=None, order=2, seed=None, **kwargs):
1269
+ """
1270
+ Estimate the Schatten norm.
1271
+
1272
+ This function estimates the norm of the freeform or a larger
1273
+ matrix containing it using free decompression.
1274
+
1275
+ Parameters
1276
+ ----------
1277
+
1278
+ size : int, default=None
1279
+ The size of the matrix containing :math:`\\mathbf{A}` to estimate
1280
+ eigenvalues of. If None, returns estimates of the eigenvalues of
1281
+ :math:`\\mathbf{A}` itself.
1282
+
1283
+ order : {float, ``''inf``, ``'-inf'``, ``'fro'``, ``'nuc'``}, default=2
1284
+ Order of the norm.
1285
+
1286
+ * float :math:`p`: Schatten p-norm.
1287
+ * ``'inf'``: Largest absolute eigenvalue
1288
+ :math:`\\max \\vert \\lambda_i \\vert)`
1289
+ * ``'-inf'``: Smallest absolute eigenvalue
1290
+ :math:`\\min \\vert \\lambda_i \\vert)`
1291
+ * ``'fro'``: Frobenius norm corresponding to :math:`p=2`
1292
+ * ``'nuc'``: Nuclear (or trace) norm corresponding to :math:`p=1`
1293
+
1294
+ seed : int, default=None
1295
+ The seed for the Quasi-Monte Carlo sampler.
1296
+
1297
+ **kwargs : dict, optional
1298
+ Pass additional options to the underlying
1299
+ :func:`FreeForm.decompress` function.
1300
+
1301
+ Returns
1302
+ -------
1303
+
1304
+ norm : float
1305
+ matrix norm
1306
+
1307
+ See Also
1308
+ --------
1309
+
1310
+ FreeForm.eigvalsh
1311
+ FreeForm.cond
1312
+ FreeForm.slogdet
1313
+ FreeForm.trace
1314
+
1315
+ Notes
1316
+ -----
1317
+
1318
+ Thes Schatten :math:`p`-norm is defined by
1319
+
1320
+ .. math::
1321
+
1322
+ \\Vert \\mathbf{A} \\Vert_p = \\left(
1323
+ \\sum_{i=1}^N \\vert \\lambda_i \\vert^p \\right)^{1/p}.
1324
+
1325
+ Examples
1326
+ --------
1327
+
1328
+ .. code-block:: python
1329
+ :emphasize-lines: 1
1330
+
1331
+ >>> from freealg import FreeForm
1332
+ """
1333
+
1334
+ eigs = self.eigvalsh(size, seed=seed, **kwargs)
1335
+
1336
+ # Check order type and convert to float
1337
+ if order == 'nuc':
1338
+ order = 1
1339
+ elif order == 'fro':
1340
+ order = 2
1341
+ elif order == 'inf':
1342
+ order = float('inf')
1343
+ elif order == '-inf':
1344
+ order = -float('inf')
1345
+ elif not isinstance(order,
1346
+ (int, float, numpy.integer, numpy.floating)) \
1347
+ and not isinstance(order, (bool, numpy.bool_)):
1348
+ raise ValueError('"order" is invalid.')
1349
+
1350
+ # Compute norm
1351
+ if numpy.isinf(order) and not numpy.isneginf(order):
1352
+ norm_ = max(numpy.abs(eigs))
1353
+
1354
+ elif numpy.isneginf(order):
1355
+ norm_ = min(numpy.abs(eigs))
1356
+
1357
+ elif isinstance(order, (int, float, numpy.integer, numpy.floating)) \
1358
+ and not isinstance(order, (bool, numpy.bool_)):
1359
+ norm_q = numpy.sum(numpy.abs(eigs)**order)
1360
+ norm_ = norm_q**(1.0 / order)
1361
+
1362
+ return norm_