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
freealg/_util.py
CHANGED
|
@@ -13,137 +13,83 @@
|
|
|
13
13
|
|
|
14
14
|
import numpy
|
|
15
15
|
import scipy
|
|
16
|
-
from scipy.stats import beta
|
|
17
|
-
from scipy.optimize import minimize
|
|
18
16
|
|
|
19
|
-
__all__ = ['
|
|
17
|
+
__all__ = ['resolve_complex_dtype', 'compute_eig', 'subsample_matrix']
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
20
|
+
# =====================
|
|
21
|
+
# resolve complex dtype
|
|
22
|
+
# =====================
|
|
25
23
|
|
|
26
|
-
def
|
|
24
|
+
def resolve_complex_dtype(dtype):
|
|
27
25
|
"""
|
|
28
|
-
|
|
26
|
+
Convert a user-supplied dtype name to a NumPy dtype object and fall back
|
|
27
|
+
safely if the requested precision is unavailable.
|
|
29
28
|
"""
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
# Normalise the string
|
|
31
|
+
dtype = str(dtype).lower()
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
if not isinstance(numpy.dtype(dtype), numpy.dtype):
|
|
34
|
+
raise ValueError(f'{dtype} is not a recognized numpy dtype.')
|
|
35
|
+
elif not numpy.issubdtype(numpy.dtype(dtype), numpy.complexfloating):
|
|
36
|
+
raise ValueError(f'{dtype} is not a complex dtype.')
|
|
34
37
|
|
|
38
|
+
if dtype in {'complex128', '128'}:
|
|
39
|
+
cdtype = numpy.complex128
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
# beta kde
|
|
38
|
-
# ========
|
|
41
|
+
elif dtype in ['complex256', '256', 'longcomplex', 'clongcomplex']:
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
----------
|
|
46
|
-
eig : (n,) 1-D array of samples
|
|
47
|
-
xs : evaluation grid (must lie within [lam_m, lam_p])
|
|
48
|
-
lam_m, lam_p : float, support endpoints (lam_m < lam_p)
|
|
49
|
-
h : bandwidth in rescaled units (0 < h < 1)
|
|
50
|
-
|
|
51
|
-
Returns
|
|
52
|
-
-------
|
|
53
|
-
pdf : ndarray same length as xs
|
|
54
|
-
"""
|
|
43
|
+
complex256_found = False
|
|
44
|
+
for name in ['complex256', 'clongcomplex']:
|
|
45
|
+
if hasattr(numpy, name):
|
|
46
|
+
cdtype = getattr(numpy, name)
|
|
47
|
+
complex256_found = True
|
|
55
48
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
if not complex256_found:
|
|
50
|
+
raise RuntimeWarning(
|
|
51
|
+
'NumPy on this platform has no 256-bit complex type. ' +
|
|
52
|
+
'Falling back to complex128.')
|
|
53
|
+
cdtype = numpy.complex128
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
t = (xs - lam_m) / span
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError('Unsupported dtype.')
|
|
63
57
|
|
|
64
|
-
|
|
65
|
-
mask = (u > 0) & (u < 1)
|
|
66
|
-
u = u[mask]
|
|
58
|
+
return cdtype
|
|
67
59
|
|
|
68
|
-
pdf = numpy.zeros_like(xs, dtype=float)
|
|
69
|
-
n = len(u)
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
# ===========
|
|
62
|
+
# compute eig
|
|
63
|
+
# ===========
|
|
64
|
+
|
|
65
|
+
def compute_eig(A, lower=False):
|
|
66
|
+
"""
|
|
67
|
+
Compute eigenvalues of symmetric matrix.
|
|
68
|
+
"""
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
pdf[(t < 0) | (t > 1)] = 0.0 # exact zeros outside
|
|
70
|
+
eig = scipy.linalg.eigvalsh(A, lower=lower, driver='ev')
|
|
80
71
|
|
|
81
|
-
return
|
|
72
|
+
return eig
|
|
82
73
|
|
|
83
74
|
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
75
|
+
# ================
|
|
76
|
+
# subsample matrix
|
|
77
|
+
# ================
|
|
87
78
|
|
|
88
|
-
def
|
|
79
|
+
def subsample_matrix(matrix, submatrix_size, seed=None):
|
|
89
80
|
"""
|
|
90
|
-
|
|
91
|
-
min 0.5 ||psi - psi0||^2
|
|
92
|
-
s.t. F_pos psi >= 0 (positivity on grid)
|
|
93
|
-
psi[0] = psi0[0] (mass)
|
|
94
|
-
f(lam_m)·psi = 0 (zero at left edge)
|
|
95
|
-
f(lam_p)·psi = 0 (zero at right edge)
|
|
81
|
+
Generate a random subsample of a larger matrix
|
|
96
82
|
"""
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# Enforce positivity
|
|
111
|
-
constraints.append({'type': 'ineq',
|
|
112
|
-
'fun': lambda psi: approx(grid, psi)})
|
|
113
|
-
|
|
114
|
-
# Enforce unit mass
|
|
115
|
-
constraints.append({
|
|
116
|
-
'type': 'eq',
|
|
117
|
-
'fun': lambda psi: numpy.trapz(approx(grid, psi), grid) - 1.0
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
# Enforce zero at left edge
|
|
121
|
-
if beta <= 0.0 and beta > -0.5:
|
|
122
|
-
constraints.append({
|
|
123
|
-
'type': 'eq',
|
|
124
|
-
'fun': lambda psi: approx(numpy.array([lam_m]), psi)[0]
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
# Enforce zero at right edge
|
|
128
|
-
if alpha <= 0.0 and alpha > -0.5:
|
|
129
|
-
constraints.append({
|
|
130
|
-
'type': 'eq',
|
|
131
|
-
'fun': lambda psi: approx(numpy.array([lam_p]), psi)[0]
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
# Solve a small quadratic programming
|
|
135
|
-
res = minimize(fun, psi0, jac=grad,
|
|
136
|
-
constraints=constraints,
|
|
137
|
-
# method='trust-constr',
|
|
138
|
-
method='SLSQP',
|
|
139
|
-
options={'maxiter': 1000, 'ftol': 1e-9, 'eps': 1e-8})
|
|
140
|
-
|
|
141
|
-
psi = res.x
|
|
142
|
-
|
|
143
|
-
# Normalize first mode to unit mass
|
|
144
|
-
x = numpy.linspace(lam_m, lam_p, 1000)
|
|
145
|
-
rho = approx(x, psi)
|
|
146
|
-
mass = numpy.trapezoid(rho, x)
|
|
147
|
-
psi[0] = psi[0] / mass
|
|
148
|
-
|
|
149
|
-
return psi
|
|
84
|
+
if matrix.shape[0] != matrix.shape[1]:
|
|
85
|
+
raise ValueError("Matrix must be square")
|
|
86
|
+
|
|
87
|
+
n = matrix.shape[0]
|
|
88
|
+
if submatrix_size > n:
|
|
89
|
+
raise ValueError("Submatrix size cannot exceed matrix size")
|
|
90
|
+
|
|
91
|
+
rng = numpy.random.default_rng(seed)
|
|
92
|
+
idx = rng.choice(n, size=submatrix_size, replace=False)
|
|
93
|
+
idx = numpy.sort(idx) # optional, preserves original ordering
|
|
94
|
+
|
|
95
|
+
return matrix[numpy.ix_(idx, idx)]
|
|
@@ -11,5 +11,11 @@ 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
|
|
17
|
+
from ._compound_poisson import CompoundPoisson
|
|
14
18
|
|
|
15
|
-
__all__ = ['MarchenkoPastur', 'Wigner', 'KestenMcKay', 'Wachter', 'Meixner'
|
|
19
|
+
__all__ = ['MarchenkoPastur', 'Wigner', 'KestenMcKay', 'Wachter', 'Meixner',
|
|
20
|
+
'ChiralBlock', 'DeformedWigner', 'DeformedMarchenkoPastur',
|
|
21
|
+
'CompoundPoisson']
|
|
@@ -0,0 +1,494 @@
|
|
|
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
|
+
Total size :math:`N = n + m` of the returned matrix.
|
|
408
|
+
|
|
409
|
+
seed : int, default=None
|
|
410
|
+
Seed for random number generator.
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
|
|
415
|
+
A : numpy.ndarray
|
|
416
|
+
Symmetric matrix of shape :math:`N \\times N`.
|
|
417
|
+
|
|
418
|
+
Notes
|
|
419
|
+
-----
|
|
420
|
+
|
|
421
|
+
Generate a :math:`(n+m) x (n+m)` matrix
|
|
422
|
+
|
|
423
|
+
.. math::
|
|
424
|
+
|
|
425
|
+
H =
|
|
426
|
+
\\begin{bmatrix}
|
|
427
|
+
\\alpha \\mathbf{I}_n & (1/\\sqrt{m})) \\mathbf{X} \\
|
|
428
|
+
(1/\\sqrt{m})) \\mathbf{X}^{\\intercal} & \\beta \\mathbf{I}_m
|
|
429
|
+
\\end{bmatrix}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
where :math:`\\mathbf{X}` has i.i.d. :math:`N(0,1)` entries and
|
|
433
|
+
:math:`n/m` approximates :math:`c`.
|
|
434
|
+
|
|
435
|
+
Examples
|
|
436
|
+
--------
|
|
437
|
+
|
|
438
|
+
.. code-block::python
|
|
439
|
+
|
|
440
|
+
>>> from freealg.distributions import MarchenkoPastur
|
|
441
|
+
>>> mp = MarchenkoPastur(1/50)
|
|
442
|
+
>>> A = mp.matrix(2000)
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
N = int(size)
|
|
446
|
+
if N <= 1:
|
|
447
|
+
raise ValueError("size must be an integer >= 2.")
|
|
448
|
+
|
|
449
|
+
# Unpack parameters
|
|
450
|
+
alpha = float(self.alpha)
|
|
451
|
+
beta = float(self.beta)
|
|
452
|
+
c = float(self.c)
|
|
453
|
+
|
|
454
|
+
rng = numpy.random.default_rng(seed)
|
|
455
|
+
|
|
456
|
+
# Choose n,m so that n/m approx c and n+m = N.
|
|
457
|
+
# Solve n = c m and n + m = N -> m = N/(c+1), n = cN/(c+1).
|
|
458
|
+
m = int(round(N / (c + 1.0)))
|
|
459
|
+
m = max(1, min(N - 1, m))
|
|
460
|
+
n = N - m
|
|
461
|
+
|
|
462
|
+
# Optionally refine to get ratio closer to c (cheap local search).
|
|
463
|
+
# This keeps deterministic behavior.
|
|
464
|
+
best_n = n
|
|
465
|
+
best_m = m
|
|
466
|
+
best_err = abs((n / float(m)) - c)
|
|
467
|
+
for dm in (-2, -1, 0, 1, 2):
|
|
468
|
+
mm = m + dm
|
|
469
|
+
if mm <= 0 or mm >= N:
|
|
470
|
+
continue
|
|
471
|
+
nn = N - mm
|
|
472
|
+
err = abs((nn / float(mm)) - c)
|
|
473
|
+
if err < best_err:
|
|
474
|
+
best_err = err
|
|
475
|
+
best_n = nn
|
|
476
|
+
best_m = mm
|
|
477
|
+
n = best_n
|
|
478
|
+
m = best_m
|
|
479
|
+
|
|
480
|
+
# Draw X (n x m) with i.i.d. entries
|
|
481
|
+
X = rng.standard_normal((n, m))
|
|
482
|
+
|
|
483
|
+
# Assemble H
|
|
484
|
+
H = numpy.zeros((N, N), dtype=numpy.float64)
|
|
485
|
+
|
|
486
|
+
H[:n, :n] = alpha * numpy.eye(n, dtype=numpy.float64)
|
|
487
|
+
H[n:, n:] = beta * numpy.eye(m, dtype=numpy.float64)
|
|
488
|
+
|
|
489
|
+
s = 1.0 / numpy.sqrt(float(m))
|
|
490
|
+
B = s * X
|
|
491
|
+
H[:n, n:] = B
|
|
492
|
+
H[n:, :n] = B.T
|
|
493
|
+
|
|
494
|
+
return H
|