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
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
- # Fallback to previous API
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,208 +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
- Notes
131
- -----
132
-
133
- In Beta kernel density estimation, the shape parameters "a" and "b" of the
134
- Beta(a, b)) distribution are computed for each data point "u" as:
135
-
136
- a = (u / h) + 1.0
137
- b = ((1.0 - u) / h) + 1.0
138
-
139
- This is a standard way of using Beta kernel (see R-package documentation:
140
- https://search.r-project.org/CRAN/refmans/DELTD/html/Beta.html
141
-
142
- These equations are derived from "moment matching" method, where
143
-
144
- Mean(Beta(a,b)) = u
145
- Var(Beta(a,b)) = (1-u) u h
146
-
147
- Solving these two equations for "a" and "b" yields the relations above.
148
- See paper (page 134)
149
- https://www.songxichen.com/Uploads/Files/Publication/Chen-CSD-99.pdf
150
- """
151
-
152
- if kernel == 'gaussian':
153
- pdf = gaussian_kde(eig, bw_method=h)(xs)
154
-
155
- # Adaptive KDE
156
- # k = KDEUnivariate(eig)
157
- # k.fit(kernel='gau', bw='silverman', fft=False, weights=None,
158
- # gridsize=1024, adaptive=True)
159
- # pdf = k.evaluate(xs)
160
-
161
- elif kernel == 'beta':
162
-
163
- span = lam_p - lam_m
164
- if span <= 0:
165
- raise ValueError('"lam_p" must be larger than "lam_m".')
166
-
167
- # map samples and grid to [0, 1]
168
- u = (eig - lam_m) / span
169
- t = (xs - lam_m) / span
170
-
171
- # keep only samples strictly inside (0,1)
172
- if (u.min() < 0) or (u.max() > 1):
173
- u = u[(u > 0) & (u < 1)]
174
-
175
- n = u.size
176
- if n == 0:
177
- return numpy.zeros_like(xs, dtype=float)
178
-
179
- # Shape parameters "a" and "b" or the kernel Beta(a, b), which is
180
- # computed for each data point "u" (see notes above). These are
181
- # vectorized.
182
- a = (u / h) + 1.0
183
- b = ((1.0 - u) / h) + 1.0
184
-
185
- # # tiny positive number to keep shape parameters > 0
186
- eps = 1e-6
187
- a = numpy.clip(a, eps, None)
188
- b = numpy.clip(b, eps, None)
189
-
190
- # Beta kernel
191
- pdf_matrix = beta.pdf(t[None, :], a[:, None], b[:, None])
192
-
193
- # Average and re-normalize back to x variable
194
- pdf = pdf_matrix.sum(axis=0) / (n * span)
195
-
196
- # Exact zeros outside [lam_m, lam_p]
197
- pdf[(t < 0) | (t > 1)] = 0.0
198
-
199
- else:
200
- raise NotImplementedError('"kernel" is invalid.')
201
-
202
- if plot:
203
- with texplot.theme(use_latex=False):
204
- fig, ax = plt.subplots(figsize=(6, 4))
205
-
206
- x_min = numpy.min(xs)
207
- x_max = numpy.max(xs)
208
- bins = numpy.linspace(x_min, x_max, _auto_bins(eig))
209
- _ = ax.hist(eig, bins, density=True, color='silver',
210
- edgecolor='none', label='Samples histogram')
211
- ax.plot(xs, pdf, color='black', label='KDE')
212
- ax.set_xlabel(r'$x$')
213
- ax.set_ylabel(r'$\\rho(x)$')
214
- ax.set_xlim([xs[0], xs[-1]])
215
- ax.set_ylim(bottom=0)
216
- ax.set_title('Kernel Density Estimation')
217
- ax.legend(fontsize='x-small')
218
- plt.show()
219
-
220
- return pdf
221
-
222
-
223
- # =============
224
- # force density
225
- # =============
226
-
227
- def force_density(psi0, support, density, grid, alpha=0.0, beta=0.0):
228
- """
229
- Starting from psi0 (raw projection), solve
230
- min 0.5 ||psi - psi0||^2
231
- s.t. F_pos psi >= 0 (positivity on grid)
232
- psi[0] = psi0[0] (mass)
233
- f(lam_m) psi = 0 (zero at left edge)
234
- f(lam_p) psi = 0 (zero at right edge)
235
- """
236
-
237
- lam_m, lam_p = support
238
-
239
- # Objective and gradient
240
- def fun(psi):
241
- return 0.5 * numpy.dot(psi-psi0, psi-psi0)
242
-
243
- def grad(psi):
244
- return psi - psi0
245
-
246
- # Constraints:
247
- constraints = []
248
-
249
- # Enforce positivity
250
- constraints.append({'type': 'ineq',
251
- 'fun': lambda psi: density(grid, psi)})
252
-
253
- # Enforce unit mass
254
- constraints.append({
255
- 'type': 'eq',
256
- 'fun': lambda psi: numpy.trapz(density(grid, psi), grid) - 1.0
257
- })
258
-
259
- # Enforce zero at left edge
260
- if beta <= 0.0 and beta > -0.5:
261
- constraints.append({
262
- 'type': 'eq',
263
- 'fun': lambda psi: density(numpy.array([lam_m]), psi)[0]
264
- })
265
-
266
- # Enforce zero at right edge
267
- if alpha <= 0.0 and alpha > -0.5:
268
- constraints.append({
269
- 'type': 'eq',
270
- 'fun': lambda psi: density(numpy.array([lam_p]), psi)[0]
271
- })
272
-
273
- # Solve a small quadratic programming
274
- res = minimize(fun, psi0, jac=grad,
275
- constraints=constraints,
276
- # method='trust-constr',
277
- method='SLSQP',
278
- options={'maxiter': 1000, 'ftol': 1e-9, 'eps': 1e-8})
279
-
280
- psi = res.x
281
-
282
- # Normalize first mode to unit mass
283
- x = numpy.linspace(lam_m, lam_p, 1000)
284
- rho = density(x, psi)
285
- mass = numpy.trapezoid(rho, x)
286
- psi[0] = psi[0] / mass
287
-
288
- 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