freealg 0.6.3__py3-none-any.whl → 0.7.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/__init__.py +8 -7
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/__init__.py +11 -0
- freealg/_algebraic_form/_continuation_algebraic.py +503 -0
- freealg/_algebraic_form/_decompress.py +648 -0
- freealg/_algebraic_form/_edge.py +352 -0
- freealg/_algebraic_form/_sheets_util.py +145 -0
- freealg/_algebraic_form/algebraic_form.py +987 -0
- freealg/_freeform/__init__.py +16 -0
- freealg/_freeform/_density_util.py +243 -0
- freealg/{_linalg.py → _freeform/_linalg.py} +1 -1
- freealg/{freeform.py → _freeform/freeform.py} +2 -1
- 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 +1 -228
- freealg/distributions/__init__.py +5 -1
- freealg/distributions/_chiral_block.py +440 -0
- freealg/distributions/_deformed_marchenko_pastur.py +617 -0
- freealg/distributions/_deformed_wigner.py +312 -0
- freealg/distributions/_marchenko_pastur.py +197 -80
- freealg/visualization/__init__.py +12 -0
- freealg/visualization/_glue_util.py +32 -0
- freealg/visualization/_rgb_hsv.py +125 -0
- {freealg-0.6.3.dist-info → freealg-0.7.0.dist-info}/METADATA +1 -1
- freealg-0.7.0.dist-info/RECORD +47 -0
- freealg-0.6.3.dist-info/RECORD +0 -26
- /freealg/{_chebyshev.py → _freeform/_chebyshev.py} +0 -0
- /freealg/{_damp.py → _freeform/_damp.py} +0 -0
- /freealg/{_decompress.py → _freeform/_decompress.py} +0 -0
- /freealg/{_jacobi.py → _freeform/_jacobi.py} +0 -0
- /freealg/{_pade.py → _freeform/_pade.py} +0 -0
- /freealg/{_plot_util.py → _freeform/_plot_util.py} +0 -0
- /freealg/{_sample.py → _freeform/_sample.py} +0 -0
- /freealg/{_series.py → _freeform/_series.py} +0 -0
- /freealg/{_support.py → _freeform/_support.py} +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.0.dist-info}/WHEEL +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.0.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.0.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.6.3.dist-info → freealg-0.7.0.dist-info}/top_level.txt +0 -0
freealg/_util.py
CHANGED
|
@@ -13,19 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import numpy
|
|
15
15
|
import scipy
|
|
16
|
-
from scipy.stats import gaussian_kde
|
|
17
|
-
from scipy.stats import beta
|
|
18
|
-
# from statsmodels.nonparametric.kde import KDEUnivariate
|
|
19
|
-
from scipy.optimize import minimize
|
|
20
|
-
import matplotlib.pyplot as plt
|
|
21
|
-
import texplot
|
|
22
|
-
from ._plot_util import _auto_bins
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
if not hasattr(numpy, 'trapezoid'):
|
|
26
|
-
numpy.trapezoid = numpy.trapz
|
|
27
|
-
|
|
28
|
-
__all__ = ['resolve_complex_dtype', 'compute_eig', 'kde', 'force_density']
|
|
17
|
+
__all__ = ['resolve_complex_dtype', 'compute_eig']
|
|
29
18
|
|
|
30
19
|
|
|
31
20
|
# =====================
|
|
@@ -81,219 +70,3 @@ def compute_eig(A, lower=False):
|
|
|
81
70
|
eig = scipy.linalg.eigvalsh(A, lower=lower, driver='ev')
|
|
82
71
|
|
|
83
72
|
return eig
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# ===
|
|
87
|
-
# kde
|
|
88
|
-
# ===
|
|
89
|
-
|
|
90
|
-
def kde(eig, xs, lam_m, lam_p, h, kernel='beta', plot=False):
|
|
91
|
-
"""
|
|
92
|
-
Kernel density estimation of eigenvalues.
|
|
93
|
-
|
|
94
|
-
Parameters
|
|
95
|
-
----------
|
|
96
|
-
|
|
97
|
-
eig : numpy.array
|
|
98
|
-
1D array of samples of size `n`.
|
|
99
|
-
|
|
100
|
-
xs : numpy.array
|
|
101
|
-
1D array of evaluation grid (must lie within ``[lam_m, lam_p]``)
|
|
102
|
-
|
|
103
|
-
lam_m : float
|
|
104
|
-
Lower end of the support endpoints with ``lam_m < lam_p``.
|
|
105
|
-
|
|
106
|
-
lam_p : float
|
|
107
|
-
Upper end of the support endpoints with ``lam_m < lam_p``.
|
|
108
|
-
|
|
109
|
-
h : float
|
|
110
|
-
Kernel bandwidth in rescaled units where ``0 < h < 1``.
|
|
111
|
-
|
|
112
|
-
kernel : {``'gaussian'``, ``'beta'``}, default= ``'beta'``
|
|
113
|
-
Kernel function using either Gaussian or Beta distribution.
|
|
114
|
-
|
|
115
|
-
plot : bool, default=False
|
|
116
|
-
If `True`, the KDE is plotted.
|
|
117
|
-
|
|
118
|
-
Returns
|
|
119
|
-
-------
|
|
120
|
-
|
|
121
|
-
pdf : numpy.ndarray
|
|
122
|
-
Probability distribution function with the same length as ``xs``.
|
|
123
|
-
|
|
124
|
-
See Also
|
|
125
|
-
--------
|
|
126
|
-
|
|
127
|
-
freealg.supp
|
|
128
|
-
freealg.sample
|
|
129
|
-
|
|
130
|
-
References
|
|
131
|
-
----------
|
|
132
|
-
|
|
133
|
-
.. [1] `R-package documentation for Beta kernel
|
|
134
|
-
<https://search.r-project.org/CRAN/refmans/DELTD/html/Beta.html>`__
|
|
135
|
-
|
|
136
|
-
.. [2] Chen, S. X. (1999). Beta Kernel estimators for density functions.
|
|
137
|
-
*Computational Statistics and Data Analysis* 31 p. 131--145.
|
|
138
|
-
|
|
139
|
-
Notes
|
|
140
|
-
-----
|
|
141
|
-
|
|
142
|
-
In Beta kernel density estimation, the shape parameters :math:`a` and
|
|
143
|
-
:math:`b` of the :math:`\\mathrm{Beta}(a, b)` distribution are computed
|
|
144
|
-
for each data point :math:`u` as:
|
|
145
|
-
|
|
146
|
-
.. math::
|
|
147
|
-
|
|
148
|
-
a = (u / h) + 1.0
|
|
149
|
-
b = ((1.0 - u) / h) + 1.0
|
|
150
|
-
|
|
151
|
-
This is a standard way of using Beta kernel (see R-package documentation
|
|
152
|
-
[1]_). These equations are derived from *moment matching* method, where
|
|
153
|
-
|
|
154
|
-
.. math::
|
|
155
|
-
|
|
156
|
-
\\mathrm{Mean}(\\mathrm{Beta}(a,b)) = u
|
|
157
|
-
\\mathrm{Var}(\\mathrm{Beta}(a,b)) = (1-u) u h
|
|
158
|
-
|
|
159
|
-
Solving these two equations for :math:`a` and :math:`b` yields the
|
|
160
|
-
relations above. See [2]_ (page 134).
|
|
161
|
-
"""
|
|
162
|
-
|
|
163
|
-
if kernel == 'gaussian':
|
|
164
|
-
pdf = gaussian_kde(eig, bw_method=h)(xs)
|
|
165
|
-
|
|
166
|
-
# Adaptive KDE
|
|
167
|
-
# k = KDEUnivariate(eig)
|
|
168
|
-
# k.fit(kernel='gau', bw='silverman', fft=False, weights=None,
|
|
169
|
-
# gridsize=1024, adaptive=True)
|
|
170
|
-
# pdf = k.evaluate(xs)
|
|
171
|
-
|
|
172
|
-
elif kernel == 'beta':
|
|
173
|
-
|
|
174
|
-
span = lam_p - lam_m
|
|
175
|
-
if span <= 0:
|
|
176
|
-
raise ValueError('"lam_p" must be larger than "lam_m".')
|
|
177
|
-
|
|
178
|
-
# map samples and grid to [0, 1]
|
|
179
|
-
u = (eig - lam_m) / span
|
|
180
|
-
t = (xs - lam_m) / span
|
|
181
|
-
|
|
182
|
-
# keep only samples strictly inside (0,1)
|
|
183
|
-
if (u.min() < 0) or (u.max() > 1):
|
|
184
|
-
u = u[(u > 0) & (u < 1)]
|
|
185
|
-
|
|
186
|
-
n = u.size
|
|
187
|
-
if n == 0:
|
|
188
|
-
return numpy.zeros_like(xs, dtype=float)
|
|
189
|
-
|
|
190
|
-
# Shape parameters "a" and "b" or the kernel Beta(a, b), which is
|
|
191
|
-
# computed for each data point "u" (see notes above). These are
|
|
192
|
-
# vectorized.
|
|
193
|
-
a = (u / h) + 1.0
|
|
194
|
-
b = ((1.0 - u) / h) + 1.0
|
|
195
|
-
|
|
196
|
-
# # tiny positive number to keep shape parameters > 0
|
|
197
|
-
eps = 1e-6
|
|
198
|
-
a = numpy.clip(a, eps, None)
|
|
199
|
-
b = numpy.clip(b, eps, None)
|
|
200
|
-
|
|
201
|
-
# Beta kernel
|
|
202
|
-
pdf_matrix = beta.pdf(t[None, :], a[:, None], b[:, None])
|
|
203
|
-
|
|
204
|
-
# Average and re-normalize back to x variable
|
|
205
|
-
pdf = pdf_matrix.sum(axis=0) / (n * span)
|
|
206
|
-
|
|
207
|
-
# Exact zeros outside [lam_m, lam_p]
|
|
208
|
-
pdf[(t < 0) | (t > 1)] = 0.0
|
|
209
|
-
|
|
210
|
-
else:
|
|
211
|
-
raise NotImplementedError('"kernel" is invalid.')
|
|
212
|
-
|
|
213
|
-
if plot:
|
|
214
|
-
with texplot.theme(use_latex=False):
|
|
215
|
-
fig, ax = plt.subplots(figsize=(6, 4))
|
|
216
|
-
|
|
217
|
-
x_min = numpy.min(xs)
|
|
218
|
-
x_max = numpy.max(xs)
|
|
219
|
-
bins = numpy.linspace(x_min, x_max, _auto_bins(eig))
|
|
220
|
-
_ = ax.hist(eig, bins, density=True, color='silver',
|
|
221
|
-
edgecolor='none', label='Samples histogram')
|
|
222
|
-
ax.plot(xs, pdf, color='black', label='KDE')
|
|
223
|
-
ax.set_xlabel(r'$x$')
|
|
224
|
-
ax.set_ylabel(r'$\\rho(x)$')
|
|
225
|
-
ax.set_xlim([xs[0], xs[-1]])
|
|
226
|
-
ax.set_ylim(bottom=0)
|
|
227
|
-
ax.set_title('Kernel Density Estimation')
|
|
228
|
-
ax.legend(fontsize='x-small')
|
|
229
|
-
plt.show()
|
|
230
|
-
|
|
231
|
-
return pdf
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# =============
|
|
235
|
-
# force density
|
|
236
|
-
# =============
|
|
237
|
-
|
|
238
|
-
def force_density(psi0, support, density, grid, alpha=0.0, beta=0.0):
|
|
239
|
-
"""
|
|
240
|
-
Starting from psi0 (raw projection), solve
|
|
241
|
-
min 0.5 ||psi - psi0||^2
|
|
242
|
-
s.t. F_pos psi >= 0 (positivity on grid)
|
|
243
|
-
psi[0] = psi0[0] (mass)
|
|
244
|
-
f(lam_m) psi = 0 (zero at left edge)
|
|
245
|
-
f(lam_p) psi = 0 (zero at right edge)
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
lam_m, lam_p = support
|
|
249
|
-
|
|
250
|
-
# Objective and gradient
|
|
251
|
-
def fun(psi):
|
|
252
|
-
return 0.5 * numpy.dot(psi-psi0, psi-psi0)
|
|
253
|
-
|
|
254
|
-
def grad(psi):
|
|
255
|
-
return psi - psi0
|
|
256
|
-
|
|
257
|
-
# Constraints:
|
|
258
|
-
constraints = []
|
|
259
|
-
|
|
260
|
-
# Enforce positivity
|
|
261
|
-
constraints.append({'type': 'ineq',
|
|
262
|
-
'fun': lambda psi: density(grid, psi)})
|
|
263
|
-
|
|
264
|
-
# Enforce unit mass
|
|
265
|
-
constraints.append({
|
|
266
|
-
'type': 'eq',
|
|
267
|
-
'fun': lambda psi: numpy.trapz(density(grid, psi), grid) - 1.0
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
# Enforce zero at left edge
|
|
271
|
-
if beta <= 0.0 and beta > -0.5:
|
|
272
|
-
constraints.append({
|
|
273
|
-
'type': 'eq',
|
|
274
|
-
'fun': lambda psi: density(numpy.array([lam_m]), psi)[0]
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
# Enforce zero at right edge
|
|
278
|
-
if alpha <= 0.0 and alpha > -0.5:
|
|
279
|
-
constraints.append({
|
|
280
|
-
'type': 'eq',
|
|
281
|
-
'fun': lambda psi: density(numpy.array([lam_p]), psi)[0]
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
# Solve a small quadratic programming
|
|
285
|
-
res = minimize(fun, psi0, jac=grad,
|
|
286
|
-
constraints=constraints,
|
|
287
|
-
# method='trust-constr',
|
|
288
|
-
method='SLSQP',
|
|
289
|
-
options={'maxiter': 1000, 'ftol': 1e-9, 'eps': 1e-8})
|
|
290
|
-
|
|
291
|
-
psi = res.x
|
|
292
|
-
|
|
293
|
-
# Normalize first mode to unit mass
|
|
294
|
-
x = numpy.linspace(lam_m, lam_p, 1000)
|
|
295
|
-
rho = density(x, psi)
|
|
296
|
-
mass = numpy.trapezoid(rho, x)
|
|
297
|
-
psi[0] = psi[0] / mass
|
|
298
|
-
|
|
299
|
-
return psi
|
|
@@ -11,5 +11,9 @@ from ._wigner import Wigner
|
|
|
11
11
|
from ._kesten_mckay import KestenMcKay
|
|
12
12
|
from ._wachter import Wachter
|
|
13
13
|
from ._meixner import Meixner
|
|
14
|
+
from ._chiral_block import ChiralBlock
|
|
15
|
+
from ._deformed_wigner import DeformedWigner
|
|
16
|
+
from ._deformed_marchenko_pastur import DeformedMarchenkoPastur
|
|
14
17
|
|
|
15
|
-
__all__ = ['MarchenkoPastur', 'Wigner', 'KestenMcKay', 'Wachter', 'Meixner'
|
|
18
|
+
__all__ = ['MarchenkoPastur', 'Wigner', 'KestenMcKay', 'Wachter', 'Meixner',
|
|
19
|
+
'ChiralBlock', 'DeformedWigner', 'DeformedMarchenkoPastur']
|
|
@@ -0,0 +1,440 @@
|
|
|
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
|
|
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
|
+
# Import
|
|
12
|
+
# ======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
import collections
|
|
16
|
+
from .._geometric_form._elliptic_functions import ellipj
|
|
17
|
+
from .._geometric_form._continuation_genus1 import mobius_z
|
|
18
|
+
|
|
19
|
+
__all__ = ['ChiralBlock']
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ============
|
|
23
|
+
# Chiral Block
|
|
24
|
+
# ============
|
|
25
|
+
|
|
26
|
+
class ChiralBlock(object):
|
|
27
|
+
"""
|
|
28
|
+
Twisted chiral block model.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
|
|
33
|
+
alpha : float
|
|
34
|
+
beta : float
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# ====
|
|
38
|
+
# init
|
|
39
|
+
# ====
|
|
40
|
+
|
|
41
|
+
def __init__(self, alpha, beta, c):
|
|
42
|
+
"""
|
|
43
|
+
Initialization.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
self.alpha = alpha
|
|
47
|
+
self.beta = beta
|
|
48
|
+
self.c = c
|
|
49
|
+
|
|
50
|
+
# =======
|
|
51
|
+
# density
|
|
52
|
+
# =======
|
|
53
|
+
|
|
54
|
+
def density(self, x):
|
|
55
|
+
"""
|
|
56
|
+
Absolutely continous density, and the atom.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# Parameters
|
|
60
|
+
alpha = self.alpha
|
|
61
|
+
beta = self.beta
|
|
62
|
+
c = self.c
|
|
63
|
+
|
|
64
|
+
t = (x - alpha) * (x - beta)
|
|
65
|
+
|
|
66
|
+
Delta_t = t * t - 2.0 * (c + 1.0) * t + (c - 1.0) * (c - 1.0)
|
|
67
|
+
s = numpy.sqrt(numpy.maximum(0.0, -Delta_t))
|
|
68
|
+
|
|
69
|
+
sgn = numpy.sign(x - alpha)
|
|
70
|
+
sgn = numpy.where(sgn == 0.0, 1.0, sgn)
|
|
71
|
+
sd = 1j * s * sgn
|
|
72
|
+
|
|
73
|
+
A = t + (c - 1.0)
|
|
74
|
+
|
|
75
|
+
xa = x - alpha
|
|
76
|
+
xa_safe = numpy.where(xa == 0.0, numpy.nan, xa)
|
|
77
|
+
|
|
78
|
+
u = (-A + sd) / (2.0 * c * xa_safe)
|
|
79
|
+
den = (t - c + 1.0) + sd
|
|
80
|
+
v = -2.0 * xa_safe / den
|
|
81
|
+
|
|
82
|
+
m = (c / (1.0 + c)) * u + (1.0 / (1.0 + c)) * v
|
|
83
|
+
rho = m.imag / numpy.pi
|
|
84
|
+
|
|
85
|
+
rho = numpy.where(Delta_t < 0.0, rho, 0.0)
|
|
86
|
+
rho = numpy.where(numpy.isfinite(rho), rho, 0.0)
|
|
87
|
+
rho = numpy.maximum(rho, 0.0)
|
|
88
|
+
|
|
89
|
+
# Atom location and weight
|
|
90
|
+
if numpy.abs(c - 1.0) < 1e-4:
|
|
91
|
+
atom_loc = None
|
|
92
|
+
atom_w = None
|
|
93
|
+
elif c > 1.0:
|
|
94
|
+
atom_loc = alpha
|
|
95
|
+
atom_w = (c - 1.0) / (c + 1.0)
|
|
96
|
+
elif c < 1.0:
|
|
97
|
+
atom_loc = beta
|
|
98
|
+
atom_w = (1.0 - c) / (c + 1.0)
|
|
99
|
+
|
|
100
|
+
return rho, atom_loc, atom_w
|
|
101
|
+
|
|
102
|
+
# ===========
|
|
103
|
+
# sqrt like t
|
|
104
|
+
# ===========
|
|
105
|
+
|
|
106
|
+
def _sqrt_like_t(self, delta, t):
|
|
107
|
+
"""
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
s = numpy.sqrt(delta)
|
|
111
|
+
flip = numpy.real(t * numpy.conjugate(s)) < 0.0
|
|
112
|
+
s = numpy.where(flip, -s, s)
|
|
113
|
+
return s
|
|
114
|
+
|
|
115
|
+
# =========
|
|
116
|
+
# stieltjes
|
|
117
|
+
# =========
|
|
118
|
+
|
|
119
|
+
def stieltjes(self, z, alt_branch=False):
|
|
120
|
+
"""
|
|
121
|
+
Physical Stieltjes transform
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
# Parameters
|
|
125
|
+
alpha = self.alpha
|
|
126
|
+
beta = self.beta
|
|
127
|
+
c = self.c
|
|
128
|
+
|
|
129
|
+
t = (z - alpha) * (z - beta)
|
|
130
|
+
delta = t * t - 2.0 * (c + 1.0) * t + (c - 1.0) * (c - 1.0)
|
|
131
|
+
|
|
132
|
+
s = self._sqrt_like_t(delta, t)
|
|
133
|
+
|
|
134
|
+
A = t + (c - 1.0)
|
|
135
|
+
|
|
136
|
+
za = z - alpha
|
|
137
|
+
za_safe = numpy.where(za == 0.0, numpy.nan, za)
|
|
138
|
+
|
|
139
|
+
u_p = (-A + s) / (2.0 * c * za_safe)
|
|
140
|
+
u_m = (-A - s) / (2.0 * c * za_safe)
|
|
141
|
+
|
|
142
|
+
den_p = (t - c + 1.0) + s
|
|
143
|
+
den_m = (t - c + 1.0) - s
|
|
144
|
+
|
|
145
|
+
v_p = -2.0 * za_safe / den_p
|
|
146
|
+
v_m = -2.0 * za_safe / den_m
|
|
147
|
+
|
|
148
|
+
m_p = (c / (1.0 + c)) * u_p + (1.0 / (1.0 + c)) * v_p
|
|
149
|
+
m_m = (c / (1.0 + c)) * u_m + (1.0 / (1.0 + c)) * v_m
|
|
150
|
+
|
|
151
|
+
mask_p = numpy.imag(z) >= 0.0
|
|
152
|
+
pick_p = \
|
|
153
|
+
numpy.where(mask_p, numpy.imag(m_p) >= 0.0, numpy.imag(m_p) <= 0.0)
|
|
154
|
+
|
|
155
|
+
m1 = numpy.where(pick_p, m_p, m_m)
|
|
156
|
+
m2 = numpy.where(pick_p, m_m, m_p)
|
|
157
|
+
|
|
158
|
+
if alt_branch:
|
|
159
|
+
return m2
|
|
160
|
+
return m1
|
|
161
|
+
|
|
162
|
+
# =======
|
|
163
|
+
# support
|
|
164
|
+
# =======
|
|
165
|
+
|
|
166
|
+
def support(self):
|
|
167
|
+
"""
|
|
168
|
+
Support
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
# Parameters
|
|
172
|
+
alpha = self.alpha
|
|
173
|
+
beta = self.beta
|
|
174
|
+
c = self.c
|
|
175
|
+
|
|
176
|
+
s = numpy.sqrt(c)
|
|
177
|
+
t_min = (s - 1.0) * (s - 1.0)
|
|
178
|
+
t_max = (s + 1.0) * (s + 1.0)
|
|
179
|
+
|
|
180
|
+
d = (alpha - beta) * (alpha - beta)
|
|
181
|
+
|
|
182
|
+
r_min = numpy.sqrt(d + 4.0 * t_min)
|
|
183
|
+
r_max = numpy.sqrt(d + 4.0 * t_max)
|
|
184
|
+
|
|
185
|
+
a1 = 0.5 * (alpha + beta - r_max)
|
|
186
|
+
b1 = 0.5 * (alpha + beta - r_min)
|
|
187
|
+
a2 = 0.5 * (alpha + beta + r_min)
|
|
188
|
+
b2 = 0.5 * (alpha + beta + r_max)
|
|
189
|
+
|
|
190
|
+
return [(a1, b1), (a2, b2)]
|
|
191
|
+
|
|
192
|
+
# ==================
|
|
193
|
+
# stieltjes on torus
|
|
194
|
+
# ==================
|
|
195
|
+
|
|
196
|
+
def stieltjes_on_torus(self, u, lam, a1, b1, a2, b2):
|
|
197
|
+
"""
|
|
198
|
+
Exact m on the torus (no fit), continuous, by:
|
|
199
|
+
1) computing the two exact candidates mA(z(u)) and mB(z(u)),
|
|
200
|
+
2) selecting a continuous branch on the torus via BFS continuation,
|
|
201
|
+
3) applying an optimal "half-cycle swap" along the phi-direction
|
|
202
|
+
(choosing the cut location automatically) to ensure global
|
|
203
|
+
consistency without breaking periodicity (fixes the equator-circle
|
|
204
|
+
issue).
|
|
205
|
+
|
|
206
|
+
Usage:
|
|
207
|
+
mT_exact = eval_m_on_torus_exact(u, lam, a1, b1, a2, b2, alpha,
|
|
208
|
+
beta, c)
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
# ---------------------------
|
|
212
|
+
# core (drop seam duplicates)
|
|
213
|
+
# ---------------------------
|
|
214
|
+
|
|
215
|
+
uc = u[:-1, :-1]
|
|
216
|
+
nphi, ntheta = uc.shape
|
|
217
|
+
|
|
218
|
+
# ----------------------------
|
|
219
|
+
# map u -> z via X = lam sn^2
|
|
220
|
+
# ----------------------------
|
|
221
|
+
|
|
222
|
+
sn, cn, dn, _ = ellipj(uc, lam)
|
|
223
|
+
Xc = lam * (sn * sn)
|
|
224
|
+
zc = mobius_z(Xc, a1, b1, a2, b2)
|
|
225
|
+
|
|
226
|
+
# -------------------------------
|
|
227
|
+
# exact branch candidates at z(u)
|
|
228
|
+
# -------------------------------
|
|
229
|
+
|
|
230
|
+
mA = self.stieltjes(zc, alt_branch=False) # candidate A
|
|
231
|
+
mB = self.stieltjes(zc, alt_branch=True) # candidate B
|
|
232
|
+
|
|
233
|
+
finA = numpy.isfinite(mA)
|
|
234
|
+
finB = numpy.isfinite(mB)
|
|
235
|
+
|
|
236
|
+
# output core and chosen flags
|
|
237
|
+
mC = numpy.full_like(mA, numpy.nan, dtype=complex)
|
|
238
|
+
|
|
239
|
+
# 0->A, 1->B, -1 unset
|
|
240
|
+
chosen = numpy.full((nphi, ntheta), -1, dtype=numpy.int8)
|
|
241
|
+
|
|
242
|
+
# -----------------------------------
|
|
243
|
+
# seed: find a point with both finite
|
|
244
|
+
# -----------------------------------
|
|
245
|
+
|
|
246
|
+
if finA[0, 0] and finB[0, 0]:
|
|
247
|
+
i0, j0 = 0, 0
|
|
248
|
+
else:
|
|
249
|
+
idx = numpy.argwhere(finA & finB)
|
|
250
|
+
if idx.size == 0:
|
|
251
|
+
raise RuntimeError("No points where both branches are finite.")
|
|
252
|
+
i0, j0 = idx[0]
|
|
253
|
+
|
|
254
|
+
# deterministic seed choice (any deterministic rule is fine)
|
|
255
|
+
# prefer candidate whose Im(m) roughly matches sign of Im(z)
|
|
256
|
+
if numpy.imag(zc[i0, j0]) >= 0:
|
|
257
|
+
pickA = (numpy.imag(mA[i0, j0]) >= numpy.imag(mB[i0, j0]))
|
|
258
|
+
else:
|
|
259
|
+
pickA = (numpy.imag(mA[i0, j0]) <= numpy.imag(mB[i0, j0]))
|
|
260
|
+
|
|
261
|
+
chosen[i0, j0] = 0 if pickA else 1
|
|
262
|
+
mC[i0, j0] = mA[i0, j0] if pickA else mB[i0, j0]
|
|
263
|
+
|
|
264
|
+
# ----------------------------------------------
|
|
265
|
+
# BFS continuation on torus (periodic neighbors)
|
|
266
|
+
# ----------------------------------------------
|
|
267
|
+
|
|
268
|
+
q = collections.deque([(i0, j0)])
|
|
269
|
+
while q:
|
|
270
|
+
i, j = q.popleft()
|
|
271
|
+
ref = mC[i, j]
|
|
272
|
+
if not numpy.isfinite(ref):
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
nbrs = [((i - 1) % nphi, j),
|
|
276
|
+
((i + 1) % nphi, j),
|
|
277
|
+
(i, (j - 1) % ntheta),
|
|
278
|
+
(i, (j + 1) % ntheta)]
|
|
279
|
+
|
|
280
|
+
for ii, jj in nbrs:
|
|
281
|
+
if chosen[ii, jj] != -1:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
a_ok = finA[ii, jj]
|
|
285
|
+
b_ok = finB[ii, jj]
|
|
286
|
+
|
|
287
|
+
if not a_ok and not b_ok:
|
|
288
|
+
chosen[ii, jj] = 2
|
|
289
|
+
mC[ii, jj] = numpy.nan
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
if a_ok and not b_ok:
|
|
293
|
+
chosen[ii, jj] = 0
|
|
294
|
+
mC[ii, jj] = mA[ii, jj]
|
|
295
|
+
q.append((ii, jj))
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
if b_ok and not a_ok:
|
|
299
|
+
chosen[ii, jj] = 1
|
|
300
|
+
mC[ii, jj] = mB[ii, jj]
|
|
301
|
+
q.append((ii, jj))
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
# both finite: choose closer to already-selected neighbor
|
|
305
|
+
# (continuation)
|
|
306
|
+
da = abs(mA[ii, jj] - ref)
|
|
307
|
+
db = abs(mB[ii, jj] - ref)
|
|
308
|
+
if da <= db:
|
|
309
|
+
chosen[ii, jj] = 0
|
|
310
|
+
mC[ii, jj] = mA[ii, jj]
|
|
311
|
+
else:
|
|
312
|
+
chosen[ii, jj] = 1
|
|
313
|
+
mC[ii, jj] = mB[ii, jj]
|
|
314
|
+
q.append((ii, jj))
|
|
315
|
+
|
|
316
|
+
# ----------------------------------------------------------------
|
|
317
|
+
# Step 3: choose the correct "half-cycle swap" cut automatically
|
|
318
|
+
#
|
|
319
|
+
# Build the "other-sheet" field mOther (swap at every point)
|
|
320
|
+
# and then choose a contiguous block of phi-rows of length L=nphi/2
|
|
321
|
+
# to swap, with cut location k chosen to minimize the two boundary
|
|
322
|
+
# jumps.
|
|
323
|
+
#
|
|
324
|
+
# This fixes the "entire equator circle wrong" issue.
|
|
325
|
+
# -----------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
if nphi % 2 != 0:
|
|
328
|
+
# If odd, we still do a near-half swap; but nphi is typically even.
|
|
329
|
+
L = nphi // 2
|
|
330
|
+
else:
|
|
331
|
+
L = nphi // 2
|
|
332
|
+
|
|
333
|
+
# swapped-everywhere alternative (only valid where chosen is 0/1)
|
|
334
|
+
mOther = numpy.full_like(mC, numpy.nan, dtype=complex)
|
|
335
|
+
ok0 = (chosen == 0)
|
|
336
|
+
ok1 = (chosen == 1)
|
|
337
|
+
mOther[ok0] = mB[ok0]
|
|
338
|
+
mOther[ok1] = mA[ok1]
|
|
339
|
+
|
|
340
|
+
# boundary cost between row i and i+1 (mod nphi) when row i uses mC (0)
|
|
341
|
+
# and row i+1 uses mOther (1) and when row i uses mOther (1) and row
|
|
342
|
+
# i+1 uses mC (0).
|
|
343
|
+
def boundary_cost_rowpair(Arow, Brow):
|
|
344
|
+
d = Arow - Brow
|
|
345
|
+
ok = numpy.isfinite(d)
|
|
346
|
+
return numpy.median(numpy.abs(d[ok])) if numpy.any(ok) \
|
|
347
|
+
else numpy.inf
|
|
348
|
+
|
|
349
|
+
# row i uses mC, row i+1 uses mOther
|
|
350
|
+
c01 = numpy.full(nphi, numpy.inf, dtype=float)
|
|
351
|
+
|
|
352
|
+
# row i uses mOther, row i+1 uses mC
|
|
353
|
+
c10 = numpy.full(nphi, numpy.inf, dtype=float)
|
|
354
|
+
|
|
355
|
+
for i in range(nphi):
|
|
356
|
+
ip = (i + 1) % nphi
|
|
357
|
+
c01[i] = boundary_cost_rowpair(mC[i, :], mOther[ip, :])
|
|
358
|
+
c10[i] = boundary_cost_rowpair(mOther[i, :], mC[ip, :])
|
|
359
|
+
|
|
360
|
+
# For a swap-block starting at k (rows k..k+L-1 swapped),
|
|
361
|
+
# the two cut boundaries are:
|
|
362
|
+
# b1 = k-1 : (unswapped -> swapped) uses c01[b1]
|
|
363
|
+
# b2 = k+L-1: (swapped -> unswapped) uses c10[b2]
|
|
364
|
+
best_k = 0
|
|
365
|
+
best_cost = numpy.inf
|
|
366
|
+
for k in range(nphi):
|
|
367
|
+
b1 = (k - 1) % nphi
|
|
368
|
+
b2 = (k + L - 1) % nphi
|
|
369
|
+
cost = c01[b1] + c10[b2]
|
|
370
|
+
if cost < best_cost:
|
|
371
|
+
best_cost = cost
|
|
372
|
+
best_k = k
|
|
373
|
+
|
|
374
|
+
# apply that optimal contiguous swap block
|
|
375
|
+
swap_rows = numpy.zeros(nphi, dtype=bool)
|
|
376
|
+
for t in range(L):
|
|
377
|
+
swap_rows[(best_k + t) % nphi] = True
|
|
378
|
+
|
|
379
|
+
mC2 = mC.copy()
|
|
380
|
+
mC2[swap_rows, :] = mOther[swap_rows, :]
|
|
381
|
+
mC = mC2
|
|
382
|
+
|
|
383
|
+
# -----------------------------
|
|
384
|
+
# rewrap seams to match u shape
|
|
385
|
+
# -----------------------------
|
|
386
|
+
|
|
387
|
+
mT = numpy.empty_like(u, dtype=complex)
|
|
388
|
+
mT[:-1, :-1] = mC
|
|
389
|
+
mT[-1, :-1] = mC[0, :]
|
|
390
|
+
mT[:-1, -1] = mC[:, 0]
|
|
391
|
+
mT[-1, -1] = mC[0, 0]
|
|
392
|
+
|
|
393
|
+
return mT
|
|
394
|
+
|
|
395
|
+
# ======
|
|
396
|
+
# matrix
|
|
397
|
+
# ======
|
|
398
|
+
|
|
399
|
+
def matrix(self, size, seed=None):
|
|
400
|
+
"""
|
|
401
|
+
Generate matrix with the spectral density of the distribution.
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
|
|
406
|
+
size : int
|
|
407
|
+
Size :math:`n` of the matrix.
|
|
408
|
+
|
|
409
|
+
seed : int, default=None
|
|
410
|
+
Seed for random number generator.
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
|
|
415
|
+
A : numpy.ndarray
|
|
416
|
+
A matrix of the size :math:`n \\times n`.
|
|
417
|
+
|
|
418
|
+
Examples
|
|
419
|
+
--------
|
|
420
|
+
|
|
421
|
+
.. code-block::python
|
|
422
|
+
|
|
423
|
+
>>> from freealg.distributions import MarchenkoPastur
|
|
424
|
+
>>> mp = MarchenkoPastur(1/50)
|
|
425
|
+
>>> A = mp.matrix(2000)
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
# Parameters
|
|
429
|
+
# m = int(size / self.lam)
|
|
430
|
+
#
|
|
431
|
+
# # Generate random matrix X (n x m) with i.i.d.
|
|
432
|
+
# rng = numpy.random.default_rng(seed)
|
|
433
|
+
# X = rng.standard_normal((size, m))
|
|
434
|
+
#
|
|
435
|
+
# # Form the sample covariance matrix A = (1/m)*XX^T.
|
|
436
|
+
# A = X @ X.T / m
|
|
437
|
+
#
|
|
438
|
+
# return A
|
|
439
|
+
|
|
440
|
+
pass
|