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.
- freealg/__init__.py +8 -2
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/__init__.py +12 -0
- freealg/_algebraic_form/_branch_points.py +288 -0
- freealg/_algebraic_form/_constraints.py +139 -0
- freealg/_algebraic_form/_continuation_algebraic.py +706 -0
- freealg/_algebraic_form/_decompress.py +641 -0
- freealg/_algebraic_form/_decompress2.py +204 -0
- freealg/_algebraic_form/_edge.py +330 -0
- freealg/_algebraic_form/_homotopy.py +323 -0
- freealg/_algebraic_form/_moments.py +448 -0
- freealg/_algebraic_form/_sheets_util.py +145 -0
- freealg/_algebraic_form/_support.py +309 -0
- freealg/_algebraic_form/algebraic_form.py +1232 -0
- freealg/_free_form/__init__.py +16 -0
- freealg/{_chebyshev.py → _free_form/_chebyshev.py} +75 -43
- freealg/_free_form/_decompress.py +993 -0
- freealg/_free_form/_density_util.py +243 -0
- freealg/_free_form/_jacobi.py +359 -0
- freealg/_free_form/_linalg.py +508 -0
- freealg/{_pade.py → _free_form/_pade.py} +42 -208
- freealg/{_plot_util.py → _free_form/_plot_util.py} +37 -22
- freealg/{_sample.py → _free_form/_sample.py} +58 -22
- freealg/_free_form/_series.py +454 -0
- freealg/_free_form/_support.py +214 -0
- freealg/_free_form/free_form.py +1362 -0
- freealg/_geometric_form/__init__.py +13 -0
- freealg/_geometric_form/_continuation_genus0.py +175 -0
- freealg/_geometric_form/_continuation_genus1.py +275 -0
- freealg/_geometric_form/_elliptic_functions.py +174 -0
- freealg/_geometric_form/_sphere_maps.py +63 -0
- freealg/_geometric_form/_torus_maps.py +118 -0
- freealg/_geometric_form/geometric_form.py +1094 -0
- freealg/_util.py +56 -110
- freealg/distributions/__init__.py +7 -1
- freealg/distributions/_chiral_block.py +494 -0
- freealg/distributions/_deformed_marchenko_pastur.py +726 -0
- freealg/distributions/_deformed_wigner.py +386 -0
- freealg/distributions/_kesten_mckay.py +29 -15
- freealg/distributions/_marchenko_pastur.py +224 -95
- freealg/distributions/_meixner.py +47 -37
- freealg/distributions/_wachter.py +29 -17
- freealg/distributions/_wigner.py +27 -14
- freealg/visualization/__init__.py +12 -0
- freealg/visualization/_glue_util.py +32 -0
- freealg/visualization/_rgb_hsv.py +125 -0
- freealg-0.7.12.dist-info/METADATA +172 -0
- freealg-0.7.12.dist-info/RECORD +53 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/WHEEL +1 -1
- freealg/_decompress.py +0 -180
- freealg/_jacobi.py +0 -218
- freealg/_support.py +0 -85
- freealg/freeform.py +0 -967
- freealg-0.1.11.dist-info/METADATA +0 -140
- freealg-0.1.11.dist-info/RECORD +0 -24
- /freealg/{_damp.py → _free_form/_damp.py} +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/LICENSE.txt +0 -0
- {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_
|