freealg 0.7.11__py3-none-any.whl → 0.7.14__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.
Files changed (36) hide show
  1. freealg/__init__.py +2 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +2 -1
  4. freealg/_algebraic_form/_constraints.py +53 -12
  5. freealg/_algebraic_form/_cusp.py +357 -0
  6. freealg/_algebraic_form/_cusp_wrap.py +268 -0
  7. freealg/_algebraic_form/_decompress.py +330 -381
  8. freealg/_algebraic_form/_decompress2.py +120 -0
  9. freealg/_algebraic_form/_decompress4.py +739 -0
  10. freealg/_algebraic_form/_decompress5.py +738 -0
  11. freealg/_algebraic_form/_decompress6.py +492 -0
  12. freealg/_algebraic_form/_decompress7.py +355 -0
  13. freealg/_algebraic_form/_decompress8.py +369 -0
  14. freealg/_algebraic_form/_decompress9.py +363 -0
  15. freealg/_algebraic_form/_decompress_new.py +431 -0
  16. freealg/_algebraic_form/_decompress_new_2.py +1631 -0
  17. freealg/_algebraic_form/_decompress_util.py +172 -0
  18. freealg/_algebraic_form/_edge.py +46 -68
  19. freealg/_algebraic_form/_homotopy.py +62 -30
  20. freealg/_algebraic_form/_homotopy2.py +289 -0
  21. freealg/_algebraic_form/_homotopy3.py +215 -0
  22. freealg/_algebraic_form/_homotopy4.py +320 -0
  23. freealg/_algebraic_form/_homotopy5.py +185 -0
  24. freealg/_algebraic_form/_moments.py +43 -57
  25. freealg/_algebraic_form/_support.py +132 -177
  26. freealg/_algebraic_form/algebraic_form.py +163 -30
  27. freealg/distributions/__init__.py +3 -1
  28. freealg/distributions/_compound_poisson.py +464 -0
  29. freealg/distributions/_deformed_marchenko_pastur.py +51 -0
  30. freealg/distributions/_deformed_wigner.py +44 -0
  31. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/METADATA +2 -1
  32. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/RECORD +36 -20
  33. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/WHEEL +1 -1
  34. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/AUTHORS.txt +0 -0
  35. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/LICENSE.txt +0 -0
  36. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,355 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+
5
+ """Free Decompression (FD) Newton solver.
6
+
7
+ This file defines `decompress_newton` used by AlgebraicForm.decompress(...,
8
+ method='one').
9
+
10
+ Implementation notes
11
+ --------------------
12
+ We solve, for each query point z and time t (tau = exp(t)), the 2x2 system
13
+ in variables (zeta, y):
14
+
15
+ F1(zeta,y) = P(zeta, y) = 0
16
+ F2(zeta,y) = z - zeta + (tau-1)/y = 0
17
+
18
+ Then w = m(t,z) = y/tau.
19
+
20
+ Speed-critical change vs earlier variants: all polynomial evaluations
21
+ (P, dP/dzeta, dP/dy) use scalar Horner without allocating power matrices.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import numpy
27
+
28
+ __all__ = ['decompress_newton']
29
+
30
+
31
+ # =====================
32
+ # scalar poly evaluation
33
+ # =====================
34
+
35
+ def _eval_a_and_da(z: complex, a_coeffs: numpy.ndarray) -> tuple[numpy.ndarray, numpy.ndarray]:
36
+ """Evaluate a_j(z) and a'_j(z) for j=0..s where P(z,y)=sum_j a_j(z) y^j.
37
+
38
+ a_coeffs has shape (deg_z+1, s+1) storing coefficients in z ascending:
39
+ a_coeffs[i,j] = coeff of z^i in a_j(z).
40
+ """
41
+ deg_z = a_coeffs.shape[0] - 1
42
+ s = a_coeffs.shape[1] - 1
43
+
44
+ a = numpy.empty(s + 1, dtype=numpy.complex128)
45
+ da = numpy.empty(s + 1, dtype=numpy.complex128)
46
+
47
+ # Horner for each column j
48
+ for j in range(s + 1):
49
+ col = a_coeffs[:, j]
50
+ # a_j(z)
51
+ v = complex(col[deg_z])
52
+ for i in range(deg_z - 1, -1, -1):
53
+ v = v * z + col[i]
54
+ a[j] = v
55
+
56
+ # a'_j(z)
57
+ if deg_z == 0:
58
+ da[j] = 0.0 + 0.0j
59
+ else:
60
+ vp = complex(deg_z * col[deg_z])
61
+ for i in range(deg_z - 1, 0, -1):
62
+ vp = vp * z + (i * col[i])
63
+ da[j] = vp
64
+
65
+ return a, da
66
+
67
+
68
+ def _eval_P_Pz_Py(z: complex, y: complex, a_coeffs: numpy.ndarray) -> tuple[complex, complex, complex]:
69
+ """Return P(z,y), Pz(z,y)=\partial_z P, Py(z,y)=\partial_y P (scalars)."""
70
+ a, da = _eval_a_and_da(z, a_coeffs)
71
+ s = a_coeffs.shape[1] - 1
72
+
73
+ # Build powers of y incrementally (cheap; s is small)
74
+ ypow = 1.0 + 0.0j
75
+ P = 0.0 + 0.0j
76
+ Pz = 0.0 + 0.0j
77
+
78
+ for j in range(s + 1):
79
+ P += a[j] * ypow
80
+ Pz += da[j] * ypow
81
+ ypow *= y
82
+
83
+ # Py
84
+ Py = 0.0 + 0.0j
85
+ if s >= 1:
86
+ ypow = 1.0 + 0.0j # y^(j-1)
87
+ for j in range(1, s + 1):
88
+ Py += (j * a[j]) * ypow
89
+ ypow *= y
90
+
91
+ return P, Pz, Py
92
+
93
+
94
+ # =========
95
+ # 2x2 newton
96
+ # =========
97
+
98
+ def _newton_2x2(
99
+ z: complex,
100
+ tau: float,
101
+ zeta0: complex,
102
+ y0: complex,
103
+ a_coeffs: numpy.ndarray,
104
+ *,
105
+ max_iter: int,
106
+ tol: float,
107
+ damping: float,
108
+ step_clip: float,
109
+ w_min: float,
110
+ require_imw_pos: bool,
111
+ im_eps: float,
112
+ ) -> tuple[complex, complex, bool, int]:
113
+ """Solve for (zeta,y) at fixed (z,tau)."""
114
+ zeta = complex(zeta0)
115
+ y = complex(y0)
116
+ tau_m1 = tau - 1.0
117
+
118
+ # Avoid singular y
119
+ if abs(y) < w_min:
120
+ y = (w_min + 0.0j)
121
+
122
+ for it in range(max_iter):
123
+ P, Pz, Py = _eval_P_Pz_Py(zeta, y, a_coeffs)
124
+ F1 = P
125
+ F2 = z - zeta + tau_m1 / y
126
+
127
+ # Stop criterion on both equations
128
+ if (abs(F1) <= tol) and (abs(F2) <= tol):
129
+ w = y / tau
130
+ if (not require_imw_pos) or (z.imag <= 0.0) or (w.imag > im_eps):
131
+ return zeta, y, True, it
132
+
133
+ # Jacobian
134
+ # F1_zeta = Pz
135
+ # F1_y = Py
136
+ # F2_zeta = -1
137
+ # F2_y = -(tau-1)/y^2
138
+ inv_y2 = 1.0 / (y * y)
139
+ J11 = Pz
140
+ J12 = Py
141
+ J21 = -1.0 + 0.0j
142
+ J22 = -(tau_m1) * inv_y2
143
+
144
+ det = J11 * J22 - J12 * J21
145
+ if det == 0.0:
146
+ break
147
+
148
+ # d = -J^{-1}F
149
+ # d_zeta = (-F1*J22 + F2*J12)/det
150
+ # d_y = ( J21*F1 - J11*F2)/det
151
+ d_zeta = (-F1 * J22 + F2 * J12) / det
152
+ d_y = (J21 * F1 - J11 * F2) / det
153
+
154
+ # Clip step
155
+ if step_clip is not None and step_clip > 0.0:
156
+ m = max(abs(d_zeta), abs(d_y))
157
+ if m > step_clip:
158
+ s = step_clip / m
159
+ d_zeta *= s
160
+ d_y *= s
161
+
162
+ lam = float(damping) if damping is not None else 1.0
163
+ if lam <= 0.0:
164
+ lam = 1.0
165
+
166
+ # Simple backtracking to enforce Im(w)>0 for z in C+
167
+ # (and avoid y crossing 0)
168
+ for _ in range(12):
169
+ zeta_try = zeta + lam * d_zeta
170
+ y_try = y + lam * d_y
171
+
172
+ if abs(y_try) < w_min:
173
+ lam *= 0.5
174
+ continue
175
+
176
+ if require_imw_pos and (z.imag > 0.0):
177
+ w_try = y_try / tau
178
+ if w_try.imag <= im_eps:
179
+ lam *= 0.5
180
+ continue
181
+
182
+ # Accept
183
+ zeta = zeta_try
184
+ y = y_try
185
+ break
186
+
187
+ else:
188
+ # could not find acceptable step
189
+ break
190
+
191
+ return zeta, y, False, max_iter
192
+
193
+
194
+ # ===============
195
+ # public interface
196
+ # ===============
197
+
198
+ def decompress_newton(
199
+ z_query,
200
+ t_all,
201
+ a_coeffs,
202
+ *,
203
+ w0_list=None,
204
+ max_iter: int = 40,
205
+ tol: float = 1e-13,
206
+ damping: float = 1.0,
207
+ step_clip: float = 5.0,
208
+ w_min: float = 1e-300,
209
+ max_split: int = 4,
210
+ require_imw_pos: bool = True,
211
+ im_eps: float = 1e-14,
212
+ sweep: bool = False,
213
+ verbose: bool = False,
214
+ debug: bool = False,
215
+ debug_idx=None,
216
+ eta_rescale: str | None = None,
217
+ **kwargs,
218
+ ):
219
+ """Compute w(t,z)=m(t,z) on a time grid using FD and Newton.
220
+
221
+ Parameters mirror earlier versions; extra kwargs are ignored.
222
+
223
+ eta_rescale:
224
+ - None (default): keep z_query fixed for all times.
225
+ - 'inv_tau': use z.imag/tau at time t. (Useful for mass checks.)
226
+
227
+ Returns
228
+ -------
229
+ W : (n_t, n_z) complex
230
+ ok : (n_t, n_z) bool
231
+ """
232
+
233
+ z_query = numpy.asarray(z_query, dtype=numpy.complex128).ravel()
234
+ t_all = numpy.asarray(t_all, dtype=float).ravel()
235
+ if t_all.size == 0:
236
+ raise ValueError('t_all is empty.')
237
+ if numpy.any(numpy.diff(t_all) < 0):
238
+ raise ValueError('t_all must be sorted increasing.')
239
+
240
+ n_z = z_query.size
241
+ n_t = t_all.size
242
+
243
+ if w0_list is None:
244
+ w0 = -1.0 / z_query
245
+ else:
246
+ w0 = numpy.asarray(w0_list, dtype=numpy.complex128).ravel()
247
+ if w0.size != n_z:
248
+ raise ValueError('w0_list must have same length as z_query.')
249
+
250
+ W = numpy.empty((n_t, n_z), dtype=numpy.complex128)
251
+ ok = numpy.zeros((n_t, n_z), dtype=bool)
252
+ W[0, :] = w0
253
+ ok[0, :] = numpy.isfinite(w0.real) & numpy.isfinite(w0.imag)
254
+
255
+ dbg_set = set()
256
+ if debug_idx is not None:
257
+ try:
258
+ dbg_set = set(int(i) for i in debug_idx)
259
+ except Exception:
260
+ dbg_set = set()
261
+
262
+ for it in range(1, n_t):
263
+ t_prev = float(t_all[it - 1])
264
+ t = float(t_all[it])
265
+ tau_prev = float(numpy.exp(t_prev))
266
+ tau = float(numpy.exp(t))
267
+
268
+ if eta_rescale == 'inv_tau':
269
+ z_t = z_query.real + 1j * (z_query.imag / tau)
270
+ else:
271
+ z_t = z_query
272
+
273
+ # warm start: previous w(t_prev,z) -> y seed
274
+ w_seed = W[it - 1, :].copy()
275
+ y_seed = tau_prev * w_seed
276
+
277
+ # build zeta seed that satisfies the constraint initially
278
+ y_safe = y_seed.copy()
279
+ tiny = numpy.abs(y_safe) < w_min
280
+ if numpy.any(tiny):
281
+ y_safe[tiny] = (w_min + 0.0j)
282
+ zeta_seed = z_t + (tau - 1.0) / y_safe
283
+
284
+ w_out = numpy.empty(n_z, dtype=numpy.complex128)
285
+ ok_out = numpy.zeros(n_z, dtype=bool)
286
+
287
+ # optional sweep within x at same time
288
+ y_last = None
289
+ for j in range(n_z):
290
+ z = z_t[j]
291
+
292
+ if sweep and (y_last is not None):
293
+ y0 = y_last
294
+ y_safe0 = y0 if abs(y0) > w_min else (w_min + 0.0j)
295
+ zeta0 = z + (tau - 1.0) / y_safe0
296
+ else:
297
+ y0 = y_seed[j]
298
+ zeta0 = zeta_seed[j]
299
+
300
+ zeta, y, okj, nit = _newton_2x2(
301
+ z, tau, zeta0, y0, a_coeffs,
302
+ max_iter=max_iter, tol=tol,
303
+ damping=damping, step_clip=step_clip,
304
+ w_min=w_min, require_imw_pos=require_imw_pos,
305
+ im_eps=im_eps,
306
+ )
307
+
308
+ if (not okj) and (max_split is not None) and (max_split > 0):
309
+ # try smaller pseudo-steps by tempering tau -> tau_mid
310
+ # (helps if time steps are too large)
311
+ for k in range(1, int(max_split) + 1):
312
+ tau_mid = tau_prev + (tau - tau_prev) * (k / float(max_split))
313
+ zeta_mid, y_mid, ok_mid, _ = _newton_2x2(
314
+ z, tau_mid, zeta0, y0, a_coeffs,
315
+ max_iter=max_iter, tol=tol,
316
+ damping=damping, step_clip=step_clip,
317
+ w_min=w_min, require_imw_pos=require_imw_pos,
318
+ im_eps=im_eps,
319
+ )
320
+ if ok_mid:
321
+ # now jump from (tau_mid) seed
322
+ zeta0b = zeta_mid
323
+ y0b = y_mid
324
+ zeta, y, okj, nit = _newton_2x2(
325
+ z, tau, zeta0b, y0b, a_coeffs,
326
+ max_iter=max_iter, tol=tol,
327
+ damping=damping, step_clip=step_clip,
328
+ w_min=w_min, require_imw_pos=require_imw_pos,
329
+ im_eps=im_eps,
330
+ )
331
+ if okj:
332
+ break
333
+
334
+ wj = y / tau
335
+ w_out[j] = wj
336
+ ok_out[j] = bool(okj)
337
+
338
+ if okj:
339
+ y_last = y
340
+
341
+ if debug and (j in dbg_set):
342
+ print(
343
+ f"[t={t:0.6f}] j={j} z={z.real:+.6f}{z.imag:+.2e}j "
344
+ f"zeta={zeta.real:+.6f}{zeta.imag:+.2e}j "
345
+ f"y={y.real:+.3e}{y.imag:+.3e}j "
346
+ f"w={wj.real:+.3e}{wj.imag:+.3e}j ok={okj} it={nit}"
347
+ )
348
+
349
+ W[it, :] = w_out
350
+ ok[it, :] = ok_out
351
+
352
+ if verbose:
353
+ print(f'[t={t:0.6f}] ok={ok_out.mean():0.3f}')
354
+
355
+ return W, ok
@@ -0,0 +1,369 @@
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
+ Robust Newton solver for Free Decompression (FD) using the characteristic
11
+ variables (zeta, y) on the spectral curve P(zeta, y)=0.
12
+
13
+ This implementation avoids solving directly in w (where zeta = z + alpha / w
14
+ introduces a pole at w=0). Instead, for each query z and time t (tau=e^t), we
15
+ solve the 2x2 complex system:
16
+
17
+ F1(zeta, y) := P(zeta, y) = 0
18
+ F2(zeta, y) := zeta - (tau - 1)/y - z = 0
19
+
20
+ Then m(t,z) = w = y/tau.
21
+
22
+ The public API matches the existing decompress_newton used by AlgebraicForm.
23
+ """
24
+
25
+ import numpy
26
+
27
+ __all__ = ['decompress_newton']
28
+
29
+
30
+ # =====================
31
+ # Polynomial evaluation
32
+ # =====================
33
+
34
+ def _powers(x, deg):
35
+ """
36
+ Returns [1, x, x^2, ..., x^deg] for each element of x.
37
+ """
38
+ x = numpy.asarray(x, dtype=numpy.complex128)
39
+ xp = numpy.ones((x.size, deg + 1), dtype=numpy.complex128)
40
+ for k in range(1, deg + 1):
41
+ xp[:, k] = xp[:, k - 1] * x
42
+ return xp
43
+
44
+
45
+ def _poly_coef_in_y(zeta, a_coeffs):
46
+ """
47
+ For each zeta, compute coefficients a_j(zeta) so that
48
+ P(zeta, y) = sum_{j=0}^s a_j(zeta) y^j
49
+ """
50
+ zeta = numpy.asarray(zeta, dtype=numpy.complex128).ravel()
51
+ deg_z = int(a_coeffs.shape[0] - 1)
52
+ s = int(a_coeffs.shape[1] - 1)
53
+
54
+ zp = _powers(zeta, deg_z)
55
+ a = numpy.empty((zeta.size, s + 1), dtype=numpy.complex128)
56
+ for j in range(s + 1):
57
+ a[:, j] = zp @ a_coeffs[:, j]
58
+ return a
59
+
60
+
61
+ def _poly_coef_in_y_dzeta(zeta, a_coeffs):
62
+ """
63
+ For each zeta, compute coefficients da_j/dzeta(zeta) so that
64
+ d/dzeta P(zeta, y) = sum_{j=0}^s (da_j/dzeta)(zeta) y^j
65
+ """
66
+ zeta = numpy.asarray(zeta, dtype=numpy.complex128).ravel()
67
+ deg_z = int(a_coeffs.shape[0] - 1)
68
+ s = int(a_coeffs.shape[1] - 1)
69
+
70
+ if deg_z <= 0:
71
+ return numpy.zeros((zeta.size, s + 1), dtype=numpy.complex128)
72
+
73
+ # derivative powers: d/dzeta zeta^i = i zeta^(i-1)
74
+ zp = _powers(zeta, deg_z - 1) # up to zeta^(deg_z-1)
75
+ da = numpy.empty((zeta.size, s + 1), dtype=numpy.complex128)
76
+ for j in range(s + 1):
77
+ col = a_coeffs[:, j]
78
+ # sum_{i=1..deg_z} i*c_{i,j} zeta^(i-1)
79
+ # build weighted coefficients for zp @ ...
80
+ w = numpy.arange(deg_z + 1, dtype=numpy.complex128) * col
81
+ da[:, j] = zp @ w[1:]
82
+ return da
83
+
84
+
85
+ def _eval_P_and_partials(zeta, y, a_coeffs):
86
+ """
87
+ Evaluate P(zeta,y), P_zeta(zeta,y), P_y(zeta,y).
88
+
89
+ Returns:
90
+ P, Pz, Py (arrays of shape (n,))
91
+ """
92
+ zeta = numpy.asarray(zeta, dtype=numpy.complex128).ravel()
93
+ y = numpy.asarray(y, dtype=numpy.complex128).ravel()
94
+
95
+ a = _poly_coef_in_y(zeta, a_coeffs) # (n, s+1)
96
+ da = _poly_coef_in_y_dzeta(zeta, a_coeffs) # (n, s+1)
97
+
98
+ s = int(a.shape[1] - 1)
99
+ # powers of y up to s
100
+ yp = _powers(y, s) # (n, s+1)
101
+
102
+ P = numpy.sum(a * yp, axis=1)
103
+
104
+ # P_zeta = sum_j da_j(zeta) y^j
105
+ Pz = numpy.sum(da * yp, axis=1)
106
+
107
+ # P_y = sum_{j>=1} j*a_j(zeta) y^{j-1}
108
+ if s == 0:
109
+ Py = numpy.zeros_like(P)
110
+ else:
111
+ Py = numpy.zeros_like(P)
112
+ # yp[:, j-1] available
113
+ for j in range(1, s + 1):
114
+ Py += (j * a[:, j]) * yp[:, j - 1]
115
+
116
+ return P, Pz, Py
117
+
118
+
119
+ # =======================
120
+ # 2x2 complex Newton step
121
+ # =======================
122
+
123
+ def _newton_2x2(z, tau, zeta0, y0, a_coeffs, max_iter, tol,
124
+ armijo, min_lam, w_min, enforce_imag=True):
125
+ """
126
+ Solve for (zeta,y) at given (z,tau) using damped Newton on the 2x2 complex
127
+ system.
128
+
129
+ Returns:
130
+ (zeta, y, ok, iters)
131
+ """
132
+ zeta = numpy.complex128(zeta0)
133
+ y = numpy.complex128(y0)
134
+
135
+ # helper to compute residual norm
136
+ def F(zeta_, y_):
137
+ P, Pz, Py = _eval_P_and_partials(numpy.array([zeta_]),
138
+ numpy.array([y_]),
139
+ a_coeffs)
140
+ P = P[0]
141
+ Pz = Pz[0]
142
+ Py = Py[0]
143
+ F1 = P
144
+ F2 = (zeta_ - (tau - 1.0) / y_ - z)
145
+ # Jacobian entries
146
+ J11 = Pz
147
+ J12 = Py
148
+ J21 = 1.0 + 0.0j
149
+ J22 = (tau - 1.0) / (y_ * y_) # d/dy of (-(tau-1)/y) is +(tau-1)/y^2
150
+ return F1, F2, J11, J12, J21, J22
151
+
152
+ # initial residual
153
+ F1, F2, J11, J12, J21, J22 = F(zeta, y)
154
+ r0 = max(abs(F1), abs(F2))
155
+
156
+ if not numpy.isfinite(r0):
157
+ return zeta, y, False, 0
158
+
159
+ for it in range(int(max_iter)):
160
+ r = max(abs(F1), abs(F2))
161
+ if r <= tol:
162
+ w = y / tau
163
+ if (abs(w) < w_min) or (not numpy.isfinite(w.real)) or (not numpy.isfinite(w.imag)):
164
+ return zeta, y, False, it
165
+ if enforce_imag and (z.imag > 0.0) and (w.imag <= 0.0):
166
+ return zeta, y, False, it
167
+ return zeta, y, True, it
168
+
169
+ # Solve 2x2 complex linear system J * d = -F
170
+ det = J11 * J22 - J12 * J21
171
+ if det == 0 or (not numpy.isfinite(det.real)) or (not numpy.isfinite(det.imag)):
172
+ return zeta, y, False, it
173
+
174
+ d_zeta = (-F1 * J22 - (-F2) * J12) / det
175
+ d_y = (J11 * (-F2) - J21 * (-F1)) / det
176
+
177
+ # Armijo damping on residual norm
178
+ lam = 1.0
179
+ if armijo is None or armijo <= 0.0:
180
+ lam = 1.0
181
+ else:
182
+ c = float(armijo)
183
+ r_curr = r
184
+ # Try steps until sufficient decrease or min_lam
185
+ while True:
186
+ zeta_try = zeta + lam * d_zeta
187
+ y_try = y + lam * d_y
188
+ F1t, F2t, J11t, J12t, J21t, J22t = F(zeta_try, y_try)
189
+ r_try = max(abs(F1t), abs(F2t))
190
+ if numpy.isfinite(r_try) and (r_try <= (1.0 - c * lam) * r_curr):
191
+ # accept
192
+ zeta, y = zeta_try, y_try
193
+ F1, F2, J11, J12, J21, J22 = F1t, F2t, J11t, J12t, J21t, J22t
194
+ break
195
+ lam *= 0.5
196
+ if lam < float(min_lam):
197
+ # accept last trial if it improves, else fail
198
+ if numpy.isfinite(r_try) and (r_try < r_curr):
199
+ zeta, y = zeta_try, y_try
200
+ F1, F2, J11, J12, J21, J22 = F1t, F2t, J11t, J12t, J21t, J22t
201
+ break
202
+ return zeta, y, False, it
203
+
204
+ # continue loop
205
+
206
+ # max_iter exceeded
207
+ return zeta, y, False, int(max_iter)
208
+
209
+
210
+ # =================
211
+ # Public entrypoint
212
+ # =================
213
+
214
+ def decompress_newton(z_query, t_all, a_coeffs, w0_list=None,
215
+ max_iter=50, tol=1e-12,
216
+ armijo=1e-4, min_lam=1e-6, w_min=1e-14,
217
+ sweep=True, verbose=False, **kwargs):
218
+ """
219
+ Parameters
220
+ ----------
221
+ z_query : array_like (complex)
222
+ Query points z where m(t,z) should be evaluated (typically x + i*delta)
223
+
224
+ t_all : array_like (float)
225
+ Time grid including t=0, increasing.
226
+
227
+ a_coeffs : ndarray
228
+ Coefficient matrix for P(z,m) in monomial basis (deg_z+1, s+1)
229
+
230
+ w0_list : array_like (complex), optional
231
+ Initial condition m(0, z_query) on the physical branch. If None, this
232
+ function will approximate it as -1/z_query (may be poor near cuts).
233
+
234
+ Other parameters mirror the existing solver interface.
235
+
236
+ Returns
237
+ -------
238
+ W : ndarray (n_t, n_z) complex
239
+ Estimated m(t,z) on the tracked branch.
240
+
241
+ ok : ndarray (n_t, n_z) bool
242
+ Convergence flag for each point.
243
+ """
244
+
245
+ z_query = numpy.asarray(z_query, dtype=numpy.complex128).ravel()
246
+ t_all = numpy.asarray(t_all, dtype=float).ravel()
247
+
248
+ if t_all.size == 0:
249
+ raise ValueError('t_all is empty.')
250
+
251
+ # enforce sorted
252
+ if numpy.any(numpy.diff(t_all) < 0):
253
+ raise ValueError('t_all must be sorted increasing.')
254
+
255
+ n_z = z_query.size
256
+ n_t = t_all.size
257
+
258
+
259
+ # If the caller does not include t=0 as the first element, prepend it.
260
+ # The Newton march below assumes W[0] is the known initial condition at t=0
261
+ # (physical branch), and evolves forward for k=1..n_t-1.
262
+ drop_t0 = False
263
+ if n_t == 0:
264
+ raise ValueError('t_all must be non-empty.')
265
+ if abs(float(t_all[0])) > 0.0:
266
+ t_all = numpy.concatenate(([0.0], t_all))
267
+ n_t = t_all.size
268
+ drop_t0 = True
269
+ if w0_list is None:
270
+ w0 = -1.0 / z_query
271
+ else:
272
+ w0 = numpy.asarray(w0_list, dtype=numpy.complex128).ravel()
273
+ if w0.size != n_z:
274
+ raise ValueError('w0_list must have same length as z_query.')
275
+
276
+ # Output arrays
277
+ W = numpy.empty((n_t, n_z), dtype=numpy.complex128)
278
+ ok = numpy.zeros((n_t, n_z), dtype=bool)
279
+
280
+ # Initialize at t=0
281
+ W[0, :] = w0
282
+ ok[0, :] = numpy.isfinite(w0.real) & numpy.isfinite(w0.imag)
283
+
284
+ # For each time step, solve independently per z with continuation seeds
285
+ for it in range(1, n_t):
286
+ t_prev = float(t_all[it - 1])
287
+ t = float(t_all[it])
288
+ tau_prev = numpy.exp(t_prev)
289
+ tau = numpy.exp(t)
290
+
291
+ # seeds from previous time
292
+ w_seed = W[it - 1, :].copy()
293
+ # y seed from previous: y = tau_prev * w
294
+ y_seed = tau_prev * w_seed
295
+
296
+ # Optional sweep: use previous x point at the same time as init
297
+ zeta_seed = numpy.empty(n_z, dtype=numpy.complex128)
298
+
299
+ # Initialize zeta so that F2 is satisfied initially (good conditioning)
300
+ # zeta = z + (tau-1)/y
301
+ # Guard y=0
302
+ y_safe = y_seed.copy()
303
+ tiny = numpy.abs(y_safe) < 1e-300
304
+ if numpy.any(tiny):
305
+ y_safe[tiny] = (1e-300 + 0.0j)
306
+ zeta_seed[:] = z_query + (tau - 1.0) / y_safe
307
+
308
+ # Sweep order: left-to-right
309
+ if sweep:
310
+ order = range(n_z)
311
+ else:
312
+ order = range(n_z)
313
+
314
+ # Storage for this time
315
+ w_out = numpy.empty(n_z, dtype=numpy.complex128)
316
+ ok_out = numpy.zeros(n_z, dtype=bool)
317
+
318
+ prev_good_idx = None
319
+ for j in order:
320
+ z = z_query[j]
321
+ # choose initial guess
322
+ y0 = y_seed[j]
323
+ zeta0 = zeta_seed[j]
324
+
325
+ if sweep and (prev_good_idx is not None):
326
+ # use last successful (zeta,y) as initial, but adjust zeta to
327
+ # satisfy constraint using current z and that y
328
+ y0 = y_last
329
+ y_safe0 = y0 if abs(y0) > 1e-300 else (1e-300 + 0.0j)
330
+ zeta0 = z + (tau - 1.0) / y_safe0
331
+
332
+ # Solve
333
+ zeta, y, okj, _ = _newton_2x2(
334
+ z, tau, zeta0, y0, a_coeffs,
335
+ max_iter=max_iter, tol=tol,
336
+ armijo=armijo, min_lam=min_lam, w_min=w_min,
337
+ enforce_imag=True
338
+ )
339
+
340
+ if not okj:
341
+ # Fallback 1: asymptotic Stieltjes seed
342
+ w_asym = -1.0 / z
343
+ y0b = tau * w_asym
344
+ y_safe0b = y0b if abs(y0b) > 1e-300 else (1e-300 + 0.0j)
345
+ zeta0b = z + (tau - 1.0) / y_safe0b
346
+
347
+ zeta, y, okj, _ = _newton_2x2(
348
+ z, tau, zeta0b, y0b, a_coeffs,
349
+ max_iter=max_iter, tol=tol,
350
+ armijo=armijo, min_lam=min_lam, w_min=w_min,
351
+ enforce_imag=True
352
+ )
353
+
354
+ wj = y / tau
355
+ w_out[j] = wj
356
+ ok_out[j] = bool(okj)
357
+
358
+ if okj:
359
+ prev_good_idx = j
360
+ y_last = y
361
+
362
+ W[it, :] = w_out
363
+ ok[it, :] = ok_out
364
+
365
+ if verbose:
366
+ print(f'[t={t:0.6f}] success={ok_out.mean():0.3f}')
367
+ if drop_t0:
368
+ return W[1:, :], ok[1:, :]
369
+ return W, ok