freealg 0.7.9__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.9"
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
  # ===============
@@ -504,6 +547,7 @@ def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
504
547
  ok : ndarray of bool, same shape as W
505
548
  Convergence flags from the accepted solve at each point.
506
549
  """
550
+
507
551
  z_list = numpy.asarray(z_list, dtype=complex).ravel()
508
552
  t_grid = numpy.asarray(t_grid, dtype=float).ravel()
509
553
  nt = t_grid.size
@@ -6,12 +6,12 @@ import numpy
6
6
  from ._moments import AlgebraicStieltjesMoments
7
7
  from tqdm import tqdm
8
8
 
9
- __all__ = ['stieltjes_poly']
9
+ __all__ = ['StieltjesPoly']
10
10
 
11
11
 
12
- # =====================
12
+ # ===========
13
13
  # select root
14
- # =====================
14
+ # ===========
15
15
 
16
16
  def select_root(roots, z, target):
17
17
  """
@@ -202,7 +202,7 @@ class StieltjesPoly(object):
202
202
 
203
203
  # Iterate over indices so we can pass Python scalars into evaluate()
204
204
  if progress:
205
- indices = tqdm(numpy.ndindex(z_arr.shape),total=z_arr.size)
205
+ indices = tqdm(numpy.ndindex(z_arr.shape), total=z_arr.size)
206
206
  else:
207
207
  indices = numpy.ndindex(z_arr.shape)
208
208
  for idx in indices:
@@ -265,7 +265,7 @@ class StieltjesPoly(object):
265
265
  # coeffs = numpy.asarray(poly_coeffs_m(z_val), dtype=numpy.complex128)
266
266
  # return numpy.roots(coeffs)
267
267
 
268
- # # If user asked for a real-axis value, interpret as boundary value from C+.
268
+ # # If user asked a real-axis value, interpret as boundary value from C+.
269
269
  # if z.imag == 0.0:
270
270
  # if eps is None:
271
271
  # eps = 1e-8 * max(1.0, abs(z))
@@ -1,9 +1,13 @@
1
+ # =======
2
+ # Imports
3
+ # =======
4
+
1
5
  import numpy
2
6
 
3
7
 
4
- # =========
8
+ # =======
5
9
  # Moments
6
- # =========
10
+ # =======
7
11
 
8
12
  class MomentsESD(object):
9
13
  """
@@ -85,9 +89,9 @@ class MomentsESD(object):
85
89
  # (a_{n,0},...,a_{n,n-1})
86
90
  self._a = {0: numpy.array([1.0])}
87
91
 
88
- # ----------
89
- # moments
90
- # ----------
92
+ # =
93
+ # m
94
+ # =
91
95
 
92
96
  def m(self, n):
93
97
  """
@@ -111,9 +115,9 @@ class MomentsESD(object):
111
115
  self._m[n] = numpy.mean(self.eig ** n)
112
116
  return self._m[n]
113
117
 
114
- # -------------
115
- # coefficients
116
- # -------------
118
+ # ======
119
+ # coeffs
120
+ # ======
117
121
 
118
122
  def coeffs(self, n):
119
123
  """
@@ -129,7 +133,8 @@ class MomentsESD(object):
129
133
  -------
130
134
 
131
135
  a_n : numpy.ndarray
132
- Array of shape ``(n,)`` containing :math:`(a_{n,0}, \\dots, a_{n,n-1})`.
136
+ Array of shape ``(n,)`` containing :math:`(a_{n,0},
137
+ \\dots, a_{n,n-1})`.
133
138
  """
134
139
 
135
140
  if n in self._a:
@@ -143,6 +148,10 @@ class MomentsESD(object):
143
148
  self._compute_row(n)
144
149
  return self._a[n]
145
150
 
151
+ # ===========
152
+ # compute row
153
+ # ===========
154
+
146
155
  def _compute_row(self, n):
147
156
  """
148
157
  Compute and memoize the coefficient row :math:`a_n`.
@@ -212,9 +221,9 @@ class MomentsESD(object):
212
221
 
213
222
  self._a[n] = a_n
214
223
 
215
- # ----------
224
+ # --------
216
225
  # evaluate
217
- # ----------
226
+ # --------
218
227
 
219
228
  def __call__(self, n, t=0.0):
220
229
  """
@@ -254,18 +263,18 @@ class MomentsESD(object):
254
263
  k = numpy.arange(n, dtype=float)
255
264
  return numpy.dot(a_n, numpy.exp(k * t))
256
265
 
266
+
257
267
  # ===========================
258
268
  # Algebraic Stieltjes Moments
259
269
  # ===========================
260
270
 
261
-
262
271
  class AlgebraicStieltjesMoments(object):
263
272
  """
264
273
  Given coefficients a[i,j] for P(z,m)=sum_{i,j} a[i,j] z^i m^j,
265
274
  compute the large-|z| branch
266
275
  m(z) = sum_{k>=0} mu_series[k] / z^{k+1}.
267
276
 
268
- Convention here: choose mu0 (the leading coefficient) by solving the
277
+ Convention here: choose mu0 (the leading coefficient) by solving the
269
278
  leading-diagonal equation and (by default) picking the root closest
270
279
  to -1, i.e. m(z) ~ -1/z.
271
280
 
@@ -281,7 +290,7 @@ class AlgebraicStieltjesMoments(object):
281
290
  if self.a.ndim != 2:
282
291
  raise ValueError("a must be a 2D NumPy array with a[i,j]=a_{ij}.")
283
292
 
284
- self.I = self.a.shape[0] - 1
293
+ self.I = self.a.shape[0] - 1 # noqa: E741
285
294
  self.J = self.a.shape[1] - 1
286
295
 
287
296
  nz = numpy.argwhere(self.a != 0)
@@ -320,7 +329,8 @@ class AlgebraicStieltjesMoments(object):
320
329
  if j > 0:
321
330
  self.A0 += j * coeff * self.mu0pow[j - 1]
322
331
  if self.A0 == 0:
323
- raise ValueError("A0 is zero for this mu0; the sequential recursion is degenerate.")
332
+ raise ValueError("A0 is zero for this mu0; the sequential " +
333
+ "recursion is degenerate.")
324
334
 
325
335
  # Stored series moments mu_series[0..]
326
336
  self._mu = [self.mu0]
@@ -344,14 +354,17 @@ class AlgebraicStieltjesMoments(object):
344
354
  coeffs[j] = self.a[i, j]
345
355
 
346
356
  if not numpy.any(coeffs != 0):
347
- raise ValueError("Leading diagonal polynomial is identically zero; cannot determine mu0.")
357
+ raise ValueError("Leading diagonal polynomial is identically " +
358
+ "zero; cannot determine mu0.")
348
359
 
349
360
  deg = int(numpy.max(numpy.nonzero(coeffs)[0]))
350
- roots = numpy.roots(coeffs[:deg + 1][::-1]) # descending powers for numpy.roots
361
+
362
+ # descending powers for numpy.roots
363
+ roots = numpy.roots(coeffs[:deg + 1][::-1])
351
364
 
352
365
  # Targetting mu0 = -1 for ~ -1/z asymptotics
353
366
  mu0 = roots[numpy.argmin(numpy.abs(roots + 1))]
354
-
367
+
355
368
  if abs(mu0.imag) < 1e-12:
356
369
  mu0 = mu0.real
357
370
  return mu0
@@ -363,7 +376,8 @@ class AlgebraicStieltjesMoments(object):
363
376
 
364
377
  # Compute f[j] = coefficient of w^k in (S_trunc(w))^j,
365
378
  # where S_trunc uses mu_0..mu_{k-1} only (i.e. mu_k treated as 0).
366
- # Key fact: in the true c[j,k], mu_k can only appear linearly as j*mu_k*mu0^{j-1}.
379
+ # Key fact: in the true c[j,k], mu_k can only appear linearly as
380
+ # j*mu_k*mu0^{j-1}.
367
381
  f = [0] * (self.J + 1)
368
382
  f[0] = 0
369
383
  for j in range(1, self.J + 1):
@@ -371,8 +385,9 @@ class AlgebraicStieltjesMoments(object):
371
385
  # sum_{t=1..k-1} mu_t * c[j-1, k-t]
372
386
  for t in range(1, k):
373
387
  ssum += self._mu[t] * self._c[j - 1][k - t]
374
- # recurrence: c[j,k] = mu0*c[j-1,k] + sum_{t=1..k-1} mu_t*c[j-1,k-t] + mu_k*c[j-1,0]
375
- # with mu_k=0 for f, and c[j-1,k]=f[j-1]
388
+ # recurrence: c[j,k] = mu0*c[j-1,k] + sum_{t=1..k-1}
389
+ # mu_t*c[j-1,k-t] + mu_k*c[j-1,0] with mu_k=0 for f,
390
+ # and c[j-1,k]=f[j-1]
376
391
  f[j] = self.mu0 * f[j - 1] + ssum
377
392
 
378
393
  # Build the linear equation for mu_k:
@@ -386,7 +401,8 @@ class AlgebraicStieltjesMoments(object):
386
401
  continue
387
402
  rest += coeff * f[j]
388
403
 
389
- # lower diagonals s=1..k contribute coeff*c[j,k-s] (already known since k-s < k)
404
+ # lower diagonals s=1..k contribute coeff*c[j,k-s] (already known
405
+ # since k-s < k)
390
406
  for s in range(1, k + 1):
391
407
  entries = self.diag.get(s)
392
408
  if not entries:
@@ -402,7 +418,8 @@ class AlgebraicStieltjesMoments(object):
402
418
  mu_k = -rest / self.A0
403
419
  self._mu.append(mu_k)
404
420
 
405
- # Now append the new column k to c using the full convolution recurrence:
421
+ # Now append the new column k to c using the full convolution
422
+ # recurrence:
406
423
  # c[j,k] = sum_{t=0..k} mu_t * c[j-1,k-t]
407
424
  for j in range(self.J + 1):
408
425
  self._c[j].append(0)
@@ -430,9 +447,10 @@ class AlgebraicStieltjesMoments(object):
430
447
  # Estimate the radius of convergence of the Stieltjes
431
448
  # series
432
449
  if N < 3:
433
- raise RuntimeError("Order is too small, choose a larger value of N")
450
+ raise RuntimeError("N is too small, choose a larger value.")
434
451
  self._ensure(N)
435
- return max([numpy.abs(self._mu[j] / self._mu[j-1]) for j in range(2,N+1)])
452
+ return max([numpy.abs(self._mu[j] / self._mu[j-1])
453
+ for j in range(2, N+1)])
436
454
 
437
455
  def stieltjes(self, z, N):
438
456
  # Estimate Stieltjes transform (root) using moment