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.
- freealg/__init__.py +2 -2
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/__init__.py +2 -1
- freealg/_algebraic_form/_constraints.py +53 -12
- freealg/_algebraic_form/_cusp.py +357 -0
- freealg/_algebraic_form/_cusp_wrap.py +268 -0
- freealg/_algebraic_form/_decompress.py +330 -381
- freealg/_algebraic_form/_decompress2.py +120 -0
- freealg/_algebraic_form/_decompress4.py +739 -0
- freealg/_algebraic_form/_decompress5.py +738 -0
- freealg/_algebraic_form/_decompress6.py +492 -0
- freealg/_algebraic_form/_decompress7.py +355 -0
- freealg/_algebraic_form/_decompress8.py +369 -0
- freealg/_algebraic_form/_decompress9.py +363 -0
- freealg/_algebraic_form/_decompress_new.py +431 -0
- freealg/_algebraic_form/_decompress_new_2.py +1631 -0
- freealg/_algebraic_form/_decompress_util.py +172 -0
- freealg/_algebraic_form/_edge.py +46 -68
- freealg/_algebraic_form/_homotopy.py +62 -30
- freealg/_algebraic_form/_homotopy2.py +289 -0
- freealg/_algebraic_form/_homotopy3.py +215 -0
- freealg/_algebraic_form/_homotopy4.py +320 -0
- freealg/_algebraic_form/_homotopy5.py +185 -0
- freealg/_algebraic_form/_moments.py +43 -57
- freealg/_algebraic_form/_support.py +132 -177
- freealg/_algebraic_form/algebraic_form.py +163 -30
- freealg/distributions/__init__.py +3 -1
- freealg/distributions/_compound_poisson.py +464 -0
- freealg/distributions/_deformed_marchenko_pastur.py +51 -0
- freealg/distributions/_deformed_wigner.py +44 -0
- {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/METADATA +2 -1
- {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/RECORD +36 -20
- {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/WHEEL +1 -1
- {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|