freealg 0.0.3__py3-none-any.whl → 0.1.0__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/__version__.py +1 -1
- freealg/_pade.py +330 -6
- freealg/_plot_util.py +6 -28
- freealg/_sample.py +85 -0
- freealg/distributions/__init__.py +4 -4
- freealg/distributions/kesten_mckay.py +559 -0
- freealg/distributions/marchenko_pastur.py +4 -3
- freealg/distributions/wachter.py +568 -0
- freealg/distributions/wigner.py +552 -0
- freealg/freeform.py +58 -27
- {freealg-0.0.3.dist-info → freealg-0.1.0.dist-info}/METADATA +2 -1
- freealg-0.1.0.dist-info/RECORD +21 -0
- freealg-0.0.3.dist-info/RECORD +0 -17
- {freealg-0.0.3.dist-info → freealg-0.1.0.dist-info}/WHEEL +0 -0
- {freealg-0.0.3.dist-info → freealg-0.1.0.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.0.3.dist-info → freealg-0.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,552 @@
|
|
|
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
|
|
6
|
+
# under the terms of the license found in the LICENSE.txt file in the root
|
|
7
|
+
# directory of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
import networkx as nx
|
|
16
|
+
from scipy.interpolate import interp1d
|
|
17
|
+
from .._plot_util import plot_density, plot_hilbert, plot_stieltjes, \
|
|
18
|
+
plot_stieltjes_on_disk, plot_samples
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from scipy.integrate import cumtrapz
|
|
22
|
+
except ImportError:
|
|
23
|
+
from scipy.integrate import cumulative_trapezoid as cumtrapz
|
|
24
|
+
|
|
25
|
+
__all__ = ['Wigner']
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ======
|
|
29
|
+
# Wigner
|
|
30
|
+
# ======
|
|
31
|
+
|
|
32
|
+
class Wigner(object):
|
|
33
|
+
"""
|
|
34
|
+
Wigner semicircle distribution.
|
|
35
|
+
|
|
36
|
+
Methods
|
|
37
|
+
-------
|
|
38
|
+
|
|
39
|
+
density
|
|
40
|
+
Spectral density of distribution.
|
|
41
|
+
|
|
42
|
+
hilbert
|
|
43
|
+
Hilbert transform of distribution.
|
|
44
|
+
|
|
45
|
+
stieltjes
|
|
46
|
+
Stieltjes transform of distribution.
|
|
47
|
+
|
|
48
|
+
sample
|
|
49
|
+
Sample from distribution.
|
|
50
|
+
|
|
51
|
+
matrix
|
|
52
|
+
Generate matrix with its empirical spectral density of distribution
|
|
53
|
+
|
|
54
|
+
Notes
|
|
55
|
+
-----
|
|
56
|
+
|
|
57
|
+
The Marchenko-Pastur distribution has the absolutely-continuous density
|
|
58
|
+
|
|
59
|
+
.. math::
|
|
60
|
+
|
|
61
|
+
\\mathrm{d} \\rho(x) = \\frac{1}{2 \\pi}
|
|
62
|
+
\\sqrt{(4 - x^2}
|
|
63
|
+
\\mathbf{1}_{x \\in [\\lambda_{-}, \\lambda_{+}]} \\mathrm{d}{x}
|
|
64
|
+
|
|
65
|
+
with :math:`\\lambda_{\\pm} = \\pm 2` being the edges of the support
|
|
66
|
+
|
|
67
|
+
References
|
|
68
|
+
----------
|
|
69
|
+
|
|
70
|
+
.. [1] Wigner, E. P. (1955). Characteristic vectors of bordered matrices
|
|
71
|
+
with infinite dimensions. Annals of Mathematics, 62(3), 548-564.421
|
|
72
|
+
|
|
73
|
+
Examples
|
|
74
|
+
--------
|
|
75
|
+
|
|
76
|
+
.. code-block:: python
|
|
77
|
+
|
|
78
|
+
>>> from freealg.distributions import Wigner
|
|
79
|
+
>>> wg = Wigner()
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# ====
|
|
83
|
+
# init
|
|
84
|
+
# ====
|
|
85
|
+
|
|
86
|
+
def __init__(self):
|
|
87
|
+
"""
|
|
88
|
+
Initialization.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
self.lam_p = 2.0
|
|
92
|
+
self.lam_m = -2.0
|
|
93
|
+
self.support = (self.lam_m, self.lam_p)
|
|
94
|
+
|
|
95
|
+
# =======
|
|
96
|
+
# density
|
|
97
|
+
# =======
|
|
98
|
+
|
|
99
|
+
def density(self, x=None, plot=False, latex=False, save=False):
|
|
100
|
+
"""
|
|
101
|
+
Density of distribution.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
|
|
106
|
+
x : numpy.array, default=None
|
|
107
|
+
The locations where density is evaluated at. If `None`, an interval
|
|
108
|
+
slightly larger than the support interval of the spectral density
|
|
109
|
+
is used.
|
|
110
|
+
|
|
111
|
+
rho : numpy.array, default=None
|
|
112
|
+
Density. If `None`, it will be computed.
|
|
113
|
+
|
|
114
|
+
plot : bool, default=False
|
|
115
|
+
If `True`, density is plotted.
|
|
116
|
+
|
|
117
|
+
latex : bool, default=False
|
|
118
|
+
If `True`, the plot is rendered using LaTeX. This option is
|
|
119
|
+
relevant only if ``plot=True``.
|
|
120
|
+
|
|
121
|
+
save : bool, default=False
|
|
122
|
+
If not `False`, the plot is saved. If a string is given, it is
|
|
123
|
+
assumed to the save filename (with the file extension). This option
|
|
124
|
+
is relevant only if ``plot=True``.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
|
|
129
|
+
rho : numpy.array
|
|
130
|
+
Density.
|
|
131
|
+
|
|
132
|
+
Examples
|
|
133
|
+
--------
|
|
134
|
+
|
|
135
|
+
.. code-block::python
|
|
136
|
+
|
|
137
|
+
>>> from freealg.distributions import Wigner
|
|
138
|
+
>>> wg = Wigner()
|
|
139
|
+
>>> rho = wg.density(plot=True)
|
|
140
|
+
|
|
141
|
+
.. image:: ../_static/images/plots/wg_density.png
|
|
142
|
+
:align: center
|
|
143
|
+
:class: custom-dark
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
# Create x if not given
|
|
147
|
+
if x is None:
|
|
148
|
+
radius = 0.5 * (self.lam_p - self.lam_m)
|
|
149
|
+
center = 0.5 * (self.lam_p + self.lam_m)
|
|
150
|
+
scale = 1.25
|
|
151
|
+
x_min = numpy.floor(center - radius * scale)
|
|
152
|
+
x_max = numpy.ceil(center + radius * scale)
|
|
153
|
+
x = numpy.linspace(x_min, x_max, 500)
|
|
154
|
+
|
|
155
|
+
rho = numpy.zeros_like(x)
|
|
156
|
+
mask = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
|
|
157
|
+
|
|
158
|
+
rho[mask] = (1.0 / (2.0 * numpy.pi)) * \
|
|
159
|
+
numpy.sqrt(4.0 - x[mask]**2)
|
|
160
|
+
|
|
161
|
+
if plot:
|
|
162
|
+
plot_density(x, rho, label='', latex=latex, save=save)
|
|
163
|
+
|
|
164
|
+
return rho
|
|
165
|
+
|
|
166
|
+
# =======
|
|
167
|
+
# hilbert
|
|
168
|
+
# =======
|
|
169
|
+
|
|
170
|
+
def hilbert(self, x=None, plot=False, latex=False, save=False):
|
|
171
|
+
"""
|
|
172
|
+
Hilbert transform of the distribution.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
|
|
177
|
+
x : numpy.array, default=None
|
|
178
|
+
The locations where Hilbert transform is evaluated at. If `None`,
|
|
179
|
+
an interval slightly larger than the support interval of the
|
|
180
|
+
spectral density is used.
|
|
181
|
+
|
|
182
|
+
plot : bool, default=False
|
|
183
|
+
If `True`, Hilbert transform is plotted.
|
|
184
|
+
|
|
185
|
+
latex : bool, default=False
|
|
186
|
+
If `True`, the plot is rendered using LaTeX. This option is
|
|
187
|
+
relevant only if ``plot=True``.
|
|
188
|
+
|
|
189
|
+
save : bool, default=False
|
|
190
|
+
If not `False`, the plot is saved. If a string is given, it is
|
|
191
|
+
assumed to the save filename (with the file extension). This option
|
|
192
|
+
is relevant only if ``plot=True``.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
|
|
197
|
+
hilb : numpy.array
|
|
198
|
+
Hilbert transform.
|
|
199
|
+
|
|
200
|
+
Examples
|
|
201
|
+
--------
|
|
202
|
+
|
|
203
|
+
.. code-block::python
|
|
204
|
+
|
|
205
|
+
>>> from freealg.distributions import Wigner
|
|
206
|
+
>>> wg = Wigner()
|
|
207
|
+
>>> hilb = wg.hilbert(plot=True)
|
|
208
|
+
|
|
209
|
+
.. image:: ../_static/images/plots/wg_hilbert.png
|
|
210
|
+
:align: center
|
|
211
|
+
:class: custom-dark
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
# Create x if not given
|
|
215
|
+
if x is None:
|
|
216
|
+
radius = 0.5 * (self.lam_p - self.lam_m)
|
|
217
|
+
center = 0.5 * (self.lam_p + self.lam_m)
|
|
218
|
+
scale = 1.25
|
|
219
|
+
x_min = numpy.floor(center - radius * scale)
|
|
220
|
+
x_max = numpy.ceil(center + radius * scale)
|
|
221
|
+
x = numpy.linspace(x_min, x_max, 500)
|
|
222
|
+
|
|
223
|
+
def _P(x):
|
|
224
|
+
return x
|
|
225
|
+
|
|
226
|
+
def _Q(x):
|
|
227
|
+
return 1.0
|
|
228
|
+
|
|
229
|
+
P = _P(x)
|
|
230
|
+
Q = _Q(x)
|
|
231
|
+
Delta2 = P**2 - 4.0 * Q
|
|
232
|
+
Delta = numpy.sqrt(numpy.maximum(Delta2, 0))
|
|
233
|
+
sign = numpy.sign(P)
|
|
234
|
+
hilb = (P - sign * Delta) / (2.0 * Q)
|
|
235
|
+
|
|
236
|
+
# using negative sign convention
|
|
237
|
+
hilb = -hilb
|
|
238
|
+
|
|
239
|
+
if plot:
|
|
240
|
+
plot_hilbert(x, hilb, support=self.support, latex=latex, save=save)
|
|
241
|
+
|
|
242
|
+
return hilb
|
|
243
|
+
|
|
244
|
+
# =======================
|
|
245
|
+
# m mp numeric vectorized
|
|
246
|
+
# =======================
|
|
247
|
+
|
|
248
|
+
def _m_mp_numeric_vectorized(self, z, alt_branch=False, tol=1e-8):
|
|
249
|
+
"""
|
|
250
|
+
Stieltjes transform (principal or secondary branch)
|
|
251
|
+
for Marchenko–Pastur distribution on upper half-plane.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
m = numpy.empty_like(z, dtype=complex)
|
|
255
|
+
|
|
256
|
+
# Use quadratic form
|
|
257
|
+
sign = -1 if alt_branch else 1
|
|
258
|
+
A = 1.0
|
|
259
|
+
B = z
|
|
260
|
+
D = B**2 - 4 * A
|
|
261
|
+
sqrtD = numpy.sqrt(D)
|
|
262
|
+
m1 = (-B + sqrtD) / (2 * A)
|
|
263
|
+
m2 = (-B - sqrtD) / (2 * A)
|
|
264
|
+
|
|
265
|
+
# pick correct branch
|
|
266
|
+
upper = z.imag >= 0
|
|
267
|
+
branch = numpy.empty_like(m1)
|
|
268
|
+
branch[upper] = numpy.where(sign*m1[upper].imag > 0, m1[upper],
|
|
269
|
+
m2[upper])
|
|
270
|
+
branch[~upper] = numpy.where(sign*m1[~upper].imag < 0, m1[~upper],
|
|
271
|
+
m2[~upper])
|
|
272
|
+
m = branch
|
|
273
|
+
|
|
274
|
+
return m
|
|
275
|
+
|
|
276
|
+
# ============
|
|
277
|
+
# m mp reflect
|
|
278
|
+
# ============
|
|
279
|
+
|
|
280
|
+
def _m_mp_reflect(self, z, alt_branch=False):
|
|
281
|
+
"""
|
|
282
|
+
Analytic continuation using Schwarz reflection.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
mask_p = z.imag >= 0.0
|
|
286
|
+
mask_n = z.imag < 0.0
|
|
287
|
+
|
|
288
|
+
m = numpy.zeros_like(z)
|
|
289
|
+
|
|
290
|
+
f = self._m_mp_numeric_vectorized
|
|
291
|
+
m[mask_p] = f(z[mask_p], alt_branch=False)
|
|
292
|
+
m[mask_n] = f(z[mask_n], alt_branch=alt_branch)
|
|
293
|
+
|
|
294
|
+
return m
|
|
295
|
+
|
|
296
|
+
# =========
|
|
297
|
+
# stieltjes
|
|
298
|
+
# =========
|
|
299
|
+
|
|
300
|
+
def stieltjes(self, x=None, y=None, plot=False, on_disk=False, latex=False,
|
|
301
|
+
save=False):
|
|
302
|
+
"""
|
|
303
|
+
Stieltjes transform of distribution.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
|
|
308
|
+
x : numpy.array, default=None
|
|
309
|
+
The x axis of the grid where the Stieltjes transform is evaluated.
|
|
310
|
+
If `None`, an interval slightly larger than the support interval of
|
|
311
|
+
the spectral density is used.
|
|
312
|
+
|
|
313
|
+
y : numpy.array, default=None
|
|
314
|
+
The y axis of the grid where the Stieltjes transform is evaluated.
|
|
315
|
+
If `None`, a grid on the interval ``[-1, 1]`` is used.
|
|
316
|
+
|
|
317
|
+
plot : bool, default=False
|
|
318
|
+
If `True`, Stieltjes transform is plotted.
|
|
319
|
+
|
|
320
|
+
on_disk : bool, default=False
|
|
321
|
+
If `True`, the Stieltjes transform is mapped on unit disk. This
|
|
322
|
+
option relevant only if ``plot=True``.
|
|
323
|
+
|
|
324
|
+
latex : bool, default=False
|
|
325
|
+
If `True`, the plot is rendered using LaTeX. This option is
|
|
326
|
+
relevant only if ``plot=True``.
|
|
327
|
+
|
|
328
|
+
save : bool, default=False
|
|
329
|
+
If not `False`, the plot is saved. If a string is given, it is
|
|
330
|
+
assumed to the save filename (with the file extension). This option
|
|
331
|
+
is relevant only if ``plot=True``.
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
|
|
336
|
+
m1 : numpy.array
|
|
337
|
+
Stieltjes transform on principal branch.
|
|
338
|
+
|
|
339
|
+
m12 : numpy.array
|
|
340
|
+
Stieltjes transform on secondary branch.
|
|
341
|
+
|
|
342
|
+
Examples
|
|
343
|
+
--------
|
|
344
|
+
|
|
345
|
+
.. code-block:: python
|
|
346
|
+
|
|
347
|
+
>>> from freealg.distributions import Wigner
|
|
348
|
+
>>> wg = Wigner()
|
|
349
|
+
>>> m1, m2 = wg.stieltjes(plot=True)
|
|
350
|
+
|
|
351
|
+
.. image:: ../_static/images/plots/wg_stieltjes.png
|
|
352
|
+
:align: center
|
|
353
|
+
:class: custom-dark
|
|
354
|
+
|
|
355
|
+
Plot on unit disk using Cayley transform:
|
|
356
|
+
|
|
357
|
+
.. code-block:: python
|
|
358
|
+
|
|
359
|
+
>>> m1, m2 = mp.stieltjes(plot=True, on_disk=True)
|
|
360
|
+
|
|
361
|
+
.. image:: ../_static/images/plots/wg_stieltjes_disk.png
|
|
362
|
+
:align: center
|
|
363
|
+
:class: custom-dark
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
if (plot is True) and (on_disk is True):
|
|
367
|
+
n_r = 1000
|
|
368
|
+
n_t = 1000
|
|
369
|
+
r_min, r_max = 0, 2.5
|
|
370
|
+
t_min, t_max = 0, 2.0 * numpy.pi
|
|
371
|
+
r = numpy.linspace(r_min, r_max, n_r)
|
|
372
|
+
t = numpy.linspace(t_min, t_max, n_t + 1)[:-1]
|
|
373
|
+
grid_r, grid_t = numpy.meshgrid(r, t)
|
|
374
|
+
|
|
375
|
+
grid_x_D = grid_r * numpy.cos(grid_t)
|
|
376
|
+
grid_y_D = grid_r * numpy.sin(grid_t)
|
|
377
|
+
zeta = grid_x_D + 1j * grid_y_D
|
|
378
|
+
|
|
379
|
+
# Cayley transform mapping zeta on D to z on H
|
|
380
|
+
z_H = 1j * (1 + zeta) / (1 - zeta)
|
|
381
|
+
|
|
382
|
+
m1_D = self._m_mp_reflect(z_H, alt_branch=False)
|
|
383
|
+
m2_D = self._m_mp_reflect(z_H, alt_branch=True)
|
|
384
|
+
|
|
385
|
+
plot_stieltjes_on_disk(r, t, m1_D, m2_D, support=self.support,
|
|
386
|
+
latex=latex, save=save)
|
|
387
|
+
|
|
388
|
+
return m1_D, m2_D
|
|
389
|
+
|
|
390
|
+
# Create x if not given
|
|
391
|
+
if x is None:
|
|
392
|
+
radius = 0.5 * (self.lam_p - self.lam_m)
|
|
393
|
+
center = 0.5 * (self.lam_p + self.lam_m)
|
|
394
|
+
scale = 2.0
|
|
395
|
+
x_min = numpy.floor(2.0 * (center - 2.0 * radius * scale)) / 2.0
|
|
396
|
+
x_max = numpy.ceil(2.0 * (center + 2.0 * radius * scale)) / 2.0
|
|
397
|
+
x = numpy.linspace(x_min, x_max, 500)
|
|
398
|
+
|
|
399
|
+
# Create y if not given
|
|
400
|
+
if y is None:
|
|
401
|
+
y = numpy.linspace(-1, 1, 400)
|
|
402
|
+
|
|
403
|
+
x_grid, y_grid = numpy.meshgrid(x, y)
|
|
404
|
+
z = x_grid + 1j * y_grid # shape (Ny, Nx)
|
|
405
|
+
|
|
406
|
+
m1 = self._m_mp_reflect(z, alt_branch=False)
|
|
407
|
+
m2 = self._m_mp_reflect(z, alt_branch=True)
|
|
408
|
+
|
|
409
|
+
if plot:
|
|
410
|
+
plot_stieltjes(x, y, m1, m2, support=self.support, latex=latex,
|
|
411
|
+
save=save)
|
|
412
|
+
|
|
413
|
+
return m1, m2
|
|
414
|
+
|
|
415
|
+
# ======
|
|
416
|
+
# sample
|
|
417
|
+
# ======
|
|
418
|
+
|
|
419
|
+
def sample(self, size, x_min=None, x_max=None, plot=False, latex=False,
|
|
420
|
+
save=False):
|
|
421
|
+
"""
|
|
422
|
+
Sample from distribution.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
|
|
427
|
+
size : int
|
|
428
|
+
Size of sample.
|
|
429
|
+
|
|
430
|
+
x_min : float, default=None
|
|
431
|
+
Minimum of sample values. If `None`, the left edge of the support
|
|
432
|
+
is used.
|
|
433
|
+
|
|
434
|
+
x_max : float, default=None
|
|
435
|
+
Maximum of sample values. If `None`, the right edge of the support
|
|
436
|
+
is used.
|
|
437
|
+
|
|
438
|
+
plot : bool, default=False
|
|
439
|
+
If `True`, samples histogram is plotted.
|
|
440
|
+
|
|
441
|
+
latex : bool, default=False
|
|
442
|
+
If `True`, the plot is rendered using LaTeX. This option is
|
|
443
|
+
relevant only if ``plot=True``.
|
|
444
|
+
|
|
445
|
+
save : bool, default=False
|
|
446
|
+
If not `False`, the plot is saved. If a string is given, it is
|
|
447
|
+
assumed to the save filename (with the file extension). This option
|
|
448
|
+
is relevant only if ``plot=True``.
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
|
|
453
|
+
s : numpy.ndarray
|
|
454
|
+
Samples.
|
|
455
|
+
|
|
456
|
+
Notes
|
|
457
|
+
-----
|
|
458
|
+
|
|
459
|
+
This method uses inverse transform sampling.
|
|
460
|
+
|
|
461
|
+
Examples
|
|
462
|
+
--------
|
|
463
|
+
|
|
464
|
+
.. code-block::python
|
|
465
|
+
|
|
466
|
+
>>> from freealg.distributions import Wigner
|
|
467
|
+
>>> wg = Wigner()
|
|
468
|
+
>>> s = wg.sample(2000)
|
|
469
|
+
|
|
470
|
+
.. image:: ../_static/images/plots/wg_samples.png
|
|
471
|
+
:align: center
|
|
472
|
+
:class: custom-dark
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
if x_min is None:
|
|
476
|
+
x_min = self.lam_m
|
|
477
|
+
|
|
478
|
+
if x_max is None:
|
|
479
|
+
x_max = self.lam_p
|
|
480
|
+
|
|
481
|
+
# Grid and PDF
|
|
482
|
+
xs = numpy.linspace(x_min, x_max, size)
|
|
483
|
+
pdf = self.density(xs)
|
|
484
|
+
|
|
485
|
+
# CDF (using cumulative trapezoidal rule)
|
|
486
|
+
cdf = cumtrapz(pdf, xs, initial=0)
|
|
487
|
+
cdf /= cdf[-1] # normalize CDF to 1
|
|
488
|
+
|
|
489
|
+
# Inverse CDF interpolator
|
|
490
|
+
inv_cdf = interp1d(cdf, xs, bounds_error=False,
|
|
491
|
+
fill_value=(x_min, x_max))
|
|
492
|
+
|
|
493
|
+
# Sample and map
|
|
494
|
+
u = numpy.random.rand(size)
|
|
495
|
+
samples = inv_cdf(u)
|
|
496
|
+
|
|
497
|
+
if plot:
|
|
498
|
+
radius = 0.5 * (self.lam_p - self.lam_m)
|
|
499
|
+
center = 0.5 * (self.lam_p + self.lam_m)
|
|
500
|
+
scale = 1.25
|
|
501
|
+
x_min = numpy.floor(center - radius * scale)
|
|
502
|
+
x_max = numpy.ceil(center + radius * scale)
|
|
503
|
+
x = numpy.linspace(x_min, x_max, 500)
|
|
504
|
+
rho = self.density(x)
|
|
505
|
+
plot_samples(x, rho, x_min, x_max, samples, latex=latex, save=save)
|
|
506
|
+
|
|
507
|
+
return samples
|
|
508
|
+
|
|
509
|
+
# ======
|
|
510
|
+
# matrix
|
|
511
|
+
# ======
|
|
512
|
+
|
|
513
|
+
def matrix(self, size):
|
|
514
|
+
"""
|
|
515
|
+
Generate matrix with the spectral density of the distribution.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
|
|
520
|
+
size : int
|
|
521
|
+
Size :math:`n` of the matrix.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
|
|
526
|
+
A : numpy.ndarray
|
|
527
|
+
A matrix of the size :math:`n \\times n`.
|
|
528
|
+
|
|
529
|
+
Examples
|
|
530
|
+
--------
|
|
531
|
+
|
|
532
|
+
.. code-block::python
|
|
533
|
+
|
|
534
|
+
>>> from freealg.distributions import Wigner
|
|
535
|
+
>>> wg = Wigner()
|
|
536
|
+
>>> A = wg.matrix(2000)
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
# Parameters
|
|
540
|
+
n = size
|
|
541
|
+
p = 1.0 / size
|
|
542
|
+
|
|
543
|
+
# Random graph
|
|
544
|
+
G = nx.erdos_renyi_graph(n, p)
|
|
545
|
+
|
|
546
|
+
# Adjancency
|
|
547
|
+
A = nx.to_numpy_array(G) # shape (n,n), 0/1 entries
|
|
548
|
+
|
|
549
|
+
# Center & scale to get the semicircle
|
|
550
|
+
A_c = (A - p) / numpy.sqrt(n * p * (1-p))
|
|
551
|
+
|
|
552
|
+
return A_c
|
freealg/freeform.py
CHANGED
|
@@ -22,6 +22,7 @@ from ._damp import jackson_damping, lanczos_damping, fejer_damping, \
|
|
|
22
22
|
from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
|
|
23
23
|
from ._pade import fit_pade, eval_pade
|
|
24
24
|
from ._decompress import decompress
|
|
25
|
+
from ._sample import qmc_sample
|
|
25
26
|
|
|
26
27
|
__all__ = ['FreeForm']
|
|
27
28
|
|
|
@@ -173,8 +174,8 @@ class FreeForm(object):
|
|
|
173
174
|
# ===
|
|
174
175
|
|
|
175
176
|
def fit(self, method='jacobi', K=10, alpha=0.0, beta=0.0, reg=0.0,
|
|
176
|
-
damp=None, force=False, pade_p=
|
|
177
|
-
latex=False, save=False):
|
|
177
|
+
damp=None, force=False, pade_p=0, pade_q=1, odd_side='left',
|
|
178
|
+
optimizer='ls', plot=False, latex=False, save=False):
|
|
178
179
|
"""
|
|
179
180
|
Fit model to eigenvalues.
|
|
180
181
|
|
|
@@ -209,15 +210,30 @@ class FreeForm(object):
|
|
|
209
210
|
If `True`, it forces the density to have unit mass and to be
|
|
210
211
|
strictly positive.
|
|
211
212
|
|
|
212
|
-
pade_p : int, default=
|
|
213
|
-
Degree of polynomial :math:`P(z)
|
|
213
|
+
pade_p : int, default=0
|
|
214
|
+
Degree of polynomial :math:`P(z)` is :math:`q+p` where :math:`p`
|
|
215
|
+
can only be ``-1``, ``0``, or ``1``. See notes below.
|
|
214
216
|
|
|
215
217
|
pade_q : int, default=1
|
|
216
|
-
Degree of polynomial :math:`Q(z)`. See notes below.
|
|
218
|
+
Degree of polynomial :math:`Q(z)` is :math:`q`. See notes below.
|
|
219
|
+
|
|
220
|
+
odd_side : {``'left'``, ``'right'``}, default= ``'left'``
|
|
221
|
+
In case of odd number of poles (when :math:`q` is odd), the extra
|
|
222
|
+
pole is set to the left or right side of the support interval,
|
|
223
|
+
while all other poles are split in half to the left and right. Note
|
|
224
|
+
that this is only for the initialization of the poles. The
|
|
225
|
+
optimizer will decide best location by moving them to the left or
|
|
226
|
+
right of the support.
|
|
227
|
+
|
|
228
|
+
optimizer : {``'ls'``, ``'de'``}, default= ``'ls'``
|
|
229
|
+
Optimizer for Pade approximation, including:
|
|
230
|
+
|
|
231
|
+
* ``'ls'``: least square (local, fast)
|
|
232
|
+
* ``'de'``: differential evolution (global, slow)
|
|
217
233
|
|
|
218
234
|
plot : bool, default=False
|
|
219
|
-
If `True`, the approximation coefficients and
|
|
220
|
-
the Hilbert
|
|
235
|
+
If `True`, the approximation coefficients and Pade approximation to
|
|
236
|
+
the Hilbert transform are plotted.
|
|
221
237
|
|
|
222
238
|
latex : bool, default=False
|
|
223
239
|
If `True`, the plot is rendered using LaTeX. This option is
|
|
@@ -234,6 +250,20 @@ class FreeForm(object):
|
|
|
234
250
|
psi : (K+1, ) numpy.ndarray
|
|
235
251
|
Coefficients of fitting Jacobi polynomials
|
|
236
252
|
|
|
253
|
+
Notes
|
|
254
|
+
-----
|
|
255
|
+
|
|
256
|
+
The Pade approximation for the glue function :math:`G(z)` is
|
|
257
|
+
|
|
258
|
+
.. math::
|
|
259
|
+
|
|
260
|
+
G(z) = \\frac{P(z)}{Q(z)},
|
|
261
|
+
|
|
262
|
+
where :math:`P(z)` and :math:`Q(z)` are polynomials of order
|
|
263
|
+
:math:`p+q` and :math:`q` respectively. Note that :math:`p` can only
|
|
264
|
+
be -1, 0, or 1, effectively making Pade approximation of order
|
|
265
|
+
:math:`q-1:q`, :math:`q:q`, or :math:`q-1:q`.
|
|
266
|
+
|
|
237
267
|
Examples
|
|
238
268
|
--------
|
|
239
269
|
|
|
@@ -299,17 +329,16 @@ class FreeForm(object):
|
|
|
299
329
|
g_supp = 2.0 * numpy.pi * self.hilbert(x_supp)
|
|
300
330
|
|
|
301
331
|
# Fit a pade approximation
|
|
302
|
-
self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m,
|
|
303
|
-
|
|
304
|
-
|
|
332
|
+
# self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m,
|
|
333
|
+
# self.lam_p, pade_p, pade_q, delta=1e-8,
|
|
334
|
+
# B=numpy.inf, S=numpy.inf)
|
|
335
|
+
self._pade_sol = fit_pade(x_supp, g_supp, self.lam_m, self.lam_p,
|
|
336
|
+
p=pade_p, q=pade_q, odd_side=odd_side,
|
|
337
|
+
safety=1.0, max_outer=40, xtol=1e-12,
|
|
338
|
+
ftol=1e-12, optimizer=optimizer, verbose=0)
|
|
305
339
|
|
|
306
340
|
if plot:
|
|
307
|
-
|
|
308
|
-
s = self._pade_sol['s']
|
|
309
|
-
a = self._pade_sol['a']
|
|
310
|
-
b = self._pade_sol['b']
|
|
311
|
-
|
|
312
|
-
g_supp_approx = eval_pade(x_supp[None, :], s, a, b)[0, :]
|
|
341
|
+
g_supp_approx = eval_pade(x_supp[None, :], self._pade_sol)[0, :]
|
|
313
342
|
plot_fit(psi, x_supp, g_supp, g_supp_approx, support=self.support,
|
|
314
343
|
latex=latex, save=save)
|
|
315
344
|
|
|
@@ -513,13 +542,8 @@ class FreeForm(object):
|
|
|
513
542
|
"""
|
|
514
543
|
"""
|
|
515
544
|
|
|
516
|
-
# Unpack optimized parameters
|
|
517
|
-
s = self._pade_sol['s']
|
|
518
|
-
a = self._pade_sol['a']
|
|
519
|
-
b = self._pade_sol['b']
|
|
520
|
-
|
|
521
545
|
# Glue function
|
|
522
|
-
g = eval_pade(z,
|
|
546
|
+
g = eval_pade(z, self._pade_sol)
|
|
523
547
|
|
|
524
548
|
return g
|
|
525
549
|
|
|
@@ -633,7 +657,7 @@ class FreeForm(object):
|
|
|
633
657
|
m1[mask_m, :] = numpy.conjugate(
|
|
634
658
|
stieltjes(numpy.conjugate(z[mask_m, :])))
|
|
635
659
|
|
|
636
|
-
# Second
|
|
660
|
+
# Second Riemann sheet
|
|
637
661
|
m2[mask_p, :] = m1[mask_p, :]
|
|
638
662
|
m2[mask_m, :] = -m1[mask_m, :] + self._glue(z[mask_m, :])
|
|
639
663
|
|
|
@@ -721,7 +745,7 @@ class FreeForm(object):
|
|
|
721
745
|
m1[mask_m] = numpy.conjugate(
|
|
722
746
|
stieltjes(numpy.conjugate(z[mask_m].reshape(-1, 1)))).reshape(-1)
|
|
723
747
|
|
|
724
|
-
# Second
|
|
748
|
+
# Second Riemann sheet
|
|
725
749
|
m2[mask_p] = m1[mask_p]
|
|
726
750
|
m2[mask_m] = -m1[mask_m] + self._glue(
|
|
727
751
|
z[mask_m].reshape(-1, 1)).reshape(-1)
|
|
@@ -734,7 +758,7 @@ class FreeForm(object):
|
|
|
734
758
|
# decompress
|
|
735
759
|
# ==========
|
|
736
760
|
|
|
737
|
-
def decompress(self, size, x=None, delta=1e-
|
|
761
|
+
def decompress(self, size, x=None, delta=1e-6, iterations=500,
|
|
738
762
|
step_size=0.1, tolerance=1e-4, plot=False, latex=False,
|
|
739
763
|
save=False):
|
|
740
764
|
"""
|
|
@@ -782,6 +806,10 @@ class FreeForm(object):
|
|
|
782
806
|
rho : numpy.array
|
|
783
807
|
Spectral density
|
|
784
808
|
|
|
809
|
+
eigs : numpy.array
|
|
810
|
+
Estimated eigenvalues as low-discrepancy samples of the estimated
|
|
811
|
+
spectral density.
|
|
812
|
+
|
|
785
813
|
See Also
|
|
786
814
|
--------
|
|
787
815
|
|
|
@@ -809,9 +837,12 @@ class FreeForm(object):
|
|
|
809
837
|
rho, x, (lb, ub) = decompress(self, size, x=x, delta=delta,
|
|
810
838
|
iterations=iterations,
|
|
811
839
|
step_size=step_size, tolerance=tolerance)
|
|
840
|
+
x, rho = x.ravel(), rho.ravel()
|
|
812
841
|
|
|
813
842
|
if plot:
|
|
814
|
-
plot_density(x
|
|
843
|
+
plot_density(x, rho, support=(lb, ub),
|
|
815
844
|
label='Decompression', latex=latex, save=save)
|
|
816
845
|
|
|
817
|
-
|
|
846
|
+
eigs = numpy.sort(qmc_sample(x, rho, size))
|
|
847
|
+
|
|
848
|
+
return rho, eigs
|