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