freealg 0.7.10__py3-none-any.whl → 0.7.11__py3-none-any.whl

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.
freealg/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.7.10"
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
@@ -18,10 +18,11 @@ from ._continuation_algebraic import sample_z_joukowski, \
18
18
  filter_z_away_from_cuts, fit_polynomial_relation, \
19
19
  sanity_check_stieltjes_branch, eval_P
20
20
  from ._edge import evolve_edges, merge_edges
21
- from ._decompress import decompress_newton
21
+ from ._decompress import build_time_grid, decompress_newton
22
22
  from ._decompress2 import decompress_coeffs
23
23
  from ._homotopy import StieltjesPoly
24
- from ._discriminant import compute_singular_points
24
+ from ._branch_points import compute_branch_points
25
+ from ._support import compute_support
25
26
  from ._moments import MomentsESD
26
27
  from .._free_form._support import supp
27
28
  from .._free_form._plot_util import plot_density, plot_hilbert, plot_stieltjes
@@ -133,9 +134,6 @@ class AlgebraicForm(object):
133
134
  # init
134
135
  # ====
135
136
 
136
- # def __init__(self, A, support=None, delta=1e-6, dtype='complex128',
137
- # **kwargs):
138
-
139
137
  def __init__(self, A, support=None, delta=1e-5, dtype='complex128',
140
138
  **kwargs):
141
139
  """
@@ -147,6 +145,7 @@ class AlgebraicForm(object):
147
145
  self._stieltjes = None
148
146
  self._moments = None
149
147
  self.support = support
148
+ self.est_support = None # Estimated from polynmial after fitting
150
149
  self.delta = delta # Offset above real axis to apply Plemelj formula
151
150
 
152
151
  # Data type for complex arrays
@@ -155,6 +154,7 @@ class AlgebraicForm(object):
155
154
  if hasattr(A, 'stieltjes') and callable(getattr(A, 'stieltjes', None)):
156
155
  # This is one of the distribution objects, like MarchenkoPastur
157
156
  self._stieltjes = A.stieltjes
157
+ self.support = A.support()
158
158
  self.n = 1
159
159
 
160
160
  elif callable(A):
@@ -182,16 +182,15 @@ class AlgebraicForm(object):
182
182
  numpy.mean(1.0/(self.eig-z[:, numpy.newaxis]), axis=-1)
183
183
  self._moments = MomentsESD(self.eig) # NOTE (never used)
184
184
 
185
- # Support
186
- if support is None:
185
+ # broad support
186
+ if self.support is None:
187
187
  if self.eig is None:
188
188
  raise RuntimeError("Support must be provided without data")
189
+
189
190
  # Detect support
190
191
  self.lam_m, self.lam_p = supp(self.eig, **kwargs)
191
- self.support = [(self.lam_m, self.lam_p)]
192
- self.broad_support = self.support[0]
192
+ self.broad_support = (self.lam_m, self.lam_p)
193
193
  else:
194
- self.support = support
195
194
  self.lam_m = min([s[0] for s in self.support])
196
195
  self.lam_p = max([s[1] for s in self.support])
197
196
  self.broad_support = (self.lam_m, self.lam_p)
@@ -251,7 +250,16 @@ class AlgebraicForm(object):
251
250
  # self.cache.clear()
252
251
 
253
252
  z_fits = []
254
- for sup in self.support:
253
+
254
+ # Sampling around support, or broad_support. This is only needed to
255
+ # ensure sampled points are not hiting the support itself is not used
256
+ # in any computation. If support is not known, use broad support.
257
+ if self.support is not None:
258
+ possible_support = self.support
259
+ else:
260
+ possible_support = self.broad_support
261
+
262
+ for sup in possible_support:
255
263
  a, b = sup
256
264
 
257
265
  for i in range(len(r)):
@@ -261,7 +269,7 @@ class AlgebraicForm(object):
261
269
  z_fit = numpy.concatenate(z_fits)
262
270
 
263
271
  # Remove points too close to any cut
264
- z_fit = filter_z_away_from_cuts(z_fit, self.support, y_eps=y_eps,
272
+ z_fit = filter_z_away_from_cuts(z_fit, possible_support, y_eps=y_eps,
265
273
  x_pad=x_pad)
266
274
 
267
275
  # Fitting (w_inf = None means adaptive weight selection)
@@ -271,11 +279,11 @@ class AlgebraicForm(object):
271
279
  triangular=triangular, normalize=normalize, mu=mu,
272
280
  mu_reg=mu_reg)
273
281
 
274
- # Compute global branch points, zeros of leading a_j, and support
275
- branch_points, a_s_zero, support = compute_singular_points(a_coeffs)
276
-
277
282
  self.a_coeffs = a_coeffs
278
283
 
284
+ # Estimate support from the fitted polynomial
285
+ self.est_support, _ = self.estimate_support(a_coeffs)
286
+
279
287
  # Reporting error
280
288
  P_res = numpy.abs(eval_P(z_fit, m1_fit, a_coeffs))
281
289
  res_max = numpy.max(P_res[numpy.isfinite(P_res)])
@@ -288,8 +296,6 @@ class AlgebraicForm(object):
288
296
  eta=max(y_eps, 1e-2), n_x=128,
289
297
  max_bad_frac=0.05)
290
298
 
291
- status['branch_points'] = branch_points
292
- status['a_s_zero'] = a_s_zero
293
299
  status['res_max'] = float(res_max)
294
300
  status['res_99_9'] = float(res_99_9)
295
301
  status['fit_metrics'] = fit_metrics
@@ -319,7 +325,64 @@ class AlgebraicForm(object):
319
325
  else:
320
326
  print('\nStieltjes sanity check: OK')
321
327
 
322
- return a_coeffs, support, status
328
+ return a_coeffs, self.est_support, status
329
+
330
+ # =====================
331
+ # inflate broad support
332
+ # =====================
333
+
334
+ def _inflate_broad_support(self, inflate=0.0):
335
+ """
336
+ """
337
+
338
+ min_supp, max_supp = self.broad_support
339
+
340
+ c_supp = 0.5 * (max_supp + min_supp)
341
+ r_supp = 0.5 * (max_supp - min_supp)
342
+
343
+ x_min = c_supp - r_supp * (1.0 + inflate)
344
+ x_max = c_supp + r_supp * (1.0 + inflate)
345
+
346
+ return x_min, x_max
347
+
348
+ # ================
349
+ # estimate support
350
+ # ================
351
+
352
+ def estimate_support(self, a_coeffs=None, n_scan=4000):
353
+ """
354
+ """
355
+
356
+ if a_coeffs is None:
357
+ if self.a_coeffs is None:
358
+ raise RuntimeError('Call "fit" first.')
359
+ else:
360
+ a_coeffs = self.a_coeffs
361
+
362
+ # Inflate a bit to make sure all points are searched
363
+ x_min, x_max = self._inflate_broad_support(inflate=0.2)
364
+
365
+ est_support, info = compute_support(a_coeffs, x_min=x_min, x_max=x_max,
366
+ n_scan=n_scan)
367
+
368
+ return est_support, info
369
+
370
+ # ======================
371
+ # estimate branch points
372
+ # ======================
373
+
374
+ def estimate_branch_points(self):
375
+ """
376
+ Compute global branch points and zeros of leading a_j
377
+ """
378
+
379
+ if self.a_coeffs is None:
380
+ raise RuntimeError('Call "fit" first.')
381
+
382
+ bp, leading_zeros, info = compute_branch_points(
383
+ self.a_coeffs, tol=1e-12, real_tol=None)
384
+
385
+ return bp, leading_zeros, info
323
386
 
324
387
  # =============
325
388
  # generate grid
@@ -464,7 +527,7 @@ class AlgebraicForm(object):
464
527
  hilb = -self._stieltjes(x).real / numpy.pi
465
528
 
466
529
  if plot:
467
- plot_hilbert(x, hilb, support=self.support, latex=latex,
530
+ plot_hilbert(x, hilb, support=self.broad_support, latex=latex,
468
531
  save=save)
469
532
 
470
533
  return hilb
@@ -592,8 +655,9 @@ class AlgebraicForm(object):
592
655
 
593
656
  def decompress(self, size, x=None, method='one', plot=False, latex=False,
594
657
  save=False, verbose=False, newton_opt={
595
- 'max_iter': 50, 'tol': 1e-12, 'armijo': 1e-4,
596
- 'min_lam': 1e-6, 'w_min': 1e-14, 'sweep': True}):
658
+ 'min_n_times': 10, 'max_iter': 50, 'tol': 1e-12,
659
+ 'armijo': 1e-4, 'min_lam': 1e-6, 'w_min': 1e-14,
660
+ 'sweep': True}):
597
661
  """
598
662
  Free decompression of spectral density.
599
663
  """
@@ -634,25 +698,23 @@ class AlgebraicForm(object):
634
698
  # Query grid on the real axis + a small imaginary buffer
635
699
  z_query = x + 1j * self.delta
636
700
 
637
- # Initial condition at t=0 (physical branch)
701
+ # Initial condition at t = 0 (physical branch)
638
702
  w0_list = self._stieltjes(z_query)
639
703
 
640
- # Times
641
- t = numpy.log(alpha)
642
-
643
- # Ensure it starts from t = 0
644
- if t[0] > 1.0:
645
- t = numpy.concatenate([numpy.zeros(1), t])
704
+ # Ensure there are at least min_n_times time t, including requested
705
+ # times, and especially time t = 0
706
+ t_all, idx_req = build_time_grid(
707
+ size, self.n, min_n_time=newton_opt.get("min_n_time", 0))
646
708
 
647
709
  # Evolve
648
710
  W, ok = decompress_newton(
649
- z_query, t, self.a_coeffs,
711
+ z_query, t_all, self.a_coeffs,
650
712
  w0_list=w0_list, **newton_opt)
651
713
 
652
- rho = W.imag / numpy.pi
714
+ rho_all = W.imag / numpy.pi
653
715
 
654
- # Remove time zero
655
- rho = rho[1:, :]
716
+ # return only the user-requested ones
717
+ rho = rho_all[idx_req]
656
718
 
657
719
  if verbose:
658
720
  print("success rate per t:", ok.mean(axis=1))
@@ -699,10 +761,19 @@ class AlgebraicForm(object):
699
761
  Evolves spectral edges.
700
762
  """
701
763
 
702
- edges, ok_edges = evolve_edges(t, self.a_coeffs, support=self.support,
703
- eta=eta, dt_max=dt_max,
704
- max_iter=max_iter, tol=tol)
764
+ if self.support is not None:
765
+ known_support = self.support
766
+ elif self.est_support is not None:
767
+ known_support = self.est_support
768
+ else:
769
+ raise RuntimeError('Call "fit" first.')
770
+
771
+ edges, ok_edges = evolve_edges(t, self.a_coeffs,
772
+ support=known_support, eta=eta,
773
+ dt_max=dt_max, max_iter=max_iter,
774
+ tol=tol)
705
775
 
776
+ # Remove spurious edges, where two edge cross and are no longer valid.
706
777
  edges2, active_k = merge_edges(edges, tol=1e-4)
707
778
 
708
779
  if verbose:
@@ -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
@@ -1,17 +1,18 @@
1
1
  freealg/__init__.py,sha256=SjcYb6HWmaclnnM-m1eC1honZRyfNBWYDYBx23kSdjo,833
2
- freealg/__version__.py,sha256=-wENBv06wDArd9jlfvr1fdogsjPVMgMwskP7VuEz-2A,23
2
+ freealg/__version__.py,sha256=wcKNUm4uZK839U_xaeQFOEaUVqbjHp-EU_6MMaml4qk,23
3
3
  freealg/_util.py,sha256=RzccUCORgzrI9NdNqwMVugiHU0uDKkJFcIyjFMUOnv8,2518
4
4
  freealg/_algebraic_form/__init__.py,sha256=MIB_jVgw2qI-JW_ypqaFSeNAB6c4GvpjNySnap_a6hg,398
5
+ freealg/_algebraic_form/_branch_points.py,sha256=jzvHszw7xFe9B15a5RZV3pGfCGtndvrKJ4GIX6F3qhc,7814
5
6
  freealg/_algebraic_form/_constraints.py,sha256=37U7nvtCTocuS7l_nfUznkPi195PY7eXFzeiikrv3B0,2448
6
- freealg/_algebraic_form/_continuation_algebraic.py,sha256=KundB9VfX61a35VRxLFyuvB5A51QdT4PD2ffAMjrKR0,19383
7
- freealg/_algebraic_form/_decompress.py,sha256=EZ005k_a8wXFsyQZie_N-_hoOvQxj2eV8aUlsNNH7UQ,21102
7
+ freealg/_algebraic_form/_continuation_algebraic.py,sha256=vVHFlMJYeXm97pgwEceJB2rGJeGOVhk_Ywg6mjoIA-g,19390
8
+ freealg/_algebraic_form/_decompress.py,sha256=uKiq5jlwmOvGriptIDz97fQiKs_F10uH6eMX1Ix43PQ,22538
8
9
  freealg/_algebraic_form/_decompress2.py,sha256=Ng9w9xmGe9M-DApp35IeNeQlvszfzT4NZx5BQn0lQ3I,2459
9
- freealg/_algebraic_form/_discriminant.py,sha256=755pproom6-xThFARaH20m4GuBwwZS2rc0Y80Yg6NzY,5331
10
10
  freealg/_algebraic_form/_edge.py,sha256=7l9QyLJDxaEY4WB6MCUFtfEZSf04wyHwH7YPHFJXSbM,10690
11
11
  freealg/_algebraic_form/_homotopy.py,sha256=q5z8YmrT_8m7L3qw_4FD1Sd5eELIvAiAHr2ucOLW258,9508
12
12
  freealg/_algebraic_form/_moments.py,sha256=u55RpvQhIMJFGsq8LZ3IlnTKxNgQPhwnPuYUS34YEyw,12400
13
13
  freealg/_algebraic_form/_sheets_util.py,sha256=6OLzWQKu-gN8rxM2rbpbN8TjNZFmD8UJ-8t9kcZdkCo,4174
14
- freealg/_algebraic_form/algebraic_form.py,sha256=TXGgTPexOUx_uXhMF9dPA8kk_uNKXoOWn32TYvmCcO8,30677
14
+ freealg/_algebraic_form/_support.py,sha256=9go_3NjmesSW1e08CiDu8oflpGmAbsh9iZRidMvlARI,7951
15
+ freealg/_algebraic_form/algebraic_form.py,sha256=L5qJFYOX5Qm8LlrBv6YwyuTxROUdn6cCjfXoDklmrlQ,32962
15
16
  freealg/_free_form/__init__.py,sha256=5cnSX7kHci3wKx6-BEFhmVY_NjjmQAq1JjWPTEqETTg,611
16
17
  freealg/_free_form/_chebyshev.py,sha256=zkyVA8NLf7uUKlJdLz4ijd_SurdsqUgkA5nHGWSybaE,6916
17
18
  freealg/_free_form/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
@@ -44,9 +45,9 @@ freealg/distributions/_wigner.py,sha256=epgx6ne6R_7to5j6-QsWIAVFJQFquWMmYgnZYMN4
44
45
  freealg/visualization/__init__.py,sha256=NLq_zwueF7ytZ8sl8zLPqm-AODxxXNvfMozHGmmklcE,435
45
46
  freealg/visualization/_glue_util.py,sha256=2oKnEYjUOS4OZfivmciVLauVr53kyHMwi6c2zRKilTQ,693
46
47
  freealg/visualization/_rgb_hsv.py,sha256=rEskxXxSlKKxIrHRslVkgxHtD010L3ge9YtcVsOPl8E,3650
47
- freealg-0.7.10.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
48
- freealg-0.7.10.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
49
- freealg-0.7.10.dist-info/METADATA,sha256=zQjA9ZEsQiqu7ooVnkTrWqcMD7Hg29_Ig0xSxFEG3-Y,5517
50
- freealg-0.7.10.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
51
- freealg-0.7.10.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
52
- freealg-0.7.10.dist-info/RECORD,,
48
+ freealg-0.7.11.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
49
+ freealg-0.7.11.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
50
+ freealg-0.7.11.dist-info/METADATA,sha256=Cinwx4ei_4R4aY743wVBq_DreuX8KPlwARHCWT--AWo,5517
51
+ freealg-0.7.11.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
52
+ freealg-0.7.11.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
53
+ freealg-0.7.11.dist-info/RECORD,,
@@ -1,226 +0,0 @@
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