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,726 @@
|
|
|
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
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
from .._algebraic_form._sheets_util import _pick_physical_root_scalar
|
|
16
|
+
|
|
17
|
+
__all__ = ['DeformedMarchenkoPastur']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =========================
|
|
21
|
+
# Deformed Marchenko Pastur
|
|
22
|
+
# =========================
|
|
23
|
+
|
|
24
|
+
class DeformedMarchenkoPastur(object):
|
|
25
|
+
"""
|
|
26
|
+
Deformed Marchenko-Pastur model.
|
|
27
|
+
|
|
28
|
+
Notes
|
|
29
|
+
-----
|
|
30
|
+
|
|
31
|
+
Silverstein / companion Stieltjes variable
|
|
32
|
+
|
|
33
|
+
For sample-covariance, free multiplicative convolution with :math:`MP_c`:
|
|
34
|
+
Let :math:`u(z)` be the *companion* Stieltjes transform (often denoted
|
|
35
|
+
:math:`\\underline{m})`. It satisfies the Silverstein equation:
|
|
36
|
+
|
|
37
|
+
.. math::
|
|
38
|
+
|
|
39
|
+
z = -1/u + c * E_H[ t / (1 + t u) ].
|
|
40
|
+
|
|
41
|
+
For H = w1 \\delta_{t1} + w2 \\delta_{t2}:
|
|
42
|
+
|
|
43
|
+
.. math::
|
|
44
|
+
|
|
45
|
+
z = -1/u + c*( w1*t1/(1+t1 u) + w2*t2/(1+t2 u) ).
|
|
46
|
+
|
|
47
|
+
Then the (ordinary) Stieltjes transform m(z) of \\mu = H \\boxtimes MP_c is
|
|
48
|
+
|
|
49
|
+
.. math::
|
|
50
|
+
|
|
51
|
+
u = -(1-c)/z + c m
|
|
52
|
+
|
|
53
|
+
(equivalently :math:`m = (u + (1-c)/z)/c` for :math:`c>0`).
|
|
54
|
+
|
|
55
|
+
This module solves for u (cubic when H has two atoms), then maps to m.
|
|
56
|
+
|
|
57
|
+
Reference for the Silverstein equation form:
|
|
58
|
+
|
|
59
|
+
.. math::
|
|
60
|
+
|
|
61
|
+
z = -1/u + c \\int t/(1 + t u) dH(t).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# ====
|
|
65
|
+
# init
|
|
66
|
+
# ====
|
|
67
|
+
|
|
68
|
+
def __init__(self, t1, t2, w1, c=1.0):
|
|
69
|
+
"""
|
|
70
|
+
Initialization.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if not (0.0 <= w1 <= 1.0):
|
|
74
|
+
raise ValueError("w1 must be in [0, 1].")
|
|
75
|
+
|
|
76
|
+
if c < 0.0:
|
|
77
|
+
raise ValueError("c must be >= 0.")
|
|
78
|
+
|
|
79
|
+
if t1 < 0.0 or t2 < 0.0:
|
|
80
|
+
raise ValueError("t1 and t2 must be >= 0 for a covariance model.")
|
|
81
|
+
|
|
82
|
+
self.t1 = t1
|
|
83
|
+
self.t2 = t2
|
|
84
|
+
self.w1 = w1
|
|
85
|
+
self.c = c
|
|
86
|
+
|
|
87
|
+
# ====================
|
|
88
|
+
# roots cubic u scalar
|
|
89
|
+
# ====================
|
|
90
|
+
|
|
91
|
+
def _roots_cubic_u_scalar(self, z):
|
|
92
|
+
"""
|
|
93
|
+
Solve the cubic for u = \\underline{m}(z) for H = w1
|
|
94
|
+
\\delta_{t1} + (1-w1)
|
|
95
|
+
\\delta_{t2}.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Unpack parameters
|
|
99
|
+
t1 = self.t1
|
|
100
|
+
t2 = self.t2
|
|
101
|
+
w1 = self.w1
|
|
102
|
+
c = self.c
|
|
103
|
+
|
|
104
|
+
w2 = 1.0 - w1
|
|
105
|
+
mu1 = w1 * t1 + w2 * t2
|
|
106
|
+
|
|
107
|
+
# Cubic coefficients for u:
|
|
108
|
+
# (z t1 t2) u^3 + ( z(t1+t2) + t1 t2(1-c) ) u^2
|
|
109
|
+
# + ( z + (t1+t2) - c*mu1 ) u + 1 = 0
|
|
110
|
+
c3 = z * (t1 * t2)
|
|
111
|
+
c2 = z * (t1 + t2) + (t1 * t2) * (1.0 - c)
|
|
112
|
+
c1 = z + (t1 + t2) - c * mu1
|
|
113
|
+
c0 = 1.0
|
|
114
|
+
|
|
115
|
+
return numpy.roots([c3, c2, c1, c0])
|
|
116
|
+
|
|
117
|
+
# ==============
|
|
118
|
+
# solve u Newton
|
|
119
|
+
# ==============
|
|
120
|
+
|
|
121
|
+
def _solve_u_newton(self, z, u0=None, max_iter=100, tol=1e-12):
|
|
122
|
+
"""
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Unpack parameters
|
|
126
|
+
t1 = self.t1
|
|
127
|
+
t2 = self.t2
|
|
128
|
+
w1 = self.w1
|
|
129
|
+
c = self.c
|
|
130
|
+
|
|
131
|
+
w2 = 1.0 - w1
|
|
132
|
+
if u0 is None:
|
|
133
|
+
u = -1.0 / z
|
|
134
|
+
else:
|
|
135
|
+
u = complex(u0)
|
|
136
|
+
|
|
137
|
+
for _ in range(int(max_iter)):
|
|
138
|
+
d1 = 1.0 + t1 * u
|
|
139
|
+
d2 = 1.0 + t2 * u
|
|
140
|
+
|
|
141
|
+
# f(u) = -1/u + c*(w1*t1/d1 + w2*t2/d2) - z
|
|
142
|
+
f = (-1.0 / u) + c * (w1 * t1 / d1 + w2 * t2 / d2) - z
|
|
143
|
+
|
|
144
|
+
# f'(u) = 1/u^2 - c*(w1*t1^2/d1^2 + w2*t2^2/d2^2)
|
|
145
|
+
fp = (1.0 / (u * u)) - c * (w1 * (t1 * t1) / (d1 * d1) +
|
|
146
|
+
w2 * (t2 * t2) / (d2 * d2))
|
|
147
|
+
|
|
148
|
+
step = f / fp
|
|
149
|
+
u2 = u - step
|
|
150
|
+
if abs(step) < tol * (1.0 + abs(u2)):
|
|
151
|
+
return u2, True
|
|
152
|
+
u = u2
|
|
153
|
+
|
|
154
|
+
return u, False
|
|
155
|
+
|
|
156
|
+
# =========
|
|
157
|
+
# stieltjes
|
|
158
|
+
# =========
|
|
159
|
+
|
|
160
|
+
def stieltjes(self, z, max_iter=100, tol=1e-12):
|
|
161
|
+
"""
|
|
162
|
+
Physical/Herglotz branch of m(z) for μ = H \\boxtimes MP_c with
|
|
163
|
+
H = w1 \\delta_{t1} + (1-w1) \\delta_{t2}.
|
|
164
|
+
Fast masked Newton in u (companion Stieltjes), keeping z's original
|
|
165
|
+
shape.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
# Unpack parameters
|
|
169
|
+
t1 = self.t1
|
|
170
|
+
t2 = self.t2
|
|
171
|
+
w1 = self.w1
|
|
172
|
+
c = self.c
|
|
173
|
+
|
|
174
|
+
z = numpy.asarray(z, dtype=numpy.complex128)
|
|
175
|
+
scalar = (z.ndim == 0)
|
|
176
|
+
if scalar:
|
|
177
|
+
z = z.reshape((1,))
|
|
178
|
+
|
|
179
|
+
c = float(c)
|
|
180
|
+
if c < 0.0:
|
|
181
|
+
raise ValueError("c must be >= 0.")
|
|
182
|
+
|
|
183
|
+
w2 = 1.0 - w1
|
|
184
|
+
|
|
185
|
+
if c == 0.0:
|
|
186
|
+
out = (w1 / (t1 - z)) + (w2 / (t2 - z))
|
|
187
|
+
return out.reshape(()) if scalar else out
|
|
188
|
+
|
|
189
|
+
# u initial guess
|
|
190
|
+
u = -1.0 / z
|
|
191
|
+
active = numpy.isfinite(u)
|
|
192
|
+
|
|
193
|
+
for _ in range(int(max_iter)):
|
|
194
|
+
if not numpy.any(active):
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
# IMPORTANT: use integer indices (works for any ndim; avoids
|
|
198
|
+
# boolean-mask aliasing issues)
|
|
199
|
+
idx = numpy.flatnonzero(active)
|
|
200
|
+
ua = u.ravel()[idx]
|
|
201
|
+
za = z.ravel()[idx]
|
|
202
|
+
|
|
203
|
+
d1 = 1.0 + t1 * ua
|
|
204
|
+
d2 = 1.0 + t2 * ua
|
|
205
|
+
|
|
206
|
+
f = (-1.0 / ua) + c * (w1 * t1 / d1 + w2 * t2 / d2) - za
|
|
207
|
+
fp = (1.0 / (ua * ua)) - c * (
|
|
208
|
+
w1 * (t1 * t1) / (d1 * d1) +
|
|
209
|
+
w2 * (t2 * t2) / (d2 * d2)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
step = f / fp
|
|
213
|
+
un = ua - step
|
|
214
|
+
|
|
215
|
+
# write back u
|
|
216
|
+
u_flat = u.ravel()
|
|
217
|
+
u_flat[idx] = un
|
|
218
|
+
|
|
219
|
+
converged = numpy.abs(step) < tol * (1.0 + numpy.abs(un))
|
|
220
|
+
still = (~converged) & numpy.isfinite(un)
|
|
221
|
+
|
|
222
|
+
# update active only at the previously-active locations
|
|
223
|
+
a_flat = active.ravel()
|
|
224
|
+
a_flat[idx] = still
|
|
225
|
+
|
|
226
|
+
# Herglotz sanity: sign(Im z) == sign(Im u)
|
|
227
|
+
sign = numpy.where(numpy.imag(z) >= 0.0, 1.0, -1.0)
|
|
228
|
+
bad = (~numpy.isfinite(u)) | (sign * numpy.imag(u) <= 0.0)
|
|
229
|
+
|
|
230
|
+
if numpy.any(bad):
|
|
231
|
+
zb = z.ravel()
|
|
232
|
+
ub = u.ravel()
|
|
233
|
+
bad_idx = numpy.flatnonzero(bad)
|
|
234
|
+
for i in bad_idx:
|
|
235
|
+
zi = zb[i]
|
|
236
|
+
u_roots = self._roots_cubic_u_scalar(zi)
|
|
237
|
+
ub[i] = _pick_physical_root_scalar(zi, u_roots)
|
|
238
|
+
u = ub.reshape(z.shape)
|
|
239
|
+
|
|
240
|
+
m = (u + (1.0 - c) / z) / c
|
|
241
|
+
|
|
242
|
+
if scalar:
|
|
243
|
+
return m.reshape(())
|
|
244
|
+
return m
|
|
245
|
+
|
|
246
|
+
# =======
|
|
247
|
+
# density
|
|
248
|
+
# =======
|
|
249
|
+
|
|
250
|
+
def density(self, x, eta=1e-3):
|
|
251
|
+
"""
|
|
252
|
+
Density via Stieltjes inversion with robust x-continuation.
|
|
253
|
+
|
|
254
|
+
Notes:
|
|
255
|
+
- Do not warm-start across x<0 (MP-type support is >=0).
|
|
256
|
+
- Reset warm-start when previous u is (nearly) real.
|
|
257
|
+
- If Newton lands on a non-Herglotz root, fall back to cubic roots +
|
|
258
|
+
pick.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
# Unpack parameters
|
|
262
|
+
t1 = self.t1
|
|
263
|
+
t2 = self.t2
|
|
264
|
+
w1 = self.w1
|
|
265
|
+
c = self.c
|
|
266
|
+
|
|
267
|
+
x = numpy.asarray(x, dtype=numpy.float64)
|
|
268
|
+
rho = numpy.zeros_like(x, dtype=numpy.float64)
|
|
269
|
+
|
|
270
|
+
c = float(c)
|
|
271
|
+
if c < 0.0:
|
|
272
|
+
raise ValueError("c must be >= 0.")
|
|
273
|
+
if c == 0.0:
|
|
274
|
+
# Degenerate: μ = H when c=0, so m(z)=E[1/(t-z)] and rho from Im m.
|
|
275
|
+
z = x + 1j * float(eta)
|
|
276
|
+
w2 = 1.0 - w1
|
|
277
|
+
m = (w1 / (t1 - z)) + (w2 / (t2 - z))
|
|
278
|
+
rho = numpy.maximum(numpy.imag(m) / numpy.pi, 0.0)
|
|
279
|
+
return rho
|
|
280
|
+
|
|
281
|
+
# MP-type spectra live on x>=0; probing code includes x<0 (x_min<0),
|
|
282
|
+
# so we keep rho(x<0)=0 and DO NOT carry warm-start across 0.
|
|
283
|
+
mask = (x >= 0.0)
|
|
284
|
+
if not numpy.any(mask):
|
|
285
|
+
return rho
|
|
286
|
+
|
|
287
|
+
xp = x[mask]
|
|
288
|
+
|
|
289
|
+
# Preserve original order (support probing uses increasing xp).
|
|
290
|
+
order = numpy.argsort(xp)
|
|
291
|
+
inv = numpy.empty_like(order)
|
|
292
|
+
inv[order] = numpy.arange(order.size)
|
|
293
|
+
|
|
294
|
+
xp_sorted = xp[order]
|
|
295
|
+
z = xp_sorted + 1j * float(eta)
|
|
296
|
+
zf = z.ravel()
|
|
297
|
+
|
|
298
|
+
u = numpy.empty_like(zf, dtype=numpy.complex128)
|
|
299
|
+
u_prev = None
|
|
300
|
+
|
|
301
|
+
# thresholds
|
|
302
|
+
imag_eps = 1e-14
|
|
303
|
+
|
|
304
|
+
w2 = 1.0 - w1
|
|
305
|
+
|
|
306
|
+
for i in range(zf.size):
|
|
307
|
+
zi = zf[i]
|
|
308
|
+
|
|
309
|
+
# Warm start only if previous iterate had meaningful imaginary part
|
|
310
|
+
# (otherwise we risk sticking to a real branch across the bulk).
|
|
311
|
+
if (u_prev is None) or (abs(u_prev.imag) <= imag_eps):
|
|
312
|
+
ui0 = -1.0 / zi
|
|
313
|
+
else:
|
|
314
|
+
ui0 = complex(u_prev)
|
|
315
|
+
|
|
316
|
+
ui, _ = self._solve_u_newton(zi, u0=ui0, max_iter=120, tol=1e-13)
|
|
317
|
+
|
|
318
|
+
# Enforce Herglotz: sign(Im z) == sign(Im u) (eta>0 => Im u must be
|
|
319
|
+
# >0)
|
|
320
|
+
if (not numpy.isfinite(ui)) or (ui.imag <= 0.0):
|
|
321
|
+
u_roots = self._roots_cubic_u_scalar(zi)
|
|
322
|
+
ui = _pick_physical_root_scalar(zi, u_roots)
|
|
323
|
+
|
|
324
|
+
u[i] = ui
|
|
325
|
+
u_prev = ui
|
|
326
|
+
|
|
327
|
+
m = (u + (1.0 - c) / zf) / c
|
|
328
|
+
rh = numpy.maximum(numpy.imag(m) / numpy.pi, 0.0)
|
|
329
|
+
|
|
330
|
+
# Unsort back
|
|
331
|
+
rh = rh.reshape(xp_sorted.shape)
|
|
332
|
+
rho[mask] = rh[inv]
|
|
333
|
+
|
|
334
|
+
return rho
|
|
335
|
+
|
|
336
|
+
# =====
|
|
337
|
+
# roots
|
|
338
|
+
# =====
|
|
339
|
+
|
|
340
|
+
def roots(self, z):
|
|
341
|
+
"""
|
|
342
|
+
Return all 3 algebraic roots of m(z) (via roots for u then mapping to
|
|
343
|
+
m).
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
# Unpack parameters
|
|
347
|
+
t1 = self.t1
|
|
348
|
+
t2 = self.t2
|
|
349
|
+
w1 = self.w1
|
|
350
|
+
c = self.c
|
|
351
|
+
|
|
352
|
+
z = numpy.asarray(z, dtype=numpy.complex128)
|
|
353
|
+
scalar = (z.ndim == 0)
|
|
354
|
+
if scalar:
|
|
355
|
+
z = z.reshape((1,))
|
|
356
|
+
|
|
357
|
+
c = float(c)
|
|
358
|
+
if c < 0.0:
|
|
359
|
+
raise ValueError("c must be >= 0.")
|
|
360
|
+
|
|
361
|
+
zf = z.ravel()
|
|
362
|
+
out = numpy.empty((zf.size, 3), dtype=numpy.complex128)
|
|
363
|
+
|
|
364
|
+
if c == 0.0:
|
|
365
|
+
w2 = 1.0 - w1
|
|
366
|
+
mr = (w1 / (t1 - zf)) + (w2 / (t2 - zf))
|
|
367
|
+
out[:, 0] = mr
|
|
368
|
+
out[:, 1] = mr
|
|
369
|
+
out[:, 2] = mr
|
|
370
|
+
else:
|
|
371
|
+
for i in range(zf.size):
|
|
372
|
+
u_roots = self._roots_cubic_u_scalar(zf[i])
|
|
373
|
+
out[i, :] = (u_roots + (1.0 - c) / zf[i]) / c
|
|
374
|
+
|
|
375
|
+
out = out.reshape(z.shape + (3,))
|
|
376
|
+
if scalar:
|
|
377
|
+
return out.reshape((3,))
|
|
378
|
+
return out
|
|
379
|
+
|
|
380
|
+
# =======
|
|
381
|
+
# support
|
|
382
|
+
# =======
|
|
383
|
+
|
|
384
|
+
def support(self, eta=2e-4, n_probe=4000, thr=5e-4, x_max=None, x_pad=0.05,
|
|
385
|
+
method='quartic'):
|
|
386
|
+
"""
|
|
387
|
+
Estimate support intervals of μ = H \\boxtimes MP_c where H = w1
|
|
388
|
+
\\delta_{t1} + (1-w1) \\delta_{t2}.
|
|
389
|
+
|
|
390
|
+
Parameters
|
|
391
|
+
----------
|
|
392
|
+
t1, t2 : float
|
|
393
|
+
Atom locations (typically >0).
|
|
394
|
+
w1 : float
|
|
395
|
+
Weight of atom at t1.
|
|
396
|
+
c : float
|
|
397
|
+
MP aspect ratio parameter.
|
|
398
|
+
method : {'quartic','probe'}
|
|
399
|
+
- 'quartic' (default): compute endpoints from the real Silverstein
|
|
400
|
+
critical equation x'(u)=0 (fast; robust for detecting split /
|
|
401
|
+
merged bulks).
|
|
402
|
+
- 'probe': legacy density probing using :func:`density` on a grid
|
|
403
|
+
(can miss tiny gaps due to finite-eta leakage).
|
|
404
|
+
|
|
405
|
+
Notes
|
|
406
|
+
-----
|
|
407
|
+
In the companion variable u = \\underline{m}(z), the real mapping is
|
|
408
|
+
|
|
409
|
+
x(u) = -1/u + c * ( w1*t1/(1+t1 u) + (1-w1)*t2/(1+t2 u) ),
|
|
410
|
+
|
|
411
|
+
and support endpoints occur at critical points where
|
|
412
|
+
|
|
413
|
+
x'(u) = 0 <=> 1/u^2 = c * ( w1*t1^2/(1+t1 u)^2 + (1-w1)*t2^2/
|
|
414
|
+
(1+t2 u)^2 ).
|
|
415
|
+
|
|
416
|
+
For two atoms, this reduces to a quartic polynomial in u, so endpoints
|
|
417
|
+
can be obtained with a handful of root solves (no expensive probing).
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
# Unpack parameters
|
|
421
|
+
t1 = self.t1
|
|
422
|
+
t2 = self.t2
|
|
423
|
+
w1 = self.w1
|
|
424
|
+
c = self.c
|
|
425
|
+
|
|
426
|
+
c = float(c)
|
|
427
|
+
if c < 0.0:
|
|
428
|
+
raise ValueError("c must be >= 0.")
|
|
429
|
+
if not (0.0 <= w1 <= 1.0):
|
|
430
|
+
raise ValueError("w1 must be in [0, 1].")
|
|
431
|
+
|
|
432
|
+
if method not in ('quartic', 'probe'):
|
|
433
|
+
raise ValueError("method must be 'quartic' or 'probe'.")
|
|
434
|
+
|
|
435
|
+
# --- fast endpoint finder via quartic in u ---
|
|
436
|
+
if method == 'quartic':
|
|
437
|
+
w2 = 1.0 - w1
|
|
438
|
+
|
|
439
|
+
# Build the quartic polynomial:
|
|
440
|
+
# A(u)^2 B(u)^2 - c u^2 ( w1 t1^2 B(u)^2 + w2 t2^2 A(u)^2 ) = 0
|
|
441
|
+
# where A(u)=1+t1 u, B(u)=1+t2 u.
|
|
442
|
+
u = numpy.poly1d([1.0, 0.0]) # u
|
|
443
|
+
A = 1.0 + float(t1) * u
|
|
444
|
+
B = 1.0 + float(t2) * u
|
|
445
|
+
A2 = A * A
|
|
446
|
+
B2 = B * B
|
|
447
|
+
P = (A2 * B2) - c * (u * u) * \
|
|
448
|
+
(w1 * (t1 * t1) * B2 + w2 * (t2 * t2) * A2)
|
|
449
|
+
|
|
450
|
+
u_roots = numpy.roots(P.c)
|
|
451
|
+
|
|
452
|
+
# keep real negative roots away from poles u=-1/t1,-1/t2 and from 0
|
|
453
|
+
poles = []
|
|
454
|
+
if float(t1) != 0.0:
|
|
455
|
+
poles.append(-1.0 / float(t1))
|
|
456
|
+
if float(t2) != 0.0:
|
|
457
|
+
poles.append(-1.0 / float(t2))
|
|
458
|
+
|
|
459
|
+
u_crit = []
|
|
460
|
+
for r in u_roots:
|
|
461
|
+
if not numpy.isfinite(r):
|
|
462
|
+
continue
|
|
463
|
+
if abs(r.imag) > 1e-10 * (1.0 + abs(r.real)):
|
|
464
|
+
continue
|
|
465
|
+
ur = float(r.real)
|
|
466
|
+
if ur >= 0.0:
|
|
467
|
+
continue
|
|
468
|
+
if abs(ur) < 1e-14:
|
|
469
|
+
continue
|
|
470
|
+
too_close = False
|
|
471
|
+
for p in poles:
|
|
472
|
+
if abs(ur - p) < 1e-10 * (1.0 + abs(p)):
|
|
473
|
+
too_close = True
|
|
474
|
+
break
|
|
475
|
+
if too_close:
|
|
476
|
+
continue
|
|
477
|
+
u_crit.append(ur)
|
|
478
|
+
|
|
479
|
+
u_crit = sorted(set(u_crit))
|
|
480
|
+
if len(u_crit) < 2:
|
|
481
|
+
# Fallback to probing if quartic degenerates numerically
|
|
482
|
+
method = 'probe'
|
|
483
|
+
else:
|
|
484
|
+
def x_of_u(uu):
|
|
485
|
+
return (-1.0 / uu) + c * (w1 * t1 / (1.0 + t1 * uu) +
|
|
486
|
+
w2 * t2 / (1.0 + t2 * uu))
|
|
487
|
+
|
|
488
|
+
x_crit = []
|
|
489
|
+
for uu in u_crit:
|
|
490
|
+
xv = x_of_u(uu)
|
|
491
|
+
if numpy.isfinite(xv):
|
|
492
|
+
x_crit.append(float(xv))
|
|
493
|
+
|
|
494
|
+
x_crit = sorted(x_crit)
|
|
495
|
+
# endpoints come in pairs; build candidate intervals
|
|
496
|
+
cand = []
|
|
497
|
+
for k in range(0, len(x_crit) - 1, 2):
|
|
498
|
+
a = x_crit[k]
|
|
499
|
+
b = x_crit[k + 1]
|
|
500
|
+
if b > a:
|
|
501
|
+
cand.append((a, b))
|
|
502
|
+
|
|
503
|
+
# validate each candidate interval by checking rho at midpoints
|
|
504
|
+
cuts = []
|
|
505
|
+
for a, b in cand:
|
|
506
|
+
mid = 0.5 * (a + b)
|
|
507
|
+
# very cheap check (one evaluation)
|
|
508
|
+
rh = float(self.density(numpy.array([mid]),
|
|
509
|
+
eta=max(eta, 1e-8))[0])
|
|
510
|
+
if numpy.isfinite(rh) and (rh > 0.0):
|
|
511
|
+
aa = max(0.0, a) # MP-type spectra should be >=0
|
|
512
|
+
cuts.append((aa, b))
|
|
513
|
+
|
|
514
|
+
# If everything validated out (rare), fall back to probe.
|
|
515
|
+
if len(cuts) > 0:
|
|
516
|
+
return cuts
|
|
517
|
+
method = 'probe'
|
|
518
|
+
|
|
519
|
+
# --- legacy probing (kept as fallback / comparison) ---
|
|
520
|
+
# Heuristic x-range
|
|
521
|
+
tmax = float(max(abs(t1), abs(t2), 1e-12))
|
|
522
|
+
if x_max is None:
|
|
523
|
+
s = (1.0 + numpy.sqrt(max(c, 0.0))) ** 2
|
|
524
|
+
x_max = 3.0 * tmax * s + 1.0
|
|
525
|
+
x_max = float(x_max)
|
|
526
|
+
|
|
527
|
+
x_min = -float(x_pad) * x_max
|
|
528
|
+
|
|
529
|
+
x = numpy.linspace(x_min, x_max, int(n_probe))
|
|
530
|
+
rho = self.density(x, eta=float(eta))
|
|
531
|
+
|
|
532
|
+
good = numpy.isfinite(rho) & (rho > float(thr))
|
|
533
|
+
if not numpy.any(good):
|
|
534
|
+
return []
|
|
535
|
+
|
|
536
|
+
idx = numpy.where(good)[0]
|
|
537
|
+
breaks = numpy.where(numpy.diff(idx) > 1)[0]
|
|
538
|
+
segments = []
|
|
539
|
+
start = idx[0]
|
|
540
|
+
for b in breaks:
|
|
541
|
+
end = idx[b]
|
|
542
|
+
segments.append((start, end))
|
|
543
|
+
start = idx[b + 1]
|
|
544
|
+
segments.append((start, idx[-1]))
|
|
545
|
+
|
|
546
|
+
def rho_scalar(x0):
|
|
547
|
+
return float(self.density(numpy.array([x0]), eta=float(eta))[0])
|
|
548
|
+
|
|
549
|
+
cuts = []
|
|
550
|
+
for i0, i1 in segments:
|
|
551
|
+
a0 = float(x[max(i0 - 1, 0)])
|
|
552
|
+
a1 = float(x[i0])
|
|
553
|
+
b0 = float(x[i1])
|
|
554
|
+
b1 = float(x[min(i1 + 1, x.size - 1)])
|
|
555
|
+
|
|
556
|
+
# left edge
|
|
557
|
+
lo, hi = a0, a1
|
|
558
|
+
for _ in range(60):
|
|
559
|
+
mid = 0.5 * (lo + hi)
|
|
560
|
+
if rho_scalar(mid) > thr:
|
|
561
|
+
hi = mid
|
|
562
|
+
else:
|
|
563
|
+
lo = mid
|
|
564
|
+
a = hi
|
|
565
|
+
|
|
566
|
+
# right edge
|
|
567
|
+
lo, hi = b0, b1
|
|
568
|
+
for _ in range(60):
|
|
569
|
+
mid = 0.5 * (lo + hi)
|
|
570
|
+
if rho_scalar(mid) > thr:
|
|
571
|
+
lo = mid
|
|
572
|
+
else:
|
|
573
|
+
hi = mid
|
|
574
|
+
b = lo
|
|
575
|
+
|
|
576
|
+
if numpy.isfinite(a) and numpy.isfinite(b) and (b > a + 1e-10):
|
|
577
|
+
cuts.append((max(0.0, a), b))
|
|
578
|
+
|
|
579
|
+
return cuts
|
|
580
|
+
|
|
581
|
+
# ======
|
|
582
|
+
# matrix
|
|
583
|
+
# ======
|
|
584
|
+
|
|
585
|
+
def matrix(self, size, seed=None):
|
|
586
|
+
"""
|
|
587
|
+
Generate matrix with the spectral density of the distribution.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
|
|
592
|
+
size : int
|
|
593
|
+
Size :math:`n` of the matrix.
|
|
594
|
+
|
|
595
|
+
seed : int, default=None
|
|
596
|
+
Seed for random number generator.
|
|
597
|
+
|
|
598
|
+
Returns
|
|
599
|
+
-------
|
|
600
|
+
|
|
601
|
+
A : numpy.ndarray
|
|
602
|
+
A matrix of the size :math:`n \\times n`.
|
|
603
|
+
|
|
604
|
+
Notes
|
|
605
|
+
-----
|
|
606
|
+
|
|
607
|
+
Generate an :math:`n x n` sample covariance matrix :math:`\\mathbf{S}`
|
|
608
|
+
whose ESD converges to :math:`H \\boxtimes MP_c`, where
|
|
609
|
+
:math:`H = w_1 \\delta_{t_1} + (1-w_1) \\delta_{t_2}`.
|
|
610
|
+
|
|
611
|
+
Finite :math:`n` construction:
|
|
612
|
+
|
|
613
|
+
* :math:`m` is chosen so that :math:`n/m` approx :math:`c` (when
|
|
614
|
+
:math:`c>0`),
|
|
615
|
+
* :math:`Z` has i.i.d. :math:`N(0,1)`,
|
|
616
|
+
* :math:`\\boldsymbol{\\Sigma}` has eigenvalues :math:`t_1`,
|
|
617
|
+
:math:`t_2` with proportions
|
|
618
|
+
:math:`w_1`, and :math:`1-w_1`,
|
|
619
|
+
* :math:`\\mathbf{S} = (1/m) \\boldsymbol{\\Sigma}^{1/2} \\mathbf{Z}
|
|
620
|
+
\\mathbf{Z}^T \\boldsymbol{\\Sigma}^{1/2}`.
|
|
621
|
+
|
|
622
|
+
Examples
|
|
623
|
+
--------
|
|
624
|
+
|
|
625
|
+
.. code-block::python
|
|
626
|
+
|
|
627
|
+
>>> from freealg.distributions import MarchenkoPastur
|
|
628
|
+
>>> mp = MarchenkoPastur(1/50)
|
|
629
|
+
>>> A = mp.matrix(2000)
|
|
630
|
+
"""
|
|
631
|
+
|
|
632
|
+
n = int(size)
|
|
633
|
+
if n <= 0:
|
|
634
|
+
raise ValueError("size must be a positive integer.")
|
|
635
|
+
|
|
636
|
+
# Unpack parameters
|
|
637
|
+
t1 = float(self.t1)
|
|
638
|
+
t2 = float(self.t2)
|
|
639
|
+
w1 = float(self.w1)
|
|
640
|
+
c = float(self.c)
|
|
641
|
+
|
|
642
|
+
rng = numpy.random.default_rng(seed)
|
|
643
|
+
|
|
644
|
+
# Choose m so that n/m approx c (for c>0). For c=0, return population
|
|
645
|
+
# Sigma.
|
|
646
|
+
if c == 0.0:
|
|
647
|
+
n1 = int(round(w1 * n))
|
|
648
|
+
n1 = max(0, min(n, n1))
|
|
649
|
+
d = numpy.empty(n, dtype=numpy.float64)
|
|
650
|
+
d[:n1] = t1
|
|
651
|
+
d[n1:] = t2
|
|
652
|
+
rng.shuffle(d)
|
|
653
|
+
return numpy.diag(d)
|
|
654
|
+
|
|
655
|
+
# m must be positive integer
|
|
656
|
+
m = int(round(n / c)) if c > 0.0 else n
|
|
657
|
+
m = max(1, m)
|
|
658
|
+
|
|
659
|
+
# Build diagonal Sigma^{1/2} with two atoms
|
|
660
|
+
n1 = int(round(w1 * n))
|
|
661
|
+
n1 = max(0, min(n, n1))
|
|
662
|
+
|
|
663
|
+
s = numpy.empty(n, dtype=numpy.float64)
|
|
664
|
+
s[:n1] = numpy.sqrt(t1)
|
|
665
|
+
s[n1:] = numpy.sqrt(t2)
|
|
666
|
+
rng.shuffle(s)
|
|
667
|
+
|
|
668
|
+
# Draw Z and form X = Sigma^{1/2} Z / sqrt(m)
|
|
669
|
+
Z = rng.standard_normal((n, m))
|
|
670
|
+
X = (s[:, None] * Z) / numpy.sqrt(m)
|
|
671
|
+
|
|
672
|
+
# Sample covariance
|
|
673
|
+
S = X @ X.T
|
|
674
|
+
|
|
675
|
+
return S
|
|
676
|
+
|
|
677
|
+
# ====
|
|
678
|
+
# poly
|
|
679
|
+
# ====
|
|
680
|
+
|
|
681
|
+
def poly(self):
|
|
682
|
+
"""
|
|
683
|
+
Return a_coeffs for the exact cubic P(z,m)=0 of the two-atom deformed
|
|
684
|
+
MP model.
|
|
685
|
+
|
|
686
|
+
This is the eliminated polynomial in m (not underline{m}).
|
|
687
|
+
a_coeffs[i, j] is the coefficient of z^i m^j.
|
|
688
|
+
Shape is (3, 4).
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
t1 = float(self.t1)
|
|
692
|
+
t2 = float(self.t2)
|
|
693
|
+
w1 = float(self.w1)
|
|
694
|
+
w2 = 1.0 - w1
|
|
695
|
+
c = float(self.c)
|
|
696
|
+
|
|
697
|
+
# mu1 = w1 * t1 + w2 * t2
|
|
698
|
+
|
|
699
|
+
a = numpy.zeros((3, 4), dtype=numpy.complex128)
|
|
700
|
+
|
|
701
|
+
# NOTE: This polynomial is defined up to a global nonzero factor.
|
|
702
|
+
# The scaling below is chosen so that the m^3 term is (-c^3 t1 t2) z^2.
|
|
703
|
+
|
|
704
|
+
# ---- m^3: (-c^3 t1 t2) z^2
|
|
705
|
+
a[2, 3] = -(c**3) * t1 * t2
|
|
706
|
+
|
|
707
|
+
# ---- m^2: -( 2 c^3 t1 t2 z - 2 c^2 t1 t2 z + c^2 (t1+t2) z^2 )
|
|
708
|
+
a[0, 2] = 0.0
|
|
709
|
+
a[1, 2] = -(2.0 * (c**3) * t1 * t2 - 2.0 * (c**2) * t1 * t2)
|
|
710
|
+
a[2, 2] = -(c**2) * (t1 + t2)
|
|
711
|
+
|
|
712
|
+
# ---- m^1:
|
|
713
|
+
# -c * [ c^2 t1 t2 - 2 c t1 t2 + t1 t2
|
|
714
|
+
# + z^2
|
|
715
|
+
# + z*( -c*w1*t1 + 2c*t1 + c*w1*t2 + c*t2 - t1 - t2 ) ]
|
|
716
|
+
a[0, 1] = -c * ((c**2) * t1 * t2 - 2.0 * c * t1 * t2 + t1 * t2)
|
|
717
|
+
a[1, 1] = -c * ((-c * w1 * t1) + (2.0 * c * t1) + (c * w1 * t2) +
|
|
718
|
+
(c * t2) - t1 - t2)
|
|
719
|
+
a[2, 1] = -c * (1.0)
|
|
720
|
+
|
|
721
|
+
# ---- m^0: -c z + c(1-c) (w2 t1 + w1 t2)
|
|
722
|
+
a[0, 0] = c * (1.0 - c) * (w2 * t1 + w1 * t2)
|
|
723
|
+
a[1, 0] = -c
|
|
724
|
+
a[2, 0] = 0.0
|
|
725
|
+
|
|
726
|
+
return a
|