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.
Files changed (59) hide show
  1. freealg/__init__.py +8 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +12 -0
  4. freealg/_algebraic_form/_branch_points.py +288 -0
  5. freealg/_algebraic_form/_constraints.py +139 -0
  6. freealg/_algebraic_form/_continuation_algebraic.py +706 -0
  7. freealg/_algebraic_form/_decompress.py +641 -0
  8. freealg/_algebraic_form/_decompress2.py +204 -0
  9. freealg/_algebraic_form/_edge.py +330 -0
  10. freealg/_algebraic_form/_homotopy.py +323 -0
  11. freealg/_algebraic_form/_moments.py +448 -0
  12. freealg/_algebraic_form/_sheets_util.py +145 -0
  13. freealg/_algebraic_form/_support.py +309 -0
  14. freealg/_algebraic_form/algebraic_form.py +1232 -0
  15. freealg/_free_form/__init__.py +16 -0
  16. freealg/{_chebyshev.py → _free_form/_chebyshev.py} +75 -43
  17. freealg/_free_form/_decompress.py +993 -0
  18. freealg/_free_form/_density_util.py +243 -0
  19. freealg/_free_form/_jacobi.py +359 -0
  20. freealg/_free_form/_linalg.py +508 -0
  21. freealg/{_pade.py → _free_form/_pade.py} +42 -208
  22. freealg/{_plot_util.py → _free_form/_plot_util.py} +37 -22
  23. freealg/{_sample.py → _free_form/_sample.py} +58 -22
  24. freealg/_free_form/_series.py +454 -0
  25. freealg/_free_form/_support.py +214 -0
  26. freealg/_free_form/free_form.py +1362 -0
  27. freealg/_geometric_form/__init__.py +13 -0
  28. freealg/_geometric_form/_continuation_genus0.py +175 -0
  29. freealg/_geometric_form/_continuation_genus1.py +275 -0
  30. freealg/_geometric_form/_elliptic_functions.py +174 -0
  31. freealg/_geometric_form/_sphere_maps.py +63 -0
  32. freealg/_geometric_form/_torus_maps.py +118 -0
  33. freealg/_geometric_form/geometric_form.py +1094 -0
  34. freealg/_util.py +56 -110
  35. freealg/distributions/__init__.py +7 -1
  36. freealg/distributions/_chiral_block.py +494 -0
  37. freealg/distributions/_deformed_marchenko_pastur.py +726 -0
  38. freealg/distributions/_deformed_wigner.py +386 -0
  39. freealg/distributions/_kesten_mckay.py +29 -15
  40. freealg/distributions/_marchenko_pastur.py +224 -95
  41. freealg/distributions/_meixner.py +47 -37
  42. freealg/distributions/_wachter.py +29 -17
  43. freealg/distributions/_wigner.py +27 -14
  44. freealg/visualization/__init__.py +12 -0
  45. freealg/visualization/_glue_util.py +32 -0
  46. freealg/visualization/_rgb_hsv.py +125 -0
  47. freealg-0.7.12.dist-info/METADATA +172 -0
  48. freealg-0.7.12.dist-info/RECORD +53 -0
  49. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/WHEEL +1 -1
  50. freealg/_decompress.py +0 -180
  51. freealg/_jacobi.py +0 -218
  52. freealg/_support.py +0 -85
  53. freealg/freeform.py +0 -967
  54. freealg-0.1.11.dist-info/METADATA +0 -140
  55. freealg-0.1.11.dist-info/RECORD +0 -24
  56. /freealg/{_damp.py → _free_form/_damp.py} +0 -0
  57. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/AUTHORS.txt +0 -0
  58. {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/LICENSE.txt +0 -0
  59. {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