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
@@ -12,7 +12,6 @@
12
12
  # =======
13
13
 
14
14
  import numpy
15
- from ._continuation_algebraic import powers
16
15
 
17
16
  __all__ = ['build_time_grid', 'decompress_newton_old', 'decompress_newton']
18
17
 
@@ -21,11 +20,11 @@ __all__ = ['build_time_grid', 'decompress_newton_old', 'decompress_newton']
21
20
  # build time grid
22
21
  # ===============
23
22
 
24
- def build_time_grid(sizes, n0, min_n_time=0):
23
+ def build_time_grid(sizes, n0, min_n_times=0):
25
24
  """
26
25
  sizes: list/array of requested matrix sizes (e.g. [2000,3000,4000,8000])
27
26
  n0: initial size (self.n)
28
- min_n_time: minimum number of time points to run Newton sweep on
27
+ min_n_times: minimum number of time points to run Newton sweep on
29
28
 
30
29
  Returns
31
30
  -------
@@ -43,7 +42,7 @@ def build_time_grid(sizes, n0, min_n_time=0):
43
42
  t_all = numpy.sort(base)
44
43
 
45
44
  # Add points only if needed: split largest gaps
46
- N = int(min_n_time) if min_n_time is not None else 0
45
+ N = int(min_n_times) if min_n_times is not None else 0
47
46
  while t_all.size < N and t_all.size >= 2:
48
47
  gaps = numpy.diff(t_all)
49
48
  k = int(numpy.argmax(gaps))
@@ -60,121 +59,8 @@ def build_time_grid(sizes, n0, min_n_time=0):
60
59
  return t_all, idx_req
61
60
 
62
61
 
63
- # ===============
64
- # eval P partials
65
- # ===============
66
-
67
- def eval_P_partials(z, m, a_coeffs):
68
- """
69
- Evaluate P(z,m) and its partial derivatives dP/dz and dP/dm.
70
-
71
- This assumes P is represented by `a_coeffs` in the monomial basis
72
-
73
- P(z, m) = sum_{j=0..s} a_j(z) * m^j,
74
- a_j(z) = sum_{i=0..deg_z} a_coeffs[i, j] * z^i.
75
-
76
- The function returns P, dP/dz, dP/dm with broadcasting over z and m.
77
-
78
- Parameters
79
- ----------
80
- z : complex or array_like of complex
81
- First argument to P.
82
- m : complex or array_like of complex
83
- Second argument to P. Must be broadcast-compatible with `z`.
84
- a_coeffs : ndarray, shape (deg_z+1, s+1)
85
- Coefficient matrix for P in the monomial basis.
86
-
87
- Returns
88
- -------
89
- P : complex or ndarray of complex
90
- Value P(z,m).
91
- Pz : complex or ndarray of complex
92
- Partial derivative dP/dz evaluated at (z,m).
93
- Pm : complex or ndarray of complex
94
- Partial derivative dP/dm evaluated at (z,m).
95
-
96
- Notes
97
- -----
98
- For scalar (z,m), this uses Horner evaluation for a_j(z) and then Horner
99
- in m. For array inputs, it uses precomputed power tables via `_powers` for
100
- simplicity.
101
-
102
- Examples
103
- --------
104
- .. code-block:: python
105
-
106
- P, Pz, Pm = eval_P_partials(1.0 + 1j, 0.2 + 0.3j, a_coeffs)
107
- """
108
62
 
109
- z = numpy.asarray(z, dtype=complex)
110
- m = numpy.asarray(m, dtype=complex)
111
63
 
112
- deg_z = int(a_coeffs.shape[0] - 1)
113
- s = int(a_coeffs.shape[1] - 1)
114
-
115
- if (z.ndim == 0) and (m.ndim == 0):
116
- zz = complex(z)
117
- mm = complex(m)
118
-
119
- a = numpy.empty(s + 1, dtype=complex)
120
- ap = numpy.empty(s + 1, dtype=complex)
121
-
122
- for j in range(s + 1):
123
- c = a_coeffs[:, j]
124
-
125
- val = 0.0 + 0.0j
126
- for i in range(deg_z, -1, -1):
127
- val = val * zz + c[i]
128
- a[j] = val
129
-
130
- dval = 0.0 + 0.0j
131
- for i in range(deg_z, 0, -1):
132
- dval = dval * zz + (i * c[i])
133
- ap[j] = dval
134
-
135
- p = a[s]
136
- pm = 0.0 + 0.0j
137
- for j in range(s - 1, -1, -1):
138
- pm = pm * mm + p
139
- p = p * mm + a[j]
140
-
141
- pz = ap[s]
142
- for j in range(s - 1, -1, -1):
143
- pz = pz * mm + ap[j]
144
-
145
- return p, pz, pm
146
-
147
- shp = numpy.broadcast(z, m).shape
148
- zz = numpy.broadcast_to(z, shp).ravel()
149
- mm = numpy.broadcast_to(m, shp).ravel()
150
-
151
- zp = powers(zz, deg_z)
152
- mp = powers(mm, s)
153
-
154
- dzp = numpy.zeros_like(zp)
155
- for i in range(1, deg_z + 1):
156
- dzp[:, i] = i * zp[:, i - 1]
157
-
158
- P = numpy.zeros(zz.size, dtype=complex)
159
- Pz = numpy.zeros(zz.size, dtype=complex)
160
- Pm = numpy.zeros(zz.size, dtype=complex)
161
-
162
- for j in range(s + 1):
163
- aj = zp @ a_coeffs[:, j]
164
- P += aj * mp[:, j]
165
-
166
- ajp = dzp @ a_coeffs[:, j]
167
- Pz += ajp * mp[:, j]
168
-
169
- if j >= 1:
170
- Pm += (j * aj) * mp[:, j - 1]
171
-
172
- return P.reshape(shp), Pz.reshape(shp), Pm.reshape(shp)
173
-
174
-
175
- # ==========
176
- # fd solve w
177
- # ==========
178
64
 
179
65
  def fd_solve_w(z, t, a_coeffs, w_init, max_iter=50, tol=1e-12,
180
66
  armijo=1e-4, min_lam=1e-6, w_min=1e-14):
@@ -245,256 +131,140 @@ def fd_solve_w(z, t, a_coeffs, w_init, max_iter=50, tol=1e-12,
245
131
  want_pos_imag = (z.imag > 0.0)
246
132
 
247
133
  for _ in range(max_iter):
248
- if not numpy.isfinite(w.real) or not numpy.isfinite(w.imag):
249
- return w, False
250
- if abs(w) < w_min:
251
- return w, False
252
- if want_pos_imag and (w.imag <= 0.0):
253
- return w, False
254
-
255
- zeta = z + alpha / w
256
- y = tau * w
257
-
258
- F, Pz, Py = eval_P_partials(zeta, y, a_coeffs)
259
- F = complex(F)
260
- Pz = complex(Pz)
261
- Py = complex(Py)
262
134
 
263
- if abs(F) <= tol:
264
- return w, True
265
-
266
- dF = (-alpha / (w * w)) * Pz + tau * Py
267
- if dF == 0.0:
135
+ a = numpy.asarray(a_coeffs, dtype=numpy.complex128)
136
+ deg_z = a.shape[0] - 1
137
+ deg_m = a.shape[1] - 1
138
+
139
+ beta = tau - 1.0
140
+
141
+ # poly_y[p] stores coeff of y^p after clearing denominators
142
+ poly_y = numpy.zeros(deg_z + deg_m + 1, dtype=numpy.complex128)
143
+
144
+ # Build polynomial: sum_{i,j} a[i,j] (z + beta/y)^i y^j * y^{deg_z}
145
+ # Expand (z + beta/y)^i = sum_{k=0}^i C(i,k) z^{i-k} (beta/y)^k
146
+ # Term contributes to power p = deg_z + j - k.
147
+ from math import comb
148
+ for i in range(deg_z + 1):
149
+ for j in range(deg_m + 1):
150
+ aij = a[i, j]
151
+ if aij == 0:
152
+ continue
153
+ for k in range(i + 1):
154
+ p = deg_z + j - k
155
+ poly_y[p] += aij * comb(i, k) * (z ** (i - k)) * (beta ** k)
156
+
157
+ # numpy.roots expects highest degree first
158
+ coeffs = poly_y[::-1]
159
+
160
+ # If leading coefficients are ~0, trim (rare but safe)
161
+ nz_lead = numpy.flatnonzero(numpy.abs(coeffs) > 0)
162
+ if nz_lead.size == 0:
268
163
  return w, False
164
+ coeffs = coeffs[nz_lead[0]:]
269
165
 
270
- step = -F / dF
166
+ roots_y = numpy.roots(coeffs)
271
167
 
272
- lam = 1.0
273
- F_abs = abs(F)
274
- ok = False
168
+ # Pick root with Im(w)>0 (if z in upper half-plane), closest to time seed
169
+ y_seed = tau * w_init
170
+ best = None
171
+ best_score = None
275
172
 
276
- while lam >= min_lam:
277
- w_new = w + lam * step
278
- if abs(w_new) < w_min:
279
- lam *= 0.5
280
- continue
281
- if want_pos_imag and (w_new.imag <= 0.0):
282
- lam *= 0.5
173
+ for y in roots_y:
174
+ if not numpy.isfinite(y.real) or not numpy.isfinite(y.imag):
283
175
  continue
284
176
 
285
- zeta_new = z + alpha / w_new
286
- y_new = tau * w_new
177
+ w_cand = y / tau
287
178
 
288
- F_new = eval_P_partials(zeta_new, y_new, a_coeffs)[0]
289
- F_new = complex(F_new)
179
+ if want_pos_imag and (w_cand.imag <= 0.0):
180
+ continue
290
181
 
291
- if abs(F_new) <= (1.0 - armijo * lam) * F_abs:
292
- w = w_new
293
- ok = True
294
- break
182
+ if abs(w_cand) < w_min:
183
+ continue
184
+
185
+ # score: stick to time continuation
186
+ score = abs(y - y_seed)
295
187
 
296
- lam *= 0.5
188
+ if (best_score is None) or (score < best_score):
189
+ best = w_cand
190
+ best_score = score
297
191
 
298
- if not ok:
192
+ if best is None:
299
193
  return w, False
300
194
 
301
- F_end = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
302
- return w, (abs(F_end) <= 10.0 * tol)
195
+ w = complex(best)
303
196
 
197
+ # final residual check
198
+ F_end = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
199
+ return w, (abs(F_end) <= 1e3 * tol)
304
200
 
305
- # =====================
306
- # decompress newton old
307
- # =====================
201
+ # -------------------
308
202
 
309
- def decompress_newton_old(z_list, t_grid, a_coeffs, w0_list=None,
310
- dt_max=0.1, sweep=True, time_rel_tol=5.0,
311
- max_iter=50, tol=1e-12, armijo=1e-4,
312
- min_lam=1e-6, w_min=1e-14):
313
- """
314
- Evolve w = m(t,z) on a fixed z grid and time grid using FD.
315
203
 
316
- Parameters
317
- ----------
318
- z_list : array_like of complex
319
- Query points z (typically x + 1j*eta with eta > 0).
320
- t_grid : array_like of float
321
- Strictly increasing time grid.
322
- a_coeffs : ndarray
323
- Coefficients defining P(zeta,y) in the monomial basis used by eval_P.
324
- w0_list : array_like of complex
325
- Initial values at t_grid[0] (typically m0(z_list) on the physical
326
- branch).
327
- dt_max : float, optional
328
- Maximum internal time step. Larger dt is handled by substepping.
329
- sweep : bool, optional
330
- If True, use spatial continuation (neighbor seeding) plus a
331
- time-consistency check to prevent branch collapse. If False, solve
332
- each z independently from the previous-time seed (faster but may
333
- branch-switch for small eta).
334
- time_rel_tol : float, optional
335
- When sweep=True, if the neighbor-seeded solution differs from the
336
- previous-time value w_prev by more than time_rel_tol*(1+|w_prev|), we
337
- also solve using the previous-time seed and select the closer one.
338
- max_iter : int, optional
339
- Maximum Newton iterations in fd_solve_w.
340
- tol : float, optional
341
- Residual tolerance in fd_solve_w.
342
- armijo : float, optional
343
- Armijo parameter for backtracking in fd_solve_w.
344
- min_lam : float, optional
345
- Minimum damping factor in fd_solve_w backtracking.
346
- w_min : float, optional
347
- Minimum |w| allowed to avoid singularity.
348
-
349
- Returns
350
- -------
351
- W : ndarray, shape (len(t_grid), len(z_list))
352
- Evolved values w(t,z).
353
- ok : ndarray of bool, same shape as W
354
- Convergence flags from the final accepted solve at each point.
355
-
356
- Notes
357
- -----
358
- For very small eta, the implicit FD equation can have multiple roots in the
359
- upper half-plane. The sweep option is a branch-selection mechanism. The
360
- time-consistency check is critical at large t to avoid propagating a
361
- nearly-real spurious root across x.
204
+ F_end = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
205
+ return w, (abs(F_end) <= 10.0 * tol)
362
206
 
363
- Examples
364
- --------
365
- .. code-block:: python
366
207
 
367
- x = numpy.linspace(-0.5, 2.5, 2000)
368
- eta = 1e-6
369
- z_query = x + 1j*eta
370
- w0_list = m1_fn(z_query)
208
+ # ============
209
+ # NEW FUNCTION
210
+ # ============
371
211
 
372
- t_grid = numpy.linspace(0.0, 4.0, 2)
373
- W, ok = fd_evolve_on_grid(
374
- z_query, t_grid, a_coeffs, w0_list=w0_list,
375
- dt_max=0.1, sweep=True, time_rel_tol=5.0,
376
- max_iter=50, tol=1e-12, armijo=1e-4, min_lam=1e-6, w_min=1e-14
377
- )
378
- rho = W.imag / numpy.pi
212
+ def fd_candidates_w(z, t, a_coeffs, w_min=1e-14):
379
213
  """
380
- z_list = numpy.asarray(z_list, dtype=complex).ravel()
381
- t_grid = numpy.asarray(t_grid, dtype=float).ravel()
382
- nt = t_grid.size
383
- nz = z_list.size
384
-
385
- W = numpy.empty((nt, nz), dtype=complex)
386
- ok = numpy.zeros((nt, nz), dtype=bool)
387
-
388
- if w0_list is None:
389
- raise ValueError(
390
- "w0_list must be provided (e.g. m1_fn(z_list) at t=0).")
391
- w_prev = numpy.asarray(w0_list, dtype=complex).ravel()
392
- if w_prev.size != nz:
393
- raise ValueError("w0_list must have same size as z_list.")
394
-
395
- W[0, :] = w_prev
396
- ok[0, :] = True
397
-
398
- sweep = bool(sweep)
399
- time_rel_tol = float(time_rel_tol)
400
-
401
- for it in range(1, nt):
402
- t0 = float(t_grid[it - 1])
403
- t1 = float(t_grid[it])
404
- dt = t1 - t0
405
- if dt <= 0.0:
406
- raise ValueError("t_grid must be strictly increasing.")
214
+ Return candidate roots w solving P(z + alpha/w, tau*w)=0 with Im(w)>0 (if Im(z)>0).
215
+ """
216
+ z = complex(z)
217
+ tau = float(numpy.exp(t))
218
+ alpha = 1.0 - 1.0 / tau
219
+ want_pos_imag = (z.imag > 0.0)
407
220
 
408
- # Internal substepping makes time-continuity a strong selector.
409
- n_sub = int(numpy.ceil(dt / float(dt_max)))
410
- if n_sub < 1:
411
- n_sub = 1
221
+ a = numpy.asarray(a_coeffs, dtype=numpy.complex128)
222
+ deg_z = a.shape[0] - 1
223
+ deg_m = a.shape[1] - 1
412
224
 
413
- for ks in range(1, n_sub + 1):
414
- t = t0 + dt * (ks / float(n_sub))
225
+ beta = tau - 1.0 # since alpha/w = (tau-1)/(tau*w) = beta / y with y=tau*w
415
226
 
416
- w_row = numpy.empty(nz, dtype=complex)
417
- ok_row = numpy.zeros(nz, dtype=bool)
227
+ poly_y = numpy.zeros(deg_z + deg_m + 1, dtype=numpy.complex128)
418
228
 
419
- if not sweep:
420
- # Independent solves: each point uses previous-time seed only.
421
- for iz in range(nz):
422
- w, success = fd_solve_w(
423
- z_list[iz], t, a_coeffs, w_prev[iz],
424
- max_iter=max_iter, tol=tol, armijo=armijo,
425
- min_lam=min_lam, w_min=w_min
426
- )
427
- w_row[iz] = w
428
- ok_row[iz] = success
429
-
430
- w_prev = w_row
229
+ from math import comb
230
+ for i in range(deg_z + 1):
231
+ for j in range(deg_m + 1):
232
+ aij = a[i, j]
233
+ if aij == 0:
431
234
  continue
235
+ for k in range(i + 1):
236
+ p = deg_z + j - k
237
+ poly_y[p] += aij * comb(i, k) * (z ** (i - k)) * (beta ** k)
238
+
239
+ coeffs = poly_y[::-1]
240
+ nz_lead = numpy.flatnonzero(numpy.abs(coeffs) > 0)
241
+ if nz_lead.size == 0:
242
+ return []
243
+
244
+ coeffs = coeffs[nz_lead[0]:]
245
+ roots_y = numpy.roots(coeffs)
246
+
247
+ cands = []
248
+ for y in roots_y:
249
+ if not numpy.isfinite(y.real) or not numpy.isfinite(y.imag):
250
+ continue
251
+ w = y / tau
252
+ if abs(w) < w_min:
253
+ continue
254
+ if want_pos_imag and (w.imag <= 0.0):
255
+ continue
256
+ # residual filter (optional but helps)
257
+ # -------------
258
+ # TEST
259
+ # F = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
260
+ # if abs(F) < 1e-6:
261
+ # cands.append(complex(w))
262
+ # ---------------
263
+ # TEST
264
+ cands.append(complex(w))
265
+ # ------------------
432
266
 
433
- # Center-out sweep seed: pick where previous-time Im is largest.
434
- i0 = int(numpy.argmax(numpy.abs(numpy.imag(w_prev))))
435
-
436
- w0, ok0 = fd_solve_w(
437
- z_list[i0], t, a_coeffs, w_prev[i0],
438
- max_iter=max_iter, tol=tol, armijo=armijo,
439
- min_lam=min_lam, w_min=w_min
440
- )
441
- w_row[i0] = w0
442
- ok_row[i0] = ok0
443
-
444
- def solve_with_choice(iz, w_neighbor):
445
- # First try neighbor-seeded Newton (spatial continuity).
446
- w_a, ok_a = fd_solve_w(
447
- z_list[iz], t, a_coeffs, w_neighbor,
448
- max_iter=max_iter, tol=tol, armijo=armijo,
449
- min_lam=min_lam, w_min=w_min
450
- )
451
-
452
- # Always keep a time-consistent fallback candidate.
453
- w_b, ok_b = fd_solve_w(
454
- z_list[iz], t, a_coeffs, w_prev[iz],
455
- max_iter=max_iter, tol=tol, armijo=armijo,
456
- min_lam=min_lam, w_min=w_min
457
- )
458
-
459
- if ok_a and ok_b:
460
- # Prefer the root closer to previous-time value (time
461
- # continuation).
462
- da = abs(w_a - w_prev[iz])
463
- db = abs(w_b - w_prev[iz])
464
-
465
- # If neighbor result is wildly off, reject it.
466
- if da > time_rel_tol * (1.0 + abs(w_prev[iz])):
467
- return w_b, True
468
-
469
- return (w_a, True) if (da <= db) else (w_b, True)
470
-
471
- if ok_a:
472
- # If only neighbor succeeded, still guard against extreme
473
- # drift.
474
- da = abs(w_a - w_prev[iz])
475
- if da > time_rel_tol * (1.0 + abs(w_prev[iz])) and ok_b:
476
- return w_b, True
477
- return w_a, True
478
-
479
- if ok_b:
480
- return w_b, True
481
-
482
- return w_a, False
483
-
484
- # Sweep right
485
- for iz in range(i0 + 1, nz):
486
- w_row[iz], ok_row[iz] = solve_with_choice(iz, w_row[iz - 1])
487
-
488
- # Sweep left
489
- for iz in range(i0 - 1, -1, -1):
490
- w_row[iz], ok_row[iz] = solve_with_choice(iz, w_row[iz + 1])
491
-
492
- w_prev = w_row
493
-
494
- W[it, :] = w_prev
495
- ok[it, :] = ok_row
496
-
497
- return W, ok
267
+ return cands
498
268
 
499
269
 
500
270
  # =================
@@ -505,7 +275,7 @@ def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
505
275
  dt_max=0.1, sweep=True, time_rel_tol=5.0,
506
276
  active_imag_eps=None, sweep_pad=20,
507
277
  max_iter=50, tol=1e-12, armijo=1e-4,
508
- min_lam=1e-6, w_min=1e-14):
278
+ min_lam=1e-6, w_min=1e-14, min_n_time=None):
509
279
  """
510
280
  Evolve w = m(t,z) on a fixed z grid and time grid using FD.
511
281
 
@@ -576,41 +346,88 @@ def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
576
346
  active_imag_eps = 50.0 * eta0 if eta0 > 0.0 else 1e-10
577
347
  active_imag_eps = float(active_imag_eps)
578
348
 
349
+ # --------------------------------------
350
+ # TEST
351
+ # def solve_with_choice(iz, w_seed):
352
+ # # Neighbor-seeded candidate (spatial continuity)
353
+ # w_a, ok_a = fd_solve_w(
354
+ # z_list[iz], t, a_coeffs, w_seed,
355
+ # max_iter=max_iter, tol=tol, armijo=armijo,
356
+ # min_lam=min_lam, w_min=w_min
357
+ # )
358
+ #
359
+ # # Time-seeded candidate (time continuation)
360
+ # w_b, ok_b = fd_solve_w(
361
+ # z_list[iz], t, a_coeffs, w_prev[iz],
362
+ # max_iter=max_iter, tol=tol, armijo=armijo,
363
+ # min_lam=min_lam, w_min=w_min
364
+ # )
365
+ #
366
+ # if ok_a and ok_b:
367
+ # da = abs(w_a - w_prev[iz])
368
+ # db = abs(w_b - w_prev[iz])
369
+ #
370
+ # # Reject neighbor result if it drifted too far in one step
371
+ # if da > time_rel_tol * (1.0 + abs(w_prev[iz])):
372
+ # return w_b, True
373
+ #
374
+ # return (w_a, True) if (da <= db) else (w_b, True)
375
+ #
376
+ # if ok_a:
377
+ # da = abs(w_a - w_prev[iz])
378
+ # if da > time_rel_tol * (1.0 + abs(w_prev[iz])) and ok_b:
379
+ # return w_b, True
380
+ # return w_a, True
381
+ #
382
+ # if ok_b:
383
+ # return w_b, True
384
+ #
385
+ # return w_a, False
386
+ # ----------------------------------------
387
+ # TEST
579
388
  def solve_with_choice(iz, w_seed):
580
- # Neighbor-seeded candidate (spatial continuity)
581
- w_a, ok_a = fd_solve_w(
582
- z_list[iz], t, a_coeffs, w_seed,
583
- max_iter=max_iter, tol=tol, armijo=armijo,
584
- min_lam=min_lam, w_min=w_min
585
- )
586
-
587
- # Time-seeded candidate (time continuation)
588
- w_b, ok_b = fd_solve_w(
589
- z_list[iz], t, a_coeffs, w_prev[iz],
590
- max_iter=max_iter, tol=tol, armijo=armijo,
591
- min_lam=min_lam, w_min=w_min
592
- )
593
-
594
- if ok_a and ok_b:
595
- da = abs(w_a - w_prev[iz])
596
- db = abs(w_b - w_prev[iz])
597
-
598
- # Reject neighbor result if it drifted too far in one step
599
- if da > time_rel_tol * (1.0 + abs(w_prev[iz])):
600
- return w_b, True
389
+ # candidate roots at this (t,z)
390
+ cands = fd_candidates_w(z_list[iz], t, a_coeffs, w_min=w_min)
391
+
392
+ # ---------------------
393
+ # TEST
394
+ if iz in (0, nz//2, nz-1):
395
+ ims = [float(w.imag) for w in cands]
396
+ print(f" iz={iz} ncand={len(cands)} Im(cands) min/med/max="
397
+ f"{(min(ims) if ims else None)}/"
398
+ f"{(numpy.median(ims) if ims else None)}/"
399
+ f"{(max(ims) if ims else None)}")
400
+ # ---------------------
401
+
402
+ if len(cands) == 0:
403
+ # fallback to your existing single-root solver
404
+ w, success = fd_solve_w(
405
+ z_list[iz], t, a_coeffs, w_prev[iz],
406
+ max_iter=max_iter, tol=tol, armijo=armijo,
407
+ min_lam=min_lam, w_min=w_min
408
+ )
409
+ return w, success
601
410
 
602
- return (w_a, True) if (da <= db) else (w_b, True)
411
+ # cost = spatial continuity + time continuity (tune weights if needed)
412
+ w_time = w_prev[iz]
413
+ w_space = w_seed
414
+ best = None
415
+ best_cost = None
603
416
 
604
- if ok_a:
605
- da = abs(w_a - w_prev[iz])
606
- if da > time_rel_tol * (1.0 + abs(w_prev[iz])) and ok_b:
607
- return w_b, True
608
- return w_a, True
417
+ for w in cands:
418
+ # TEST
419
+ # cost = abs(w - w_space) + 0.25 * abs(w - w_time)
420
+ # TEST
421
+ # prefer continuity, but also prefer larger Im(w) to stay on the bulk branch
422
+ cost = abs(w - w_space) + 0.25 * abs(w - w_time) - 5.0 * w.imag
423
+ # --------------
609
424
 
610
- if ok_b:
611
- return w_b, True
425
+ if (best_cost is None) or (cost < best_cost):
426
+ best = w
427
+ best_cost = cost
612
428
 
613
- return w_a, False
429
+ return best, True
430
+ # ----------------------------------------
614
431
 
615
432
  for it in range(1, nt):
616
433
  t0 = float(t_grid[it - 1])
@@ -648,15 +465,107 @@ def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
648
465
  # Define "active" region from previous time: inside bulks
649
466
  # Im(w_prev) is O(1), outside bulks Im(w_prev) is ~O(eta). Dilate
650
467
  # by sweep_pad to allow edges to move.
468
+
469
+ # ------------------------------
470
+ # TEST
471
+ # active = (numpy.abs(numpy.imag(w_prev)) > active_imag_eps)
472
+ # active_pad = active.copy()
473
+ # if sweep_pad > 0 and numpy.any(active):
474
+ # idx = numpy.flatnonzero(active)
475
+ # for i in idx:
476
+ # lo = 0 if (i - sweep_pad) < 0 else (i - sweep_pad)
477
+ # hi = \
478
+ # nz if (i + sweep_pad + 1) > nz else (i + sweep_pad + 1)
479
+ # active_pad[lo:hi] = True
480
+ # ------------------------------
481
+ # TEST
651
482
  active = (numpy.abs(numpy.imag(w_prev)) > active_imag_eps)
652
- active_pad = active.copy()
653
- if sweep_pad > 0 and numpy.any(active):
654
- idx = numpy.flatnonzero(active)
655
- for i in idx:
656
- lo = 0 if (i - sweep_pad) < 0 else (i - sweep_pad)
657
- hi = \
658
- nz if (i + sweep_pad + 1) > nz else (i + sweep_pad + 1)
659
- active_pad[lo:hi] = True
483
+
484
+ # Split active indices into contiguous blocks (bulks)
485
+ pad_label = -numpy.ones(nz, dtype=numpy.int64) # bulk id per index
486
+ active_pad = numpy.zeros(nz, dtype=bool)
487
+
488
+ idx = numpy.flatnonzero(active)
489
+ if idx.size > 0:
490
+ cuts = numpy.where(numpy.diff(idx) > 1)[0]
491
+ blocks = numpy.split(idx, cuts + 1)
492
+
493
+ # Build padded intervals + centers
494
+ centers = []
495
+ pads = []
496
+ for b in blocks:
497
+ centers.append(int((b[0] + b[-1]) // 2))
498
+ lo = int(max(0, b[0] - sweep_pad))
499
+ hi = int(min(nz - 1, b[-1] + sweep_pad))
500
+ pads.append((lo, hi))
501
+
502
+ # Union of padded regions
503
+ for lo, hi in pads:
504
+ active_pad[lo:hi + 1] = True
505
+
506
+ # Assign each padded index to the nearest bulk center (no overlap label)
507
+ idx_u = numpy.flatnonzero(active_pad)
508
+ c = numpy.asarray(centers, dtype=numpy.int64)
509
+ dist = numpy.abs(idx_u[:, None] - c[None, :])
510
+ winner = numpy.argmin(dist, axis=1).astype(numpy.int64)
511
+ pad_label[idx_u] = winner
512
+ # ------------------------------
513
+
514
+ # ------------------------------
515
+ # TEST
516
+ def _ranges(idxs):
517
+ if idxs.size == 0:
518
+ return []
519
+ cuts = numpy.where(numpy.diff(idxs) > 1)[0]
520
+ blocks = numpy.split(idxs, cuts + 1)
521
+ return [(int(b[0]), int(b[-1])) for b in blocks]
522
+
523
+ print(" pad_label>=0 ranges:", _ranges(numpy.flatnonzero(pad_label >= 0)))
524
+ print(" overlap(-2) ranges:", _ranges(numpy.flatnonzero(pad_label == -2)))
525
+ # ------------------------------
526
+
527
+
528
+
529
+ # ----------------------------------------------
530
+
531
+ # TEST
532
+ # eta = float(abs(z_list[0].imag))
533
+ #
534
+ # # Barrier: points that look like "gap" (tiny Im(w_prev))
535
+ # barrier_eps = 10.0 * eta # try 5*eta or 10*eta
536
+ # barrier = (numpy.abs(numpy.imag(w_prev)) <= barrier_eps)
537
+
538
+
539
+
540
+ # TEST
541
+ # -------------------------
542
+ # --- diagnostics ---
543
+ active = (numpy.abs(numpy.imag(w_prev)) > active_imag_eps)
544
+ idx_active = numpy.flatnonzero(active)
545
+ idx_pad = numpy.flatnonzero(active_pad)
546
+
547
+ def _ranges(idxs):
548
+ if idxs.size == 0:
549
+ return []
550
+ cuts = numpy.where(numpy.diff(idxs) > 1)[0]
551
+ blocks = numpy.split(idxs, cuts + 1)
552
+ return [(b[0], b[-1]) for b in blocks]
553
+
554
+ print(f"[t={t:.6g}] eta={z_list[0].imag:.2e} active_eps={active_imag_eps:.2e} "
555
+ f"active_n={idx_active.size}/{nz} pad_n={idx_pad.size}/{nz} "
556
+ f"active_ranges={_ranges(idx_active)} pad_ranges={_ranges(idx_pad)}")
557
+
558
+ # Track the physical “gap” region around between bulks by looking at low Im(w_prev)
559
+ gap = numpy.abs(numpy.imag(w_prev)) <= 5.0 * z_list[0].imag
560
+ ig = numpy.flatnonzero(gap)
561
+ if ig.size > 0:
562
+ print(f" gap_ranges(Im<=5eta)={_ranges(ig)} "
563
+ f"Im(w) min/med/max = {numpy.min(w_prev.imag):.3e}/"
564
+ f"{numpy.median(w_prev.imag):.3e}/{numpy.max(w_prev.imag):.3e}")
565
+
566
+ # ------------------
567
+
568
+
660
569
 
661
570
  # Left-to-right: use neighbor seed only within padded active
662
571
  # regions, so we don't propagate a branch across the gap between
@@ -665,16 +574,51 @@ def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
665
574
  if iz == 0:
666
575
  w_seed = w_prev[iz]
667
576
  else:
668
- if active_pad[iz] and active_pad[iz - 1]:
577
+ # TEST
578
+ # if active_pad[iz] and active_pad[iz - 1]:
579
+ # w_seed = w_row[iz - 1]
580
+ # else:
581
+ # w_seed = w_prev[iz]
582
+ # TEST
583
+ # if (active_pad[iz] and active_pad[iz - 1] and
584
+ # (not barrier[iz]) and (not barrier[iz - 1])):
585
+ # w_seed = w_row[iz - 1]
586
+ # else:
587
+ # w_seed = w_prev[iz]
588
+ # ----------------------
589
+ # TEST
590
+ # if (active_pad[iz] and active_pad[iz - 1] and
591
+ # (pad_label[iz] == pad_label[iz - 1]) and
592
+ # (pad_label[iz] >= 0)):
593
+ # -----------------
594
+ # TEST
595
+ if (active_pad[iz] and active_pad[iz - 1] and
596
+ (pad_label[iz] == pad_label[iz - 1]) and
597
+ (pad_label[iz] >= 0)):
669
598
  w_seed = w_row[iz - 1]
670
599
  else:
671
600
  w_seed = w_prev[iz]
601
+ # ----------------------
602
+
672
603
 
673
604
  w_row[iz], ok_row[iz] = solve_with_choice(iz, w_seed)
674
605
 
675
606
  # Right-to-left refinement: helps stabilize left edges of bulks.
676
607
  for iz in range(nz - 2, -1, -1):
677
- if active_pad[iz] and active_pad[iz + 1]:
608
+ # TEST
609
+ # if active_pad[iz] and active_pad[iz + 1]:
610
+ # TEST
611
+ # if (active_pad[iz] and active_pad[iz + 1] and
612
+ # (not barrier[iz]) and (not barrier[iz + 1])):
613
+ # TEST
614
+ # if (active_pad[iz] and active_pad[iz + 1] and
615
+ # (pad_label[iz] == pad_label[iz + 1]) and
616
+ # (pad_label[iz] >= 0)):
617
+ # TEST
618
+ if (active_pad[iz] and active_pad[iz + 1] and
619
+ (pad_label[iz] == pad_label[iz + 1]) and
620
+ (pad_label[iz] >= 0)):
621
+
678
622
  w_seed = w_row[iz + 1]
679
623
  w_new, ok_new = solve_with_choice(iz, w_seed)
680
624
  if ok_new:
@@ -684,6 +628,11 @@ def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
684
628
  w_row[iz] = w_new
685
629
  ok_row[iz] = True
686
630
 
631
+
632
+
633
+ # TEST
634
+ print(f'solved_ok={ok_row.sum()}/{nz} (this substep)')
635
+
687
636
  w_prev = w_row
688
637
 
689
638
  W[it, :] = w_prev