freealg 0.7.10__tar.gz → 0.7.11__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 (65) hide show
  1. {freealg-0.7.10 → freealg-0.7.11}/PKG-INFO +1 -1
  2. freealg-0.7.11/freealg/__version__.py +1 -0
  3. freealg-0.7.11/freealg/_algebraic_form/_branch_points.py +288 -0
  4. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_continuation_algebraic.py +1 -1
  5. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_decompress.py +44 -1
  6. freealg-0.7.11/freealg/_algebraic_form/_support.py +309 -0
  7. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/algebraic_form.py +106 -35
  8. {freealg-0.7.10 → freealg-0.7.11}/freealg.egg-info/PKG-INFO +1 -1
  9. {freealg-0.7.10 → freealg-0.7.11}/freealg.egg-info/SOURCES.txt +2 -1
  10. freealg-0.7.10/freealg/__version__.py +0 -1
  11. freealg-0.7.10/freealg/_algebraic_form/_discriminant.py +0 -226
  12. {freealg-0.7.10 → freealg-0.7.11}/AUTHORS.txt +0 -0
  13. {freealg-0.7.10 → freealg-0.7.11}/CHANGELOG.rst +0 -0
  14. {freealg-0.7.10 → freealg-0.7.11}/LICENSE.txt +0 -0
  15. {freealg-0.7.10 → freealg-0.7.11}/MANIFEST.in +0 -0
  16. {freealg-0.7.10 → freealg-0.7.11}/README.rst +0 -0
  17. {freealg-0.7.10 → freealg-0.7.11}/freealg/__init__.py +0 -0
  18. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/__init__.py +0 -0
  19. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_constraints.py +0 -0
  20. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_decompress2.py +0 -0
  21. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_edge.py +0 -0
  22. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_homotopy.py +0 -0
  23. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_moments.py +0 -0
  24. {freealg-0.7.10 → freealg-0.7.11}/freealg/_algebraic_form/_sheets_util.py +0 -0
  25. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/__init__.py +0 -0
  26. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_chebyshev.py +0 -0
  27. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_damp.py +0 -0
  28. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_decompress.py +0 -0
  29. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_density_util.py +0 -0
  30. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_jacobi.py +0 -0
  31. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_linalg.py +0 -0
  32. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_pade.py +0 -0
  33. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_plot_util.py +0 -0
  34. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_sample.py +0 -0
  35. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_series.py +0 -0
  36. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/_support.py +0 -0
  37. {freealg-0.7.10 → freealg-0.7.11}/freealg/_free_form/free_form.py +0 -0
  38. {freealg-0.7.10 → freealg-0.7.11}/freealg/_geometric_form/__init__.py +0 -0
  39. {freealg-0.7.10 → freealg-0.7.11}/freealg/_geometric_form/_continuation_genus0.py +0 -0
  40. {freealg-0.7.10 → freealg-0.7.11}/freealg/_geometric_form/_continuation_genus1.py +0 -0
  41. {freealg-0.7.10 → freealg-0.7.11}/freealg/_geometric_form/_elliptic_functions.py +0 -0
  42. {freealg-0.7.10 → freealg-0.7.11}/freealg/_geometric_form/_sphere_maps.py +0 -0
  43. {freealg-0.7.10 → freealg-0.7.11}/freealg/_geometric_form/_torus_maps.py +0 -0
  44. {freealg-0.7.10 → freealg-0.7.11}/freealg/_geometric_form/geometric_form.py +0 -0
  45. {freealg-0.7.10 → freealg-0.7.11}/freealg/_util.py +0 -0
  46. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/__init__.py +0 -0
  47. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_chiral_block.py +0 -0
  48. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_deformed_marchenko_pastur.py +0 -0
  49. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_deformed_wigner.py +0 -0
  50. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_kesten_mckay.py +0 -0
  51. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_marchenko_pastur.py +0 -0
  52. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_meixner.py +0 -0
  53. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_wachter.py +0 -0
  54. {freealg-0.7.10 → freealg-0.7.11}/freealg/distributions/_wigner.py +0 -0
  55. {freealg-0.7.10 → freealg-0.7.11}/freealg/visualization/__init__.py +0 -0
  56. {freealg-0.7.10 → freealg-0.7.11}/freealg/visualization/_glue_util.py +0 -0
  57. {freealg-0.7.10 → freealg-0.7.11}/freealg/visualization/_rgb_hsv.py +0 -0
  58. {freealg-0.7.10 → freealg-0.7.11}/freealg.egg-info/dependency_links.txt +0 -0
  59. {freealg-0.7.10 → freealg-0.7.11}/freealg.egg-info/not-zip-safe +0 -0
  60. {freealg-0.7.10 → freealg-0.7.11}/freealg.egg-info/requires.txt +0 -0
  61. {freealg-0.7.10 → freealg-0.7.11}/freealg.egg-info/top_level.txt +0 -0
  62. {freealg-0.7.10 → freealg-0.7.11}/pyproject.toml +0 -0
  63. {freealg-0.7.10 → freealg-0.7.11}/requirements.txt +0 -0
  64. {freealg-0.7.10 → freealg-0.7.11}/setup.cfg +0 -0
  65. {freealg-0.7.10 → freealg-0.7.11}/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.11
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.11"
@@ -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
@@ -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
  }
@@ -14,7 +14,50 @@
14
14
  import numpy
15
15
  from ._continuation_algebraic import powers
16
16
 
17
- __all__ = ['decompress_newton_old', 'decompress_newton']
17
+ __all__ = ['build_time_grid', 'decompress_newton_old', 'decompress_newton']
18
+
19
+
20
+ # ===============
21
+ # build time grid
22
+ # ===============
23
+
24
+ def build_time_grid(sizes, n0, min_n_time=0):
25
+ """
26
+ sizes: list/array of requested matrix sizes (e.g. [2000,3000,4000,8000])
27
+ n0: initial size (self.n)
28
+ min_n_time: minimum number of time points to run Newton sweep on
29
+
30
+ Returns
31
+ -------
32
+ t_all: sorted time grid to run solver on
33
+ idx_req: indices of requested times inside t_all (same order as sizes)
34
+ """
35
+
36
+ sizes = numpy.asarray(sizes, dtype=float)
37
+ alpha = sizes / float(n0)
38
+ t_req = numpy.log(alpha)
39
+
40
+ # Always include t=0 and T=max(t_req)
41
+ T = float(numpy.max(t_req)) if t_req.size else 0.0
42
+ base = numpy.unique(numpy.r_[0.0, t_req, T])
43
+ t_all = numpy.sort(base)
44
+
45
+ # Add points only if needed: split largest gaps
46
+ N = int(min_n_time) if min_n_time is not None else 0
47
+ while t_all.size < N and t_all.size >= 2:
48
+ gaps = numpy.diff(t_all)
49
+ k = int(numpy.argmax(gaps))
50
+ mid = 0.5 * (t_all[k] + t_all[k+1])
51
+ t_all = numpy.sort(numpy.unique(numpy.r_[t_all, mid]))
52
+
53
+ # Map each requested time to an index in t_all (stable, no float drama)
54
+ # (t_req values came from same construction, so they should match exactly;
55
+ # still: use searchsorted + assert)
56
+ idx_req = numpy.searchsorted(t_all, t_req)
57
+ # optional sanity:
58
+ # assert numpy.allclose(t_all[idx_req], t_req, rtol=0, atol=0)
59
+
60
+ return t_all, idx_req
18
61
 
19
62
 
20
63
  # ===============
@@ -0,0 +1,309 @@
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
+ import numpy.polynomial.polynomial as poly
16
+
17
+ __all__ = ['compute_support']
18
+
19
+
20
+ # ======================
21
+ # poly coeffs in m and z
22
+ # ======================
23
+
24
+ def _poly_coeffs_in_m_at_z(a_coeffs, z):
25
+
26
+ s = a_coeffs.shape[1] - 1
27
+ a = numpy.empty(s + 1, dtype=numpy.complex128)
28
+ for j in range(s + 1):
29
+ a[j] = poly.polyval(z, a_coeffs[:, j])
30
+ return a
31
+
32
+
33
+ # ===============
34
+ # roots poly in m
35
+ # ===============
36
+
37
+ def _roots_poly_in_m(c_asc, tol=0.0):
38
+
39
+ c = numpy.asarray(c_asc, dtype=numpy.complex128).ravel()
40
+ if c.size <= 1:
41
+ return numpy.array([], dtype=numpy.complex128)
42
+
43
+ k = c.size - 1
44
+ while k > 0 and abs(c[k]) <= tol:
45
+ k -= 1
46
+ c = c[:k + 1]
47
+ if c.size <= 1:
48
+ return numpy.array([], dtype=numpy.complex128)
49
+
50
+ return numpy.roots(c[::-1])
51
+
52
+
53
+ # ================
54
+ # dPdm coeffs at z
55
+ # ================
56
+
57
+ def _dPdm_coeffs_at_z(a_coeffs, z):
58
+
59
+ a = _poly_coeffs_in_m_at_z(a_coeffs, z)
60
+ s = a.size - 1
61
+ if s <= 0:
62
+ return numpy.array([0.0 + 0.0j], dtype=numpy.complex128)
63
+ d = numpy.empty(s, dtype=numpy.complex128)
64
+ for j in range(1, s + 1):
65
+ d[j - 1] = j * a[j]
66
+ return d
67
+
68
+
69
+ # ==============
70
+ # P and partials
71
+ # ==============
72
+
73
+ def _P_and_partials(a_coeffs, z, m):
74
+
75
+ s = a_coeffs.shape[1] - 1
76
+
77
+ a = numpy.empty(s + 1, dtype=numpy.complex128)
78
+ da = numpy.empty(s + 1, dtype=numpy.complex128)
79
+ for j in range(s + 1):
80
+ a[j] = poly.polyval(z, a_coeffs[:, j])
81
+ da[j] = poly.polyval(z, poly.polyder(a_coeffs[:, j]))
82
+
83
+ mpow = 1.0 + 0.0j
84
+ P = 0.0 + 0.0j
85
+ Pz = 0.0 + 0.0j
86
+ for j in range(s + 1):
87
+ P += a[j] * mpow
88
+ Pz += da[j] * mpow
89
+ mpow *= m
90
+
91
+ Pm = 0.0 + 0.0j
92
+ Pmm = 0.0 + 0.0j
93
+ Pzm = 0.0 + 0.0j
94
+ for j in range(1, s + 1):
95
+ Pm += j * a[j] * (m ** (j - 1))
96
+ Pzm += j * da[j] * (m ** (j - 1))
97
+ for j in range(2, s + 1):
98
+ Pmm += j * (j - 1) * a[j] * (m ** (j - 2))
99
+
100
+ return P, Pz, Pm, Pzm, Pmm, a
101
+
102
+
103
+ # ===========
104
+ # newton edge
105
+ # ===========
106
+
107
+ def _newton_edge(a_coeffs, x0, m0, tol=1e-12, max_iter=50):
108
+
109
+ x = float(x0)
110
+ m = float(m0)
111
+
112
+ for _ in range(max_iter):
113
+ z = x + 0.0j
114
+ P, Pz, Pm, Pzm, Pmm, _ = _P_and_partials(a_coeffs, z, m)
115
+
116
+ f0 = float(numpy.real(P))
117
+ f1 = float(numpy.real(Pm))
118
+
119
+ j00 = float(numpy.real(Pz))
120
+ j01 = float(numpy.real(Pm))
121
+ j10 = float(numpy.real(Pzm))
122
+ j11 = float(numpy.real(Pmm))
123
+
124
+ det = j00 * j11 - j01 * j10
125
+ if det == 0.0 or (not numpy.isfinite(det)):
126
+ return x, m, False
127
+
128
+ dx = (-f0 * j11 + f1 * j01) / det
129
+ dm = (-j00 * f1 + j10 * f0) / det
130
+
131
+ x += dx
132
+ m += dm
133
+
134
+ if abs(dx) + abs(dm) < tol:
135
+ return x, m, True
136
+
137
+ return x, m, False
138
+
139
+
140
+ # =============
141
+ # cluster edges
142
+ # =============
143
+
144
+ def _cluster_edges(edges, x_tol):
145
+
146
+ if len(edges) == 0:
147
+ return numpy.array([], dtype=float)
148
+
149
+ edges = numpy.array(sorted(edges), dtype=float)
150
+ out = [edges[0]]
151
+ for e in edges[1:]:
152
+ if abs(e - out[-1]) > x_tol:
153
+ out.append(e)
154
+ return numpy.array(out, dtype=float)
155
+
156
+
157
+ # =======================
158
+ # pick physical root at z
159
+ # =======================
160
+
161
+ def _pick_physical_root_at_z(a_coeffs, z, im_sign=+1):
162
+
163
+ a = _poly_coeffs_in_m_at_z(a_coeffs, z)
164
+ r = _roots_poly_in_m(a)
165
+ if r.size == 0:
166
+ return numpy.nan + 1j * numpy.nan
167
+
168
+ w_ref = -1.0 / z
169
+ idx = int(numpy.argmin(numpy.abs(r - w_ref)))
170
+ w = r[idx]
171
+
172
+ # optional strictness: if it violates Herglotz, declare failure
173
+ if not numpy.isfinite(w.real) or not numpy.isfinite(w.imag):
174
+ return w
175
+ if (im_sign * w.imag) <= 0.0:
176
+ return w
177
+
178
+ return w
179
+
180
+
181
+ # ===============
182
+ # compute support
183
+ # ===============
184
+
185
+ def compute_support(a_coeffs,
186
+ x_min,
187
+ x_max,
188
+ n_scan=4000,
189
+ y_eps=1e-3,
190
+ im_sign=+1,
191
+ root_tol=0.0,
192
+ edge_rel_tol=1e-6,
193
+ edge_x_cluster_tol=1e-3,
194
+ newton_tol=1e-12):
195
+ """
196
+ Fast support from fitted polynomial using branch-point system P=0, Pm=0.
197
+
198
+ Returns
199
+ -------
200
+ support : list of (a,b)
201
+ info : dict (edges, rel_res_curve, etc.)
202
+ """
203
+
204
+ a_coeffs = numpy.asarray(a_coeffs)
205
+ x_grid = numpy.linspace(float(x_min), float(x_max), int(n_scan))
206
+
207
+ # For each x, find best real critical point m (Pm=0) minimizing rel
208
+ # residual.
209
+ rel = numpy.full(x_grid.size, numpy.inf, dtype=float)
210
+ m_star = numpy.full(x_grid.size, numpy.nan, dtype=float)
211
+
212
+ for i, x in enumerate(x_grid):
213
+ z = x + 0.0j
214
+ dcoef = _dPdm_coeffs_at_z(a_coeffs, z)
215
+ mr = _roots_poly_in_m(dcoef, tol=root_tol)
216
+
217
+ best = numpy.inf
218
+ best_m = numpy.nan
219
+
220
+ for w in mr:
221
+ # accept nearly-real roots; numerical roots can have small imag
222
+ # part
223
+ if abs(w.imag) > 1e-6 * (1.0 + abs(w.real)):
224
+ continue
225
+ m = float(w.real)
226
+ P, _, _, _, _, a = _P_and_partials(a_coeffs, z, m)
227
+
228
+ denom = 1.0
229
+ am = 1.0
230
+ for j in range(a.size):
231
+ denom += abs(a[j]) * abs(am)
232
+ am *= m
233
+
234
+ r = abs(numpy.real(P)) / denom
235
+ if numpy.isfinite(r) and r < best:
236
+ best = float(r)
237
+ best_m = m
238
+
239
+ rel[i] = best
240
+ m_star[i] = best_m
241
+
242
+ # Pick candidate edges as local minima of rel(x), below an automatic scale.
243
+ rel_f = rel[numpy.isfinite(rel)]
244
+ if rel_f.size == 0:
245
+ return [], {"edges": numpy.array([], dtype=float), "n_edges": 0}
246
+
247
+ med = float(numpy.median(rel_f))
248
+ min_rel = float(numpy.min(rel_f))
249
+
250
+ # accept local minima up to a factor above the best one, but never abov
251
+ # background scale
252
+ thr = min(0.1 * med, max(float(edge_rel_tol), 1e4 * min_rel))
253
+
254
+ edges0 = []
255
+ seeds = []
256
+
257
+ for i in range(1, x_grid.size - 1):
258
+ if not numpy.isfinite(rel[i]):
259
+ continue
260
+ if rel[i] <= rel[i - 1] and rel[i] <= rel[i + 1] and rel[i] < thr and \
261
+ numpy.isfinite(m_star[i]):
262
+ edges0.append(float(x_grid[i]))
263
+ seeds.append((float(x_grid[i]), float(m_star[i])))
264
+
265
+ # Refine each seed by 2D Newton (x,m)
266
+ edges = []
267
+ for x0, m0 in seeds:
268
+ xe, me, ok = _newton_edge(a_coeffs, x0, m0, tol=newton_tol)
269
+ if ok and numpy.isfinite(xe) and numpy.isfinite(me):
270
+ edges.append(float(xe))
271
+
272
+ edges = _cluster_edges(edges, edge_x_cluster_tol)
273
+ edges.sort()
274
+
275
+ # Build support by testing midpoints between consecutive real edges
276
+ support = []
277
+ m_im_tol = 1e-10
278
+
279
+ for i in range(edges.size - 1):
280
+ a = float(edges[i])
281
+ b = float(edges[i + 1])
282
+ if b <= a:
283
+ continue
284
+
285
+ xmid = 0.5 * (a + b)
286
+
287
+ # roots of P(xmid, m) with real coefficients
288
+ a_m = _poly_coeffs_in_m_at_z(a_coeffs, xmid + 0.0j)
289
+ r = _roots_poly_in_m(a_m, tol=root_tol)
290
+
291
+ # interval is support iff there exists a non-real root (complex pair)
292
+ if numpy.any(numpy.abs(numpy.imag(r)) > m_im_tol):
293
+ support.append((a, b))
294
+
295
+ info = {
296
+ "edges": edges,
297
+ "n_edges": int(edges.size),
298
+ "support": support,
299
+ "n_support": int(len(support)),
300
+ "x_grid": x_grid,
301
+ "rel": rel,
302
+ "thr": float(thr),
303
+ "x_min": float(x_min),
304
+ "x_max": float(x_max),
305
+ "n_scan": int(n_scan),
306
+ "y_eps": float(y_eps),
307
+ }
308
+
309
+ return support, info