freealg 0.7.6__tar.gz → 0.7.7__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 (62) hide show
  1. {freealg-0.7.6 → freealg-0.7.7}/PKG-INFO +1 -1
  2. freealg-0.7.7/freealg/__version__.py +1 -0
  3. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/_continuation_algebraic.py +14 -6
  4. freealg-0.7.7/freealg/_algebraic_form/_discriminant.py +226 -0
  5. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/algebraic_form.py +12 -6
  6. {freealg-0.7.6 → freealg-0.7.7}/freealg.egg-info/PKG-INFO +1 -1
  7. {freealg-0.7.6 → freealg-0.7.7}/freealg.egg-info/SOURCES.txt +1 -0
  8. freealg-0.7.6/freealg/__version__.py +0 -1
  9. {freealg-0.7.6 → freealg-0.7.7}/AUTHORS.txt +0 -0
  10. {freealg-0.7.6 → freealg-0.7.7}/CHANGELOG.rst +0 -0
  11. {freealg-0.7.6 → freealg-0.7.7}/LICENSE.txt +0 -0
  12. {freealg-0.7.6 → freealg-0.7.7}/MANIFEST.in +0 -0
  13. {freealg-0.7.6 → freealg-0.7.7}/README.rst +0 -0
  14. {freealg-0.7.6 → freealg-0.7.7}/freealg/__init__.py +0 -0
  15. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/__init__.py +0 -0
  16. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/_constraints.py +0 -0
  17. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/_decompress.py +0 -0
  18. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/_decompress2.py +0 -0
  19. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/_edge.py +0 -0
  20. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/_homotopy.py +0 -0
  21. {freealg-0.7.6 → freealg-0.7.7}/freealg/_algebraic_form/_sheets_util.py +0 -0
  22. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/__init__.py +0 -0
  23. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_chebyshev.py +0 -0
  24. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_damp.py +0 -0
  25. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_decompress.py +0 -0
  26. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_density_util.py +0 -0
  27. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_jacobi.py +0 -0
  28. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_linalg.py +0 -0
  29. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_pade.py +0 -0
  30. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_plot_util.py +0 -0
  31. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_sample.py +0 -0
  32. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_series.py +0 -0
  33. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/_support.py +0 -0
  34. {freealg-0.7.6 → freealg-0.7.7}/freealg/_free_form/free_form.py +0 -0
  35. {freealg-0.7.6 → freealg-0.7.7}/freealg/_geometric_form/__init__.py +0 -0
  36. {freealg-0.7.6 → freealg-0.7.7}/freealg/_geometric_form/_continuation_genus0.py +0 -0
  37. {freealg-0.7.6 → freealg-0.7.7}/freealg/_geometric_form/_continuation_genus1.py +0 -0
  38. {freealg-0.7.6 → freealg-0.7.7}/freealg/_geometric_form/_elliptic_functions.py +0 -0
  39. {freealg-0.7.6 → freealg-0.7.7}/freealg/_geometric_form/_sphere_maps.py +0 -0
  40. {freealg-0.7.6 → freealg-0.7.7}/freealg/_geometric_form/_torus_maps.py +0 -0
  41. {freealg-0.7.6 → freealg-0.7.7}/freealg/_geometric_form/geometric_form.py +0 -0
  42. {freealg-0.7.6 → freealg-0.7.7}/freealg/_util.py +0 -0
  43. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/__init__.py +0 -0
  44. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_chiral_block.py +0 -0
  45. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_deformed_marchenko_pastur.py +0 -0
  46. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_deformed_wigner.py +0 -0
  47. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_kesten_mckay.py +0 -0
  48. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_marchenko_pastur.py +0 -0
  49. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_meixner.py +0 -0
  50. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_wachter.py +0 -0
  51. {freealg-0.7.6 → freealg-0.7.7}/freealg/distributions/_wigner.py +0 -0
  52. {freealg-0.7.6 → freealg-0.7.7}/freealg/visualization/__init__.py +0 -0
  53. {freealg-0.7.6 → freealg-0.7.7}/freealg/visualization/_glue_util.py +0 -0
  54. {freealg-0.7.6 → freealg-0.7.7}/freealg/visualization/_rgb_hsv.py +0 -0
  55. {freealg-0.7.6 → freealg-0.7.7}/freealg.egg-info/dependency_links.txt +0 -0
  56. {freealg-0.7.6 → freealg-0.7.7}/freealg.egg-info/not-zip-safe +0 -0
  57. {freealg-0.7.6 → freealg-0.7.7}/freealg.egg-info/requires.txt +0 -0
  58. {freealg-0.7.6 → freealg-0.7.7}/freealg.egg-info/top_level.txt +0 -0
  59. {freealg-0.7.6 → freealg-0.7.7}/pyproject.toml +0 -0
  60. {freealg-0.7.6 → freealg-0.7.7}/requirements.txt +0 -0
  61. {freealg-0.7.6 → freealg-0.7.7}/setup.cfg +0 -0
  62. {freealg-0.7.6 → freealg-0.7.7}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.7.6
3
+ Version: 0.7.7
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
@@ -0,0 +1 @@
1
+ __version__ = "0.7.7"
@@ -227,7 +227,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
227
227
  dtype=float)
228
228
  AN = numpy.vstack([AN, L])
229
229
 
230
- _, _, vhN = numpy.linalg.svd(AN, full_matrices=False)
230
+ _, svals, vhN = numpy.linalg.svd(AN, full_matrices=False)
231
231
  y = vhN[-1, :]
232
232
  coef_scaled = N @ y
233
233
 
@@ -245,7 +245,8 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
245
245
  dtype=float)
246
246
  As_aug = numpy.vstack([As_aug, L])
247
247
 
248
- _, _, vh = numpy.linalg.svd(As_aug, full_matrices=False)
248
+ _, svals, vh = numpy.linalg.svd(As_aug,
249
+ full_matrices=False)
249
250
  coef_scaled = vh[-1, :]
250
251
  coef = coef_scaled / s_col
251
252
  else:
@@ -255,7 +256,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
255
256
  dtype=float)
256
257
  As = numpy.vstack([As, L])
257
258
 
258
- _, _, vh = numpy.linalg.svd(As, full_matrices=False)
259
+ _, svals, vh = numpy.linalg.svd(As, full_matrices=False)
259
260
  coef_scaled = vh[-1, :]
260
261
  coef = coef_scaled / s_col
261
262
 
@@ -265,7 +266,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
265
266
  L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
266
267
  As = numpy.vstack([As, L])
267
268
 
268
- _, _, vh = numpy.linalg.svd(As, full_matrices=False)
269
+ _, svals, vh = numpy.linalg.svd(As, full_matrices=False)
269
270
  coef_scaled = vh[-1, :]
270
271
  coef = coef_scaled / s_col
271
272
 
@@ -275,7 +276,7 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
275
276
  L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
276
277
  As = numpy.vstack([As, L])
277
278
 
278
- _, _, vh = numpy.linalg.svd(As, full_matrices=False)
279
+ _, svals, vh = numpy.linalg.svd(As, full_matrices=False)
279
280
  coef_scaled = vh[-1, :]
280
281
  coef = coef_scaled / s_col
281
282
 
@@ -286,7 +287,14 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
286
287
  if normalize:
287
288
  full = _normalize_coefficients(full)
288
289
 
289
- return full
290
+ # Diagnostic metrics
291
+ fit_metrics = {
292
+ 's_min': svals[-1],
293
+ 'gap_ratio': float(svals[-2] / svals[-1]),
294
+ 'n_small': float(int(numpy.sum(svals <= svals[0] * 1e-12))),
295
+ }
296
+
297
+ return full, fit_metrics
290
298
 
291
299
 
292
300
  # =============================
@@ -0,0 +1,226 @@
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 as np
15
+ import numpy.polynomial.polynomial as poly
16
+
17
+ __all__ = ['compute_singular_points']
18
+
19
+
20
+ # =========
21
+ # ploy trim
22
+ # =========
23
+
24
+ def _poly_trim(p, tol):
25
+
26
+ p = np.asarray(p, dtype=complex).ravel()
27
+ if p.size == 0:
28
+ return np.zeros(1, dtype=complex)
29
+ k = p.size - 1
30
+ while k > 0 and abs(p[k]) <= tol:
31
+ k -= 1
32
+ return p[:k + 1].copy()
33
+
34
+
35
+ # ============
36
+ # poly is zero
37
+ # ============
38
+
39
+ def _poly_is_zero(p, tol):
40
+
41
+ p = _poly_trim(p, tol)
42
+ return (p.size == 1) and (abs(p[0]) <= tol)
43
+
44
+
45
+ # ========
46
+ # poly add
47
+ # ========
48
+
49
+ def _poly_add(a, b, tol):
50
+
51
+ return _poly_trim(poly.polyadd(a, b), tol)
52
+
53
+
54
+ # ========
55
+ # poly sub
56
+ # ========
57
+
58
+ def _poly_sub(a, b, tol):
59
+
60
+ return _poly_trim(poly.polysub(a, b), tol)
61
+
62
+
63
+ # =======
64
+ # ply mul
65
+ # =======
66
+
67
+ def _poly_mul(a, b, tol):
68
+
69
+ return _poly_trim(poly.polymul(a, b), tol)
70
+
71
+
72
+ # ==============
73
+ # poly div exact
74
+ # ==============
75
+
76
+ def _poly_div_exact(a, b, tol):
77
+
78
+ a = _poly_trim(a, tol)
79
+ b = _poly_trim(b, tol)
80
+ if _poly_is_zero(b, tol):
81
+ raise ZeroDivisionError("poly division by zero")
82
+
83
+ q, r = poly.polydiv(a, b)
84
+ r = _poly_trim(r, tol)
85
+
86
+ # Bareiss expects exact division; with floats it's only approximate.
87
+ # If the remainder is small, drop it.
88
+ scale = max(1.0, np.linalg.norm(a))
89
+ if np.linalg.norm(r) > 1e3 * tol * scale:
90
+ # Fallback: still drop remainder (keeps algorithm running).
91
+ # This is acceptable because we only need the resultant roots
92
+ # robustly, not exact symbolic coefficients.
93
+ pass
94
+
95
+ return _poly_trim(q, tol)
96
+
97
+
98
+ # ================
99
+ # det bareiss poly
100
+ # ================
101
+
102
+ def _det_bareiss_poly(M, tol):
103
+
104
+ n = len(M)
105
+ A = [[_poly_trim(M[i][j], tol) for j in range(n)] for i in range(n)]
106
+
107
+ denom = np.array([1.0], dtype=complex)
108
+ sign = 1.0
109
+
110
+ for k in range(n - 1):
111
+ if _poly_is_zero(A[k][k], tol):
112
+ piv = -1
113
+ for i in range(k + 1, n):
114
+ if not _poly_is_zero(A[i][k], tol):
115
+ piv = i
116
+ break
117
+ if piv == -1:
118
+ return np.array([0.0], dtype=complex)
119
+ A[k], A[piv] = A[piv], A[k]
120
+ sign *= -1.0
121
+
122
+ pivot = A[k][k]
123
+ for i in range(k + 1, n):
124
+ for j in range(k + 1, n):
125
+ num = _poly_sub(_poly_mul(A[i][j], pivot, tol),
126
+ _poly_mul(A[i][k], A[k][j], tol),
127
+ tol)
128
+ if k > 0:
129
+ A[i][j] = _poly_div_exact(num, denom, tol)
130
+ else:
131
+ A[i][j] = _poly_trim(num, tol)
132
+
133
+ denom = pivot
134
+
135
+ return _poly_trim(sign * A[n - 1][n - 1], tol)
136
+
137
+
138
+ # ===================
139
+ # cluster real points
140
+ # ===================
141
+
142
+ def _cluster_real_points(x, eps):
143
+
144
+ x = np.asarray(x, dtype=float).ravel()
145
+ if x.size == 0:
146
+ return x
147
+ x = np.sort(x)
148
+ uniq = []
149
+ for v in x:
150
+ if (len(uniq) == 0) or (abs(v - uniq[-1]) > eps):
151
+ uniq.append(float(v))
152
+ else:
153
+ uniq[-1] = 0.5 * (uniq[-1] + float(v))
154
+ return np.asarray(uniq, dtype=float)
155
+
156
+
157
+ # =======================
158
+ # compute singular points
159
+ # =======================
160
+
161
+ def compute_singular_points(a_coeffs, tol=1e-12, real_tol=None):
162
+ """
163
+ a_coeffs[i,j] is coefficient of z^i m^j, shape (deg_z+1, s+1).
164
+
165
+ Returns
166
+ -------
167
+
168
+ z_bp : complex array, roots of Disc_m(P)(z)
169
+ a_s_zero : complex array, roots of leading coefficient a_s(z)
170
+ support : list of (a,b) from real-ish branch points paired consecutively
171
+ """
172
+
173
+ a_coeffs = np.asarray(a_coeffs)
174
+ s = a_coeffs.shape[1] - 1
175
+ if s < 1:
176
+ return (np.array([], dtype=complex),
177
+ np.array([], dtype=complex),
178
+ [])
179
+
180
+ if real_tol is None:
181
+ real_tol = 1e3 * tol
182
+
183
+ a = [_poly_trim(a_coeffs[:, j], tol) for j in range(s + 1)]
184
+
185
+ a_s = a[s]
186
+ a_s_zero = np.roots(a_s[::-1]) if a_s.size > 1 else \
187
+ np.array([], dtype=complex)
188
+
189
+ b = []
190
+ for j in range(s):
191
+ b.append(_poly_trim((j + 1) * a[j + 1], tol))
192
+
193
+ mdeg = s
194
+ ndeg = s - 1
195
+ N = mdeg + ndeg # 2s-1
196
+
197
+ z0 = np.array([0.0], dtype=complex)
198
+ M = [[z0 for _ in range(N)] for __ in range(N)]
199
+
200
+ for r in range(ndeg):
201
+ for j in range(mdeg + 1):
202
+ M[r][r + j] = a[j]
203
+
204
+ for r in range(mdeg):
205
+ rr = ndeg + r
206
+ for j in range(ndeg + 1):
207
+ M[rr][r + j] = b[j]
208
+
209
+ res = _det_bareiss_poly(M, tol)
210
+ if res.size <= 1:
211
+ z_bp = np.array([], dtype=complex)
212
+ else:
213
+ z_bp = np.roots(res[::-1])
214
+
215
+ support = []
216
+ if z_bp.size > 0:
217
+ zr = z_bp[np.abs(z_bp.imag) <= real_tol].real
218
+ zr = _cluster_real_points(zr, eps=1e2 * real_tol)
219
+ m2 = (zr.size // 2) * 2
220
+ for k in range(0, m2, 2):
221
+ a0 = float(zr[k])
222
+ b0 = float(zr[k + 1])
223
+ if b0 > a0:
224
+ support.append((a0, b0))
225
+
226
+ return z_bp, a_s_zero, support
@@ -21,6 +21,7 @@ from ._edge import evolve_edges, merge_edges
21
21
  from ._decompress import decompress_newton
22
22
  from ._decompress2 import decompress_coeffs
23
23
  from ._homotopy import stieltjes_poly
24
+ from ._discriminant import compute_singular_points
24
25
  from .._free_form._support import supp
25
26
  from .._free_form._plot_util import plot_density
26
27
 
@@ -262,11 +263,13 @@ class AlgebraicForm(object):
262
263
 
263
264
  # Fitting (w_inf = None means adaptive weight selection)
264
265
  m1_fit = self.stieltjes(z_fit)
265
- a_coeffs = fit_polynomial_relation(z_fit, m1_fit, s=deg_m, deg_z=deg_z,
266
- ridge_lambda=reg,
267
- triangular=triangular,
268
- normalize=normalize, mu=mu,
269
- mu_reg=mu_reg)
266
+ a_coeffs, fit_metrics = fit_polynomial_relation(
267
+ z_fit, m1_fit, s=deg_m, deg_z=deg_z, ridge_lambda=reg,
268
+ triangular=triangular, normalize=normalize, mu=mu,
269
+ mu_reg=mu_reg)
270
+
271
+ # Compute global branhc points, zeros of leading a_j, and support
272
+ branch_points, a_s_zero, support = compute_singular_points(a_coeffs)
270
273
 
271
274
  self.a_coeffs = a_coeffs
272
275
 
@@ -282,8 +285,11 @@ class AlgebraicForm(object):
282
285
  eta=max(y_eps, 1e-2), n_x=128,
283
286
  max_bad_frac=0.05)
284
287
 
288
+ status['branch_points'] = branch_points
289
+ status['a_s_zero'] = a_s_zero
285
290
  status['res_max'] = float(res_max)
286
291
  status['res_99_9'] = float(res_99_9)
292
+ status['fit_metrics'] = fit_metrics
287
293
  self.status = status
288
294
 
289
295
  if verbose:
@@ -309,7 +315,7 @@ class AlgebraicForm(object):
309
315
  else:
310
316
  print('\nStieltjes sanity check: OK')
311
317
 
312
- return a_coeffs, status
318
+ return a_coeffs, support, status
313
319
 
314
320
  # =============
315
321
  # generate grid
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.7.6
3
+ Version: 0.7.7
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
@@ -21,6 +21,7 @@ freealg/_algebraic_form/_constraints.py
21
21
  freealg/_algebraic_form/_continuation_algebraic.py
22
22
  freealg/_algebraic_form/_decompress.py
23
23
  freealg/_algebraic_form/_decompress2.py
24
+ freealg/_algebraic_form/_discriminant.py
24
25
  freealg/_algebraic_form/_edge.py
25
26
  freealg/_algebraic_form/_homotopy.py
26
27
  freealg/_algebraic_form/_sheets_util.py
@@ -1 +0,0 @@
1
- __version__ = "0.7.6"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes