freealg 0.7.6__tar.gz → 0.7.8__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 (64) hide show
  1. {freealg-0.7.6 → freealg-0.7.8}/PKG-INFO +1 -1
  2. freealg-0.7.8/freealg/__version__.py +1 -0
  3. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_continuation_algebraic.py +14 -6
  4. freealg-0.7.8/freealg/_algebraic_form/_discriminant.py +226 -0
  5. freealg-0.7.8/freealg/_algebraic_form/_homotopy.py +280 -0
  6. freealg-0.7.8/freealg/_algebraic_form/_moments.py +450 -0
  7. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/algebraic_form.py +23 -25
  8. {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/PKG-INFO +1 -1
  9. {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/SOURCES.txt +2 -0
  10. freealg-0.7.6/freealg/__version__.py +0 -1
  11. freealg-0.7.6/freealg/_algebraic_form/_homotopy.py +0 -138
  12. {freealg-0.7.6 → freealg-0.7.8}/AUTHORS.txt +0 -0
  13. {freealg-0.7.6 → freealg-0.7.8}/CHANGELOG.rst +0 -0
  14. {freealg-0.7.6 → freealg-0.7.8}/LICENSE.txt +0 -0
  15. {freealg-0.7.6 → freealg-0.7.8}/MANIFEST.in +0 -0
  16. {freealg-0.7.6 → freealg-0.7.8}/README.rst +0 -0
  17. {freealg-0.7.6 → freealg-0.7.8}/freealg/__init__.py +0 -0
  18. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/__init__.py +0 -0
  19. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_constraints.py +0 -0
  20. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_decompress.py +0 -0
  21. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_decompress2.py +0 -0
  22. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_edge.py +0 -0
  23. {freealg-0.7.6 → freealg-0.7.8}/freealg/_algebraic_form/_sheets_util.py +0 -0
  24. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/__init__.py +0 -0
  25. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_chebyshev.py +0 -0
  26. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_damp.py +0 -0
  27. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_decompress.py +0 -0
  28. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_density_util.py +0 -0
  29. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_jacobi.py +0 -0
  30. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_linalg.py +0 -0
  31. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_pade.py +0 -0
  32. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_plot_util.py +0 -0
  33. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_sample.py +0 -0
  34. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_series.py +0 -0
  35. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/_support.py +0 -0
  36. {freealg-0.7.6 → freealg-0.7.8}/freealg/_free_form/free_form.py +0 -0
  37. {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/__init__.py +0 -0
  38. {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_continuation_genus0.py +0 -0
  39. {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_continuation_genus1.py +0 -0
  40. {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_elliptic_functions.py +0 -0
  41. {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_sphere_maps.py +0 -0
  42. {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/_torus_maps.py +0 -0
  43. {freealg-0.7.6 → freealg-0.7.8}/freealg/_geometric_form/geometric_form.py +0 -0
  44. {freealg-0.7.6 → freealg-0.7.8}/freealg/_util.py +0 -0
  45. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/__init__.py +0 -0
  46. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_chiral_block.py +0 -0
  47. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_deformed_marchenko_pastur.py +0 -0
  48. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_deformed_wigner.py +0 -0
  49. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_kesten_mckay.py +0 -0
  50. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_marchenko_pastur.py +0 -0
  51. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_meixner.py +0 -0
  52. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_wachter.py +0 -0
  53. {freealg-0.7.6 → freealg-0.7.8}/freealg/distributions/_wigner.py +0 -0
  54. {freealg-0.7.6 → freealg-0.7.8}/freealg/visualization/__init__.py +0 -0
  55. {freealg-0.7.6 → freealg-0.7.8}/freealg/visualization/_glue_util.py +0 -0
  56. {freealg-0.7.6 → freealg-0.7.8}/freealg/visualization/_rgb_hsv.py +0 -0
  57. {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/dependency_links.txt +0 -0
  58. {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/not-zip-safe +0 -0
  59. {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/requires.txt +0 -0
  60. {freealg-0.7.6 → freealg-0.7.8}/freealg.egg-info/top_level.txt +0 -0
  61. {freealg-0.7.6 → freealg-0.7.8}/pyproject.toml +0 -0
  62. {freealg-0.7.6 → freealg-0.7.8}/requirements.txt +0 -0
  63. {freealg-0.7.6 → freealg-0.7.8}/setup.cfg +0 -0
  64. {freealg-0.7.6 → freealg-0.7.8}/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.8
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.8"
@@ -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
@@ -0,0 +1,280 @@
1
+ # =======
2
+ # Imports
3
+ # =======
4
+
5
+ import numpy
6
+ from ._moments import AlgebraicStieltjesMoments
7
+
8
+ __all__ = ['stieltjes_poly']
9
+
10
+
11
+ # =====================
12
+ # select root
13
+ # =====================
14
+
15
+ def select_root(roots, z, target):
16
+ """
17
+ Select the root among Herglotz candidates at a given z closest to a
18
+ given target
19
+
20
+ Parameters
21
+ ----------
22
+ roots : array_like of complex
23
+ Candidate roots for m at the given z.
24
+ z : complex
25
+ Evaluation point. The Stieltjes/Herglotz branch satisfies
26
+ sign(Im(m)) = sign(Im(z)) away from the real axis.
27
+ target : complex
28
+ Previous continuation value used to enforce continuity, or
29
+ target value.
30
+
31
+ Returns
32
+ -------
33
+ w : complex
34
+ Selected root corresponding to the Stieltjes branch.
35
+ """
36
+
37
+ z = complex(z)
38
+ roots = numpy.asarray(roots, dtype=numpy.complex128).ravel()
39
+
40
+ if roots.size == 0:
41
+ raise ValueError("roots must contain at least one candidate root.")
42
+
43
+ desired_sign = numpy.sign(z.imag)
44
+
45
+ # Apply a soft Herglotz sign filter: prefer roots with Im(w) having the
46
+ # same sign as Im(z), allowing tiny numerical violations near the axis.
47
+ imag_roots = numpy.imag(roots)
48
+
49
+ good = roots[numpy.sign(imag_roots) == desired_sign]
50
+ if good.size == 0:
51
+ good = roots[(imag_roots * desired_sign) > -1e-12]
52
+
53
+ candidates = good if good.size > 0 else roots
54
+ idx = int(numpy.argmin(numpy.abs(candidates - target)))
55
+ return candidates[idx]
56
+
57
+
58
+ # ==============
59
+ # stieltjes poly
60
+ # ==============
61
+
62
+ class StieltjesPoly(object):
63
+ """
64
+ Stieltjes-branch evaluator for an algebraic equation P(z, m) = 0.
65
+
66
+ This class represents the Stieltjes-branch solution m(z) of an algebraic
67
+ equation defined by a polynomial relation
68
+
69
+ P(z, m) = 0,
70
+
71
+ where P is a polynomial in z and m with monomial-basis coefficients.
72
+ The coefficient matrix ``a`` is fixed at construction time, and all
73
+ quantities depending only on ``a`` are precomputed. Evaluation at a
74
+ complex point ``z`` is performed via :meth:`evaluate`. The instance is
75
+ also callable; :meth:`__call__` supports scalar or vector inputs and
76
+ applies :meth:`evaluate` elementwise.
77
+
78
+ The Stieltjes branch is selected by initializing in the appropriate
79
+ half-plane using an asymptotic Stieltjes estimate and then performing
80
+ homotopy continuation along a straight-line path in the complex plane.
81
+
82
+ Parameters
83
+ ----------
84
+ a : ndarray, shape (L, K)
85
+ Coefficient matrix defining P(z, m) in the monomial basis. For fixed
86
+ z, the coefficients of the polynomial in m are assembled from powers
87
+ of z.
88
+ eps : float or None, optional
89
+ If Im(z) == 0, use z + i*eps as the boundary evaluation point.
90
+ If None and Im(z) == 0, eps is set to 1e-8 * max(1, |z|).
91
+ height : float, default = 2.0
92
+ Imaginary height used for the starting point z0 in the same
93
+ half-plane as the evaluation point.
94
+ steps : int, default = 100
95
+ Number of continuation steps along the homotopy path.
96
+ order : int, default = 15
97
+ Number of moments in Stieltjes estimate
98
+
99
+ Methods
100
+ -------
101
+ evaluate(z)
102
+ Evaluate the Stieltjes-branch solution m(z) at a single complex point.
103
+
104
+ __call__(z)
105
+ If ``z`` is scalar, returns ``evaluate(z, ...)``.
106
+ If ``z`` is array-like, returns an array of the same shape, where each
107
+ entry is computed by calling ``evaluate`` on the corresponding element.
108
+
109
+ Notes
110
+ -----
111
+ If an input ``z`` value is real (Im(z) == 0), the evaluation is interpreted
112
+ as a boundary value by replacing that element with z + i*eps. If ``eps`` is
113
+ None, eps is chosen per element as 1e-8 * max(1, |z|).
114
+ """
115
+
116
+ def __init__(self, a, eps=None, height=2.0, steps=100, order=15):
117
+ a = numpy.asarray(a)
118
+ if a.ndim != 2:
119
+ raise ValueError("a must be a 2D array.")
120
+
121
+ self.a = a
122
+ self.a_l, _ = a.shape
123
+ self.eps = eps
124
+ self.height = height
125
+ self.steps = steps
126
+ self.order = order
127
+
128
+ # Objects depending only on a
129
+ self.mom = AlgebraicStieltjesMoments(a)
130
+ self._zpows_exp = numpy.arange(self.a_l)
131
+ self.rad = 1.0 + self.height * self.mom.radius(self.order)
132
+
133
+ def _poly_coeffs_m(self, z_val):
134
+ z_powers = z_val ** self._zpows_exp
135
+ return (z_powers @ self.a)[::-1]
136
+
137
+ def _poly_roots(self, z_val):
138
+ coeffs = numpy.asarray(self._poly_coeffs_m(z_val),
139
+ dtype=numpy.complex128)
140
+ return numpy.roots(coeffs)
141
+
142
+ def evaluate(self, z, eps=None, height=2.0, steps=100, order=15):
143
+ """
144
+ Evaluate the Stieltjes-branch solution m(z) at a single point.
145
+
146
+ Parameters are as in the original function, except ``a`` is fixed at
147
+ construction time.
148
+ """
149
+ z = complex(z)
150
+
151
+ if steps < 1:
152
+ raise ValueError("steps must be a positive integer.")
153
+
154
+ # Boundary-value interpretation on the real axis
155
+ if z.imag == 0.0:
156
+ if self.eps is None:
157
+ eps_loc = 1e-8 * max(1.0, abs(z))
158
+ else:
159
+ eps_loc = float(self.eps)
160
+ z_eval = z + 1j * eps_loc
161
+ else:
162
+ z_eval = z
163
+
164
+ half_sign = numpy.sign(z_eval.imag)
165
+ if half_sign == 0.0:
166
+ half_sign = 1.0
167
+
168
+ # If z is outside radius of convergence, no homotopy
169
+ # necessary
170
+ if numpy.abs(z) > self.rad:
171
+ target = self.mom.stieltjes(z, self.order)
172
+ return select_root(self._poly_roots(z), z, target)
173
+
174
+ z0 = 1j * float(half_sign) * self.rad
175
+ target = self.mom.stieltjes(z0, self.order)
176
+
177
+ # Initialize at z0
178
+ w_prev = select_root(self._poly_roots(z0), z0, target)
179
+
180
+ # Straight-line homotopy continuation
181
+ for tau in numpy.linspace(0.0, 1.0, int(self.steps) + 1)[1:]:
182
+ z_tau = z0 + tau * (z_eval - z0)
183
+ w_prev = select_root(self._poly_roots(z_tau), z_tau, w_prev)
184
+
185
+ return w_prev
186
+
187
+ def __call__(self, z):
188
+ # Scalar fast-path
189
+ if numpy.isscalar(z):
190
+ return self.evaluate(z)
191
+
192
+ # Array-like: evaluate elementwise, preserving shape
193
+ z_arr = numpy.asarray(z)
194
+ out = numpy.empty(z_arr.shape, dtype=numpy.complex128)
195
+
196
+ # Iterate over indices so we can pass Python scalars into evaluate()
197
+ for idx in numpy.ndindex(z_arr.shape):
198
+ out[idx] = self.evaluate(z_arr[idx])
199
+
200
+ return out
201
+
202
+
203
+ # def stieltjes_poly(z, a, eps=None, height=2., steps=100, order=15):
204
+ # """
205
+ # Evaluate the Stieltjes-branch solution m(z) of an algebraic equation.
206
+
207
+ # The coefficients `a` define a polynomial relation
208
+ # P(z, m) = 0,
209
+ # where P is a polynomial in z and m with monomial-basis coefficients
210
+ # arranged so that for fixed z, the coefficients of the polynomial in m
211
+ # can be assembled from powers of z.
212
+
213
+ # Parameters
214
+ # ----------
215
+ # z : complex
216
+ # Evaluation point. Must be a single value.
217
+ # a : ndarray, shape (L, K)
218
+ # Coefficient matrix defining P(z, m) in the monomial basis.
219
+ # eps : float or None, optional
220
+ # If Im(z) == 0, use z + i*eps as the boundary evaluation point.
221
+ # If None and Im(z) == 0, eps is set to 1e-8 * max(1, |z|).
222
+ # height : float, default = 2.0
223
+ # Imaginary height used for the starting point z0 in the same
224
+ # half-plane as the evaluation point.
225
+ # steps : int, default = 100
226
+ # Number of continuation steps along the homotopy path.
227
+ # order : int, default = 15
228
+ # Number of moments in Stieltjes estimate
229
+
230
+ # Returns
231
+ # -------
232
+ # w : complex
233
+ # Value of the Stieltjes-branch solution m(z) (or m(z+i*eps) if z is
234
+ # real).
235
+ # """
236
+
237
+ # z = complex(z)
238
+ # a = numpy.asarray(a)
239
+
240
+ # if a.ndim != 2:
241
+ # raise ValueError('a must be a 2D array.')
242
+
243
+ # if steps < 1:
244
+ # raise ValueError("steps must be a positive integer.")
245
+
246
+ # a_l, _ = a.shape
247
+ # mom = AlgebraicStieltjesMoments(a)
248
+
249
+ # def poly_coeffs_m(z_val):
250
+ # z_powers = z_val ** numpy.arange(a_l)
251
+ # return (z_powers @ a)[::-1]
252
+
253
+ # def poly_roots(z_val):
254
+ # coeffs = numpy.asarray(poly_coeffs_m(z_val), dtype=numpy.complex128)
255
+ # return numpy.roots(coeffs)
256
+
257
+ # # If user asked for a real-axis value, interpret as boundary value from C+.
258
+ # if z.imag == 0.0:
259
+ # if eps is None:
260
+ # eps = 1e-8 * max(1.0, abs(z))
261
+ # z_eval = z + 1j * float(eps)
262
+ # else:
263
+ # z_eval = z
264
+
265
+ # half_sign = numpy.sign(z_eval.imag)
266
+ # if half_sign == 0.0:
267
+ # half_sign = 1.0
268
+
269
+ # z0 = 1j * float(half_sign) * (1. + height * mom.radius(order))
270
+ # target = mom.stieltjes(z0, order)
271
+
272
+ # # Initialize at z0 via asymptotic / Im-sign selection.
273
+ # w_prev = select_root(poly_roots(z0), z0, target)
274
+
275
+ # # Straight-line homotopy from z0 to z_eval.
276
+ # for tau in numpy.linspace(0.0, 1.0, int(steps) + 1)[1:]:
277
+ # z_tau = z0 + tau * (z_eval - z0)
278
+ # w_prev = select_root(poly_roots(z_tau), z_tau, w_prev)
279
+
280
+ # return w_prev