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,448 @@
|
|
|
1
|
+
# =======
|
|
2
|
+
# Imports
|
|
3
|
+
# =======
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# =======
|
|
9
|
+
# Moments
|
|
10
|
+
# =======
|
|
11
|
+
|
|
12
|
+
class Moments(object):
|
|
13
|
+
"""
|
|
14
|
+
Moments :math:`\\mu_n(t)` generated from eigenvalues, under
|
|
15
|
+
free decompression, where
|
|
16
|
+
|
|
17
|
+
.. math::
|
|
18
|
+
|
|
19
|
+
m_n = \\mu_n(0) = \\mathbb{E}[\\lambda^n],
|
|
20
|
+
|
|
21
|
+
and :math:`\\lambda` denotes an eigenvalue sample.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
|
|
26
|
+
source : array_like or callable
|
|
27
|
+
Either
|
|
28
|
+
|
|
29
|
+
* a 1D array of eigenvalues (or samples), or
|
|
30
|
+
* a callable returning the raw moments at zero, ``source(n) = m_n``.
|
|
31
|
+
|
|
32
|
+
If an array is provided, moments are estimated via sample averages.
|
|
33
|
+
If a callable is provided, it is assumed to return exact values of
|
|
34
|
+
:math:`m_n`.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
|
|
39
|
+
eig : numpy.ndarray or None
|
|
40
|
+
Eigenvalue samples, if provided.
|
|
41
|
+
|
|
42
|
+
Methods
|
|
43
|
+
-------
|
|
44
|
+
|
|
45
|
+
m
|
|
46
|
+
Compute the raw moment :math:`m_n = \\mathbb{E}[\\lambda^n]`.
|
|
47
|
+
|
|
48
|
+
coeffs
|
|
49
|
+
Compute the coefficient vector :math:`a_n`.
|
|
50
|
+
|
|
51
|
+
__call__
|
|
52
|
+
Evaluate :math:`\\mu_n(t)` for a given :math:`n` and :math:`t`.
|
|
53
|
+
|
|
54
|
+
Notes
|
|
55
|
+
-----
|
|
56
|
+
|
|
57
|
+
The recursion memoizes:
|
|
58
|
+
|
|
59
|
+
* Moments ``_m[n] = m_n``.
|
|
60
|
+
* Coefficients ``_a[n] = a_n`` where ``a_n`` has length ``n`` and contains
|
|
61
|
+
:math:`(a_{n,0}, \\dots, a_{n,n-1})`.
|
|
62
|
+
|
|
63
|
+
The coefficient row :math:`a_n` is computed using an intermediate quantity
|
|
64
|
+
:math:`R_{n,k}` formed via discrete convolutions of previous rows.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# ====
|
|
68
|
+
# init
|
|
69
|
+
# ====
|
|
70
|
+
|
|
71
|
+
def __init__(self, source):
|
|
72
|
+
"""
|
|
73
|
+
Initialization.
|
|
74
|
+
"""
|
|
75
|
+
self.eig = None
|
|
76
|
+
self._moment_fn = None
|
|
77
|
+
|
|
78
|
+
if callable(source):
|
|
79
|
+
self._moment_fn = source
|
|
80
|
+
else:
|
|
81
|
+
self.eig = numpy.asarray(source, dtype=float)
|
|
82
|
+
|
|
83
|
+
# Memoized moments m_n
|
|
84
|
+
self._m = {0: 1.0}
|
|
85
|
+
|
|
86
|
+
# Memoized coefficients a[n] = array of length n
|
|
87
|
+
# (a_{n,0},...,a_{n,n-1})
|
|
88
|
+
self._a = {0: numpy.array([1.0])}
|
|
89
|
+
|
|
90
|
+
# =
|
|
91
|
+
# m
|
|
92
|
+
# =
|
|
93
|
+
|
|
94
|
+
def m(self, n):
|
|
95
|
+
"""
|
|
96
|
+
Compute raw moment :math:`m_n`.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
|
|
101
|
+
n : int
|
|
102
|
+
Order of the moment.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
|
|
107
|
+
m_n : float
|
|
108
|
+
The raw moment :math:`m_n = \\mathbb{E}[\\lambda^n]`.
|
|
109
|
+
|
|
110
|
+
Notes
|
|
111
|
+
-----
|
|
112
|
+
|
|
113
|
+
If the instance was initialized with eigenvalue samples, the moment is
|
|
114
|
+
estimated by the sample mean of ``eig**n``. If initialized with a
|
|
115
|
+
callable, the callable is used directly.
|
|
116
|
+
"""
|
|
117
|
+
n = int(n)
|
|
118
|
+
if n < 0:
|
|
119
|
+
raise ValueError("Moment order n must be >= 0.")
|
|
120
|
+
|
|
121
|
+
if n not in self._m:
|
|
122
|
+
if self._moment_fn is not None:
|
|
123
|
+
self._m[n] = float(self._moment_fn(n))
|
|
124
|
+
else:
|
|
125
|
+
self._m[n] = float(numpy.mean(self.eig ** n))
|
|
126
|
+
|
|
127
|
+
return self._m[n]
|
|
128
|
+
|
|
129
|
+
# ======
|
|
130
|
+
# coeffs
|
|
131
|
+
# ======
|
|
132
|
+
|
|
133
|
+
def coeffs(self, n):
|
|
134
|
+
"""
|
|
135
|
+
Get coefficients :math:`a_n` for :math:`\\mu_n(t)`.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
|
|
140
|
+
n : int
|
|
141
|
+
Order of :math:`\\mu_n(t)`.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
|
|
146
|
+
a_n : numpy.ndarray
|
|
147
|
+
Array of shape ``(n,)`` containing :math:`(a_{n,0},
|
|
148
|
+
\\dots, a_{n,n-1})`.
|
|
149
|
+
"""
|
|
150
|
+
n = int(n)
|
|
151
|
+
if n < 0:
|
|
152
|
+
raise ValueError("Order n must be >= 0.")
|
|
153
|
+
|
|
154
|
+
if n in self._a:
|
|
155
|
+
return self._a[n]
|
|
156
|
+
|
|
157
|
+
# Ensure previous rows exist
|
|
158
|
+
for r in range(1, n):
|
|
159
|
+
if r not in self._a:
|
|
160
|
+
self._compute_row(r)
|
|
161
|
+
|
|
162
|
+
self._compute_row(n)
|
|
163
|
+
return self._a[n]
|
|
164
|
+
|
|
165
|
+
# ===========
|
|
166
|
+
# compute row
|
|
167
|
+
# ===========
|
|
168
|
+
|
|
169
|
+
def _compute_row(self, n):
|
|
170
|
+
"""
|
|
171
|
+
Compute and memoize the coefficient row :math:`a_n`.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
|
|
176
|
+
n : int
|
|
177
|
+
Row index to compute.
|
|
178
|
+
"""
|
|
179
|
+
if n in self._a:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
if n == 1:
|
|
183
|
+
self._a[1] = numpy.array([self.m(1)])
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# Ensure all smaller rows exist
|
|
187
|
+
for r in range(1, n):
|
|
188
|
+
if r not in self._a:
|
|
189
|
+
self._compute_row(r)
|
|
190
|
+
|
|
191
|
+
a_n = numpy.zeros(n, dtype=float)
|
|
192
|
+
|
|
193
|
+
# Compute R_{n,k} via convolutions
|
|
194
|
+
R = numpy.zeros(n - 1, dtype=float)
|
|
195
|
+
for i in range(1, n):
|
|
196
|
+
conv = numpy.convolve(self._a[i], self._a[n - i])
|
|
197
|
+
R += conv[: n - 1]
|
|
198
|
+
|
|
199
|
+
k = numpy.arange(n - 1, dtype=float)
|
|
200
|
+
factors = (1.0 + 0.5 * k) / (n - 1 - k)
|
|
201
|
+
a_n[: n - 1] = factors * R
|
|
202
|
+
|
|
203
|
+
# k = n-1 from the initial condition mu_n(0) = m_n
|
|
204
|
+
a_n[n - 1] = self.m(n) - a_n[: n - 1].sum()
|
|
205
|
+
|
|
206
|
+
self._a[n] = a_n
|
|
207
|
+
|
|
208
|
+
# --------
|
|
209
|
+
# evaluate
|
|
210
|
+
# --------
|
|
211
|
+
|
|
212
|
+
def __call__(self, n, t=0.0):
|
|
213
|
+
"""
|
|
214
|
+
Evaluate :math:`\\mu_n(t)`.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
|
|
219
|
+
n : int
|
|
220
|
+
Order of :math:`\\mu_n(t)`.
|
|
221
|
+
|
|
222
|
+
t : float, default=0.0
|
|
223
|
+
Deformation parameter.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
|
|
228
|
+
mu_n : float
|
|
229
|
+
The value of :math:`\\mu_n(t)`.
|
|
230
|
+
|
|
231
|
+
Notes
|
|
232
|
+
-----
|
|
233
|
+
|
|
234
|
+
This function evaluates
|
|
235
|
+
|
|
236
|
+
.. math::
|
|
237
|
+
|
|
238
|
+
\\mu_n(t) = \\sum_{k=0}^{n-1} a_{n,k} \\, e^{k t}.
|
|
239
|
+
|
|
240
|
+
For ``n == 0``, it returns ``1.0``.
|
|
241
|
+
"""
|
|
242
|
+
n = int(n)
|
|
243
|
+
if n < 0:
|
|
244
|
+
raise ValueError("Order n must be >= 0.")
|
|
245
|
+
if n == 0:
|
|
246
|
+
return 1.0
|
|
247
|
+
|
|
248
|
+
a_n = self.coeffs(n)
|
|
249
|
+
k = numpy.arange(n, dtype=float)
|
|
250
|
+
return float(numpy.dot(a_n, numpy.exp(k * t)))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ===========================
|
|
255
|
+
# Algebraic Stieltjes Moments
|
|
256
|
+
# ===========================
|
|
257
|
+
|
|
258
|
+
class AlgebraicStieltjesMoments(object):
|
|
259
|
+
"""
|
|
260
|
+
Given coefficients a[i,j] for P(z,m)=sum_{i,j} a[i,j] z^i m^j,
|
|
261
|
+
compute the large-|z| branch
|
|
262
|
+
m(z) = sum_{k>=0} mu_series[k] / z^{k+1}.
|
|
263
|
+
|
|
264
|
+
Convention here: choose mu0 (the leading coefficient) by solving the
|
|
265
|
+
leading-diagonal equation and (by default) picking the root closest
|
|
266
|
+
to -1, i.e. m(z) ~ -1/z.
|
|
267
|
+
|
|
268
|
+
The returned 'moments(N)' are normalized density moments:
|
|
269
|
+
mu_density[k] = mu_series[k] / mu_series[0]
|
|
270
|
+
so mu_density[0] = 1.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def __init__(self, a, mu0=None):
|
|
274
|
+
self.a = numpy.asarray(a)
|
|
275
|
+
# Ensure valid
|
|
276
|
+
self.a[-1, 0] = 0.0
|
|
277
|
+
if self.a.ndim != 2:
|
|
278
|
+
raise ValueError("a must be a 2D NumPy array with a[i,j]=a_{ij}.")
|
|
279
|
+
|
|
280
|
+
self.I = self.a.shape[0] - 1 # noqa: E741
|
|
281
|
+
self.J = self.a.shape[1] - 1
|
|
282
|
+
|
|
283
|
+
nz = numpy.argwhere(self.a != 0)
|
|
284
|
+
if nz.size == 0:
|
|
285
|
+
raise ValueError("All coefficients are zero.")
|
|
286
|
+
|
|
287
|
+
# r = max(i-j) over nonzero terms
|
|
288
|
+
self.r = int(numpy.max(nz[:, 0] - nz[:, 1]))
|
|
289
|
+
|
|
290
|
+
# Group coefficients by diagonal offset s = r - (i-j) >= 0
|
|
291
|
+
# diag[s] is list of (j, a_ij) for which i-j = r-s
|
|
292
|
+
self.diag = {}
|
|
293
|
+
for i, j in nz:
|
|
294
|
+
i = int(i)
|
|
295
|
+
j = int(j)
|
|
296
|
+
coeff = self.a[i, j]
|
|
297
|
+
s = self.r - (i - j)
|
|
298
|
+
if s >= 0:
|
|
299
|
+
self.diag.setdefault(int(s), []).append((j, coeff))
|
|
300
|
+
|
|
301
|
+
# Choose mu0 (series leading coefficient). This should be
|
|
302
|
+
# -1 for m(z) ~ -1/z, but it may only hold approximately.
|
|
303
|
+
if mu0 is None:
|
|
304
|
+
self.mu0 = self._solve_mu0()
|
|
305
|
+
else:
|
|
306
|
+
self.mu0 = mu0
|
|
307
|
+
|
|
308
|
+
# Precompute mu0^p up to p=J
|
|
309
|
+
self.mu0pow = [1]
|
|
310
|
+
for _ in range(self.J):
|
|
311
|
+
self.mu0pow.append(self.mu0pow[-1] * self.mu0)
|
|
312
|
+
|
|
313
|
+
# Linear coefficient A0 = sum_{i-j=r} j a_ij mu0^{j-1}
|
|
314
|
+
self.A0 = 0
|
|
315
|
+
for j, coeff in self.diag.get(0, []):
|
|
316
|
+
if j > 0:
|
|
317
|
+
self.A0 += j * coeff * self.mu0pow[j - 1]
|
|
318
|
+
if self.A0 == 0:
|
|
319
|
+
raise ValueError("A0 is zero for this mu0; the sequential " +
|
|
320
|
+
"recursion is degenerate.")
|
|
321
|
+
|
|
322
|
+
# Stored series moments mu_series[0..]
|
|
323
|
+
self._mu = [self.mu0]
|
|
324
|
+
|
|
325
|
+
# Convolution table c[j][n] = coefficient of w^n in (S(w))^j,
|
|
326
|
+
# where S(w) = sum_{t>=0} mu_series[t] w^t and m(z)=w S(w), w=1/z.
|
|
327
|
+
#
|
|
328
|
+
# We store c as lists growing in n: c[j][n] for j=0..J.
|
|
329
|
+
self._c = [[0] for _ in range(self.J + 1)]
|
|
330
|
+
self._c[0][0] = 1
|
|
331
|
+
for j in range(1, self.J + 1):
|
|
332
|
+
self._c[j][0] = self.mu0pow[j]
|
|
333
|
+
|
|
334
|
+
def _solve_mu0(self):
|
|
335
|
+
# Leading diagonal polynomial L(m) = sum_{i-j=r} a_ij m^j.
|
|
336
|
+
# That means i = j + r, so coefficient is a[j+r, j] if in bounds.
|
|
337
|
+
coeffs = numpy.zeros(self.J + 1, dtype=numpy.complex128)
|
|
338
|
+
for j in range(self.J + 1):
|
|
339
|
+
i = j + self.r
|
|
340
|
+
if 0 <= i <= self.I:
|
|
341
|
+
coeffs[j] = self.a[i, j]
|
|
342
|
+
|
|
343
|
+
if not numpy.any(coeffs != 0):
|
|
344
|
+
raise ValueError("Leading diagonal polynomial is identically " +
|
|
345
|
+
"zero; cannot determine mu0.")
|
|
346
|
+
|
|
347
|
+
deg = int(numpy.max(numpy.nonzero(coeffs)[0]))
|
|
348
|
+
|
|
349
|
+
# descending powers for numpy.roots
|
|
350
|
+
roots = numpy.roots(coeffs[:deg + 1][::-1])
|
|
351
|
+
|
|
352
|
+
# Targetting mu0 = -1 for ~ -1/z asymptotics
|
|
353
|
+
mu0 = roots[numpy.argmin(numpy.abs(roots + 1))]
|
|
354
|
+
|
|
355
|
+
if abs(mu0.imag) < 1e-12:
|
|
356
|
+
mu0 = mu0.real
|
|
357
|
+
return mu0
|
|
358
|
+
|
|
359
|
+
def _ensure(self, N):
|
|
360
|
+
# Compute mu_series up to index N (inclusive)
|
|
361
|
+
while len(self._mu) <= N:
|
|
362
|
+
k = len(self._mu) # compute mu_k
|
|
363
|
+
|
|
364
|
+
# Compute f[j] = coefficient of w^k in (S_trunc(w))^j,
|
|
365
|
+
# where S_trunc uses mu_0..mu_{k-1} only (i.e. mu_k treated as 0).
|
|
366
|
+
# Key fact: in the true c[j,k], mu_k can only appear linearly as
|
|
367
|
+
# j*mu_k*mu0^{j-1}.
|
|
368
|
+
f = [0] * (self.J + 1)
|
|
369
|
+
f[0] = 0
|
|
370
|
+
for j in range(1, self.J + 1):
|
|
371
|
+
ssum = 0
|
|
372
|
+
# sum_{t=1..k-1} mu_t * c[j-1, k-t]
|
|
373
|
+
for t in range(1, k):
|
|
374
|
+
ssum += self._mu[t] * self._c[j - 1][k - t]
|
|
375
|
+
# recurrence: c[j,k] = mu0*c[j-1,k] + sum_{t=1..k-1}
|
|
376
|
+
# mu_t*c[j-1,k-t] + mu_k*c[j-1,0] with mu_k=0 for f,
|
|
377
|
+
# and c[j-1,k]=f[j-1]
|
|
378
|
+
f[j] = self.mu0 * f[j - 1] + ssum
|
|
379
|
+
|
|
380
|
+
# Build the linear equation for mu_k:
|
|
381
|
+
# A0*mu_k + rest = 0
|
|
382
|
+
rest = 0
|
|
383
|
+
|
|
384
|
+
# s=0 diagonal contributes coeff*(f[j]) (the mu_k-free part)
|
|
385
|
+
for j, coeff in self.diag.get(0, []):
|
|
386
|
+
if j == 0:
|
|
387
|
+
# only affects k=0, but we never come here with k=0
|
|
388
|
+
continue
|
|
389
|
+
rest += coeff * f[j]
|
|
390
|
+
|
|
391
|
+
# lower diagonals s=1..k contribute coeff*c[j,k-s] (already known
|
|
392
|
+
# since k-s < k)
|
|
393
|
+
for s in range(1, k + 1):
|
|
394
|
+
entries = self.diag.get(s)
|
|
395
|
+
if not entries:
|
|
396
|
+
continue
|
|
397
|
+
n = k - s
|
|
398
|
+
for j, coeff in entries:
|
|
399
|
+
if j == 0:
|
|
400
|
+
if n == 0:
|
|
401
|
+
rest += coeff
|
|
402
|
+
else:
|
|
403
|
+
rest += coeff * self._c[j][n]
|
|
404
|
+
|
|
405
|
+
mu_k = -rest / self.A0
|
|
406
|
+
self._mu.append(mu_k)
|
|
407
|
+
|
|
408
|
+
# Now append the new column k to c using the full convolution
|
|
409
|
+
# recurrence:
|
|
410
|
+
# c[j,k] = sum_{t=0..k} mu_t * c[j-1,k-t]
|
|
411
|
+
for j in range(self.J + 1):
|
|
412
|
+
self._c[j].append(0)
|
|
413
|
+
|
|
414
|
+
self._c[0][k] = 0
|
|
415
|
+
for j in range(1, self.J + 1):
|
|
416
|
+
val = 0
|
|
417
|
+
for t in range(0, k + 1):
|
|
418
|
+
val += self._mu[t] * self._c[j - 1][k - t]
|
|
419
|
+
self._c[j][k] = val
|
|
420
|
+
|
|
421
|
+
# --- API ---
|
|
422
|
+
|
|
423
|
+
def __call__(self, k):
|
|
424
|
+
self._ensure(k)
|
|
425
|
+
return self._mu[k] / self._mu[0]
|
|
426
|
+
|
|
427
|
+
def moments(self, N):
|
|
428
|
+
# normalized density moments so moment 0 is 1
|
|
429
|
+
self._ensure(N)
|
|
430
|
+
mu0 = self._mu[0]
|
|
431
|
+
return numpy.array([self._mu[k] / mu0 for k in range(N + 1)])
|
|
432
|
+
|
|
433
|
+
def radius(self, N):
|
|
434
|
+
# Estimate the radius of convergence of the Stieltjes
|
|
435
|
+
# series
|
|
436
|
+
if N < 3:
|
|
437
|
+
raise RuntimeError("N is too small, choose a larger value.")
|
|
438
|
+
self._ensure(N)
|
|
439
|
+
return max([numpy.abs(self._mu[j] / self._mu[j-1])
|
|
440
|
+
for j in range(2, N+1)])
|
|
441
|
+
|
|
442
|
+
def stieltjes(self, z, N):
|
|
443
|
+
# Estimate Stieltjes transform (root) using moment
|
|
444
|
+
# expansion
|
|
445
|
+
z = numpy.asarray(z)
|
|
446
|
+
mu = self.moments(N)
|
|
447
|
+
return -numpy.sum(z[..., numpy.newaxis]**(-numpy.arange(N+1)-1) * mu,
|
|
448
|
+
axis=-1)
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
|
|
16
|
+
__all__ = ['_pick_physical_root_scalar', 'track_roots_on_grid',
|
|
17
|
+
'infer_m1_partners_on_cuts']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =========================
|
|
21
|
+
# pick physical root scalar
|
|
22
|
+
# =========================
|
|
23
|
+
|
|
24
|
+
def _pick_physical_root_scalar(z, roots):
|
|
25
|
+
"""
|
|
26
|
+
Pick the Herglotz root: Im(root) has the same sign as Im(z).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
s = 1.0 if (z.imag >= 0.0) else -1.0
|
|
30
|
+
k = int(numpy.argmax(s * roots.imag))
|
|
31
|
+
return roots[k]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ============
|
|
35
|
+
# permutations
|
|
36
|
+
# ============
|
|
37
|
+
|
|
38
|
+
def _permutations(items):
|
|
39
|
+
|
|
40
|
+
items = list(items)
|
|
41
|
+
if len(items) <= 1:
|
|
42
|
+
yield tuple(items)
|
|
43
|
+
return
|
|
44
|
+
for i in range(len(items)):
|
|
45
|
+
rest = items[:i] + items[i + 1:]
|
|
46
|
+
for p in _permutations(rest):
|
|
47
|
+
yield (items[i],) + p
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ===================
|
|
51
|
+
# track roots on grid
|
|
52
|
+
# ===================
|
|
53
|
+
|
|
54
|
+
def track_roots_on_grid(m_all, z=None, i0=0, j0=0):
|
|
55
|
+
|
|
56
|
+
m_all = numpy.asarray(m_all, dtype=numpy.complex128)
|
|
57
|
+
n_y, n_x, s = m_all.shape
|
|
58
|
+
|
|
59
|
+
sheets = numpy.full_like(m_all, numpy.nan + 1j * numpy.nan)
|
|
60
|
+
|
|
61
|
+
perms = numpy.array(list(_permutations(range(s))), dtype=int)
|
|
62
|
+
|
|
63
|
+
def sort_seed(v):
|
|
64
|
+
v = numpy.asarray(v, dtype=numpy.complex128)
|
|
65
|
+
order = numpy.argsort(-numpy.imag(v))
|
|
66
|
+
return v[order]
|
|
67
|
+
|
|
68
|
+
v0 = m_all[i0, j0, :]
|
|
69
|
+
if numpy.all(numpy.isfinite(v0)):
|
|
70
|
+
sheets[i0, j0, :] = sort_seed(v0)
|
|
71
|
+
|
|
72
|
+
for i in range(i0, n_y):
|
|
73
|
+
for j in range((j0 if i == i0 else 0), n_x):
|
|
74
|
+
if i == i0 and j == j0:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
v = m_all[i, j, :]
|
|
78
|
+
if not numpy.all(numpy.isfinite(v)):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if j > 0 and numpy.all(numpy.isfinite(sheets[i, j - 1, :])):
|
|
82
|
+
ref = sheets[i, j - 1, :]
|
|
83
|
+
elif i > 0 and numpy.all(numpy.isfinite(sheets[i - 1, j, :])):
|
|
84
|
+
ref = sheets[i - 1, j, :]
|
|
85
|
+
else:
|
|
86
|
+
sheets[i, j, :] = sort_seed(v)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
v_perm = v[perms]
|
|
90
|
+
cost = numpy.abs(v_perm - ref[None, :]).sum(axis=1)
|
|
91
|
+
p = perms[int(numpy.argmin(cost))]
|
|
92
|
+
sheets[i, j, :] = v[p]
|
|
93
|
+
|
|
94
|
+
if z is not None:
|
|
95
|
+
z = numpy.asarray(z)
|
|
96
|
+
if z.shape != (n_y, n_x):
|
|
97
|
+
raise ValueError("z must have shape (n_y, n_x) matching m_all.")
|
|
98
|
+
mask_up = numpy.imag(z) > 0.0
|
|
99
|
+
scores = numpy.full(s, -numpy.inf, dtype=numpy.float64)
|
|
100
|
+
for r in range(s):
|
|
101
|
+
v = sheets[:, :, r]
|
|
102
|
+
vv = v[mask_up]
|
|
103
|
+
finite = numpy.isfinite(vv)
|
|
104
|
+
if numpy.any(finite):
|
|
105
|
+
scores[r] = float(numpy.mean(numpy.imag(vv[finite])))
|
|
106
|
+
r_phys = int(numpy.argmax(scores))
|
|
107
|
+
perm = [r_phys] + [r for r in range(s) if r != r_phys]
|
|
108
|
+
sheets = sheets[:, :, perm]
|
|
109
|
+
|
|
110
|
+
return sheets
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# =========================
|
|
114
|
+
# infer m1 partners on cuts
|
|
115
|
+
# =========================
|
|
116
|
+
|
|
117
|
+
def infer_m1_partners_on_cuts(z, sheets, support):
|
|
118
|
+
# sheets: [m1, m2, m3] arrays on the same z-grid
|
|
119
|
+
X = numpy.real(z[0, :])
|
|
120
|
+
ycol = numpy.imag(z[:, 0])
|
|
121
|
+
|
|
122
|
+
# pick nearest rows just above and below 0
|
|
123
|
+
i_up = numpy.where(ycol > 0)[0][0]
|
|
124
|
+
i_dn = numpy.where(ycol < 0)[0][-1]
|
|
125
|
+
|
|
126
|
+
partners = []
|
|
127
|
+
for (a, b) in support:
|
|
128
|
+
x0 = 0.5 * (a + b)
|
|
129
|
+
j = int(numpy.argmin(numpy.abs(X - x0)))
|
|
130
|
+
|
|
131
|
+
m1_up = sheets[0][i_up, j]
|
|
132
|
+
m1_dn = sheets[0][i_dn, j]
|
|
133
|
+
|
|
134
|
+
# who matches across the cut?
|
|
135
|
+
d_up_to_dn = [abs(m1_up - sheets[k][i_dn, j])
|
|
136
|
+
for k in range(len(sheets))]
|
|
137
|
+
d_dn_to_up = [abs(m1_dn - sheets[k][i_up, j])
|
|
138
|
+
for k in range(len(sheets))]
|
|
139
|
+
|
|
140
|
+
# ignore k=0 (trivial match away from cuts); take best among {1,2}
|
|
141
|
+
k1 = min([1, 2], key=lambda k: d_up_to_dn[k] + d_dn_to_up[k])
|
|
142
|
+
partners.append(k1)
|
|
143
|
+
|
|
144
|
+
# e.g. [1,2] means I1 swaps with m2, I2 swaps with m3
|
|
145
|
+
return partners
|