freealg 0.6.2__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.
Files changed (44) hide show
  1. freealg/__init__.py +8 -7
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +11 -0
  4. freealg/_algebraic_form/_continuation_algebraic.py +503 -0
  5. freealg/_algebraic_form/_decompress.py +648 -0
  6. freealg/_algebraic_form/_edge.py +352 -0
  7. freealg/_algebraic_form/_sheets_util.py +145 -0
  8. freealg/_algebraic_form/algebraic_form.py +987 -0
  9. freealg/_freeform/__init__.py +16 -0
  10. freealg/{_decompress.py → _freeform/_decompress.py} +0 -10
  11. freealg/_freeform/_density_util.py +243 -0
  12. freealg/{_linalg.py → _freeform/_linalg.py} +1 -1
  13. freealg/{_pade.py → _freeform/_pade.py} +0 -1
  14. freealg/{freeform.py → _freeform/freeform.py} +2 -31
  15. freealg/_geometric_form/__init__.py +13 -0
  16. freealg/_geometric_form/_continuation_genus0.py +175 -0
  17. freealg/_geometric_form/_continuation_genus1.py +275 -0
  18. freealg/_geometric_form/_elliptic_functions.py +174 -0
  19. freealg/_geometric_form/_sphere_maps.py +63 -0
  20. freealg/_geometric_form/_torus_maps.py +118 -0
  21. freealg/_geometric_form/geometric_form.py +1094 -0
  22. freealg/_util.py +1 -217
  23. freealg/distributions/__init__.py +5 -1
  24. freealg/distributions/_chiral_block.py +440 -0
  25. freealg/distributions/_deformed_marchenko_pastur.py +617 -0
  26. freealg/distributions/_deformed_wigner.py +312 -0
  27. freealg/distributions/_marchenko_pastur.py +197 -80
  28. freealg/visualization/__init__.py +12 -0
  29. freealg/visualization/_glue_util.py +32 -0
  30. freealg/visualization/_rgb_hsv.py +125 -0
  31. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/METADATA +9 -11
  32. freealg-0.7.0.dist-info/RECORD +47 -0
  33. freealg-0.6.2.dist-info/RECORD +0 -26
  34. /freealg/{_chebyshev.py → _freeform/_chebyshev.py} +0 -0
  35. /freealg/{_damp.py → _freeform/_damp.py} +0 -0
  36. /freealg/{_jacobi.py → _freeform/_jacobi.py} +0 -0
  37. /freealg/{_plot_util.py → _freeform/_plot_util.py} +0 -0
  38. /freealg/{_sample.py → _freeform/_sample.py} +0 -0
  39. /freealg/{_series.py → _freeform/_series.py} +0 -0
  40. /freealg/{_support.py → _freeform/_support.py} +0 -0
  41. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/WHEEL +0 -0
  42. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/licenses/AUTHORS.txt +0 -0
  43. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/licenses/LICENSE.txt +0 -0
  44. {freealg-0.6.2.dist-info → freealg-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,617 @@
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
+ self.t1 = t1
74
+ self.t2 = t2
75
+ self.w1 = w1
76
+ self.c = c
77
+
78
+ # ====================
79
+ # roots cubic u scalar
80
+ # ====================
81
+
82
+ def _roots_cubic_u_scalar(self, z):
83
+ """
84
+ Solve the cubic for u = \\underline{m}(z) for H = w1
85
+ \\delta_{t1} + (1-w1)
86
+ \\delta_{t2}.
87
+ """
88
+
89
+ # Unpack parameters
90
+ t1 = self.t1
91
+ t2 = self.t2
92
+ w1 = self.w1
93
+ c = self.c
94
+
95
+ w2 = 1.0 - w1
96
+ mu1 = w1 * t1 + w2 * t2
97
+
98
+ # Cubic coefficients for u:
99
+ # (z t1 t2) u^3 + ( z(t1+t2) + t1 t2(1-c) ) u^2
100
+ # + ( z + (t1+t2) - c*mu1 ) u + 1 = 0
101
+ c3 = z * (t1 * t2)
102
+ c2 = z * (t1 + t2) + (t1 * t2) * (1.0 - c)
103
+ c1 = z + (t1 + t2) - c * mu1
104
+ c0 = 1.0
105
+
106
+ return numpy.roots([c3, c2, c1, c0])
107
+
108
+ # ==============
109
+ # solve u Newton
110
+ # ==============
111
+
112
+ def _solve_u_newton(self, z, u0=None, max_iter=100, tol=1e-12):
113
+ """
114
+ """
115
+
116
+ # Unpack parameters
117
+ t1 = self.t1
118
+ t2 = self.t2
119
+ w1 = self.w1
120
+ c = self.c
121
+
122
+ w2 = 1.0 - w1
123
+ if u0 is None:
124
+ u = -1.0 / z
125
+ else:
126
+ u = complex(u0)
127
+
128
+ for _ in range(int(max_iter)):
129
+ d1 = 1.0 + t1 * u
130
+ d2 = 1.0 + t2 * u
131
+
132
+ # f(u) = -1/u + c*(w1*t1/d1 + w2*t2/d2) - z
133
+ f = (-1.0 / u) + c * (w1 * t1 / d1 + w2 * t2 / d2) - z
134
+
135
+ # f'(u) = 1/u^2 - c*(w1*t1^2/d1^2 + w2*t2^2/d2^2)
136
+ fp = (1.0 / (u * u)) - c * (w1 * (t1 * t1) / (d1 * d1) +
137
+ w2 * (t2 * t2) / (d2 * d2))
138
+
139
+ step = f / fp
140
+ u2 = u - step
141
+ if abs(step) < tol * (1.0 + abs(u2)):
142
+ return u2, True
143
+ u = u2
144
+
145
+ return u, False
146
+
147
+ # =========
148
+ # stieltjes
149
+ # =========
150
+
151
+ def stieltjes(self, z, max_iter=100, tol=1e-12):
152
+ """
153
+ Physical/Herglotz branch of m(z) for μ = H \\boxtimes MP_c with
154
+ H = w1 \\delta_{t1} + (1-w1) \\delta_{t2}.
155
+ Fast masked Newton in u (companion Stieltjes), keeping z's original
156
+ shape.
157
+ """
158
+
159
+ # Unpack parameters
160
+ t1 = self.t1
161
+ t2 = self.t2
162
+ w1 = self.w1
163
+ c = self.c
164
+
165
+ z = numpy.asarray(z, dtype=numpy.complex128)
166
+ scalar = (z.ndim == 0)
167
+ if scalar:
168
+ z = z.reshape((1,))
169
+
170
+ c = float(c)
171
+ if c < 0.0:
172
+ raise ValueError("c must be >= 0.")
173
+
174
+ w2 = 1.0 - w1
175
+
176
+ if c == 0.0:
177
+ out = (w1 / (t1 - z)) + (w2 / (t2 - z))
178
+ return out.reshape(()) if scalar else out
179
+
180
+ # u initial guess
181
+ u = -1.0 / z
182
+ active = numpy.isfinite(u)
183
+
184
+ for _ in range(int(max_iter)):
185
+ if not numpy.any(active):
186
+ break
187
+
188
+ # IMPORTANT: use integer indices (works for any ndim; avoids
189
+ # boolean-mask aliasing issues)
190
+ idx = numpy.flatnonzero(active)
191
+ ua = u.ravel()[idx]
192
+ za = z.ravel()[idx]
193
+
194
+ d1 = 1.0 + t1 * ua
195
+ d2 = 1.0 + t2 * ua
196
+
197
+ f = (-1.0 / ua) + c * (w1 * t1 / d1 + w2 * t2 / d2) - za
198
+ fp = (1.0 / (ua * ua)) - c * (
199
+ w1 * (t1 * t1) / (d1 * d1) +
200
+ w2 * (t2 * t2) / (d2 * d2)
201
+ )
202
+
203
+ step = f / fp
204
+ un = ua - step
205
+
206
+ # write back u
207
+ u_flat = u.ravel()
208
+ u_flat[idx] = un
209
+
210
+ converged = numpy.abs(step) < tol * (1.0 + numpy.abs(un))
211
+ still = (~converged) & numpy.isfinite(un)
212
+
213
+ # update active only at the previously-active locations
214
+ a_flat = active.ravel()
215
+ a_flat[idx] = still
216
+
217
+ # Herglotz sanity: sign(Im z) == sign(Im u)
218
+ sign = numpy.where(numpy.imag(z) >= 0.0, 1.0, -1.0)
219
+ bad = (~numpy.isfinite(u)) | (sign * numpy.imag(u) <= 0.0)
220
+
221
+ if numpy.any(bad):
222
+ zb = z.ravel()
223
+ ub = u.ravel()
224
+ bad_idx = numpy.flatnonzero(bad)
225
+ for i in bad_idx:
226
+ zi = zb[i]
227
+ u_roots = self._roots_cubic_u_scalar(zi)
228
+ ub[i] = _pick_physical_root_scalar(zi, u_roots)
229
+ u = ub.reshape(z.shape)
230
+
231
+ m = (u + (1.0 - c) / z) / c
232
+
233
+ if scalar:
234
+ return m.reshape(())
235
+ return m
236
+
237
+ # =======
238
+ # density
239
+ # =======
240
+
241
+ def density(self, x, eta=1e-3):
242
+ """
243
+ Density via Stieltjes inversion with robust x-continuation.
244
+
245
+ Notes:
246
+ - Do not warm-start across x<0 (MP-type support is >=0).
247
+ - Reset warm-start when previous u is (nearly) real.
248
+ - If Newton lands on a non-Herglotz root, fall back to cubic roots +
249
+ pick.
250
+ """
251
+
252
+ # Unpack parameters
253
+ t1 = self.t1
254
+ t2 = self.t2
255
+ w1 = self.w1
256
+ c = self.c
257
+
258
+ x = numpy.asarray(x, dtype=numpy.float64)
259
+ rho = numpy.zeros_like(x, dtype=numpy.float64)
260
+
261
+ c = float(c)
262
+ if c < 0.0:
263
+ raise ValueError("c must be >= 0.")
264
+ if c == 0.0:
265
+ # Degenerate: μ = H when c=0, so m(z)=E[1/(t-z)] and rho from Im m.
266
+ z = x + 1j * float(eta)
267
+ w2 = 1.0 - w1
268
+ m = (w1 / (t1 - z)) + (w2 / (t2 - z))
269
+ rho = numpy.maximum(numpy.imag(m) / numpy.pi, 0.0)
270
+ return rho
271
+
272
+ # MP-type spectra live on x>=0; probing code includes x<0 (x_min<0),
273
+ # so we keep rho(x<0)=0 and DO NOT carry warm-start across 0.
274
+ mask = (x >= 0.0)
275
+ if not numpy.any(mask):
276
+ return rho
277
+
278
+ xp = x[mask]
279
+
280
+ # Preserve original order (support probing uses increasing xp).
281
+ order = numpy.argsort(xp)
282
+ inv = numpy.empty_like(order)
283
+ inv[order] = numpy.arange(order.size)
284
+
285
+ xp_sorted = xp[order]
286
+ z = xp_sorted + 1j * float(eta)
287
+ zf = z.ravel()
288
+
289
+ u = numpy.empty_like(zf, dtype=numpy.complex128)
290
+ u_prev = None
291
+
292
+ # thresholds
293
+ imag_eps = 1e-14
294
+
295
+ w2 = 1.0 - w1
296
+
297
+ for i in range(zf.size):
298
+ zi = zf[i]
299
+
300
+ # Warm start only if previous iterate had meaningful imaginary part
301
+ # (otherwise we risk sticking to a real branch across the bulk).
302
+ if (u_prev is None) or (abs(u_prev.imag) <= imag_eps):
303
+ ui0 = -1.0 / zi
304
+ else:
305
+ ui0 = complex(u_prev)
306
+
307
+ ui, _ = self._solve_u_newton(zi, u0=ui0, max_iter=120, tol=1e-13)
308
+
309
+ # Enforce Herglotz: sign(Im z) == sign(Im u) (eta>0 => Im u must be
310
+ # >0)
311
+ if (not numpy.isfinite(ui)) or (ui.imag <= 0.0):
312
+ u_roots = self._roots_cubic_u_scalar(zi)
313
+ ui = _pick_physical_root_scalar(zi, u_roots)
314
+
315
+ u[i] = ui
316
+ u_prev = ui
317
+
318
+ m = (u + (1.0 - c) / zf) / c
319
+ rh = numpy.maximum(numpy.imag(m) / numpy.pi, 0.0)
320
+
321
+ # Unsort back
322
+ rh = rh.reshape(xp_sorted.shape)
323
+ rho[mask] = rh[inv]
324
+
325
+ return rho
326
+
327
+ # =====
328
+ # roots
329
+ # =====
330
+
331
+ def roots(self, z):
332
+ """
333
+ Return all 3 algebraic roots of m(z) (via roots for u then mapping to
334
+ m).
335
+ """
336
+
337
+ # Unpack parameters
338
+ t1 = self.t1
339
+ t2 = self.t2
340
+ w1 = self.w1
341
+ c = self.c
342
+
343
+ z = numpy.asarray(z, dtype=numpy.complex128)
344
+ scalar = (z.ndim == 0)
345
+ if scalar:
346
+ z = z.reshape((1,))
347
+
348
+ c = float(c)
349
+ if c < 0.0:
350
+ raise ValueError("c must be >= 0.")
351
+
352
+ zf = z.ravel()
353
+ out = numpy.empty((zf.size, 3), dtype=numpy.complex128)
354
+
355
+ if c == 0.0:
356
+ w2 = 1.0 - w1
357
+ mr = (w1 / (t1 - zf)) + (w2 / (t2 - zf))
358
+ out[:, 0] = mr
359
+ out[:, 1] = mr
360
+ out[:, 2] = mr
361
+ else:
362
+ for i in range(zf.size):
363
+ u_roots = self._roots_cubic_u_scalar(zf[i])
364
+ out[i, :] = (u_roots + (1.0 - c) / zf[i]) / c
365
+
366
+ out = out.reshape(z.shape + (3,))
367
+ if scalar:
368
+ return out.reshape((3,))
369
+ return out
370
+
371
+ # =======
372
+ # support
373
+ # =======
374
+
375
+ def support(self, eta=2e-4, n_probe=4000, thr=5e-4, x_max=None, x_pad=0.05,
376
+ method='quartic'):
377
+ """
378
+ Estimate support intervals of μ = H \\boxtimes MP_c where H = w1
379
+ \\delta_{t1} + (1-w1) \\delta_{t2}.
380
+
381
+ Parameters
382
+ ----------
383
+ t1, t2 : float
384
+ Atom locations (typically >0).
385
+ w1 : float
386
+ Weight of atom at t1.
387
+ c : float
388
+ MP aspect ratio parameter.
389
+ method : {'quartic','probe'}
390
+ - 'quartic' (default): compute endpoints from the real Silverstein
391
+ critical equation x'(u)=0 (fast; robust for detecting split /
392
+ merged bulks).
393
+ - 'probe': legacy density probing using :func:`density` on a grid
394
+ (can miss tiny gaps due to finite-eta leakage).
395
+
396
+ Notes
397
+ -----
398
+ In the companion variable u = \\underline{m}(z), the real mapping is
399
+
400
+ x(u) = -1/u + c * ( w1*t1/(1+t1 u) + (1-w1)*t2/(1+t2 u) ),
401
+
402
+ and support endpoints occur at critical points where
403
+
404
+ x'(u) = 0 <=> 1/u^2 = c * ( w1*t1^2/(1+t1 u)^2 + (1-w1)*t2^2/
405
+ (1+t2 u)^2 ).
406
+
407
+ For two atoms, this reduces to a quartic polynomial in u, so endpoints
408
+ can be obtained with a handful of root solves (no expensive probing).
409
+ """
410
+
411
+ # Unpack parameters
412
+ t1 = self.t1
413
+ t2 = self.t2
414
+ w1 = self.w1
415
+ c = self.c
416
+
417
+ c = float(c)
418
+ if c < 0.0:
419
+ raise ValueError("c must be >= 0.")
420
+ if not (0.0 <= w1 <= 1.0):
421
+ raise ValueError("w1 must be in [0, 1].")
422
+
423
+ if method not in ('quartic', 'probe'):
424
+ raise ValueError("method must be 'quartic' or 'probe'.")
425
+
426
+ # --- fast endpoint finder via quartic in u ---
427
+ if method == 'quartic':
428
+ w2 = 1.0 - w1
429
+
430
+ # Build the quartic polynomial:
431
+ # A(u)^2 B(u)^2 - c u^2 ( w1 t1^2 B(u)^2 + w2 t2^2 A(u)^2 ) = 0
432
+ # where A(u)=1+t1 u, B(u)=1+t2 u.
433
+ u = numpy.poly1d([1.0, 0.0]) # u
434
+ A = 1.0 + float(t1) * u
435
+ B = 1.0 + float(t2) * u
436
+ A2 = A * A
437
+ B2 = B * B
438
+ P = (A2 * B2) - c * (u * u) * \
439
+ (w1 * (t1 * t1) * B2 + w2 * (t2 * t2) * A2)
440
+
441
+ u_roots = numpy.roots(P.c)
442
+
443
+ # keep real negative roots away from poles u=-1/t1,-1/t2 and from 0
444
+ poles = []
445
+ if float(t1) != 0.0:
446
+ poles.append(-1.0 / float(t1))
447
+ if float(t2) != 0.0:
448
+ poles.append(-1.0 / float(t2))
449
+
450
+ u_crit = []
451
+ for r in u_roots:
452
+ if not numpy.isfinite(r):
453
+ continue
454
+ if abs(r.imag) > 1e-10 * (1.0 + abs(r.real)):
455
+ continue
456
+ ur = float(r.real)
457
+ if ur >= 0.0:
458
+ continue
459
+ if abs(ur) < 1e-14:
460
+ continue
461
+ too_close = False
462
+ for p in poles:
463
+ if abs(ur - p) < 1e-10 * (1.0 + abs(p)):
464
+ too_close = True
465
+ break
466
+ if too_close:
467
+ continue
468
+ u_crit.append(ur)
469
+
470
+ u_crit = sorted(set(u_crit))
471
+ if len(u_crit) < 2:
472
+ # Fallback to probing if quartic degenerates numerically
473
+ method = 'probe'
474
+ else:
475
+ def x_of_u(uu):
476
+ return (-1.0 / uu) + c * (w1 * t1 / (1.0 + t1 * uu) +
477
+ w2 * t2 / (1.0 + t2 * uu))
478
+
479
+ x_crit = []
480
+ for uu in u_crit:
481
+ xv = x_of_u(uu)
482
+ if numpy.isfinite(xv):
483
+ x_crit.append(float(xv))
484
+
485
+ x_crit = sorted(x_crit)
486
+ # endpoints come in pairs; build candidate intervals
487
+ cand = []
488
+ for k in range(0, len(x_crit) - 1, 2):
489
+ a = x_crit[k]
490
+ b = x_crit[k + 1]
491
+ if b > a:
492
+ cand.append((a, b))
493
+
494
+ # validate each candidate interval by checking rho at midpoints
495
+ cuts = []
496
+ for a, b in cand:
497
+ mid = 0.5 * (a + b)
498
+ # very cheap check (one evaluation)
499
+ rh = float(self.density(numpy.array([mid]),
500
+ eta=max(eta, 1e-8))[0])
501
+ if numpy.isfinite(rh) and (rh > 0.0):
502
+ aa = max(0.0, a) # MP-type spectra should be >=0
503
+ cuts.append((aa, b))
504
+
505
+ # If everything validated out (rare), fall back to probe.
506
+ if len(cuts) > 0:
507
+ return cuts
508
+ method = 'probe'
509
+
510
+ # --- legacy probing (kept as fallback / comparison) ---
511
+ # Heuristic x-range
512
+ tmax = float(max(abs(t1), abs(t2), 1e-12))
513
+ if x_max is None:
514
+ s = (1.0 + numpy.sqrt(max(c, 0.0))) ** 2
515
+ x_max = 3.0 * tmax * s + 1.0
516
+ x_max = float(x_max)
517
+
518
+ x_min = -float(x_pad) * x_max
519
+
520
+ x = numpy.linspace(x_min, x_max, int(n_probe))
521
+ rho = self.density(x, eta=float(eta))
522
+
523
+ good = numpy.isfinite(rho) & (rho > float(thr))
524
+ if not numpy.any(good):
525
+ return []
526
+
527
+ idx = numpy.where(good)[0]
528
+ breaks = numpy.where(numpy.diff(idx) > 1)[0]
529
+ segments = []
530
+ start = idx[0]
531
+ for b in breaks:
532
+ end = idx[b]
533
+ segments.append((start, end))
534
+ start = idx[b + 1]
535
+ segments.append((start, idx[-1]))
536
+
537
+ def rho_scalar(x0):
538
+ return float(self.density(numpy.array([x0]), eta=float(eta))[0])
539
+
540
+ cuts = []
541
+ for i0, i1 in segments:
542
+ a0 = float(x[max(i0 - 1, 0)])
543
+ a1 = float(x[i0])
544
+ b0 = float(x[i1])
545
+ b1 = float(x[min(i1 + 1, x.size - 1)])
546
+
547
+ # left edge
548
+ lo, hi = a0, a1
549
+ for _ in range(60):
550
+ mid = 0.5 * (lo + hi)
551
+ if rho_scalar(mid) > thr:
552
+ hi = mid
553
+ else:
554
+ lo = mid
555
+ a = hi
556
+
557
+ # right edge
558
+ lo, hi = b0, b1
559
+ for _ in range(60):
560
+ mid = 0.5 * (lo + hi)
561
+ if rho_scalar(mid) > thr:
562
+ lo = mid
563
+ else:
564
+ hi = mid
565
+ b = lo
566
+
567
+ if numpy.isfinite(a) and numpy.isfinite(b) and (b > a + 1e-10):
568
+ cuts.append((max(0.0, a), b))
569
+
570
+ return cuts
571
+
572
+ # ======
573
+ # matrix
574
+ # ======
575
+
576
+ def matrix(self, size, seed=None):
577
+ """
578
+ Generate matrix with the spectral density of the distribution.
579
+
580
+ Parameters
581
+ ----------
582
+
583
+ size : int
584
+ Size :math:`n` of the matrix.
585
+
586
+ seed : int, default=None
587
+ Seed for random number generator.
588
+
589
+ Returns
590
+ -------
591
+
592
+ A : numpy.ndarray
593
+ A matrix of the size :math:`n \\times n`.
594
+
595
+ Examples
596
+ --------
597
+
598
+ .. code-block::python
599
+
600
+ >>> from freealg.distributions import MarchenkoPastur
601
+ >>> mp = MarchenkoPastur(1/50)
602
+ >>> A = mp.matrix(2000)
603
+ """
604
+
605
+ # Parameters
606
+ # m = int(size / self.lam)
607
+ #
608
+ # # Generate random matrix X (n x m) with i.i.d.
609
+ # rng = numpy.random.default_rng(seed)
610
+ # X = rng.standard_normal((size, m))
611
+ #
612
+ # # Form the sample covariance matrix A = (1/m)*XX^T.
613
+ # A = X @ X.T / m
614
+ #
615
+ # return A
616
+
617
+ pass