freealg 0.7.5__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.5 → freealg-0.7.7}/PKG-INFO +1 -1
  2. freealg-0.7.7/freealg/__version__.py +1 -0
  3. freealg-0.7.7/freealg/_algebraic_form/_constraints.py +98 -0
  4. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/_continuation_algebraic.py +89 -8
  5. freealg-0.7.7/freealg/_algebraic_form/_discriminant.py +226 -0
  6. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/algebraic_form.py +55 -9
  7. {freealg-0.7.5 → freealg-0.7.7}/freealg/_util.py +24 -1
  8. {freealg-0.7.5 → freealg-0.7.7}/freealg.egg-info/PKG-INFO +1 -1
  9. {freealg-0.7.5 → freealg-0.7.7}/freealg.egg-info/SOURCES.txt +2 -0
  10. freealg-0.7.5/freealg/__version__.py +0 -1
  11. {freealg-0.7.5 → freealg-0.7.7}/AUTHORS.txt +0 -0
  12. {freealg-0.7.5 → freealg-0.7.7}/CHANGELOG.rst +0 -0
  13. {freealg-0.7.5 → freealg-0.7.7}/LICENSE.txt +0 -0
  14. {freealg-0.7.5 → freealg-0.7.7}/MANIFEST.in +0 -0
  15. {freealg-0.7.5 → freealg-0.7.7}/README.rst +0 -0
  16. {freealg-0.7.5 → freealg-0.7.7}/freealg/__init__.py +0 -0
  17. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/__init__.py +0 -0
  18. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/_decompress.py +0 -0
  19. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/_decompress2.py +0 -0
  20. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/_edge.py +0 -0
  21. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/_homotopy.py +0 -0
  22. {freealg-0.7.5 → freealg-0.7.7}/freealg/_algebraic_form/_sheets_util.py +0 -0
  23. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/__init__.py +0 -0
  24. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_chebyshev.py +0 -0
  25. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_damp.py +0 -0
  26. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_decompress.py +0 -0
  27. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_density_util.py +0 -0
  28. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_jacobi.py +0 -0
  29. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_linalg.py +0 -0
  30. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_pade.py +0 -0
  31. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_plot_util.py +0 -0
  32. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_sample.py +0 -0
  33. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_series.py +0 -0
  34. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/_support.py +0 -0
  35. {freealg-0.7.5 → freealg-0.7.7}/freealg/_free_form/free_form.py +0 -0
  36. {freealg-0.7.5 → freealg-0.7.7}/freealg/_geometric_form/__init__.py +0 -0
  37. {freealg-0.7.5 → freealg-0.7.7}/freealg/_geometric_form/_continuation_genus0.py +0 -0
  38. {freealg-0.7.5 → freealg-0.7.7}/freealg/_geometric_form/_continuation_genus1.py +0 -0
  39. {freealg-0.7.5 → freealg-0.7.7}/freealg/_geometric_form/_elliptic_functions.py +0 -0
  40. {freealg-0.7.5 → freealg-0.7.7}/freealg/_geometric_form/_sphere_maps.py +0 -0
  41. {freealg-0.7.5 → freealg-0.7.7}/freealg/_geometric_form/_torus_maps.py +0 -0
  42. {freealg-0.7.5 → freealg-0.7.7}/freealg/_geometric_form/geometric_form.py +0 -0
  43. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/__init__.py +0 -0
  44. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_chiral_block.py +0 -0
  45. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_deformed_marchenko_pastur.py +0 -0
  46. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_deformed_wigner.py +0 -0
  47. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_kesten_mckay.py +0 -0
  48. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_marchenko_pastur.py +0 -0
  49. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_meixner.py +0 -0
  50. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_wachter.py +0 -0
  51. {freealg-0.7.5 → freealg-0.7.7}/freealg/distributions/_wigner.py +0 -0
  52. {freealg-0.7.5 → freealg-0.7.7}/freealg/visualization/__init__.py +0 -0
  53. {freealg-0.7.5 → freealg-0.7.7}/freealg/visualization/_glue_util.py +0 -0
  54. {freealg-0.7.5 → freealg-0.7.7}/freealg/visualization/_rgb_hsv.py +0 -0
  55. {freealg-0.7.5 → freealg-0.7.7}/freealg.egg-info/dependency_links.txt +0 -0
  56. {freealg-0.7.5 → freealg-0.7.7}/freealg.egg-info/not-zip-safe +0 -0
  57. {freealg-0.7.5 → freealg-0.7.7}/freealg.egg-info/requires.txt +0 -0
  58. {freealg-0.7.5 → freealg-0.7.7}/freealg.egg-info/top_level.txt +0 -0
  59. {freealg-0.7.5 → freealg-0.7.7}/pyproject.toml +0 -0
  60. {freealg-0.7.5 → freealg-0.7.7}/requirements.txt +0 -0
  61. {freealg-0.7.5 → freealg-0.7.7}/setup.cfg +0 -0
  62. {freealg-0.7.5 → 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.5
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"
@@ -0,0 +1,98 @@
1
+
2
+ # SPDX-FileCopyrightText: Copyright 2025, Siavash Ameli <sameli@berkeley.edu>
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+ # SPDX-FileType: SOURCE
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify it under
7
+ # the terms of the license found in the LICENSE.txt file in the root directory
8
+ # of this source tree.
9
+
10
+
11
+ # =======
12
+ # Imports
13
+ # =======
14
+
15
+ import numpy
16
+
17
+ __all__ = ['build_moment_constraint_matrix']
18
+
19
+
20
+ # ==========
21
+ # series mul
22
+ # ==========
23
+
24
+ def _series_mul(a, b, q_max):
25
+
26
+ na = min(len(a), q_max + 1)
27
+ nb = min(len(b), q_max + 1)
28
+ out = numpy.zeros(q_max + 1, dtype=float)
29
+ for i in range(na):
30
+ if a[i] == 0.0:
31
+ continue
32
+ j_max = min(nb - 1, q_max - i)
33
+ if j_max >= 0:
34
+ out[i:i + j_max + 1] += a[i] * b[:j_max + 1]
35
+ return out
36
+
37
+
38
+ # ==========
39
+ # series pow
40
+ # ==========
41
+
42
+ def _series_pow(mser, j, q_max):
43
+ if j == 0:
44
+ out = numpy.zeros(q_max + 1, dtype=float)
45
+ out[0] = 1.0
46
+ return out
47
+ out = mser.copy()
48
+ for _ in range(1, j):
49
+ out = _series_mul(out, mser, q_max)
50
+ return out
51
+
52
+
53
+ # ===============================
54
+ # build moment constraints matrix
55
+ # ===============================
56
+
57
+ def build_moment_constraint_matrix(pairs, deg_z, s, mu):
58
+
59
+ mu = numpy.asarray(mu, dtype=float).ravel()
60
+ if mu.size == 0:
61
+ return numpy.zeros((0, len(pairs)), dtype=float)
62
+
63
+ # m(z) = -sum_{p>=0} mu_p / z^{p+1}; t = 1/z so m(t) = -sum mu_p t^{p+1}
64
+ r = mu.size - 1
65
+ q_max = r
66
+
67
+ mser = numpy.zeros(q_max + 1, dtype=float)
68
+ for p in range(mu.size):
69
+ q = p + 1
70
+ if q <= q_max:
71
+ mser[q] = -float(mu[p])
72
+
73
+ # Precompute (m(t))^j coefficients up to t^{q_max}
74
+ mpow = []
75
+ for j in range(s + 1):
76
+ mpow.append(_series_pow(mser, j, q_max))
77
+
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
+ n_coef = len(pairs)
81
+ B = numpy.zeros((q_max + 1, n_coef), dtype=float)
82
+
83
+ for k, (i, j) in enumerate(pairs):
84
+ shift = deg_z - i
85
+ if shift < 0:
86
+ continue
87
+ mj = mpow[j]
88
+ for q in range(q_max + 1):
89
+ qq = q - shift
90
+ if 0 <= qq <= q_max:
91
+ B[q, k] = mj[qq]
92
+
93
+ # Drop all-zero rows (can happen if index-set can't support higher moments)
94
+ row_norm = numpy.linalg.norm(B, axis=1)
95
+ keep = row_norm > 0.0
96
+ B = B[keep, :]
97
+
98
+ return B
@@ -13,6 +13,7 @@
13
13
 
14
14
  import numpy
15
15
  from .._geometric_form._continuation_genus0 import joukowski_z
16
+ from ._constraints import build_moment_constraint_matrix
16
17
 
17
18
  __all__ = ['sample_z_joukowski', 'filter_z_away_from_cuts', 'powers',
18
19
  'fit_polynomial_relation', 'sanity_check_stieltjes_branch',
@@ -131,7 +132,11 @@ def powers(x, deg):
131
132
  # =======================
132
133
 
133
134
  def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
134
- triangular=None, normalize=False):
135
+ triangular=None, normalize=False,
136
+ mu=None, mu_reg=None):
137
+ """
138
+ Fits polynomial P(z, m) = 0 with samples from the physical branch.
139
+ """
135
140
 
136
141
  z = numpy.asarray(z, dtype=complex).ravel()
137
142
  m = numpy.asarray(m, dtype=complex).ravel()
@@ -198,13 +203,82 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
198
203
  s_col[s_col == 0.0] = 1.0
199
204
  As = Ar / s_col[None, :]
200
205
 
201
- if ridge_lambda > 0.0:
202
- L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
203
- As = numpy.vstack([As, L])
206
+ # Optional moment constraints B c = 0 (hard via nullspace, soft via
207
+ # weighted rows)
208
+ if mu is not None:
209
+ B = build_moment_constraint_matrix(pairs, deg_z, s, mu)
210
+ if B.shape[0] > 0:
211
+ Bs = B / s_col[None, :]
212
+
213
+ if mu_reg is None:
214
+ # Hard constraints: solve in nullspace of Bs
215
+ uB, sB, vhB = numpy.linalg.svd(Bs, full_matrices=True)
216
+ tolB = 1e-12 * (sB[0] if sB.size else 1.0)
217
+ rankB = int(numpy.sum(sB > tolB))
218
+ if rankB >= n_coef:
219
+ raise RuntimeError(
220
+ 'Moment constraints leave no feasible coefficients.')
221
+
222
+ N = vhB[rankB:, :].T # (n_coef, n_free)
223
+ AN = As @ N
224
+
225
+ if ridge_lambda > 0.0:
226
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(N.shape[1],
227
+ dtype=float)
228
+ AN = numpy.vstack([AN, L])
229
+
230
+ _, svals, vhN = numpy.linalg.svd(AN, full_matrices=False)
231
+ y = vhN[-1, :]
232
+ coef_scaled = N @ y
233
+
234
+ coef = coef_scaled / s_col
235
+
236
+ else:
237
+ mu_reg = float(mu_reg)
238
+ if mu_reg > 0.0:
239
+ As_aug = As
240
+ Bs_w = numpy.sqrt(mu_reg) * Bs
241
+ As_aug = numpy.vstack([As_aug, Bs_w])
242
+
243
+ if ridge_lambda > 0.0:
244
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef,
245
+ dtype=float)
246
+ As_aug = numpy.vstack([As_aug, L])
247
+
248
+ _, svals, vh = numpy.linalg.svd(As_aug,
249
+ full_matrices=False)
250
+ coef_scaled = vh[-1, :]
251
+ coef = coef_scaled / s_col
252
+ else:
253
+ # mu_reg == 0 => ignore constraints
254
+ if ridge_lambda > 0.0:
255
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef,
256
+ dtype=float)
257
+ As = numpy.vstack([As, L])
258
+
259
+ _, svals, vh = numpy.linalg.svd(As, full_matrices=False)
260
+ coef_scaled = vh[-1, :]
261
+ coef = coef_scaled / s_col
262
+
263
+ else:
264
+ # B has no effective rows -> proceed unconstrained
265
+ if ridge_lambda > 0.0:
266
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
267
+ As = numpy.vstack([As, L])
204
268
 
205
- _, _, vh = numpy.linalg.svd(As, full_matrices=False)
206
- coef_scaled = vh[-1, :]
207
- coef = coef_scaled / s_col
269
+ _, svals, vh = numpy.linalg.svd(As, full_matrices=False)
270
+ coef_scaled = vh[-1, :]
271
+ coef = coef_scaled / s_col
272
+
273
+ else:
274
+ # No moment constraints
275
+ if ridge_lambda > 0.0:
276
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
277
+ As = numpy.vstack([As, L])
278
+
279
+ _, svals, vh = numpy.linalg.svd(As, full_matrices=False)
280
+ coef_scaled = vh[-1, :]
281
+ coef = coef_scaled / s_col
208
282
 
209
283
  full = numpy.zeros((deg_z + 1, s + 1), dtype=complex)
210
284
  for k, (i, j) in enumerate(pairs):
@@ -213,7 +287,14 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
213
287
  if normalize:
214
288
  full = _normalize_coefficients(full)
215
289
 
216
- 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
217
298
 
218
299
 
219
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
 
@@ -194,6 +195,7 @@ class AlgebraicForm(object):
194
195
 
195
196
  # Initialize
196
197
  self.a_coeffs = None # Polynomial coefficients
198
+ self.status = None # Fitting status
197
199
  self.cache = {} # Cache inner-computations
198
200
 
199
201
  # ===
@@ -207,10 +209,38 @@ class AlgebraicForm(object):
207
209
  y_eps=2e-2,
208
210
  x_pad=0.0,
209
211
  triangular=None,
212
+ mu=None,
213
+ mu_reg=None,
210
214
  normalize=False,
211
215
  verbose=False):
212
216
  """
213
- Fits polynomial.
217
+ Fit polynomial.
218
+
219
+ Parameters
220
+ ----------
221
+
222
+ deg_m : int
223
+ Degree :math:`\\deg_m(P)`
224
+
225
+ deg_z : int
226
+ Degree :math:`\\deg_z(P)`
227
+
228
+ mu : array_like, default=None
229
+ If an array :math:`[\\mu_0, \\mu_`, \\dots, \\mu_r]` is given,
230
+ it enforces the first :math:`r+1` moments. Note that :math:`\\mu_0`
231
+ should be :math:`1` to ensure unit mass. See also ``mu_reg`.
232
+
233
+ mu_reg: float, default=None
234
+ If `None`, the constraints ``mu`` are applied as hard constraint.
235
+ If a positive number, the constraints are applied as a soft
236
+ constraints with regularisation ``mu_reg``.
237
+
238
+ Notes
239
+ -----
240
+
241
+ When the input data are from an exact model, hard moment constraint is
242
+ preferred over soft constraint as the latter can hurt an already a good
243
+ fit.
214
244
  """
215
245
 
216
246
  # Very important: reset cache whenever this function is called. This
@@ -231,11 +261,15 @@ class AlgebraicForm(object):
231
261
  z_fit = filter_z_away_from_cuts(z_fit, self.support, y_eps=y_eps,
232
262
  x_pad=x_pad)
233
263
 
264
+ # Fitting (w_inf = None means adaptive weight selection)
234
265
  m1_fit = self.stieltjes(z_fit)
235
- a_coeffs = fit_polynomial_relation(z_fit, m1_fit, s=deg_m, deg_z=deg_z,
236
- ridge_lambda=reg,
237
- triangular=triangular,
238
- normalize=normalize)
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)
239
273
 
240
274
  self.a_coeffs = a_coeffs
241
275
 
@@ -251,8 +285,12 @@ class AlgebraicForm(object):
251
285
  eta=max(y_eps, 1e-2), n_x=128,
252
286
  max_bad_frac=0.05)
253
287
 
254
- status['res_max'] = res_max
255
- status['res_99_9'] = res_99_9
288
+ status['branch_points'] = branch_points
289
+ status['a_s_zero'] = a_s_zero
290
+ status['res_max'] = float(res_max)
291
+ status['res_99_9'] = float(res_99_9)
292
+ status['fit_metrics'] = fit_metrics
293
+ self.status = status
256
294
 
257
295
  if verbose:
258
296
  print(f'fit residual max : {res_max:>0.4e}')
@@ -277,7 +315,7 @@ class AlgebraicForm(object):
277
315
  else:
278
316
  print('\nStieltjes sanity check: OK')
279
317
 
280
- return a_coeffs, status
318
+ return a_coeffs, support, status
281
319
 
282
320
  # =============
283
321
  # generate grid
@@ -657,11 +695,16 @@ class AlgebraicForm(object):
657
695
  z_query = x + 1j * self.delta
658
696
 
659
697
  # Initial condition at t=0 (physical branch)
660
- w0_list = self.stieltjes(z_query)
698
+ # w0_list = self.stieltjes(z_query)
699
+ stieltjes = numpy.vectorize(m_fn)
700
+ w0_list = stieltjes(z_query)
661
701
 
662
702
  # Times
663
703
  t = numpy.log(alpha)
664
704
 
705
+ # Ensure it starts from t = 0
706
+ t = numpy.concatenate([numpy.zeros(1), t])
707
+
665
708
  # Evolve
666
709
  W, ok = decompress_newton(
667
710
  z_query, t, self.a_coeffs,
@@ -669,6 +712,9 @@ class AlgebraicForm(object):
669
712
 
670
713
  rho = W.imag / numpy.pi
671
714
 
715
+ # Remove time zero
716
+ rho = rho[1:, :]
717
+
672
718
  if verbose:
673
719
  print("success rate per t:", ok.mean(axis=1))
674
720
 
@@ -14,7 +14,7 @@
14
14
  import numpy
15
15
  import scipy
16
16
 
17
- __all__ = ['resolve_complex_dtype', 'compute_eig']
17
+ __all__ = ['resolve_complex_dtype', 'compute_eig', 'subsample_matrix']
18
18
 
19
19
 
20
20
  # =====================
@@ -70,3 +70,26 @@ def compute_eig(A, lower=False):
70
70
  eig = scipy.linalg.eigvalsh(A, lower=lower, driver='ev')
71
71
 
72
72
  return eig
73
+
74
+
75
+ # ================
76
+ # subsample matrix
77
+ # ================
78
+
79
+ def subsample_matrix(matrix, submatrix_size, seed=None):
80
+ """
81
+ Generate a random subsample of a larger matrix
82
+ """
83
+
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)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.7.5
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
@@ -17,9 +17,11 @@ freealg.egg-info/not-zip-safe
17
17
  freealg.egg-info/requires.txt
18
18
  freealg.egg-info/top_level.txt
19
19
  freealg/_algebraic_form/__init__.py
20
+ freealg/_algebraic_form/_constraints.py
20
21
  freealg/_algebraic_form/_continuation_algebraic.py
21
22
  freealg/_algebraic_form/_decompress.py
22
23
  freealg/_algebraic_form/_decompress2.py
24
+ freealg/_algebraic_form/_discriminant.py
23
25
  freealg/_algebraic_form/_edge.py
24
26
  freealg/_algebraic_form/_homotopy.py
25
27
  freealg/_algebraic_form/_sheets_util.py
@@ -1 +0,0 @@
1
- __version__ = "0.7.5"
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