freealg 0.7.10__tar.gz → 0.7.12__tar.gz
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-0.7.10 → freealg-0.7.12}/PKG-INFO +2 -1
- {freealg-0.7.10 → freealg-0.7.12}/freealg/__init__.py +2 -2
- freealg-0.7.12/freealg/__version__.py +1 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/__init__.py +2 -1
- freealg-0.7.12/freealg/_algebraic_form/_branch_points.py +288 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_constraints.py +53 -12
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_continuation_algebraic.py +1 -1
- freealg-0.7.12/freealg/_algebraic_form/_decompress.py +641 -0
- freealg-0.7.12/freealg/_algebraic_form/_decompress2.py +204 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_edge.py +46 -68
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_homotopy.py +62 -30
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_moments.py +44 -57
- freealg-0.7.12/freealg/_algebraic_form/_support.py +309 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/algebraic_form.py +233 -48
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/__init__.py +3 -1
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_deformed_marchenko_pastur.py +51 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_deformed_wigner.py +44 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/PKG-INFO +2 -1
- {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/SOURCES.txt +2 -1
- {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/requires.txt +1 -0
- {freealg-0.7.10 → freealg-0.7.12}/requirements.txt +2 -1
- freealg-0.7.10/freealg/__version__.py +0 -1
- freealg-0.7.10/freealg/_algebraic_form/_decompress.py +0 -649
- freealg-0.7.10/freealg/_algebraic_form/_decompress2.py +0 -86
- freealg-0.7.10/freealg/_algebraic_form/_discriminant.py +0 -226
- {freealg-0.7.10 → freealg-0.7.12}/AUTHORS.txt +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/CHANGELOG.rst +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/LICENSE.txt +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/MANIFEST.in +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/README.rst +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_sheets_util.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/__init__.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_chebyshev.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_damp.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_decompress.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_density_util.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_jacobi.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_linalg.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_pade.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_plot_util.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_sample.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_series.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_support.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/free_form.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/__init__.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_continuation_genus0.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_continuation_genus1.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_elliptic_functions.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_sphere_maps.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_torus_maps.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/geometric_form.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/_util.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_chiral_block.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_kesten_mckay.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_marchenko_pastur.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_meixner.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_wachter.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_wigner.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/visualization/__init__.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/visualization/_glue_util.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg/visualization/_rgb_hsv.py +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/dependency_links.txt +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/not-zip-safe +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/top_level.txt +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/pyproject.toml +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/setup.cfg +0 -0
- {freealg-0.7.10 → freealg-0.7.12}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: freealg
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.12
|
|
4
4
|
Summary: Free probability for large matrices
|
|
5
5
|
Home-page: https://github.com/ameli/freealg
|
|
6
6
|
Download-URL: https://github.com/ameli/freealg/archive/main.zip
|
|
@@ -37,6 +37,7 @@ Requires-Dist: matplotlib
|
|
|
37
37
|
Requires-Dist: colorcet
|
|
38
38
|
Requires-Dist: statsmodels
|
|
39
39
|
Requires-Dist: numba
|
|
40
|
+
Requires-Dist: tqdm
|
|
40
41
|
Provides-Extra: test
|
|
41
42
|
Requires-Dist: tox; extra == "test"
|
|
42
43
|
Requires-Dist: pytest-cov; extra == "test"
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
from ._free_form import FreeForm, eigvalsh, cond, norm, trace, slogdet, supp, \
|
|
10
10
|
sample, kde
|
|
11
|
-
from ._algebraic_form import AlgebraicForm
|
|
11
|
+
from ._algebraic_form import AlgebraicForm, decompress_newton
|
|
12
12
|
from ._geometric_form import GeometricForm
|
|
13
13
|
from . import visualization
|
|
14
14
|
from . import distributions
|
|
15
15
|
|
|
16
16
|
__all__ = ['FreeForm', 'distributions', 'visualization', 'eigvalsh', 'cond',
|
|
17
17
|
'norm', 'trace', 'slogdet', 'supp', 'sample', 'kde',
|
|
18
|
-
'AlgebraicForm', 'GeometricForm']
|
|
18
|
+
'AlgebraicForm', 'GeometricForm', 'decompress_newton']
|
|
19
19
|
|
|
20
20
|
from .__version__ import __version__ # noqa: F401 E402
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.12"
|
|
@@ -0,0 +1,288 @@
|
|
|
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 under
|
|
6
|
+
# the terms of the license found in the LICENSE.txt file in the root directory
|
|
7
|
+
# of this source tree.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
|
|
16
|
+
__all__ = ['compute_branch_points']
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# =========
|
|
20
|
+
# poly trim
|
|
21
|
+
# =========
|
|
22
|
+
|
|
23
|
+
def _poly_trim(p, tol):
|
|
24
|
+
p = numpy.asarray(p, dtype=float)
|
|
25
|
+
if p.size == 0:
|
|
26
|
+
return p
|
|
27
|
+
k = p.size - 1
|
|
28
|
+
while k > 0 and abs(p[k]) <= tol:
|
|
29
|
+
k -= 1
|
|
30
|
+
return p[: k + 1]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ========
|
|
34
|
+
# poly add
|
|
35
|
+
# ========
|
|
36
|
+
|
|
37
|
+
def _poly_add(a, b, tol):
|
|
38
|
+
|
|
39
|
+
n = max(len(a), len(b))
|
|
40
|
+
out = numpy.zeros(n, dtype=float)
|
|
41
|
+
out[: len(a)] += a
|
|
42
|
+
out[: len(b)] += b
|
|
43
|
+
|
|
44
|
+
return _poly_trim(out, tol)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ========
|
|
48
|
+
# poly sub
|
|
49
|
+
# ========
|
|
50
|
+
|
|
51
|
+
def _poly_sub(a, b, tol):
|
|
52
|
+
|
|
53
|
+
n = max(len(a), len(b))
|
|
54
|
+
out = numpy.zeros(n, dtype=float)
|
|
55
|
+
out[: len(a)] += a
|
|
56
|
+
out[: len(b)] -= b
|
|
57
|
+
|
|
58
|
+
return _poly_trim(out, tol)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ========
|
|
62
|
+
# poly mul
|
|
63
|
+
# ========
|
|
64
|
+
|
|
65
|
+
def _poly_mul(a, b, tol):
|
|
66
|
+
|
|
67
|
+
a = _poly_trim(a, tol)
|
|
68
|
+
b = _poly_trim(b, tol)
|
|
69
|
+
if a.size == 0 or b.size == 0:
|
|
70
|
+
return numpy.zeros(1, dtype=float)
|
|
71
|
+
out = numpy.convolve(a, b)
|
|
72
|
+
return _poly_trim(out, tol)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ===============
|
|
76
|
+
# poly div approx
|
|
77
|
+
# ===============
|
|
78
|
+
|
|
79
|
+
def _poly_div_approx(a, b, tol):
|
|
80
|
+
"""
|
|
81
|
+
Polynomial division q,r = a/b in ascending powers (numpy.polynomial
|
|
82
|
+
convention). Returns q (ascending). Remainder is ignored if it is
|
|
83
|
+
small-ish.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
a = _poly_trim(a, tol)
|
|
87
|
+
b = _poly_trim(b, tol)
|
|
88
|
+
if b.size == 0 or (b.size == 1 and abs(b[0]) <= tol):
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
"division by (near) zero polynomial in branch point resultant")
|
|
91
|
+
# numpy.polydiv uses descending powers, so flip.
|
|
92
|
+
qd, rd = numpy.polydiv(a[::-1], b[::-1])
|
|
93
|
+
q = qd[::-1]
|
|
94
|
+
r = rd[::-1]
|
|
95
|
+
# Accept small remainder (Bareiss should be exact in exact arithmetic).
|
|
96
|
+
# If not small, we still proceed with the quotient (robustness over
|
|
97
|
+
# exactness).
|
|
98
|
+
scale = max(1.0, numpy.linalg.norm(a))
|
|
99
|
+
if numpy.linalg.norm(_poly_trim(r, tol)) > 1e6 * tol * scale:
|
|
100
|
+
pass
|
|
101
|
+
return _poly_trim(q, tol)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# =================
|
|
105
|
+
# det baresiss poly
|
|
106
|
+
# =================
|
|
107
|
+
|
|
108
|
+
def _det_bareiss_poly(M, tol):
|
|
109
|
+
"""
|
|
110
|
+
Fraction-free determinant for a matrix with polynomial entries in z.
|
|
111
|
+
Polynomials are stored as 1D arrays of ascending coefficients.
|
|
112
|
+
Returns det as ascending coefficients.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
n = len(M)
|
|
116
|
+
A = [[_poly_trim(M[i][j], tol) for j in range(n)] for i in range(n)]
|
|
117
|
+
denom = numpy.array([1.0], dtype=float)
|
|
118
|
+
|
|
119
|
+
for k in range(n - 1):
|
|
120
|
+
pivot = A[k][k]
|
|
121
|
+
if pivot.size == 1 and abs(pivot[0]) <= tol:
|
|
122
|
+
swap = None
|
|
123
|
+
for i in range(k + 1, n):
|
|
124
|
+
if not (A[i][k].size == 1 and abs(A[i][k][0]) <= tol):
|
|
125
|
+
swap = i
|
|
126
|
+
break
|
|
127
|
+
if swap is None:
|
|
128
|
+
return numpy.zeros(1, dtype=float)
|
|
129
|
+
A[k], A[swap] = A[swap], A[k]
|
|
130
|
+
pivot = A[k][k]
|
|
131
|
+
|
|
132
|
+
for i in range(k + 1, n):
|
|
133
|
+
for j in range(k + 1, n):
|
|
134
|
+
num = _poly_sub(
|
|
135
|
+
_poly_mul(A[i][j], pivot, tol),
|
|
136
|
+
_poly_mul(A[i][k], A[k][j], tol),
|
|
137
|
+
tol,
|
|
138
|
+
)
|
|
139
|
+
if k > 0:
|
|
140
|
+
A[i][j] = _poly_div_approx(num, denom, tol)
|
|
141
|
+
else:
|
|
142
|
+
A[i][j] = _poly_trim(num, tol)
|
|
143
|
+
|
|
144
|
+
denom = pivot
|
|
145
|
+
|
|
146
|
+
for i in range(k + 1, n):
|
|
147
|
+
A[i][k] = numpy.array([0.0], dtype=float)
|
|
148
|
+
A[k][i] = numpy.array([0.0], dtype=float)
|
|
149
|
+
|
|
150
|
+
return _poly_trim(A[n - 1][n - 1], tol)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ======================
|
|
154
|
+
# resultant discriminant
|
|
155
|
+
# ======================
|
|
156
|
+
|
|
157
|
+
def _resultant_discriminant(a_coeffs, tol):
|
|
158
|
+
"""
|
|
159
|
+
Numerically compute Disc_m(P)(z) as a polynomial in z (ascending coeffs),
|
|
160
|
+
via Sylvester determinant evaluation on a circle + interpolation.
|
|
161
|
+
|
|
162
|
+
a_coeffs[i,j] is coeff of z^i m^j, shape (deg_z+1, s+1).
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
import numpy
|
|
166
|
+
|
|
167
|
+
a_coeffs = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
168
|
+
deg_z = a_coeffs.shape[0] - 1
|
|
169
|
+
s = a_coeffs.shape[1] - 1
|
|
170
|
+
if s < 1 or deg_z < 0:
|
|
171
|
+
return numpy.zeros(1, dtype=numpy.complex128)
|
|
172
|
+
|
|
173
|
+
# Degree bound: deg_z(Disc) <= (2s-1)*deg_z
|
|
174
|
+
D = (2 * s - 1) * deg_z
|
|
175
|
+
if D <= 0:
|
|
176
|
+
return numpy.zeros(1, dtype=numpy.complex128)
|
|
177
|
+
|
|
178
|
+
def eval_disc(z):
|
|
179
|
+
# Build P(m) coeffs in descending powers of m: p_desc[k] = coeff of
|
|
180
|
+
# m^(s-k)
|
|
181
|
+
p_asc = numpy.zeros(s + 1, dtype=numpy.complex128)
|
|
182
|
+
for j in range(s + 1):
|
|
183
|
+
p_asc[j] = numpy.polyval(a_coeffs[:, j][::-1], z) # a_j(z)
|
|
184
|
+
p_desc = p_asc[::-1]
|
|
185
|
+
|
|
186
|
+
# Q(m) = dP/dm, descending
|
|
187
|
+
q_asc = numpy.zeros(s, dtype=numpy.complex128)
|
|
188
|
+
for j in range(1, s + 1):
|
|
189
|
+
q_asc[j - 1] = j * p_asc[j]
|
|
190
|
+
q_desc = q_asc[::-1]
|
|
191
|
+
|
|
192
|
+
# Sylvester matrix of P (deg s) and Q (deg s-1): size (2s-1)x(2s-1)
|
|
193
|
+
n = 2 * s - 1
|
|
194
|
+
S = numpy.zeros((n, n), dtype=numpy.complex128)
|
|
195
|
+
|
|
196
|
+
# First (s-1) rows: shifts of P
|
|
197
|
+
for r in range(s - 1):
|
|
198
|
+
S[r, r:r + (s + 1)] = p_desc
|
|
199
|
+
|
|
200
|
+
# Next s rows: shifts of Q
|
|
201
|
+
for r in range(s):
|
|
202
|
+
rr = (s - 1) + r
|
|
203
|
+
S[rr, r:r + s] = q_desc
|
|
204
|
+
|
|
205
|
+
return numpy.linalg.det(S)
|
|
206
|
+
|
|
207
|
+
# Sample points on a circle; scale radius using coefficient magnitude
|
|
208
|
+
# (simple heuristic) (This only affects conditioning of interpolation, not
|
|
209
|
+
# correctness.)
|
|
210
|
+
scale = float(numpy.max(numpy.abs(a_coeffs))) \
|
|
211
|
+
if numpy.max(numpy.abs(a_coeffs)) > 0 else 1.0
|
|
212
|
+
R = 1.0 + 0.1 * scale
|
|
213
|
+
|
|
214
|
+
N = D + 1
|
|
215
|
+
k = numpy.arange(N, dtype=float)
|
|
216
|
+
z_samp = R * numpy.exp(2.0j * numpy.pi * k / float(N))
|
|
217
|
+
d_samp = numpy.array([eval_disc(z) for z in z_samp],
|
|
218
|
+
dtype=numpy.complex128)
|
|
219
|
+
|
|
220
|
+
# Interpolate disc(z) = sum_{j=0}^D c[j] z^j (ascending)
|
|
221
|
+
V = (z_samp[:, None] ** numpy.arange(D + 1)[None, :]).astype(
|
|
222
|
+
numpy.complex128)
|
|
223
|
+
c, _, _, _ = numpy.linalg.lstsq(V, d_samp, rcond=None)
|
|
224
|
+
|
|
225
|
+
# Trim tiny coefficients
|
|
226
|
+
c = _poly_trim(c, tol)
|
|
227
|
+
if c.size == 0:
|
|
228
|
+
c = numpy.zeros(1, dtype=numpy.complex128)
|
|
229
|
+
|
|
230
|
+
# If numerics leave small imag, kill it (disc should be real-coeff if
|
|
231
|
+
# a_coeffs real)
|
|
232
|
+
if numpy.linalg.norm(c.imag) <= \
|
|
233
|
+
1e3 * tol * max(1.0, numpy.linalg.norm(c.real)):
|
|
234
|
+
c = c.real.astype(numpy.float64)
|
|
235
|
+
|
|
236
|
+
return c
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# =====================
|
|
240
|
+
# compute branch points
|
|
241
|
+
# =====================
|
|
242
|
+
|
|
243
|
+
def compute_branch_points(a_coeffs, tol=1e-12, real_tol=None):
|
|
244
|
+
"""
|
|
245
|
+
Compute global branch points of the affine curve P(z,m)=0 by
|
|
246
|
+
z-roots of Disc_m(P)(z) = Res_m(P, dP/dm).
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
z_bp : complex ndarray
|
|
251
|
+
a_s_zero : complex ndarray
|
|
252
|
+
info : dict
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
a_coeffs = numpy.asarray(a_coeffs, dtype=float)
|
|
256
|
+
s = a_coeffs.shape[1] - 1
|
|
257
|
+
if s < 1:
|
|
258
|
+
if real_tol is None:
|
|
259
|
+
real_tol = 1e3 * tol
|
|
260
|
+
return \
|
|
261
|
+
numpy.array([], dtype=complex), \
|
|
262
|
+
numpy.array([], dtype=complex), \
|
|
263
|
+
{
|
|
264
|
+
"disc": numpy.zeros(1, dtype=float),
|
|
265
|
+
"tol": float(tol),
|
|
266
|
+
"real_tol": float(real_tol),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if real_tol is None:
|
|
270
|
+
real_tol = 1e3 * tol
|
|
271
|
+
|
|
272
|
+
a_s = _poly_trim(a_coeffs[:, s], tol)
|
|
273
|
+
a_s_zero = numpy.roots(a_s[::-1]) if a_s.size > 1 else \
|
|
274
|
+
numpy.array([], dtype=complex)
|
|
275
|
+
|
|
276
|
+
disc = _resultant_discriminant(a_coeffs, tol)
|
|
277
|
+
if disc.size <= 1:
|
|
278
|
+
z_bp = numpy.array([], dtype=complex)
|
|
279
|
+
else:
|
|
280
|
+
z_bp = numpy.roots(disc[::-1])
|
|
281
|
+
|
|
282
|
+
info = {
|
|
283
|
+
"disc": disc,
|
|
284
|
+
"tol": float(tol),
|
|
285
|
+
"real_tol": float(real_tol),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return z_bp, a_s_zero, info
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# SPDX-FileCopyrightText: Copyright 2025, Siavash Ameli <sameli@berkeley.edu>
|
|
3
2
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
3
|
# SPDX-FileType: SOURCE
|
|
@@ -54,15 +53,61 @@ def _series_pow(mser, j, q_max):
|
|
|
54
53
|
# build moment constraints matrix
|
|
55
54
|
# ===============================
|
|
56
55
|
|
|
56
|
+
# def build_moment_constraint_matrix(pairs, deg_z, s, mu):
|
|
57
|
+
#
|
|
58
|
+
# mu = numpy.asarray(mu, dtype=float).ravel()
|
|
59
|
+
# if mu.size == 0:
|
|
60
|
+
# return numpy.zeros((0, len(pairs)), dtype=float)
|
|
61
|
+
#
|
|
62
|
+
# # m(z) = -sum_{p>=0} mu_p / z^{p+1}; t = 1/z so m(t) = -sum mu_p t^{p+1}
|
|
63
|
+
# r = mu.size - 1
|
|
64
|
+
# q_max = r
|
|
65
|
+
#
|
|
66
|
+
# mser = numpy.zeros(q_max + 1, dtype=float)
|
|
67
|
+
# for p in range(mu.size):
|
|
68
|
+
# q = p + 1
|
|
69
|
+
# if q <= q_max:
|
|
70
|
+
# mser[q] = -float(mu[p])
|
|
71
|
+
#
|
|
72
|
+
# # Precompute (m(t))^j coefficients up to t^{q_max}
|
|
73
|
+
# mpow = []
|
|
74
|
+
# for j in range(s + 1):
|
|
75
|
+
# mpow.append(_series_pow(mser, j, q_max))
|
|
76
|
+
#
|
|
77
|
+
# # Constraints: coeff of t^q in Q(t) := t^{deg_z} P(1/t, m(t)) must be 0
|
|
78
|
+
# # Q(t) = sum_{i,j} c_{i,j} * t^{deg_z - i} * (m(t))^j
|
|
79
|
+
# n_coef = len(pairs)
|
|
80
|
+
# B = numpy.zeros((q_max + 1, n_coef), dtype=float)
|
|
81
|
+
#
|
|
82
|
+
# for k, (i, j) in enumerate(pairs):
|
|
83
|
+
# shift = deg_z - i
|
|
84
|
+
# if shift < 0:
|
|
85
|
+
# continue
|
|
86
|
+
# mj = mpow[j]
|
|
87
|
+
# for q in range(q_max + 1):
|
|
88
|
+
# qq = q - shift
|
|
89
|
+
# if 0 <= qq <= q_max:
|
|
90
|
+
# B[q, k] = mj[qq]
|
|
91
|
+
#
|
|
92
|
+
# # Drop all-zero rows (can happen if index-set can't support higher
|
|
93
|
+
# # moments)
|
|
94
|
+
# row_norm = numpy.linalg.norm(B, axis=1)
|
|
95
|
+
# keep = row_norm > 0.0
|
|
96
|
+
# B = B[keep, :]
|
|
97
|
+
#
|
|
98
|
+
# return B
|
|
99
|
+
|
|
57
100
|
def build_moment_constraint_matrix(pairs, deg_z, s, mu):
|
|
58
101
|
|
|
59
102
|
mu = numpy.asarray(mu, dtype=float).ravel()
|
|
60
103
|
if mu.size == 0:
|
|
61
104
|
return numpy.zeros((0, len(pairs)), dtype=float)
|
|
62
105
|
|
|
63
|
-
#
|
|
106
|
+
# mu has entries mu_0..mu_r
|
|
64
107
|
r = mu.size - 1
|
|
65
|
-
|
|
108
|
+
|
|
109
|
+
# Need t^{r+1} in m(t) = -sum mu_p t^{p+1}, otherwise mu_0 is dropped.
|
|
110
|
+
q_max = r + 1
|
|
66
111
|
|
|
67
112
|
mser = numpy.zeros(q_max + 1, dtype=float)
|
|
68
113
|
for p in range(mu.size):
|
|
@@ -70,29 +115,25 @@ def build_moment_constraint_matrix(pairs, deg_z, s, mu):
|
|
|
70
115
|
if q <= q_max:
|
|
71
116
|
mser[q] = -float(mu[p])
|
|
72
117
|
|
|
73
|
-
# Precompute (m(t))^j coefficients up to t^{q_max}
|
|
74
118
|
mpow = []
|
|
75
119
|
for j in range(s + 1):
|
|
76
120
|
mpow.append(_series_pow(mser, j, q_max))
|
|
77
121
|
|
|
78
|
-
# Constraints: coeff of t^q in Q(t) := t^{deg_z} P(1/t, m(t)) must be 0
|
|
79
|
-
# Q(t) = sum_{i,j} c_{i,j} * t^{deg_z - i} * (m(t))^j
|
|
80
122
|
n_coef = len(pairs)
|
|
81
|
-
|
|
123
|
+
|
|
124
|
+
# We only want constraints for l=0..r -> that's q = 0..r in Q(t)
|
|
125
|
+
B = numpy.zeros((r + 1, n_coef), dtype=float)
|
|
82
126
|
|
|
83
127
|
for k, (i, j) in enumerate(pairs):
|
|
84
128
|
shift = deg_z - i
|
|
85
129
|
if shift < 0:
|
|
86
130
|
continue
|
|
87
131
|
mj = mpow[j]
|
|
88
|
-
for q in range(
|
|
132
|
+
for q in range(r + 1):
|
|
89
133
|
qq = q - shift
|
|
90
134
|
if 0 <= qq <= q_max:
|
|
91
135
|
B[q, k] = mj[qq]
|
|
92
136
|
|
|
93
|
-
# Drop all-zero rows (can happen if index-set can't support higher moments)
|
|
94
137
|
row_norm = numpy.linalg.norm(B, axis=1)
|
|
95
138
|
keep = row_norm > 0.0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return B
|
|
139
|
+
return B[keep, :]
|
|
@@ -289,7 +289,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
|
|
|
289
289
|
|
|
290
290
|
# Diagnostic metrics
|
|
291
291
|
fit_metrics = {
|
|
292
|
-
's_min': svals[-1],
|
|
292
|
+
's_min': float(svals[-1]),
|
|
293
293
|
'gap_ratio': float(svals[-2] / svals[-1]),
|
|
294
294
|
'n_small': float(int(numpy.sum(svals <= svals[0] * 1e-12))),
|
|
295
295
|
}
|