freealg 0.7.12__py3-none-any.whl → 0.7.15__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 +1 -1
- freealg/_algebraic_form/_cusp.py +357 -0
- freealg/_algebraic_form/_cusp_wrap.py +268 -0
- freealg/_algebraic_form/_decompress2.py +2 -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/_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 +0 -1
- freealg/_algebraic_form/_support.py +132 -177
- freealg/_algebraic_form/algebraic_form.py +21 -2
- freealg/distributions/_compound_poisson.py +481 -0
- freealg/distributions/_deformed_marchenko_pastur.py +6 -7
- {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/METADATA +1 -1
- {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/RECORD +28 -12
- {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/WHEEL +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
# SPDX-FileType: SOURCE
|
|
4
|
+
#
|
|
5
|
+
# Free Decompression (FD) solver for algebraic Stieltjes transforms.
|
|
6
|
+
#
|
|
7
|
+
# Public API (used by AlgebraicForm.decompress):
|
|
8
|
+
# build_time_grid(size, n0, min_n_times=0) -> (t_all, idx_req)
|
|
9
|
+
# decompress_newton(z_list, t_grid, a_coeffs, w0_list=None, **opts) -> (W, ok)
|
|
10
|
+
#
|
|
11
|
+
# Core equation (FD):
|
|
12
|
+
# tau = exp(t) - 1
|
|
13
|
+
# zeta = z - tau*w
|
|
14
|
+
# Solve: P(zeta, w) = 0 where P(z,w)=sum_{i,j} a[i,j] z^i w^j
|
|
15
|
+
# i.e. F(w) := P(z - tau*w, w) = 0.
|
|
16
|
+
#
|
|
17
|
+
# This rewrite focuses on *robust branch tracking* (multi-start Newton + 2-pass
|
|
18
|
+
# Viterbi with "active-region" tiny-im penalty), and optional per-time density
|
|
19
|
+
# renormalization for mass preservation when the polynomial fit is imperfect.
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
__all__ = ["build_time_grid", "decompress_newton"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# =================
|
|
30
|
+
# Time grid helper
|
|
31
|
+
# =================
|
|
32
|
+
|
|
33
|
+
def build_time_grid(size, n0, min_n_times=0):
|
|
34
|
+
"""
|
|
35
|
+
Build a monotone time grid for FD.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
size : array_like
|
|
40
|
+
Requested size ratios, i.e., n(t)/n0 = size. Can include 1.
|
|
41
|
+
n0 : int
|
|
42
|
+
Initial matrix size.
|
|
43
|
+
min_n_times : int, default=0
|
|
44
|
+
Ensures at least this many intermediate time points between
|
|
45
|
+
successive requested times, based on implied integer sizes.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
t_all : numpy.ndarray
|
|
50
|
+
Full time grid (including intermediates), sorted.
|
|
51
|
+
idx_req : numpy.ndarray
|
|
52
|
+
Indices into t_all corresponding to the originally requested times.
|
|
53
|
+
"""
|
|
54
|
+
size = np.asarray(size, dtype=float).ravel()
|
|
55
|
+
if size.size == 0:
|
|
56
|
+
raise ValueError("size must be non-empty")
|
|
57
|
+
if np.any(size <= 0.0):
|
|
58
|
+
raise ValueError("size must be > 0")
|
|
59
|
+
|
|
60
|
+
t_req = np.log(size)
|
|
61
|
+
order = np.argsort(t_req)
|
|
62
|
+
t_req_sorted = t_req[order]
|
|
63
|
+
|
|
64
|
+
n0 = int(n0)
|
|
65
|
+
if n0 <= 0:
|
|
66
|
+
raise ValueError("n0 must be a positive integer")
|
|
67
|
+
|
|
68
|
+
t_all = [float(t_req_sorted[0])]
|
|
69
|
+
for k in range(1, t_req_sorted.size):
|
|
70
|
+
t0 = float(t_req_sorted[k - 1])
|
|
71
|
+
t1 = float(t_req_sorted[k])
|
|
72
|
+
if t1 <= t0:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if int(min_n_times) <= 0:
|
|
76
|
+
t_all.append(t1)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
nA = max(1, int(round(n0 * np.exp(t0))))
|
|
80
|
+
nB = max(1, int(round(n0 * np.exp(t1))))
|
|
81
|
+
dn = max(1, nB - nA)
|
|
82
|
+
step_n = max(1, int(np.ceil(dn / float(min_n_times))))
|
|
83
|
+
n_grid = list(range(nA, nB, step_n))
|
|
84
|
+
if n_grid[-1] != nB:
|
|
85
|
+
n_grid.append(nB)
|
|
86
|
+
for nn in n_grid[1:]:
|
|
87
|
+
t_all.append(float(np.log(nn / float(n0))))
|
|
88
|
+
|
|
89
|
+
t_all = np.asarray(t_all, dtype=float)
|
|
90
|
+
|
|
91
|
+
idx_req_sorted = np.empty(t_req_sorted.size, dtype=int)
|
|
92
|
+
for i, t in enumerate(t_req_sorted):
|
|
93
|
+
idx_req_sorted[i] = int(np.argmin(np.abs(t_all - float(t))))
|
|
94
|
+
|
|
95
|
+
inv = np.empty_like(order)
|
|
96
|
+
inv[order] = np.arange(order.size)
|
|
97
|
+
idx_req = idx_req_sorted[inv]
|
|
98
|
+
return t_all, idx_req
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ===================
|
|
102
|
+
# Polynomial utilities
|
|
103
|
+
# ===================
|
|
104
|
+
|
|
105
|
+
def _poly_coef_in_w(z, a_coeffs):
|
|
106
|
+
"""
|
|
107
|
+
For fixed z, return coefficients c[j] so that P(z,w)=sum_j c[j] w^j.
|
|
108
|
+
a_coeffs[i,j] corresponds to z^i w^j.
|
|
109
|
+
"""
|
|
110
|
+
z = complex(z)
|
|
111
|
+
a = np.asarray(a_coeffs, dtype=np.complex128)
|
|
112
|
+
deg_z = int(a.shape[0] - 1)
|
|
113
|
+
# Horner in z for each j
|
|
114
|
+
zp = 1.0 + 0.0j
|
|
115
|
+
c = np.array(a[0, :], dtype=np.complex128)
|
|
116
|
+
for i in range(1, deg_z + 1):
|
|
117
|
+
zp *= z
|
|
118
|
+
c = c + a[i, :] * zp
|
|
119
|
+
return c # shape (s+1,)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _eval_P(z, w, a_coeffs):
|
|
123
|
+
c = _poly_coef_in_w(z, a_coeffs)
|
|
124
|
+
# Horner in w
|
|
125
|
+
ww = complex(w)
|
|
126
|
+
out = 0.0 + 0.0j
|
|
127
|
+
for cj in c[::-1]:
|
|
128
|
+
out = out * ww + cj
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _eval_dP_dw(z, w, a_coeffs):
|
|
133
|
+
"""
|
|
134
|
+
d/dw P(z,w)
|
|
135
|
+
"""
|
|
136
|
+
c = _poly_coef_in_w(z, a_coeffs) # c[j] w^j
|
|
137
|
+
ww = complex(w)
|
|
138
|
+
# derivative coefficients: j*c[j]
|
|
139
|
+
out = 0.0 + 0.0j
|
|
140
|
+
for j in range(c.size - 1, 0, -1):
|
|
141
|
+
out = out * ww + (j * c[j])
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _eval_dP_dz(z, w, a_coeffs):
|
|
146
|
+
"""
|
|
147
|
+
d/dz P(z,w)
|
|
148
|
+
"""
|
|
149
|
+
z = complex(z)
|
|
150
|
+
w = complex(w)
|
|
151
|
+
a = np.asarray(a_coeffs, dtype=np.complex128)
|
|
152
|
+
deg_z = int(a.shape[0] - 1)
|
|
153
|
+
# compute b[j] = sum_{i>=1} i*a[i,j]*z^{i-1}
|
|
154
|
+
if deg_z <= 0:
|
|
155
|
+
return 0.0 + 0.0j
|
|
156
|
+
b = np.zeros((a.shape[1],), dtype=np.complex128)
|
|
157
|
+
zp = 1.0 + 0.0j
|
|
158
|
+
for i in range(1, deg_z + 1):
|
|
159
|
+
b = b + (i * a[i, :]) * zp
|
|
160
|
+
zp *= z
|
|
161
|
+
# evaluate in w: sum_j b[j] w^j
|
|
162
|
+
out = 0.0 + 0.0j
|
|
163
|
+
for bj in b[::-1]:
|
|
164
|
+
out = out * w + bj
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _fd_F_and_dF(w, z, tau, a_coeffs):
|
|
169
|
+
"""
|
|
170
|
+
F(w) = P(z - tau*w, w).
|
|
171
|
+
dF/dw = dP/dz * (-tau) + dP/dw evaluated at (zeta, w).
|
|
172
|
+
"""
|
|
173
|
+
zeta = z - tau * w
|
|
174
|
+
F = _eval_P(zeta, w, a_coeffs)
|
|
175
|
+
dPdw = _eval_dP_dw(zeta, w, a_coeffs)
|
|
176
|
+
dPdz = _eval_dP_dz(zeta, w, a_coeffs)
|
|
177
|
+
dF = dPdw - tau * dPdz
|
|
178
|
+
return F, dF
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# =================
|
|
182
|
+
# Newton (scalar)
|
|
183
|
+
# =================
|
|
184
|
+
|
|
185
|
+
def _newton_fd_scalar(
|
|
186
|
+
z,
|
|
187
|
+
t,
|
|
188
|
+
a_coeffs,
|
|
189
|
+
w_init,
|
|
190
|
+
*,
|
|
191
|
+
max_iter=60,
|
|
192
|
+
tol=1e-12,
|
|
193
|
+
armijo=True,
|
|
194
|
+
min_lam=1e-6,
|
|
195
|
+
w_min=0.0,
|
|
196
|
+
):
|
|
197
|
+
"""
|
|
198
|
+
Newton solve for one z at one t.
|
|
199
|
+
Returns (w, ok, n_iter, final_res).
|
|
200
|
+
"""
|
|
201
|
+
z = complex(z)
|
|
202
|
+
t = float(t)
|
|
203
|
+
tau = float(np.expm1(t)) # exp(t)-1, stable for small t
|
|
204
|
+
w = complex(w_init)
|
|
205
|
+
|
|
206
|
+
# guard against nan seeds
|
|
207
|
+
if not (np.isfinite(w.real) and np.isfinite(w.imag)):
|
|
208
|
+
w = -1.0 / z
|
|
209
|
+
|
|
210
|
+
# optional floor on imaginary (avoid falling to lower half due to roundoff)
|
|
211
|
+
if w_min > 0.0 and w.imag < w_min:
|
|
212
|
+
w = complex(w.real, w_min)
|
|
213
|
+
|
|
214
|
+
# initial
|
|
215
|
+
F, dF = _fd_F_and_dF(w, z, tau, a_coeffs)
|
|
216
|
+
res0 = abs(F)
|
|
217
|
+
if not np.isfinite(res0):
|
|
218
|
+
return complex(np.nan, np.nan), False, 0, np.inf
|
|
219
|
+
|
|
220
|
+
for it in range(max_iter):
|
|
221
|
+
if abs(F) <= tol * (1.0 + res0):
|
|
222
|
+
return w, True, it + 1, abs(F)
|
|
223
|
+
|
|
224
|
+
# if derivative is degenerate, bail
|
|
225
|
+
if not np.isfinite(dF.real) or not np.isfinite(dF.imag) or abs(dF) == 0.0:
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
step = -F / dF
|
|
229
|
+
lam = 1.0
|
|
230
|
+
|
|
231
|
+
if armijo:
|
|
232
|
+
# Armijo on |F| (cheap, robust)
|
|
233
|
+
f0 = abs(F)
|
|
234
|
+
# Try to avoid huge steps
|
|
235
|
+
if abs(step) > 10.0 * (1.0 + abs(w)):
|
|
236
|
+
step = step * (10.0 * (1.0 + abs(w)) / abs(step))
|
|
237
|
+
|
|
238
|
+
while lam >= min_lam:
|
|
239
|
+
w_new = w + lam * step
|
|
240
|
+
if w_min > 0.0 and w_new.imag < w_min:
|
|
241
|
+
w_new = complex(w_new.real, w_min)
|
|
242
|
+
F_new, dF_new = _fd_F_and_dF(w_new, z, tau, a_coeffs)
|
|
243
|
+
f1 = abs(F_new)
|
|
244
|
+
if np.isfinite(f1) and (f1 <= (1.0 - 1e-4 * lam) * f0):
|
|
245
|
+
w, F, dF = w_new, F_new, dF_new
|
|
246
|
+
break
|
|
247
|
+
lam *= 0.5
|
|
248
|
+
else:
|
|
249
|
+
# failed to find descent
|
|
250
|
+
break
|
|
251
|
+
else:
|
|
252
|
+
w = w + step
|
|
253
|
+
if w_min > 0.0 and w.imag < w_min:
|
|
254
|
+
w = complex(w.real, w_min)
|
|
255
|
+
F, dF = _fd_F_and_dF(w, z, tau, a_coeffs)
|
|
256
|
+
|
|
257
|
+
# final
|
|
258
|
+
F, _ = _fd_F_and_dF(w, z, tau, a_coeffs)
|
|
259
|
+
ok = np.isfinite(F.real) and np.isfinite(F.imag) and (abs(F) <= 1e3 * tol * (1.0 + res0))
|
|
260
|
+
return w, bool(ok), max_iter, abs(F)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ==========================
|
|
264
|
+
# Candidate generation (per z)
|
|
265
|
+
# ==========================
|
|
266
|
+
|
|
267
|
+
def _make_default_seeds(z, w_prev, w_left, w_right):
|
|
268
|
+
seeds = []
|
|
269
|
+
if w_prev is not None and np.isfinite(w_prev.real) and np.isfinite(w_prev.imag):
|
|
270
|
+
seeds.append(complex(w_prev))
|
|
271
|
+
if w_left is not None and np.isfinite(w_left.real) and np.isfinite(w_left.imag):
|
|
272
|
+
seeds.append(complex(w_left))
|
|
273
|
+
if w_right is not None and np.isfinite(w_right.real) and np.isfinite(w_right.imag):
|
|
274
|
+
seeds.append(complex(w_right))
|
|
275
|
+
seeds.append(complex(-1.0 / z))
|
|
276
|
+
return seeds
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _dedup_cands(cands, tol=1e-10):
|
|
280
|
+
if len(cands) == 0:
|
|
281
|
+
return np.empty((0,), dtype=np.complex128)
|
|
282
|
+
out = []
|
|
283
|
+
for w in cands:
|
|
284
|
+
keep = True
|
|
285
|
+
for u in out:
|
|
286
|
+
if abs(w - u) <= tol * (1.0 + abs(u)):
|
|
287
|
+
keep = False
|
|
288
|
+
break
|
|
289
|
+
if keep:
|
|
290
|
+
out.append(w)
|
|
291
|
+
return np.asarray(out, dtype=np.complex128)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _fd_candidates(
|
|
295
|
+
z,
|
|
296
|
+
t,
|
|
297
|
+
a_coeffs,
|
|
298
|
+
seeds,
|
|
299
|
+
*,
|
|
300
|
+
max_iter=60,
|
|
301
|
+
tol=1e-12,
|
|
302
|
+
armijo=True,
|
|
303
|
+
min_lam=1e-6,
|
|
304
|
+
w_min=0.0,
|
|
305
|
+
keep_best=8,
|
|
306
|
+
):
|
|
307
|
+
"""
|
|
308
|
+
Multi-start Newton candidates.
|
|
309
|
+
Returns (cands, ok_flags, resids).
|
|
310
|
+
"""
|
|
311
|
+
cands = []
|
|
312
|
+
oks = []
|
|
313
|
+
ress = []
|
|
314
|
+
for s in seeds:
|
|
315
|
+
w, ok, _, res = _newton_fd_scalar(
|
|
316
|
+
z, t, a_coeffs, s,
|
|
317
|
+
max_iter=max_iter, tol=tol,
|
|
318
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min
|
|
319
|
+
)
|
|
320
|
+
if np.isfinite(w.real) and np.isfinite(w.imag):
|
|
321
|
+
cands.append(w)
|
|
322
|
+
oks.append(ok)
|
|
323
|
+
ress.append(res)
|
|
324
|
+
|
|
325
|
+
if len(cands) == 0:
|
|
326
|
+
return np.empty((0,), np.complex128), np.empty((0,), bool), np.empty((0,), float)
|
|
327
|
+
|
|
328
|
+
cands = np.asarray(cands, dtype=np.complex128)
|
|
329
|
+
oks = np.asarray(oks, dtype=bool)
|
|
330
|
+
ress = np.asarray(ress, dtype=float)
|
|
331
|
+
|
|
332
|
+
# sort by residual
|
|
333
|
+
idx = np.argsort(ress)
|
|
334
|
+
cands = cands[idx]
|
|
335
|
+
oks = oks[idx]
|
|
336
|
+
ress = ress[idx]
|
|
337
|
+
|
|
338
|
+
# keep unique / best
|
|
339
|
+
keep = []
|
|
340
|
+
for i in range(cands.size):
|
|
341
|
+
w = cands[i]
|
|
342
|
+
if len(keep) >= int(keep_best):
|
|
343
|
+
break
|
|
344
|
+
dup = False
|
|
345
|
+
for j in keep:
|
|
346
|
+
if abs(w - cands[j]) <= 1e-10 * (1.0 + abs(cands[j])):
|
|
347
|
+
dup = True
|
|
348
|
+
break
|
|
349
|
+
if not dup:
|
|
350
|
+
keep.append(i)
|
|
351
|
+
|
|
352
|
+
cands = cands[keep]
|
|
353
|
+
oks = oks[keep]
|
|
354
|
+
ress = ress[keep]
|
|
355
|
+
return cands, oks, ress
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# =====================
|
|
359
|
+
# Viterbi (1D tracking)
|
|
360
|
+
# =====================
|
|
361
|
+
|
|
362
|
+
def _viterbi_track(
|
|
363
|
+
z_list,
|
|
364
|
+
cand_list,
|
|
365
|
+
w_prev=None,
|
|
366
|
+
*,
|
|
367
|
+
lam_space=1.0,
|
|
368
|
+
lam_time=0.25,
|
|
369
|
+
lam_asym=0.5,
|
|
370
|
+
lam_tiny_im=0.0,
|
|
371
|
+
tiny_im=1e-7,
|
|
372
|
+
lam_res=0.5,
|
|
373
|
+
edge_k=8,
|
|
374
|
+
):
|
|
375
|
+
"""
|
|
376
|
+
Track one candidate per z along the 1D grid using DP.
|
|
377
|
+
|
|
378
|
+
cand_list: list of arrays of candidates for each iz (variable length)
|
|
379
|
+
Returns: w_path (nz,), ok (nz,)
|
|
380
|
+
"""
|
|
381
|
+
nz = z_list.size
|
|
382
|
+
K = max((c.size for c in cand_list), default=0)
|
|
383
|
+
if K == 0:
|
|
384
|
+
return np.full((nz,), np.nan + 1j*np.nan, np.complex128), np.zeros((nz,), bool)
|
|
385
|
+
|
|
386
|
+
# pad to rectangular with NaNs
|
|
387
|
+
R = np.full((nz, K), np.nan + 1j*np.nan, dtype=np.complex128)
|
|
388
|
+
for i in range(nz):
|
|
389
|
+
c = cand_list[i]
|
|
390
|
+
if c.size:
|
|
391
|
+
R[i, :c.size] = c
|
|
392
|
+
|
|
393
|
+
# unary costs
|
|
394
|
+
unary = np.full((nz, K), np.inf, dtype=np.float64)
|
|
395
|
+
|
|
396
|
+
# asymptotic anchors (ends)
|
|
397
|
+
targetL = -1.0 / z_list[0]
|
|
398
|
+
targetR = -1.0 / z_list[-1]
|
|
399
|
+
|
|
400
|
+
for i in range(nz):
|
|
401
|
+
zi = z_list[i]
|
|
402
|
+
for k in range(K):
|
|
403
|
+
w = R[i, k]
|
|
404
|
+
if not np.isfinite(w.real) or not np.isfinite(w.imag):
|
|
405
|
+
continue
|
|
406
|
+
c = 0.0
|
|
407
|
+
|
|
408
|
+
# residual proxy: prefer smaller |z*w + 1| far from support
|
|
409
|
+
c += lam_asym * float(abs(zi * w + 1.0))
|
|
410
|
+
|
|
411
|
+
# time continuity
|
|
412
|
+
if w_prev is not None and np.isfinite(w_prev[i].real) and np.isfinite(w_prev[i].imag):
|
|
413
|
+
c += lam_time * float(abs(w - w_prev[i]))
|
|
414
|
+
|
|
415
|
+
# tiny-im penalty (used in pass-2, inside active regions)
|
|
416
|
+
if lam_tiny_im != 0.0:
|
|
417
|
+
im = float(w.imag)
|
|
418
|
+
if im < tiny_im:
|
|
419
|
+
c += lam_tiny_im * float((tiny_im - im) / max(tiny_im, 1e-30))
|
|
420
|
+
|
|
421
|
+
unary[i, k] = c
|
|
422
|
+
|
|
423
|
+
# boundary anchoring (stronger at ends)
|
|
424
|
+
if edge_k > 0:
|
|
425
|
+
kk = min(int(edge_k), max(1, nz // 2))
|
|
426
|
+
for i in range(kk):
|
|
427
|
+
unary[i, :] += 10.0 * lam_res * np.abs(R[i, :] - targetL)
|
|
428
|
+
for i in range(nz - kk, nz):
|
|
429
|
+
unary[i, :] += 10.0 * lam_res * np.abs(R[i, :] - targetR)
|
|
430
|
+
|
|
431
|
+
# DP
|
|
432
|
+
dp = np.full((nz, K), np.inf, dtype=np.float64)
|
|
433
|
+
prev = np.full((nz, K), -1, dtype=np.int64)
|
|
434
|
+
|
|
435
|
+
dp[0, :] = unary[0, :]
|
|
436
|
+
|
|
437
|
+
for i in range(1, nz):
|
|
438
|
+
wi = R[i, :]
|
|
439
|
+
wj = R[i - 1, :]
|
|
440
|
+
for k in range(K):
|
|
441
|
+
if not np.isfinite(unary[i, k]) or not np.isfinite(wi[k]):
|
|
442
|
+
continue
|
|
443
|
+
best_val = np.inf
|
|
444
|
+
best_j = -1
|
|
445
|
+
for j in range(K):
|
|
446
|
+
if not np.isfinite(dp[i - 1, j]) or not np.isfinite(wj[j]):
|
|
447
|
+
continue
|
|
448
|
+
val = dp[i - 1, j] + lam_space * float(abs(wi[k] - wj[j]))
|
|
449
|
+
if val < best_val:
|
|
450
|
+
best_val = val
|
|
451
|
+
best_j = j
|
|
452
|
+
if best_j >= 0:
|
|
453
|
+
dp[i, k] = best_val + unary[i, k]
|
|
454
|
+
prev[i, k] = best_j
|
|
455
|
+
|
|
456
|
+
k_end = int(np.argmin(dp[-1, :]))
|
|
457
|
+
w_path = np.full((nz,), np.nan + 1j*np.nan, dtype=np.complex128)
|
|
458
|
+
if not np.isfinite(dp[-1, k_end]):
|
|
459
|
+
return w_path, np.zeros((nz,), bool)
|
|
460
|
+
|
|
461
|
+
k = k_end
|
|
462
|
+
for i in range(nz - 1, -1, -1):
|
|
463
|
+
w_path[i] = R[i, k]
|
|
464
|
+
k = prev[i, k]
|
|
465
|
+
if i > 0 and k < 0:
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
ok = np.isfinite(w_path.real) & np.isfinite(w_path.imag)
|
|
469
|
+
return w_path, ok
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ======================
|
|
473
|
+
# Active region detection
|
|
474
|
+
# ======================
|
|
475
|
+
|
|
476
|
+
def _infer_active_mask(w_path, *, imag_floor, q=0.90, pad=8):
|
|
477
|
+
"""
|
|
478
|
+
Active region: where imag is meaningfully above floor.
|
|
479
|
+
Two-bulk friendly: uses quantile-based threshold (robust).
|
|
480
|
+
"""
|
|
481
|
+
im = np.maximum(np.asarray(w_path.imag, dtype=float), 0.0)
|
|
482
|
+
im_finite = im[np.isfinite(im)]
|
|
483
|
+
if im_finite.size == 0:
|
|
484
|
+
return np.zeros((im.size,), dtype=bool)
|
|
485
|
+
|
|
486
|
+
thr = max(float(imag_floor), float(np.quantile(im_finite, q) * 0.10))
|
|
487
|
+
active = im >= thr
|
|
488
|
+
|
|
489
|
+
if pad > 0 and active.any():
|
|
490
|
+
idx = np.flatnonzero(active)
|
|
491
|
+
lo = max(0, int(idx[0]) - int(pad))
|
|
492
|
+
hi = min(active.size, int(idx[-1]) + int(pad) + 1)
|
|
493
|
+
active2 = np.zeros_like(active)
|
|
494
|
+
active2[lo:hi] = True
|
|
495
|
+
|
|
496
|
+
# also pad each contiguous block
|
|
497
|
+
# (fast enough for nz<=1e4)
|
|
498
|
+
active = active2
|
|
499
|
+
return active
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# =====================
|
|
503
|
+
# Mass renormalization
|
|
504
|
+
# =====================
|
|
505
|
+
|
|
506
|
+
def _renormalize_density(z_list, w_path, target_mass=1.0):
|
|
507
|
+
"""
|
|
508
|
+
Scale imag(w) to match target_mass using trapezoidal rule on x = Re(z).
|
|
509
|
+
"""
|
|
510
|
+
x = np.asarray(z_list.real, dtype=float)
|
|
511
|
+
rho = np.maximum(w_path.imag / np.pi, 0.0)
|
|
512
|
+
m = float(np.trapezoid(rho, x))
|
|
513
|
+
if not np.isfinite(m) or m <= 0.0:
|
|
514
|
+
return w_path, m, False
|
|
515
|
+
s = float(target_mass / m)
|
|
516
|
+
# scale only imaginary part (keep Hilbert approx)
|
|
517
|
+
w_new = w_path.real + 1j * (w_path.imag * s)
|
|
518
|
+
return w_new.astype(np.complex128), m, True
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ==========================
|
|
522
|
+
# Main decompression API
|
|
523
|
+
# ==========================
|
|
524
|
+
|
|
525
|
+
def decompress_newton(
|
|
526
|
+
z_list: np.ndarray,
|
|
527
|
+
t_grid: np.ndarray,
|
|
528
|
+
a_coeffs: np.ndarray,
|
|
529
|
+
w0_list: np.ndarray | None = None,
|
|
530
|
+
*,
|
|
531
|
+
dt_max: float = 0.05,
|
|
532
|
+
max_iter: int = 80,
|
|
533
|
+
tol: float = 1e-12,
|
|
534
|
+
armijo: bool = True,
|
|
535
|
+
min_lam: float = 1e-6,
|
|
536
|
+
w_min: float = 0.0,
|
|
537
|
+
keep_best: int = 8,
|
|
538
|
+
# branch tracking
|
|
539
|
+
lam_space: float = 1.0,
|
|
540
|
+
lam_time: float = 0.25,
|
|
541
|
+
lam_asym: float = 0.5,
|
|
542
|
+
# pass-2 active region penalty
|
|
543
|
+
active_imag_eps: float = 1e-8,
|
|
544
|
+
lam_tiny_im: float = 5.0,
|
|
545
|
+
active_q: float = 0.90,
|
|
546
|
+
sweep_pad: int = 10,
|
|
547
|
+
# mass
|
|
548
|
+
renorm_mass: bool = True,
|
|
549
|
+
target_mass: float = 1.0,
|
|
550
|
+
# debug
|
|
551
|
+
viterbi_opt: dict | None = None,
|
|
552
|
+
sweep: bool = False,
|
|
553
|
+
time_rel_tol: float = 1e-3,
|
|
554
|
+
):
|
|
555
|
+
"""
|
|
556
|
+
Solve FD for a set of complex query points z_list over t_grid.
|
|
557
|
+
|
|
558
|
+
Parameters (key ones)
|
|
559
|
+
---------------------
|
|
560
|
+
z_list : (nz,) complex
|
|
561
|
+
Query points (typically x + 1j*delta), in the desired x order.
|
|
562
|
+
t_grid : (nt,) float
|
|
563
|
+
Times (typically log(size ratios)), increasing.
|
|
564
|
+
w0_list : (nz,) complex
|
|
565
|
+
Initial w at t=t_grid[0] (typically physical m(z)).
|
|
566
|
+
renorm_mass : bool
|
|
567
|
+
If True, scales Im(w) at each time to match target_mass.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
-------
|
|
571
|
+
W : (nt, nz) complex
|
|
572
|
+
ok : (nt, nz) bool
|
|
573
|
+
"""
|
|
574
|
+
z_list = np.asarray(z_list, dtype=np.complex128).ravel()
|
|
575
|
+
t_grid = np.asarray(t_grid, dtype=np.float64).ravel()
|
|
576
|
+
nt = t_grid.size
|
|
577
|
+
nz = z_list.size
|
|
578
|
+
if nz == 0 or nt == 0:
|
|
579
|
+
raise ValueError("z_list and t_grid must be non-empty")
|
|
580
|
+
|
|
581
|
+
if w0_list is None:
|
|
582
|
+
w0_list = -1.0 / z_list
|
|
583
|
+
w0_list = np.asarray(w0_list, dtype=np.complex128).ravel()
|
|
584
|
+
if w0_list.size != nz:
|
|
585
|
+
raise ValueError("w0_list must have same length as z_list.")
|
|
586
|
+
|
|
587
|
+
# debug / overrides
|
|
588
|
+
vopt = {} if viterbi_opt is None else dict(viterbi_opt)
|
|
589
|
+
lam_space = float(vopt.get("lam_space", lam_space))
|
|
590
|
+
lam_time = float(vopt.get("lam_time", lam_time))
|
|
591
|
+
lam_asym = float(vopt.get("lam_asym", lam_asym))
|
|
592
|
+
lam_tiny_im = float(vopt.get("lam_tiny_im", lam_tiny_im))
|
|
593
|
+
active_q = float(vopt.get("active_q", active_q))
|
|
594
|
+
debug_path = vopt.get("debug_path", None)
|
|
595
|
+
debug_every = int(vopt.get("debug_every", max(1, nt // 10)))
|
|
596
|
+
debug_iz = vopt.get("debug_iz", None)
|
|
597
|
+
|
|
598
|
+
W = np.empty((nt, nz), dtype=np.complex128)
|
|
599
|
+
ok = np.ones((nt, nz), dtype=bool)
|
|
600
|
+
|
|
601
|
+
W[0, :] = w0_list
|
|
602
|
+
|
|
603
|
+
debug_pack = []
|
|
604
|
+
if debug_iz is None:
|
|
605
|
+
debug_iz = []
|
|
606
|
+
|
|
607
|
+
for it in range(1, nt):
|
|
608
|
+
t_target = float(t_grid[it])
|
|
609
|
+
t_prev = float(t_grid[it - 1])
|
|
610
|
+
dt = t_target - t_prev
|
|
611
|
+
n_sub = max(1, int(np.ceil(abs(dt) / max(float(dt_max), 1e-12))))
|
|
612
|
+
sub_ts = np.linspace(t_prev, t_target, n_sub + 1)[1:]
|
|
613
|
+
|
|
614
|
+
w_prev = W[it - 1].copy()
|
|
615
|
+
|
|
616
|
+
# optional "sweep": warm-start by single Newton sweep at same t_target
|
|
617
|
+
# (kept for API compatibility; not required)
|
|
618
|
+
if sweep:
|
|
619
|
+
pass
|
|
620
|
+
|
|
621
|
+
for t_sub in sub_ts:
|
|
622
|
+
cand_list = []
|
|
623
|
+
# build candidates for each z
|
|
624
|
+
for iz in range(nz):
|
|
625
|
+
wL = w_prev[iz - 1] if iz > 0 else None
|
|
626
|
+
wR = w_prev[iz + 1] if iz + 1 < nz else None
|
|
627
|
+
seeds = _make_default_seeds(z_list[iz], w_prev[iz], wL, wR)
|
|
628
|
+
|
|
629
|
+
# also: push-forward seed from characteristic (often helps)
|
|
630
|
+
tau = float(np.expm1(float(t_sub)))
|
|
631
|
+
# w solves at same z, so zeta ~ z - tau*w_prev
|
|
632
|
+
seeds.append(complex(w_prev[iz])) # already
|
|
633
|
+
seeds.append(complex(-1.0 / (z_list[iz] - tau * w_prev[iz] + 1e-30)))
|
|
634
|
+
|
|
635
|
+
cands, oks, ress = _fd_candidates(
|
|
636
|
+
z_list[iz], float(t_sub), a_coeffs, seeds,
|
|
637
|
+
max_iter=max_iter, tol=tol,
|
|
638
|
+
armijo=armijo, min_lam=min_lam, w_min=w_min,
|
|
639
|
+
keep_best=keep_best,
|
|
640
|
+
)
|
|
641
|
+
# keep only finite
|
|
642
|
+
cand_list.append(cands)
|
|
643
|
+
|
|
644
|
+
# pass-1 viterbi (smooth/continuous)
|
|
645
|
+
w1, ok1 = _viterbi_track(
|
|
646
|
+
z_list, cand_list, w_prev,
|
|
647
|
+
lam_space=lam_space,
|
|
648
|
+
lam_time=lam_time,
|
|
649
|
+
lam_asym=lam_asym,
|
|
650
|
+
lam_tiny_im=0.0,
|
|
651
|
+
tiny_im=active_imag_eps,
|
|
652
|
+
edge_k=int(vopt.get("edge_k", 8)),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
# infer active regions (for 2-bulk cases)
|
|
656
|
+
active = _infer_active_mask(
|
|
657
|
+
w1,
|
|
658
|
+
imag_floor=max(active_imag_eps, 1e-12),
|
|
659
|
+
q=active_q,
|
|
660
|
+
pad=int(vopt.get("sweep_pad", sweep_pad)),
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# pass-2 viterbi: penalize tiny imag inside active region only
|
|
664
|
+
if lam_tiny_im != 0.0 and np.any(active):
|
|
665
|
+
cand_list2 = cand_list # same candidates
|
|
666
|
+
# create per-node tiny-im penalty by splitting into blocks:
|
|
667
|
+
# we'll do it by running viterbi twice: first on full, then
|
|
668
|
+
# override tiny-im penalty by masking.
|
|
669
|
+
# implement by modifying candidates by adding imaginary floor costs
|
|
670
|
+
# in unary: easiest is to run a custom viterbi with per-index lam.
|
|
671
|
+
# We'll approximate by selecting w2 where active via high penalty,
|
|
672
|
+
# else use pass-1 result.
|
|
673
|
+
# To keep it simple/robust: run global viterbi with penalty, but
|
|
674
|
+
# only at active indices.
|
|
675
|
+
# We'll do it by temporarily replacing non-active candidates with
|
|
676
|
+
# the best-by-residual (pass-1) to avoid over-penalizing gap.
|
|
677
|
+
cand_mod = []
|
|
678
|
+
for i in range(nz):
|
|
679
|
+
if active[i]:
|
|
680
|
+
cand_mod.append(cand_list2[i])
|
|
681
|
+
else:
|
|
682
|
+
# keep only the best candidate close to pass-1
|
|
683
|
+
c = cand_list2[i]
|
|
684
|
+
if c.size == 0:
|
|
685
|
+
cand_mod.append(c)
|
|
686
|
+
else:
|
|
687
|
+
j = int(np.argmin(np.abs(c - w1[i])))
|
|
688
|
+
cand_mod.append(c[j:j+1])
|
|
689
|
+
|
|
690
|
+
w2, ok2 = _viterbi_track(
|
|
691
|
+
z_list, cand_mod, w_prev,
|
|
692
|
+
lam_space=lam_space,
|
|
693
|
+
lam_time=lam_time,
|
|
694
|
+
lam_asym=lam_asym,
|
|
695
|
+
lam_tiny_im=lam_tiny_im,
|
|
696
|
+
tiny_im=max(active_imag_eps, 1e-12),
|
|
697
|
+
edge_k=int(vopt.get("edge_k", 8)),
|
|
698
|
+
)
|
|
699
|
+
else:
|
|
700
|
+
w2, ok2 = w1, ok1
|
|
701
|
+
|
|
702
|
+
# finalize this substep
|
|
703
|
+
w_prev = w2
|
|
704
|
+
ok_sub = ok2
|
|
705
|
+
|
|
706
|
+
# mass renorm (optional)
|
|
707
|
+
mass0 = np.nan
|
|
708
|
+
if renorm_mass:
|
|
709
|
+
w_prev, mass0, _ = _renormalize_density(z_list, w_prev, target_mass=target_mass)
|
|
710
|
+
|
|
711
|
+
# debug snapshot
|
|
712
|
+
if debug_path is not None and ((it % debug_every) == 0):
|
|
713
|
+
pack = {
|
|
714
|
+
"it": int(it),
|
|
715
|
+
"t": float(t_sub),
|
|
716
|
+
"z_real": z_list.real.copy(),
|
|
717
|
+
"w": w_prev.copy(),
|
|
718
|
+
"ok": ok_sub.copy(),
|
|
719
|
+
"active": active.copy(),
|
|
720
|
+
"mass": float(mass0) if np.isfinite(mass0) else np.nan,
|
|
721
|
+
}
|
|
722
|
+
if debug_iz:
|
|
723
|
+
pack["debug_iz"] = np.asarray(debug_iz, dtype=int)
|
|
724
|
+
pack["w_debug"] = w_prev[np.asarray(debug_iz, dtype=int)].copy()
|
|
725
|
+
debug_pack.append(pack)
|
|
726
|
+
|
|
727
|
+
W[it, :] = w_prev
|
|
728
|
+
ok[it, :] = np.isfinite(w_prev.real) & np.isfinite(w_prev.imag)
|
|
729
|
+
|
|
730
|
+
if debug_path is not None and len(debug_pack) > 0:
|
|
731
|
+
try:
|
|
732
|
+
# store as npz with object array
|
|
733
|
+
os.makedirs(os.path.dirname(debug_path) or ".", exist_ok=True)
|
|
734
|
+
np.savez_compressed(debug_path, debug=np.array(debug_pack, dtype=object))
|
|
735
|
+
except Exception:
|
|
736
|
+
pass
|
|
737
|
+
|
|
738
|
+
return W, ok
|