freealg 0.7.12__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.
@@ -0,0 +1,492 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ """
5
+ FD decompression with correct characteristic map + robust root selection.
6
+
7
+ Keeps public API:
8
+ - build_time_grid(size, n0, min_n_times=..., include_t0=True) -> (t_all, idx_req)
9
+ - decompress_newton(z_list, t_grid, a_coeffs, w0_list=None, **newton_opt) -> (W, ok)
10
+
11
+ IMPORTANT: This implements the characteristic transform consistent with:
12
+ τ(t)=e^t, α(t)=1-τ^{-1},
13
+ P(z + α w^{-1}, τ w) = 0,
14
+ where P(ζ,y)=0 is the algebraic relation for m0.
15
+
16
+ We construct a polynomial in w:
17
+ Q(w) := w^{deg_z} * P(z + α/w, τ w),
18
+ which has degree deg_z + deg_m (no artificial extra zero roots).
19
+
20
+ Root selection:
21
+ - Herglotz (your sign): Im(w) >= -herglotz_tol for Im(z)>0
22
+ - Homotopy anchor in η: start at η_hi, track down to η_lo
23
+ - Filter roots near anchor, then Viterbi along x
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import math
29
+ import numpy as np
30
+
31
+ __all__ = ["build_time_grid", "decompress_newton"]
32
+
33
+
34
+ def _inside_support_mask(x: np.ndarray, edges_row: np.ndarray, pad: float) -> np.ndarray:
35
+ """
36
+ edges_row: [a1,b1,a2,b2,...] with NaNs allowed (ghost edges).
37
+ Returns mask for x inside union of intervals, with optional padding.
38
+ """
39
+ mask = np.zeros_like(x, dtype=bool)
40
+ m = edges_row.size
41
+ for j in range(0, m, 2):
42
+ a = edges_row[j]
43
+ b = edges_row[j+1]
44
+ if not (np.isfinite(a) and np.isfinite(b)):
45
+ continue
46
+ aa = float(a) - float(pad)
47
+ bb = float(b) + float(pad)
48
+ mask |= (x >= aa) & (x <= bb)
49
+ return mask
50
+
51
+
52
+
53
+ def build_time_grid(size, n0, min_n_times=0, include_t0=True):
54
+ n0 = float(n0)
55
+ size = np.asarray(size, dtype=float).ravel()
56
+ if size.size == 0:
57
+ if include_t0:
58
+ return np.array([0.0], dtype=float), np.array([0], dtype=int)
59
+ return np.empty((0,), dtype=float), np.empty((0,), dtype=int)
60
+
61
+ t_sizes = np.log(size / n0)
62
+
63
+ t_req = t_sizes.copy()
64
+ if include_t0:
65
+ t_req = np.concatenate((np.array([0.0], dtype=float), t_req))
66
+
67
+ t_req = np.unique(t_req)
68
+ t_req.sort()
69
+
70
+ if int(min_n_times) > 0 and t_req.size < int(min_n_times):
71
+ t_all = np.linspace(float(t_req[0]), float(t_req[-1]), int(min_n_times))
72
+ else:
73
+ t_all = t_req
74
+
75
+ t_all = np.unique(t_all)
76
+ t_all.sort()
77
+
78
+ idx_req = np.array([int(np.argmin(np.abs(t_all - ts))) for ts in t_sizes], dtype=int)
79
+ return t_all, idx_req
80
+
81
+
82
+ # ===========================
83
+ # Polynomial curve utilities
84
+ # ===========================
85
+
86
+ def _poly_w_coeffs(z: complex, t: float, a_coeffs: np.ndarray) -> np.ndarray:
87
+ """
88
+ Build Q(w) coeffs (descending) for:
89
+ Q(w) = w^{deg_z} * P(z + α/w, τ w)
90
+ where τ=e^t, α=1-1/τ.
91
+ """
92
+ a = np.asarray(a_coeffs, dtype=np.complex128)
93
+ deg_z = a.shape[0] - 1
94
+ deg_m = a.shape[1] - 1
95
+
96
+ tau = math.exp(float(t))
97
+ alpha = 1.0 - 1.0 / tau
98
+ z = complex(z)
99
+
100
+ deg_Q = deg_z + deg_m
101
+ c = np.zeros((deg_Q + 1,), dtype=np.complex128) # ascending
102
+
103
+ # term: a_{i,j} (z + alpha/w)^i (tau*w)^j
104
+ # expand (z + alpha/w)^i = sum_{k=0}^i C(i,k) z^{i-k} (alpha/w)^k
105
+ # multiply by w^{deg_z}: exponent of w is deg_z + j - k (>=0).
106
+ for i in range(deg_z + 1):
107
+ for j in range(deg_m + 1):
108
+ aij = a[i, j]
109
+ if aij == 0:
110
+ continue
111
+ for k in range(i + 1):
112
+ p = deg_z + j - k
113
+ if p < 0 or p > deg_Q:
114
+ continue
115
+ c[p] += aij * math.comb(i, k) * (z ** (i - k)) * ((alpha) ** k) * (tau ** j)
116
+
117
+ nz = np.flatnonzero(np.abs(c) > 0)
118
+ if nz.size == 0:
119
+ return np.array([0.0], dtype=np.complex128)
120
+ p_max = int(nz.max())
121
+ c = c[:p_max + 1]
122
+ return c[::-1].copy()
123
+
124
+
125
+ def _herglotz_ok(w: complex, z: complex, tol: float = 0.0) -> bool:
126
+ z = complex(z)
127
+ w = complex(w)
128
+ if z.imag <= 0.0:
129
+ return True
130
+ return (w.imag >= -float(tol))
131
+
132
+
133
+ def _asym_score(z: complex, w: complex) -> float:
134
+ return float(abs(complex(z) * complex(w) + 1.0))
135
+
136
+
137
+ def _newton_poly_root(coeff_desc: np.ndarray, w0: complex, max_iter: int, tol: float,
138
+ armijo: bool = True, min_lam: float = 1e-4):
139
+ w = complex(w0)
140
+ dcoeff = np.polyder(coeff_desc)
141
+ for _ in range(int(max_iter)):
142
+ f = np.polyval(coeff_desc, w)
143
+ if not np.isfinite(f):
144
+ return w, False
145
+ if abs(f) <= float(tol) * (1.0 + abs(w)):
146
+ return w, True
147
+ df = np.polyval(dcoeff, w)
148
+ if (not np.isfinite(df)) or df == 0:
149
+ return w, False
150
+ step = f / df
151
+
152
+ if not armijo:
153
+ w = w - step
154
+ continue
155
+
156
+ f0 = abs(f)
157
+ lam = 1.0
158
+ while lam >= float(min_lam):
159
+ w_try = w - lam * step
160
+ f_try = np.polyval(coeff_desc, w_try)
161
+ if np.isfinite(f_try) and abs(f_try) <= (1.0 - 0.5 * lam) * f0:
162
+ w = w_try
163
+ break
164
+ lam *= 0.5
165
+ else:
166
+ w = w - float(min_lam) * step
167
+
168
+ f = np.polyval(coeff_desc, w)
169
+ return w, bool(np.isfinite(f) and abs(f) <= float(tol) * (1.0 + abs(w)))
170
+
171
+
172
+ def _roots_of_Q(z: complex, t: float, a_coeffs: np.ndarray):
173
+ coeff_desc = _poly_w_coeffs(z, t, a_coeffs)
174
+ if coeff_desc.size <= 1:
175
+ return coeff_desc, np.empty((0,), np.complex128)
176
+ r = np.roots(coeff_desc)
177
+ r = r[np.isfinite(r)]
178
+ return coeff_desc, r.astype(np.complex128, copy=False)
179
+
180
+
181
+ def _physical_anchor_for_x(x: float, t: float, a_coeffs: np.ndarray,
182
+ eta_hi: float, eta_lo: float, n_eta: int,
183
+ herglotz_tol: float,
184
+ max_iter: int, tol: float,
185
+ armijo: bool, min_lam: float):
186
+ etas = np.linspace(float(eta_hi), float(eta_lo), int(n_eta))
187
+ z0 = complex(x, etas[0])
188
+
189
+ coeff0, roots0 = _roots_of_Q(z0, t, a_coeffs)
190
+ if roots0.size == 0:
191
+ return -1.0 / z0, False
192
+
193
+ # pick best among Herglotz candidates by asymptotic score
194
+ good = [w for w in roots0 if _herglotz_ok(w, z0, herglotz_tol)]
195
+ if len(good) == 0:
196
+ good = list(roots0)
197
+ good = np.asarray(good, dtype=np.complex128)
198
+ sc = np.array([_asym_score(z0, w) for w in good], dtype=float)
199
+ w = good[int(np.argmin(sc))]
200
+
201
+ # refine and track down
202
+ w, _ = _newton_poly_root(coeff0, w, max_iter=max_iter, tol=tol, armijo=armijo, min_lam=min_lam)
203
+
204
+ for eta in etas[1:]:
205
+ z = complex(x, eta)
206
+ coeff, _ = _roots_of_Q(z, t, a_coeffs)
207
+ w, ok2 = _newton_poly_root(coeff, w, max_iter=max_iter, tol=tol, armijo=armijo, min_lam=min_lam)
208
+ if not ok2:
209
+ _coeff, roots = _roots_of_Q(z, t, a_coeffs)
210
+ if roots.size == 0:
211
+ return w, False
212
+ roots = np.asarray(roots, dtype=np.complex128)
213
+ mask = np.array([_herglotz_ok(ww, z, herglotz_tol) for ww in roots], dtype=bool)
214
+ cand = roots[mask] if np.any(mask) else roots
215
+ jj = int(np.argmin(np.abs(cand - w)))
216
+ w = cand[jj]
217
+ w, ok2 = _newton_poly_root(coeff, w, max_iter=max_iter, tol=tol, armijo=armijo, min_lam=min_lam)
218
+ if not ok2:
219
+ return w, False
220
+
221
+ return w, True
222
+
223
+
224
+ def _candidate_filter(roots: np.ndarray, z: complex, herglotz_tol: float,
225
+ anchor: complex | None, anchor_radius: float,
226
+ *, w_min: float = 0.0,
227
+ im_floor: float | None = None):
228
+ if roots.size == 0:
229
+ return np.empty((0,), np.complex128)
230
+ keep = []
231
+ for w in roots:
232
+ if abs(w) <= float(w_min):
233
+ continue
234
+ if not _herglotz_ok(w, z, herglotz_tol):
235
+ continue
236
+ if im_floor is not None and z.imag > 0.0:
237
+ if w.imag < float(im_floor):
238
+ continue
239
+ if anchor is not None:
240
+ if abs(w - anchor) > float(anchor_radius) * (1.0 + abs(anchor)):
241
+ continue
242
+ keep.append(complex(w))
243
+ if len(keep) == 0:
244
+ return np.empty((0,), np.complex128)
245
+ # dedup
246
+ out = []
247
+ for w in keep:
248
+ if all(abs(w-u) > 1e-9*(1.0+abs(u)) for u in out):
249
+ out.append(w)
250
+ return np.asarray(out, dtype=np.complex128)
251
+
252
+
253
+ def _viterbi_path(cand_list, z_list, w_prev,
254
+ lam_time, lam_space, lam_asym, lam_im2,
255
+ edge_k):
256
+ nz = len(cand_list)
257
+ chosen = np.empty((nz,), dtype=np.complex128)
258
+ ok = np.ones((nz,), dtype=bool)
259
+ big = 1e300
260
+
261
+ sizes = np.array([c.size for c in cand_list], dtype=int)
262
+ for i in range(nz):
263
+ if sizes[i] == 0:
264
+ chosen[i] = w_prev[i]
265
+ ok[i] = False
266
+
267
+ i = 0
268
+ while i < nz:
269
+ if sizes[i] == 0:
270
+ i += 1
271
+ continue
272
+ j = i
273
+ while j < nz and sizes[j] > 0:
274
+ j += 1
275
+
276
+ block = list(range(i, j))
277
+ c0 = cand_list[i]
278
+ m0 = c0.size
279
+
280
+ dp = np.full((m0,), big, dtype=float)
281
+ bp = [None] * (j - i)
282
+
283
+ node = lam_time * (np.abs(c0 - w_prev[i]) ** 2)
284
+ if edge_k > 0 and (i < edge_k or i >= nz - edge_k):
285
+ node = node + lam_asym * np.array([_asym_score(z_list[i], w) for w in c0], dtype=float)
286
+ if lam_im2 != 0.0:
287
+ node = node + lam_im2 * (np.imag(c0) ** 2)
288
+ dp = node
289
+ bp[0] = np.full((m0,), -1, dtype=int)
290
+
291
+ for kpos, idx in enumerate(block[1:], start=1):
292
+ ck = cand_list[idx]
293
+ mk = ck.size
294
+ new_dp = np.full((mk,), big, dtype=float)
295
+ new_bp = np.full((mk,), -1, dtype=int)
296
+
297
+ node = lam_time * (np.abs(ck - w_prev[idx]) ** 2)
298
+ if edge_k > 0 and (idx < edge_k or idx >= nz - edge_k):
299
+ node = node + lam_asym * np.array([_asym_score(z_list[idx], w) for w in ck], dtype=float)
300
+ if lam_im2 != 0.0:
301
+ node = node + lam_im2 * (np.imag(ck) ** 2)
302
+
303
+ prev_c = cand_list[idx - 1]
304
+ for q in range(mk):
305
+ vals = dp + lam_space * (np.abs(ck[q] - prev_c) ** 2)
306
+ best = int(np.argmin(vals))
307
+ new_dp[q] = float(vals[best] + node[q])
308
+ new_bp[q] = best
309
+
310
+ dp = new_dp
311
+ bp[kpos] = new_bp
312
+
313
+ end_q = int(np.argmin(dp))
314
+ for kpos in range(len(block) - 1, -1, -1):
315
+ idx = block[kpos]
316
+ chosen[idx] = cand_list[idx][end_q]
317
+ end_q = int(bp[kpos][end_q])
318
+
319
+ i = j
320
+
321
+ return chosen, ok
322
+
323
+
324
+ # =====================
325
+ # Main decompression API
326
+ # =====================
327
+
328
+ def decompress_newton(
329
+ z_list,
330
+ t_grid,
331
+ a_coeffs,
332
+ w0_list=None,
333
+ *,
334
+ dt_max=0.05,
335
+ return_success_rate=False,
336
+ viterbi=True,
337
+ viterbi_opt=None,
338
+ # edge-guided selection (optional)
339
+ edge_use=False,
340
+ edge_support=None,
341
+ edge_pad=0.0,
342
+ im_floor_rel=0.15,
343
+ w_min=1e-14,
344
+ # homotopy (eta) options
345
+ eta_hi=3.0,
346
+ n_eta=24,
347
+ anchor_radius=0.6,
348
+ # newton options for homotopy
349
+ max_iter=60,
350
+ tol=1e-12,
351
+ armijo=True,
352
+ min_lam=1e-4,
353
+ # herglotz convention
354
+ herglotz_tol=0.0,
355
+ **_,
356
+ ):
357
+ z_list = np.asarray(z_list, dtype=np.complex128).ravel()
358
+ t_grid = np.asarray(t_grid, dtype=float).ravel()
359
+ if z_list.size == 0 or t_grid.size == 0:
360
+ raise ValueError("z_list and t_grid must be non-empty")
361
+
362
+ t_grid = np.unique(t_grid)
363
+ t_grid.sort()
364
+ if np.any(np.diff(t_grid) <= 0.0):
365
+ raise ValueError("t_grid must be strictly increasing")
366
+
367
+ nt = t_grid.size
368
+ nz = z_list.size
369
+ x = z_list.real
370
+ eta_lo = float(np.median(z_list.imag))
371
+
372
+ real_edges = None
373
+ if edge_use:
374
+ try:
375
+ from ._edge import evolve_edges, merge_edges
376
+ if edge_support is None:
377
+ raise ValueError("edge_support must be provided when edge_use=True")
378
+ complex_edges = evolve_edges(t_grid, a_coeffs, support=edge_support)
379
+ # merge_edges in your package expects edges array (nt, 2k) and returns (real_merged_edges, active_k)
380
+ real_edges, _active_k = merge_edges(complex_edges, t_grid)
381
+ except Exception:
382
+ real_edges = None
383
+
384
+ if w0_list is None:
385
+ w0_list = -1.0 / z_list
386
+ w_prev = np.asarray(w0_list, dtype=np.complex128).ravel()
387
+ if w_prev.size != nz:
388
+ raise ValueError("w0_list length must match z_list")
389
+
390
+ vopt = {} if viterbi_opt is None else dict(viterbi_opt)
391
+ lam_time = float(vopt.get("lam_time", 0.25))
392
+ lam_space = float(vopt.get("lam_space", 1.0))
393
+ lam_asym = float(vopt.get("lam_asym", 0.2))
394
+ lam_im2 = float(vopt.get("lam_im2", 0.0))
395
+ edge_k = int(vopt.get("edge_k", 8))
396
+
397
+ W = np.empty((nt, nz), dtype=np.complex128)
398
+ ok = np.ones((nt, nz), dtype=bool)
399
+ W[0, :] = w_prev
400
+ success = np.ones((nt,), dtype=float)
401
+ success[0] = 1.0
402
+
403
+ for it in range(1, nt):
404
+ t1 = float(t_grid[it])
405
+ t0 = float(t_grid[it - 1])
406
+ dt = t1 - t0
407
+ n_sub = max(1, int(np.ceil(abs(dt) / max(float(dt_max), 1e-12))))
408
+ sub_ts = np.linspace(t0, t1, n_sub + 1)[1:]
409
+
410
+ ok_row = np.ones((nz,), dtype=bool)
411
+
412
+ for t_sub in sub_ts:
413
+ anchors = np.empty((nz,), dtype=np.complex128)
414
+ anchor_ok = np.ones((nz,), dtype=bool)
415
+ for iz in range(nz):
416
+ w_a, ok_a = _physical_anchor_for_x(
417
+ float(x[iz]), float(t_sub), a_coeffs,
418
+ eta_hi=float(eta_hi), eta_lo=float(eta_lo),
419
+ n_eta=int(n_eta),
420
+ herglotz_tol=float(herglotz_tol),
421
+ max_iter=int(max_iter), tol=float(tol),
422
+ armijo=bool(armijo), min_lam=float(min_lam),
423
+ )
424
+ anchors[iz] = w_a
425
+ anchor_ok[iz] = ok_a
426
+
427
+ cand_list = []
428
+ for iz in range(nz):
429
+ z = z_list[iz]
430
+ coeff, roots = _roots_of_Q(z, float(t_sub), a_coeffs)
431
+
432
+ anc = anchors[iz]
433
+ im_floor = None
434
+ if real_edges is not None:
435
+ inside = _inside_support_mask(np.array([float(x[iz])]), real_edges[int(np.argmin(np.abs(t_grid - float(t_sub))))], pad=float(edge_pad))[0]
436
+ if inside:
437
+ # require a fraction of the max positive imaginary root to avoid collapsing early
438
+ im_pos = np.max(np.maximum(0.0, roots.imag)) if roots.size > 0 else 0.0
439
+ if im_pos > 0.0:
440
+ im_floor = float(im_floor_rel) * float(im_pos)
441
+ cands = _candidate_filter(
442
+ roots, z, herglotz_tol=float(herglotz_tol),
443
+ anchor=anc if anchor_ok[iz] else None,
444
+ anchor_radius=float(anchor_radius),
445
+ w_min=float(w_min),
446
+ im_floor=im_floor,
447
+ )
448
+
449
+ # fallback: take nearest to anchor
450
+ if cands.size == 0 and roots.size > 0:
451
+ roots = roots.astype(np.complex128, copy=False)
452
+ roots2 = roots[np.abs(roots) > float(w_min)]
453
+ if roots2.size == 0:
454
+ roots2 = roots
455
+ jj = int(np.argmin(np.abs(roots2 - anc)))
456
+ cands = np.array([roots2[jj]], dtype=np.complex128)
457
+
458
+ # include refined anchor if it lands on a root
459
+ if coeff.size > 1:
460
+ anc_ref, ok_ref = _newton_poly_root(coeff, anc, max_iter=max_iter, tol=tol,
461
+ armijo=armijo, min_lam=min_lam)
462
+ if ok_ref and _herglotz_ok(anc_ref, z, tol=herglotz_tol):
463
+ cands = np.unique(np.concatenate((cands, np.array([anc_ref], dtype=np.complex128))))
464
+
465
+ cand_list.append(cands)
466
+
467
+ if viterbi:
468
+ w_path, ok_path = _viterbi_path(
469
+ cand_list, z_list, w_prev,
470
+ lam_time, lam_space, lam_asym, lam_im2, edge_k
471
+ )
472
+ w_prev = w_path
473
+ ok_row &= ok_path
474
+ else:
475
+ new_row = np.empty((nz,), dtype=np.complex128)
476
+ for iz in range(nz):
477
+ c = cand_list[iz]
478
+ if c.size == 0:
479
+ new_row[iz] = w_prev[iz]
480
+ ok_row[iz] = False
481
+ continue
482
+ jj = int(np.argmin(np.abs(c - w_prev[iz]) ** 2))
483
+ new_row[iz] = c[jj]
484
+ w_prev = new_row
485
+
486
+ W[it, :] = w_prev
487
+ ok[it, :] = ok_row
488
+ success[it] = float(np.mean(ok_row))
489
+
490
+ if return_success_rate:
491
+ return W, ok, success
492
+ return W, ok