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.
Files changed (67) hide show
  1. {freealg-0.7.10 → freealg-0.7.12}/PKG-INFO +2 -1
  2. {freealg-0.7.10 → freealg-0.7.12}/freealg/__init__.py +2 -2
  3. freealg-0.7.12/freealg/__version__.py +1 -0
  4. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/__init__.py +2 -1
  5. freealg-0.7.12/freealg/_algebraic_form/_branch_points.py +288 -0
  6. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_constraints.py +53 -12
  7. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_continuation_algebraic.py +1 -1
  8. freealg-0.7.12/freealg/_algebraic_form/_decompress.py +641 -0
  9. freealg-0.7.12/freealg/_algebraic_form/_decompress2.py +204 -0
  10. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_edge.py +46 -68
  11. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_homotopy.py +62 -30
  12. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_moments.py +44 -57
  13. freealg-0.7.12/freealg/_algebraic_form/_support.py +309 -0
  14. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/algebraic_form.py +233 -48
  15. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/__init__.py +3 -1
  16. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_deformed_marchenko_pastur.py +51 -0
  17. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_deformed_wigner.py +44 -0
  18. {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/PKG-INFO +2 -1
  19. {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/SOURCES.txt +2 -1
  20. {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/requires.txt +1 -0
  21. {freealg-0.7.10 → freealg-0.7.12}/requirements.txt +2 -1
  22. freealg-0.7.10/freealg/__version__.py +0 -1
  23. freealg-0.7.10/freealg/_algebraic_form/_decompress.py +0 -649
  24. freealg-0.7.10/freealg/_algebraic_form/_decompress2.py +0 -86
  25. freealg-0.7.10/freealg/_algebraic_form/_discriminant.py +0 -226
  26. {freealg-0.7.10 → freealg-0.7.12}/AUTHORS.txt +0 -0
  27. {freealg-0.7.10 → freealg-0.7.12}/CHANGELOG.rst +0 -0
  28. {freealg-0.7.10 → freealg-0.7.12}/LICENSE.txt +0 -0
  29. {freealg-0.7.10 → freealg-0.7.12}/MANIFEST.in +0 -0
  30. {freealg-0.7.10 → freealg-0.7.12}/README.rst +0 -0
  31. {freealg-0.7.10 → freealg-0.7.12}/freealg/_algebraic_form/_sheets_util.py +0 -0
  32. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/__init__.py +0 -0
  33. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_chebyshev.py +0 -0
  34. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_damp.py +0 -0
  35. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_decompress.py +0 -0
  36. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_density_util.py +0 -0
  37. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_jacobi.py +0 -0
  38. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_linalg.py +0 -0
  39. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_pade.py +0 -0
  40. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_plot_util.py +0 -0
  41. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_sample.py +0 -0
  42. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_series.py +0 -0
  43. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/_support.py +0 -0
  44. {freealg-0.7.10 → freealg-0.7.12}/freealg/_free_form/free_form.py +0 -0
  45. {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/__init__.py +0 -0
  46. {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_continuation_genus0.py +0 -0
  47. {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_continuation_genus1.py +0 -0
  48. {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_elliptic_functions.py +0 -0
  49. {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_sphere_maps.py +0 -0
  50. {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/_torus_maps.py +0 -0
  51. {freealg-0.7.10 → freealg-0.7.12}/freealg/_geometric_form/geometric_form.py +0 -0
  52. {freealg-0.7.10 → freealg-0.7.12}/freealg/_util.py +0 -0
  53. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_chiral_block.py +0 -0
  54. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_kesten_mckay.py +0 -0
  55. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_marchenko_pastur.py +0 -0
  56. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_meixner.py +0 -0
  57. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_wachter.py +0 -0
  58. {freealg-0.7.10 → freealg-0.7.12}/freealg/distributions/_wigner.py +0 -0
  59. {freealg-0.7.10 → freealg-0.7.12}/freealg/visualization/__init__.py +0 -0
  60. {freealg-0.7.10 → freealg-0.7.12}/freealg/visualization/_glue_util.py +0 -0
  61. {freealg-0.7.10 → freealg-0.7.12}/freealg/visualization/_rgb_hsv.py +0 -0
  62. {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/dependency_links.txt +0 -0
  63. {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/not-zip-safe +0 -0
  64. {freealg-0.7.10 → freealg-0.7.12}/freealg.egg-info/top_level.txt +0 -0
  65. {freealg-0.7.10 → freealg-0.7.12}/pyproject.toml +0 -0
  66. {freealg-0.7.10 → freealg-0.7.12}/setup.cfg +0 -0
  67. {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.10
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"
@@ -7,5 +7,6 @@
7
7
  # directory of this source tree.
8
8
 
9
9
  from .algebraic_form import AlgebraicForm
10
+ from ._decompress7 import decompress_newton
10
11
 
11
- __all__ = ['AlgebraicForm']
12
+ __all__ = ['AlgebraicForm', 'decompress_newton']
@@ -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
- # m(z) = -sum_{p>=0} mu_p / z^{p+1}; t = 1/z so m(t) = -sum mu_p t^{p+1}
106
+ # mu has entries mu_0..mu_r
64
107
  r = mu.size - 1
65
- q_max = r
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
- B = numpy.zeros((q_max + 1, n_coef), dtype=float)
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(q_max + 1):
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
- B = B[keep, :]
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
  }