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,1631 @@
|
|
|
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
|
+
# =======
|
|
11
|
+
# Imports
|
|
12
|
+
# =======
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
from ._continuation_algebraic import powers
|
|
16
|
+
|
|
17
|
+
__all__ = ['build_time_grid', 'decompress_newton_old', 'decompress_newton']
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ===============
|
|
21
|
+
# build time grid
|
|
22
|
+
# ===============
|
|
23
|
+
|
|
24
|
+
def build_time_grid(sizes, n0, min_n_times=0):
|
|
25
|
+
"""
|
|
26
|
+
sizes: list/array of requested matrix sizes (e.g. [2000,3000,4000,8000])
|
|
27
|
+
n0: initial size (self.n)
|
|
28
|
+
min_n_times: minimum number of time points to run Newton sweep on
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
t_all: sorted time grid to run solver on
|
|
33
|
+
idx_req: indices of requested times inside t_all (same order as sizes)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
sizes = numpy.asarray(sizes, dtype=float)
|
|
37
|
+
alpha = sizes / float(n0)
|
|
38
|
+
t_req = numpy.log(alpha)
|
|
39
|
+
|
|
40
|
+
# Always include t=0 and T=max(t_req)
|
|
41
|
+
T = float(numpy.max(t_req)) if t_req.size else 0.0
|
|
42
|
+
base = numpy.unique(numpy.r_[0.0, t_req, T])
|
|
43
|
+
t_all = numpy.sort(base)
|
|
44
|
+
|
|
45
|
+
# Add points only if needed: split largest gaps
|
|
46
|
+
N = int(min_n_times) if min_n_times is not None else 0
|
|
47
|
+
while t_all.size < N and t_all.size >= 2:
|
|
48
|
+
gaps = numpy.diff(t_all)
|
|
49
|
+
k = int(numpy.argmax(gaps))
|
|
50
|
+
mid = 0.5 * (t_all[k] + t_all[k+1])
|
|
51
|
+
t_all = numpy.sort(numpy.unique(numpy.r_[t_all, mid]))
|
|
52
|
+
|
|
53
|
+
# Map each requested time to an index in t_all (stable, no float drama)
|
|
54
|
+
# (t_req values came from same construction, so they should match exactly;
|
|
55
|
+
# still: use searchsorted + assert)
|
|
56
|
+
idx_req = numpy.searchsorted(t_all, t_req)
|
|
57
|
+
# optional sanity:
|
|
58
|
+
# assert numpy.allclose(t_all[idx_req], t_req, rtol=0, atol=0)
|
|
59
|
+
|
|
60
|
+
return t_all, idx_req
|
|
61
|
+
|
|
62
|
+
|
|
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
|
+
|
|
109
|
+
z = numpy.asarray(z, dtype=complex)
|
|
110
|
+
m = numpy.asarray(m, dtype=complex)
|
|
111
|
+
|
|
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
|
+
|
|
179
|
+
# def fd_solve_w(z, t, a_coeffs, w_init, max_iter=50, tol=1e-12,
|
|
180
|
+
# armijo=1e-4, min_lam=1e-6, w_min=1e-14):
|
|
181
|
+
# """
|
|
182
|
+
# Solve for w = m(t,z) from the implicit FD equation using damped Newton.
|
|
183
|
+
#
|
|
184
|
+
# We solve in w the equation
|
|
185
|
+
#
|
|
186
|
+
# F(w) = P(z + alpha/w, tau*w) = 0,
|
|
187
|
+
#
|
|
188
|
+
# where tau = exp(t) and alpha = 1 - 1/tau.
|
|
189
|
+
#
|
|
190
|
+
# A backtracking (Armijo) line search is used to stabilize Newton updates.
|
|
191
|
+
# When Im(z) > 0, the iterate is constrained to remain in the upper
|
|
192
|
+
# half-plane (Im(w) > 0), enforcing the Herglotz branch.
|
|
193
|
+
#
|
|
194
|
+
# Parameters
|
|
195
|
+
# ----------
|
|
196
|
+
# z : complex
|
|
197
|
+
# Query point in the complex plane.
|
|
198
|
+
# t : float
|
|
199
|
+
# Time parameter (tau = exp(t)).
|
|
200
|
+
# a_coeffs : ndarray
|
|
201
|
+
# Coefficients defining P(zeta,y) in the monomial basis.
|
|
202
|
+
# w_init : complex
|
|
203
|
+
# Initial guess for w.
|
|
204
|
+
# max_iter : int, optional
|
|
205
|
+
# Maximum number of Newton iterations.
|
|
206
|
+
# tol : float, optional
|
|
207
|
+
# Residual tolerance on |F(w)|.
|
|
208
|
+
# armijo : float, optional
|
|
209
|
+
# Armijo parameter for backtracking sufficient decrease.
|
|
210
|
+
# min_lam : float, optional
|
|
211
|
+
# Minimum damping factor allowed in backtracking.
|
|
212
|
+
# w_min : float, optional
|
|
213
|
+
# Minimum |w| allowed to avoid singularity in z + alpha/w.
|
|
214
|
+
#
|
|
215
|
+
# Returns
|
|
216
|
+
# -------
|
|
217
|
+
# w : complex
|
|
218
|
+
# The computed solution (last iterate if not successful).
|
|
219
|
+
# success : bool
|
|
220
|
+
# True if convergence criteria were met, False otherwise.
|
|
221
|
+
#
|
|
222
|
+
# Notes
|
|
223
|
+
# -----
|
|
224
|
+
# This function does not choose the correct branch globally by itself; it
|
|
225
|
+
# relies on a good initialization strategy (e.g. time continuation and/or
|
|
226
|
+
# x-sweeps) to avoid converging to a different valid root of the implicit
|
|
227
|
+
# equation.
|
|
228
|
+
#
|
|
229
|
+
# Examples
|
|
230
|
+
# --------
|
|
231
|
+
# .. code-block:: python
|
|
232
|
+
#
|
|
233
|
+
# w, ok = fd_solve_w(
|
|
234
|
+
# z=0.5 + 1e-6j, t=2.0, a_coeffs=a_coeffs, w_init=m1_fn(0.5 + 1e-6j),
|
|
235
|
+
# max_iter=50, tol=1e-12
|
|
236
|
+
# )
|
|
237
|
+
# """
|
|
238
|
+
#
|
|
239
|
+
# z = complex(z)
|
|
240
|
+
# w = complex(w_init)
|
|
241
|
+
#
|
|
242
|
+
# tau = float(numpy.exp(t))
|
|
243
|
+
# alpha = 1.0 - 1.0 / tau
|
|
244
|
+
#
|
|
245
|
+
# want_pos_imag = (z.imag > 0.0)
|
|
246
|
+
#
|
|
247
|
+
# 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
|
+
#
|
|
263
|
+
# if abs(F) <= tol:
|
|
264
|
+
# return w, True
|
|
265
|
+
#
|
|
266
|
+
# dF = (-alpha / (w * w)) * Pz + tau * Py
|
|
267
|
+
# if dF == 0.0:
|
|
268
|
+
# return w, False
|
|
269
|
+
#
|
|
270
|
+
# step = -F / dF
|
|
271
|
+
#
|
|
272
|
+
# lam = 1.0
|
|
273
|
+
# F_abs = abs(F)
|
|
274
|
+
# ok = False
|
|
275
|
+
#
|
|
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
|
|
283
|
+
# continue
|
|
284
|
+
#
|
|
285
|
+
# zeta_new = z + alpha / w_new
|
|
286
|
+
# y_new = tau * w_new
|
|
287
|
+
#
|
|
288
|
+
# F_new = eval_P_partials(zeta_new, y_new, a_coeffs)[0]
|
|
289
|
+
# F_new = complex(F_new)
|
|
290
|
+
#
|
|
291
|
+
# if abs(F_new) <= (1.0 - armijo * lam) * F_abs:
|
|
292
|
+
# w = w_new
|
|
293
|
+
# ok = True
|
|
294
|
+
# break
|
|
295
|
+
#
|
|
296
|
+
# lam *= 0.5
|
|
297
|
+
#
|
|
298
|
+
# if not ok:
|
|
299
|
+
# return w, False
|
|
300
|
+
#
|
|
301
|
+
# F_end = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
|
|
302
|
+
# return w, (abs(F_end) <= 10.0 * tol)
|
|
303
|
+
|
|
304
|
+
# def fd_solve_w(z, t, a_coeffs, w_init, max_iter=50, tol=1e-12,
|
|
305
|
+
# armijo=1e-4, min_lam=1e-6, w_min=1e-14):
|
|
306
|
+
# """
|
|
307
|
+
# Solve for w = m(t,z) from the implicit FD equation using damped Newton.
|
|
308
|
+
#
|
|
309
|
+
# We solve in w the equation
|
|
310
|
+
#
|
|
311
|
+
# F(w) = P(z + alpha/w, tau*w) = 0,
|
|
312
|
+
#
|
|
313
|
+
# where tau = exp(t) and alpha = 1 - 1/tau.
|
|
314
|
+
#
|
|
315
|
+
# A backtracking (Armijo) line search is used to stabilize Newton updates.
|
|
316
|
+
# When Im(z) > 0, the iterate is constrained to remain in the upper
|
|
317
|
+
# half-plane (Im(w) > 0), enforcing the Herglotz branch.
|
|
318
|
+
#
|
|
319
|
+
# Parameters
|
|
320
|
+
# ----------
|
|
321
|
+
# z : complex
|
|
322
|
+
# Query point in the complex plane.
|
|
323
|
+
# t : float
|
|
324
|
+
# Time parameter (tau = exp(t)).
|
|
325
|
+
# a_coeffs : ndarray
|
|
326
|
+
# Coefficients defining P(zeta,y) in the monomial basis.
|
|
327
|
+
# w_init : complex
|
|
328
|
+
# Initial guess for w.
|
|
329
|
+
# max_iter : int, optional
|
|
330
|
+
# Maximum number of Newton iterations.
|
|
331
|
+
# tol : float, optional
|
|
332
|
+
# Residual tolerance on |F(w)|.
|
|
333
|
+
# armijo : float, optional
|
|
334
|
+
# Armijo parameter for backtracking sufficient decrease.
|
|
335
|
+
# min_lam : float, optional
|
|
336
|
+
# Minimum damping factor allowed in backtracking.
|
|
337
|
+
# w_min : float, optional
|
|
338
|
+
# Minimum |w| allowed to avoid singularity in z + alpha/w.
|
|
339
|
+
#
|
|
340
|
+
# Returns
|
|
341
|
+
# -------
|
|
342
|
+
# w : complex
|
|
343
|
+
# The computed solution (last iterate if not successful).
|
|
344
|
+
# success : bool
|
|
345
|
+
# True if convergence criteria were met, False otherwise.
|
|
346
|
+
#
|
|
347
|
+
# Notes
|
|
348
|
+
# -----
|
|
349
|
+
# This function does not choose the correct branch globally by itself; it
|
|
350
|
+
# relies on a good initialization strategy (e.g. time continuation and/or
|
|
351
|
+
# x-sweeps) to avoid converging to a different valid root of the implicit
|
|
352
|
+
# equation.
|
|
353
|
+
#
|
|
354
|
+
# Examples
|
|
355
|
+
# --------
|
|
356
|
+
# .. code-block:: python
|
|
357
|
+
#
|
|
358
|
+
# w, ok = fd_solve_w(
|
|
359
|
+
# z=0.5 + 1e-6j, t=2.0, a_coeffs=a_coeffs, w_init=m1_fn(0.5 + 1e-6j),
|
|
360
|
+
# max_iter=50, tol=1e-12
|
|
361
|
+
# )
|
|
362
|
+
# """
|
|
363
|
+
#
|
|
364
|
+
# z = complex(z)
|
|
365
|
+
# w = complex(w_init)
|
|
366
|
+
#
|
|
367
|
+
# tau = float(numpy.exp(t))
|
|
368
|
+
# alpha = 1.0 - 1.0 / tau
|
|
369
|
+
#
|
|
370
|
+
# want_pos_imag = (z.imag > 0.0)
|
|
371
|
+
#
|
|
372
|
+
# for _ in range(max_iter):
|
|
373
|
+
#
|
|
374
|
+
# # ----------------
|
|
375
|
+
#
|
|
376
|
+
# # if not numpy.isfinite(w.real) or not numpy.isfinite(w.imag):
|
|
377
|
+
# # return w, False
|
|
378
|
+
# # if abs(w) < w_min:
|
|
379
|
+
# # return w, False
|
|
380
|
+
# # if want_pos_imag and (w.imag <= 0.0):
|
|
381
|
+
# # return w, False
|
|
382
|
+
# #
|
|
383
|
+
# # zeta = z + alpha / w
|
|
384
|
+
# # y = tau * w
|
|
385
|
+
# #
|
|
386
|
+
# # F, Pz, Py = eval_P_partials(zeta, y, a_coeffs)
|
|
387
|
+
# # F = complex(F)
|
|
388
|
+
# # Pz = complex(Pz)
|
|
389
|
+
# # Py = complex(Py)
|
|
390
|
+
# #
|
|
391
|
+
# # if abs(F) <= tol:
|
|
392
|
+
# # return w, True
|
|
393
|
+
# #
|
|
394
|
+
# # dF = (-alpha / (w * w)) * Pz + tau * Py
|
|
395
|
+
# # if dF == 0.0:
|
|
396
|
+
# # return w, False
|
|
397
|
+
# #
|
|
398
|
+
# # step = -F / dF
|
|
399
|
+
# #
|
|
400
|
+
# # lam = 1.0
|
|
401
|
+
# # F_abs = abs(F)
|
|
402
|
+
# # ok = False
|
|
403
|
+
# #
|
|
404
|
+
# # while lam >= min_lam:
|
|
405
|
+
# # w_new = w + lam * step
|
|
406
|
+
# # if abs(w_new) < w_min:
|
|
407
|
+
# # lam *= 0.5
|
|
408
|
+
# # continue
|
|
409
|
+
# # if want_pos_imag and (w_new.imag <= 0.0):
|
|
410
|
+
# # lam *= 0.5
|
|
411
|
+
# # continue
|
|
412
|
+
# #
|
|
413
|
+
# # zeta_new = z + alpha / w_new
|
|
414
|
+
# # y_new = tau * w_new
|
|
415
|
+
# #
|
|
416
|
+
# # F_new = eval_P_partials(zeta_new, y_new, a_coeffs)[0]
|
|
417
|
+
# # F_new = complex(F_new)
|
|
418
|
+
# #
|
|
419
|
+
# # if abs(F_new) <= (1.0 - armijo * lam) * F_abs:
|
|
420
|
+
# # w = w_new
|
|
421
|
+
# # ok = True
|
|
422
|
+
# # break
|
|
423
|
+
# #
|
|
424
|
+
# # lam *= 0.5
|
|
425
|
+
# #
|
|
426
|
+
# # if not ok:
|
|
427
|
+
# # return w, False
|
|
428
|
+
#
|
|
429
|
+
# # ---------------
|
|
430
|
+
#
|
|
431
|
+
# # TEST
|
|
432
|
+
#
|
|
433
|
+
# # -------------------------
|
|
434
|
+
# # Polynomial root selection
|
|
435
|
+
# # -------------------------
|
|
436
|
+
# # We solve: P(z + alpha/w, tau*w) = 0.
|
|
437
|
+
# # Let y = tau*w. Then alpha/w = alpha*tau/y = (tau - 1)/y.
|
|
438
|
+
# # So we solve in y:
|
|
439
|
+
# # P(z + beta/y, y) = 0, beta = tau - 1.
|
|
440
|
+
# # Multiply by y^deg_z to clear denominators and get a polynomial in y.
|
|
441
|
+
#
|
|
442
|
+
# a = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
443
|
+
# deg_z = a.shape[0] - 1
|
|
444
|
+
# deg_m = a.shape[1] - 1
|
|
445
|
+
#
|
|
446
|
+
# beta = tau - 1.0
|
|
447
|
+
#
|
|
448
|
+
# # poly_y[p] stores coeff of y^p after clearing denominators
|
|
449
|
+
# poly_y = numpy.zeros(deg_z + deg_m + 1, dtype=numpy.complex128)
|
|
450
|
+
#
|
|
451
|
+
# # Build polynomial: sum_{i,j} a[i,j] (z + beta/y)^i y^j * y^{deg_z}
|
|
452
|
+
# # Expand (z + beta/y)^i = sum_{k=0}^i C(i,k) z^{i-k} (beta/y)^k
|
|
453
|
+
# # Term contributes to power p = deg_z + j - k.
|
|
454
|
+
# from math import comb
|
|
455
|
+
# for i in range(deg_z + 1):
|
|
456
|
+
# for j in range(deg_m + 1):
|
|
457
|
+
# aij = a[i, j]
|
|
458
|
+
# if aij == 0:
|
|
459
|
+
# continue
|
|
460
|
+
# for k in range(i + 1):
|
|
461
|
+
# p = deg_z + j - k
|
|
462
|
+
# poly_y[p] += aij * comb(i, k) * (z ** (i - k)) * (beta ** k)
|
|
463
|
+
#
|
|
464
|
+
# # numpy.roots expects highest degree first
|
|
465
|
+
# coeffs = poly_y[::-1]
|
|
466
|
+
#
|
|
467
|
+
# # If leading coefficients are ~0, trim (rare but safe)
|
|
468
|
+
# nz_lead = numpy.flatnonzero(numpy.abs(coeffs) > 0)
|
|
469
|
+
# if nz_lead.size == 0:
|
|
470
|
+
# return w, False
|
|
471
|
+
# coeffs = coeffs[nz_lead[0]:]
|
|
472
|
+
#
|
|
473
|
+
# roots_y = numpy.roots(coeffs)
|
|
474
|
+
#
|
|
475
|
+
# # Pick root with Im(w)>0 (if z in upper half-plane), closest to time seed
|
|
476
|
+
# y_seed = tau * w_init
|
|
477
|
+
# best = None
|
|
478
|
+
# best_score = None
|
|
479
|
+
#
|
|
480
|
+
# for y in roots_y:
|
|
481
|
+
# if not numpy.isfinite(y.real) or not numpy.isfinite(y.imag):
|
|
482
|
+
# continue
|
|
483
|
+
#
|
|
484
|
+
# w_cand = y / tau
|
|
485
|
+
#
|
|
486
|
+
# if want_pos_imag and (w_cand.imag <= 0.0):
|
|
487
|
+
# continue
|
|
488
|
+
#
|
|
489
|
+
# if abs(w_cand) < w_min:
|
|
490
|
+
# continue
|
|
491
|
+
#
|
|
492
|
+
# # score: stick to time continuation
|
|
493
|
+
# score = abs(y - y_seed)
|
|
494
|
+
#
|
|
495
|
+
# if (best_score is None) or (score < best_score):
|
|
496
|
+
# best = w_cand
|
|
497
|
+
# best_score = score
|
|
498
|
+
#
|
|
499
|
+
# if best is None:
|
|
500
|
+
# return w, False
|
|
501
|
+
#
|
|
502
|
+
# w = complex(best)
|
|
503
|
+
#
|
|
504
|
+
# # final residual check
|
|
505
|
+
# F_end = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
|
|
506
|
+
# return w, (abs(F_end) <= 1e3 * tol)
|
|
507
|
+
#
|
|
508
|
+
# # -------------------
|
|
509
|
+
#
|
|
510
|
+
#
|
|
511
|
+
# F_end = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
|
|
512
|
+
# return w, (abs(F_end) <= 10.0 * tol)
|
|
513
|
+
|
|
514
|
+
def fd_solve_w(z, t, a_coeffs, w_init, max_iter=50, tol=1e-12,
|
|
515
|
+
armijo=1e-4, min_lam=1e-6, w_min=1e-14):
|
|
516
|
+
"""
|
|
517
|
+
Damped Newton solve for w from F_t(z,w)=P(z+alpha/w, tau*w)=0.
|
|
518
|
+
|
|
519
|
+
Convention: m(z)=∫ rho(x)/(x-z) dx, so for z in C^+ we want Im(w)>0.
|
|
520
|
+
"""
|
|
521
|
+
z = complex(z)
|
|
522
|
+
w = complex(w_init)
|
|
523
|
+
|
|
524
|
+
tau = float(numpy.exp(t))
|
|
525
|
+
alpha = 1.0 - 1.0 / tau
|
|
526
|
+
|
|
527
|
+
want_pos_imag = (z.imag > 0.0)
|
|
528
|
+
|
|
529
|
+
# quick validity check on init
|
|
530
|
+
if (not numpy.isfinite(w.real)) or (not numpy.isfinite(w.imag)):
|
|
531
|
+
return w, False
|
|
532
|
+
if abs(w) < w_min:
|
|
533
|
+
return w, False
|
|
534
|
+
if want_pos_imag and (w.imag <= 0.0):
|
|
535
|
+
# nudge into upper half-plane (do NOT flip sign; just perturb)
|
|
536
|
+
w = complex(w.real, max(1e-15, abs(w.imag)))
|
|
537
|
+
|
|
538
|
+
for _ in range(max_iter):
|
|
539
|
+
|
|
540
|
+
if (not numpy.isfinite(w.real)) or (not numpy.isfinite(w.imag)):
|
|
541
|
+
return w, False
|
|
542
|
+
if abs(w) < w_min:
|
|
543
|
+
return w, False
|
|
544
|
+
if want_pos_imag and (w.imag <= 0.0):
|
|
545
|
+
return w, False
|
|
546
|
+
|
|
547
|
+
zeta = z + alpha / w
|
|
548
|
+
y = tau * w
|
|
549
|
+
|
|
550
|
+
F, Pz, Py = eval_P_partials(zeta, y, a_coeffs)
|
|
551
|
+
F = complex(F)
|
|
552
|
+
Pz = complex(Pz)
|
|
553
|
+
Py = complex(Py)
|
|
554
|
+
|
|
555
|
+
F_abs = abs(F)
|
|
556
|
+
if F_abs <= tol:
|
|
557
|
+
return w, True
|
|
558
|
+
|
|
559
|
+
dF = (-alpha / (w * w)) * Pz + tau * Py
|
|
560
|
+
dF = complex(dF)
|
|
561
|
+
if dF == 0.0 or (not numpy.isfinite(dF.real)) or (not numpy.isfinite(dF.imag)):
|
|
562
|
+
return w, False
|
|
563
|
+
|
|
564
|
+
step = -F / dF
|
|
565
|
+
|
|
566
|
+
# backtracking on |F| decrease
|
|
567
|
+
lam = 1.0
|
|
568
|
+
ok = False
|
|
569
|
+
while lam >= min_lam:
|
|
570
|
+
w_new = w + lam * step
|
|
571
|
+
|
|
572
|
+
if (not numpy.isfinite(w_new.real)) or (not numpy.isfinite(w_new.imag)):
|
|
573
|
+
lam *= 0.5
|
|
574
|
+
continue
|
|
575
|
+
if abs(w_new) < w_min:
|
|
576
|
+
lam *= 0.5
|
|
577
|
+
continue
|
|
578
|
+
if want_pos_imag and (w_new.imag <= 0.0):
|
|
579
|
+
lam *= 0.5
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
F_new = eval_P_partials(z + alpha / w_new, tau * w_new, a_coeffs)[0]
|
|
583
|
+
F_new = complex(F_new)
|
|
584
|
+
|
|
585
|
+
# Armijo-like sufficient decrease on residual norm
|
|
586
|
+
if abs(F_new) <= (1.0 - armijo * lam) * F_abs:
|
|
587
|
+
w = w_new
|
|
588
|
+
ok = True
|
|
589
|
+
break
|
|
590
|
+
|
|
591
|
+
lam *= 0.5
|
|
592
|
+
|
|
593
|
+
if not ok:
|
|
594
|
+
return w, False
|
|
595
|
+
|
|
596
|
+
# if max_iter hit, accept only if residual is reasonably small
|
|
597
|
+
F_end = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
|
|
598
|
+
F_end = complex(F_end)
|
|
599
|
+
return w, (abs(F_end) <= 10.0 * tol)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ============
|
|
604
|
+
# NEW FUNCTION
|
|
605
|
+
# ============
|
|
606
|
+
|
|
607
|
+
def fd_candidates_w(z, t, a_coeffs, w_min=1e-14):
|
|
608
|
+
"""
|
|
609
|
+
Return candidate roots w solving P(z + alpha/w, tau*w)=0 with Im(w)>0 (if Im(z)>0).
|
|
610
|
+
"""
|
|
611
|
+
z = complex(z)
|
|
612
|
+
tau = float(numpy.exp(t))
|
|
613
|
+
alpha = 1.0 - 1.0 / tau
|
|
614
|
+
want_pos_imag = (z.imag > 0.0)
|
|
615
|
+
|
|
616
|
+
a = numpy.asarray(a_coeffs, dtype=numpy.complex128)
|
|
617
|
+
deg_z = a.shape[0] - 1
|
|
618
|
+
deg_m = a.shape[1] - 1
|
|
619
|
+
|
|
620
|
+
beta = tau - 1.0 # since alpha/w = (tau-1)/(tau*w) = beta / y with y=tau*w
|
|
621
|
+
|
|
622
|
+
poly_y = numpy.zeros(deg_z + deg_m + 1, dtype=numpy.complex128)
|
|
623
|
+
|
|
624
|
+
from math import comb
|
|
625
|
+
for i in range(deg_z + 1):
|
|
626
|
+
for j in range(deg_m + 1):
|
|
627
|
+
aij = a[i, j]
|
|
628
|
+
if aij == 0:
|
|
629
|
+
continue
|
|
630
|
+
for k in range(i + 1):
|
|
631
|
+
p = deg_z + j - k
|
|
632
|
+
poly_y[p] += aij * comb(i, k) * (z ** (i - k)) * (beta ** k)
|
|
633
|
+
|
|
634
|
+
coeffs = poly_y[::-1]
|
|
635
|
+
nz_lead = numpy.flatnonzero(numpy.abs(coeffs) > 0)
|
|
636
|
+
if nz_lead.size == 0:
|
|
637
|
+
return []
|
|
638
|
+
|
|
639
|
+
coeffs = coeffs[nz_lead[0]:]
|
|
640
|
+
roots_y = numpy.roots(coeffs)
|
|
641
|
+
|
|
642
|
+
cands = []
|
|
643
|
+
for y in roots_y:
|
|
644
|
+
if not numpy.isfinite(y.real) or not numpy.isfinite(y.imag):
|
|
645
|
+
continue
|
|
646
|
+
w = y / tau
|
|
647
|
+
if abs(w) < w_min:
|
|
648
|
+
continue
|
|
649
|
+
if want_pos_imag and (w.imag <= 0.0):
|
|
650
|
+
continue
|
|
651
|
+
# residual filter (optional but helps)
|
|
652
|
+
# -------------
|
|
653
|
+
# TEST
|
|
654
|
+
# F = eval_P_partials(z + alpha / w, tau * w, a_coeffs)[0]
|
|
655
|
+
# if abs(F) < 1e-6:
|
|
656
|
+
# cands.append(complex(w))
|
|
657
|
+
# ---------------
|
|
658
|
+
# TEST
|
|
659
|
+
cands.append(complex(w))
|
|
660
|
+
# ------------------
|
|
661
|
+
|
|
662
|
+
return cands
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# =====================
|
|
666
|
+
# decompress newton old
|
|
667
|
+
# =====================
|
|
668
|
+
|
|
669
|
+
# def decompress_newton_old(z_list, t_grid, a_coeffs, w0_list=None,
|
|
670
|
+
# dt_max=0.1, sweep=True, time_rel_tol=5.0,
|
|
671
|
+
# max_iter=50, tol=1e-12, armijo=1e-4,
|
|
672
|
+
# min_lam=1e-6, w_min=1e-14):
|
|
673
|
+
# """
|
|
674
|
+
# Evolve w = m(t,z) on a fixed z grid and time grid using FD.
|
|
675
|
+
#
|
|
676
|
+
# Parameters
|
|
677
|
+
# ----------
|
|
678
|
+
# z_list : array_like of complex
|
|
679
|
+
# Query points z (typically x + 1j*eta with eta > 0).
|
|
680
|
+
# t_grid : array_like of float
|
|
681
|
+
# Strictly increasing time grid.
|
|
682
|
+
# a_coeffs : ndarray
|
|
683
|
+
# Coefficients defining P(zeta,y) in the monomial basis used by eval_P.
|
|
684
|
+
# w0_list : array_like of complex
|
|
685
|
+
# Initial values at t_grid[0] (typically m0(z_list) on the physical
|
|
686
|
+
# branch).
|
|
687
|
+
# dt_max : float, optional
|
|
688
|
+
# Maximum internal time step. Larger dt is handled by substepping.
|
|
689
|
+
# sweep : bool, optional
|
|
690
|
+
# If True, use spatial continuation (neighbor seeding) plus a
|
|
691
|
+
# time-consistency check to prevent branch collapse. If False, solve
|
|
692
|
+
# each z independently from the previous-time seed (faster but may
|
|
693
|
+
# branch-switch for small eta).
|
|
694
|
+
# time_rel_tol : float, optional
|
|
695
|
+
# When sweep=True, if the neighbor-seeded solution differs from the
|
|
696
|
+
# previous-time value w_prev by more than time_rel_tol*(1+|w_prev|), we
|
|
697
|
+
# also solve using the previous-time seed and select the closer one.
|
|
698
|
+
# max_iter : int, optional
|
|
699
|
+
# Maximum Newton iterations in fd_solve_w.
|
|
700
|
+
# tol : float, optional
|
|
701
|
+
# Residual tolerance in fd_solve_w.
|
|
702
|
+
# armijo : float, optional
|
|
703
|
+
# Armijo parameter for backtracking in fd_solve_w.
|
|
704
|
+
# min_lam : float, optional
|
|
705
|
+
# Minimum damping factor in fd_solve_w backtracking.
|
|
706
|
+
# w_min : float, optional
|
|
707
|
+
# Minimum |w| allowed to avoid singularity.
|
|
708
|
+
#
|
|
709
|
+
# Returns
|
|
710
|
+
# -------
|
|
711
|
+
# W : ndarray, shape (len(t_grid), len(z_list))
|
|
712
|
+
# Evolved values w(t,z).
|
|
713
|
+
# ok : ndarray of bool, same shape as W
|
|
714
|
+
# Convergence flags from the final accepted solve at each point.
|
|
715
|
+
#
|
|
716
|
+
# Notes
|
|
717
|
+
# -----
|
|
718
|
+
# For very small eta, the implicit FD equation can have multiple roots in the
|
|
719
|
+
# upper half-plane. The sweep option is a branch-selection mechanism. The
|
|
720
|
+
# time-consistency check is critical at large t to avoid propagating a
|
|
721
|
+
# nearly-real spurious root across x.
|
|
722
|
+
#
|
|
723
|
+
# Examples
|
|
724
|
+
# --------
|
|
725
|
+
# .. code-block:: python
|
|
726
|
+
#
|
|
727
|
+
# x = numpy.linspace(-0.5, 2.5, 2000)
|
|
728
|
+
# eta = 1e-6
|
|
729
|
+
# z_query = x + 1j*eta
|
|
730
|
+
# w0_list = m1_fn(z_query)
|
|
731
|
+
#
|
|
732
|
+
# t_grid = numpy.linspace(0.0, 4.0, 2)
|
|
733
|
+
# W, ok = fd_evolve_on_grid(
|
|
734
|
+
# z_query, t_grid, a_coeffs, w0_list=w0_list,
|
|
735
|
+
# dt_max=0.1, sweep=True, time_rel_tol=5.0,
|
|
736
|
+
# max_iter=50, tol=1e-12, armijo=1e-4, min_lam=1e-6, w_min=1e-14
|
|
737
|
+
# )
|
|
738
|
+
# rho = W.imag / numpy.pi
|
|
739
|
+
# """
|
|
740
|
+
# z_list = numpy.asarray(z_list, dtype=complex).ravel()
|
|
741
|
+
# t_grid = numpy.asarray(t_grid, dtype=float).ravel()
|
|
742
|
+
# nt = t_grid.size
|
|
743
|
+
# nz = z_list.size
|
|
744
|
+
#
|
|
745
|
+
# W = numpy.empty((nt, nz), dtype=complex)
|
|
746
|
+
# ok = numpy.zeros((nt, nz), dtype=bool)
|
|
747
|
+
#
|
|
748
|
+
# if w0_list is None:
|
|
749
|
+
# raise ValueError(
|
|
750
|
+
# "w0_list must be provided (e.g. m1_fn(z_list) at t=0).")
|
|
751
|
+
# w_prev = numpy.asarray(w0_list, dtype=complex).ravel()
|
|
752
|
+
# if w_prev.size != nz:
|
|
753
|
+
# raise ValueError("w0_list must have same size as z_list.")
|
|
754
|
+
#
|
|
755
|
+
# W[0, :] = w_prev
|
|
756
|
+
# ok[0, :] = True
|
|
757
|
+
#
|
|
758
|
+
# sweep = bool(sweep)
|
|
759
|
+
# time_rel_tol = float(time_rel_tol)
|
|
760
|
+
#
|
|
761
|
+
# for it in range(1, nt):
|
|
762
|
+
# t0 = float(t_grid[it - 1])
|
|
763
|
+
# t1 = float(t_grid[it])
|
|
764
|
+
# dt = t1 - t0
|
|
765
|
+
# if dt <= 0.0:
|
|
766
|
+
# raise ValueError("t_grid must be strictly increasing.")
|
|
767
|
+
#
|
|
768
|
+
# # Internal substepping makes time-continuity a strong selector.
|
|
769
|
+
# n_sub = int(numpy.ceil(dt / float(dt_max)))
|
|
770
|
+
# if n_sub < 1:
|
|
771
|
+
# n_sub = 1
|
|
772
|
+
#
|
|
773
|
+
# for ks in range(1, n_sub + 1):
|
|
774
|
+
# t = t0 + dt * (ks / float(n_sub))
|
|
775
|
+
#
|
|
776
|
+
# w_row = numpy.empty(nz, dtype=complex)
|
|
777
|
+
# ok_row = numpy.zeros(nz, dtype=bool)
|
|
778
|
+
#
|
|
779
|
+
# if not sweep:
|
|
780
|
+
# # Independent solves: each point uses previous-time seed only.
|
|
781
|
+
# for iz in range(nz):
|
|
782
|
+
# w, success = fd_solve_w(
|
|
783
|
+
# z_list[iz], t, a_coeffs, w_prev[iz],
|
|
784
|
+
# max_iter=max_iter, tol=tol, armijo=armijo,
|
|
785
|
+
# min_lam=min_lam, w_min=w_min
|
|
786
|
+
# )
|
|
787
|
+
# w_row[iz] = w
|
|
788
|
+
# ok_row[iz] = success
|
|
789
|
+
#
|
|
790
|
+
# w_prev = w_row
|
|
791
|
+
# continue
|
|
792
|
+
#
|
|
793
|
+
# # Center-out sweep seed: pick where previous-time Im is largest.
|
|
794
|
+
# i0 = int(numpy.argmax(numpy.abs(numpy.imag(w_prev))))
|
|
795
|
+
#
|
|
796
|
+
# w0, ok0 = fd_solve_w(
|
|
797
|
+
# z_list[i0], t, a_coeffs, w_prev[i0],
|
|
798
|
+
# max_iter=max_iter, tol=tol, armijo=armijo,
|
|
799
|
+
# min_lam=min_lam, w_min=w_min
|
|
800
|
+
# )
|
|
801
|
+
# w_row[i0] = w0
|
|
802
|
+
# ok_row[i0] = ok0
|
|
803
|
+
#
|
|
804
|
+
# # -----------------
|
|
805
|
+
# # slove with choice
|
|
806
|
+
# # -----------------
|
|
807
|
+
#
|
|
808
|
+
# def solve_with_choice(iz, w_neighbor):
|
|
809
|
+
# # First try neighbor-seeded Newton (spatial continuity).
|
|
810
|
+
# w_a, ok_a = fd_solve_w(
|
|
811
|
+
# z_list[iz], t, a_coeffs, w_neighbor,
|
|
812
|
+
# max_iter=max_iter, tol=tol, armijo=armijo,
|
|
813
|
+
# min_lam=min_lam, w_min=w_min
|
|
814
|
+
# )
|
|
815
|
+
#
|
|
816
|
+
# # Always keep a time-consistent fallback candidate.
|
|
817
|
+
# w_b, ok_b = fd_solve_w(
|
|
818
|
+
# z_list[iz], t, a_coeffs, w_prev[iz],
|
|
819
|
+
# max_iter=max_iter, tol=tol, armijo=armijo,
|
|
820
|
+
# min_lam=min_lam, w_min=w_min
|
|
821
|
+
# )
|
|
822
|
+
#
|
|
823
|
+
# if ok_a and ok_b:
|
|
824
|
+
# # Prefer the root closer to previous-time value (time
|
|
825
|
+
# # continuation).
|
|
826
|
+
# da = abs(w_a - w_prev[iz])
|
|
827
|
+
# db = abs(w_b - w_prev[iz])
|
|
828
|
+
#
|
|
829
|
+
# # If neighbor result is wildly off, reject it.
|
|
830
|
+
# if da > time_rel_tol * (1.0 + abs(w_prev[iz])):
|
|
831
|
+
# return w_b, True
|
|
832
|
+
#
|
|
833
|
+
# return (w_a, True) if (da <= db) else (w_b, True)
|
|
834
|
+
#
|
|
835
|
+
# if ok_a:
|
|
836
|
+
# # If only neighbor succeeded, still guard against extreme
|
|
837
|
+
# # drift.
|
|
838
|
+
# da = abs(w_a - w_prev[iz])
|
|
839
|
+
# if da > time_rel_tol * (1.0 + abs(w_prev[iz])) and ok_b:
|
|
840
|
+
# return w_b, True
|
|
841
|
+
# return w_a, True
|
|
842
|
+
#
|
|
843
|
+
# if ok_b:
|
|
844
|
+
# return w_b, True
|
|
845
|
+
#
|
|
846
|
+
# return w_a, False
|
|
847
|
+
#
|
|
848
|
+
# # Sweep right
|
|
849
|
+
# for iz in range(i0 + 1, nz):
|
|
850
|
+
# w_row[iz], ok_row[iz] = solve_with_choice(iz, w_row[iz - 1])
|
|
851
|
+
#
|
|
852
|
+
# # Sweep left
|
|
853
|
+
# for iz in range(i0 - 1, -1, -1):
|
|
854
|
+
# w_row[iz], ok_row[iz] = solve_with_choice(iz, w_row[iz + 1])
|
|
855
|
+
#
|
|
856
|
+
# w_prev = w_row
|
|
857
|
+
#
|
|
858
|
+
# W[it, :] = w_prev
|
|
859
|
+
# ok[it, :] = ok_row
|
|
860
|
+
#
|
|
861
|
+
# return W, ok
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
# =================
|
|
865
|
+
# decompress newton
|
|
866
|
+
# =================
|
|
867
|
+
|
|
868
|
+
# def decompress_newton(z_list, t_grid, a_coeffs, w0_list=None,
|
|
869
|
+
# dt_max=0.1, sweep=True, time_rel_tol=5.0,
|
|
870
|
+
# active_imag_eps=None, sweep_pad=20,
|
|
871
|
+
# max_iter=50, tol=1e-12, armijo=1e-4,
|
|
872
|
+
# min_lam=1e-6, w_min=1e-14):
|
|
873
|
+
# """
|
|
874
|
+
# Evolve w = m(t,z) on a fixed z grid and time grid using FD.
|
|
875
|
+
#
|
|
876
|
+
# Parameters
|
|
877
|
+
# ----------
|
|
878
|
+
# z_list : array_like of complex
|
|
879
|
+
# Query points z (typically x + 1j*eta with eta > 0), ordered along x.
|
|
880
|
+
# t_grid : array_like of float
|
|
881
|
+
# Strictly increasing time grid.
|
|
882
|
+
# a_coeffs : ndarray
|
|
883
|
+
# Coefficients defining P(zeta,y) in the monomial basis.
|
|
884
|
+
# w0_list : array_like of complex
|
|
885
|
+
# Initial values at t_grid[0] (typically m0(z_list) on the physical
|
|
886
|
+
# branch).
|
|
887
|
+
# dt_max : float, optional
|
|
888
|
+
# Maximum internal time step. Larger dt is handled by substepping.
|
|
889
|
+
# sweep : bool, optional
|
|
890
|
+
# If True, enforce spatial continuity within active (bulk) regions and
|
|
891
|
+
# allow edge activation via padding. If False, solve each z independently
|
|
892
|
+
# from previous-time seeds (may fail to "activate" new support near
|
|
893
|
+
# edges).
|
|
894
|
+
# time_rel_tol : float, optional
|
|
895
|
+
# When sweep=True, reject neighbor-propagated solutions that drift too
|
|
896
|
+
# far from the previous-time value, using a time-consistent fallback.
|
|
897
|
+
# active_imag_eps : float or None, optional
|
|
898
|
+
# Threshold on |Im(w_prev)| to define active/bulk indices. If None, it is
|
|
899
|
+
# set to 50*Im(z_list[0]) (works well when z_list=x+i*eta).
|
|
900
|
+
# sweep_pad : int, optional
|
|
901
|
+
# Number of indices used to dilate the active region. This is crucial for
|
|
902
|
+
# multi-bulk laws so that edges can move and points just outside a bulk
|
|
903
|
+
# can be initialized from the interior.
|
|
904
|
+
# max_iter, tol, armijo, min_lam, w_min : optional
|
|
905
|
+
# Newton/backtracking controls passed to fd_solve_w.
|
|
906
|
+
#
|
|
907
|
+
# Returns
|
|
908
|
+
# -------
|
|
909
|
+
# W : ndarray, shape (len(t_grid), len(z_list))
|
|
910
|
+
# Evolved values w(t,z).
|
|
911
|
+
# ok : ndarray of bool, same shape as W
|
|
912
|
+
# Convergence flags from the accepted solve at each point.
|
|
913
|
+
# """
|
|
914
|
+
#
|
|
915
|
+
# z_list = numpy.asarray(z_list, dtype=complex).ravel()
|
|
916
|
+
# t_grid = numpy.asarray(t_grid, dtype=float).ravel()
|
|
917
|
+
# nt = t_grid.size
|
|
918
|
+
# nz = z_list.size
|
|
919
|
+
#
|
|
920
|
+
# W = numpy.empty((nt, nz), dtype=complex)
|
|
921
|
+
# ok = numpy.zeros((nt, nz), dtype=bool)
|
|
922
|
+
#
|
|
923
|
+
# if w0_list is None:
|
|
924
|
+
# raise ValueError(
|
|
925
|
+
# "w0_list must be provided (e.g. m1_fn(z_list) at t=0).")
|
|
926
|
+
# w_prev = numpy.asarray(w0_list, dtype=complex).ravel()
|
|
927
|
+
# if w_prev.size != nz:
|
|
928
|
+
# raise ValueError("w0_list must have same size as z_list.")
|
|
929
|
+
#
|
|
930
|
+
# W[0, :] = w_prev
|
|
931
|
+
# ok[0, :] = True
|
|
932
|
+
#
|
|
933
|
+
# sweep = bool(sweep)
|
|
934
|
+
# time_rel_tol = float(time_rel_tol)
|
|
935
|
+
# sweep_pad = int(sweep_pad)
|
|
936
|
+
#
|
|
937
|
+
# # If z_list is x + i*eta, use eta to set an automatic activity threshold.
|
|
938
|
+
# if active_imag_eps is None:
|
|
939
|
+
# eta0 = float(abs(z_list[0].imag))
|
|
940
|
+
# active_imag_eps = 50.0 * eta0 if eta0 > 0.0 else 1e-10
|
|
941
|
+
# active_imag_eps = float(active_imag_eps)
|
|
942
|
+
#
|
|
943
|
+
# # --------------------------------------
|
|
944
|
+
# # TEST
|
|
945
|
+
# # def solve_with_choice(iz, w_seed):
|
|
946
|
+
# # # Neighbor-seeded candidate (spatial continuity)
|
|
947
|
+
# # w_a, ok_a = fd_solve_w(
|
|
948
|
+
# # z_list[iz], t, a_coeffs, w_seed,
|
|
949
|
+
# # max_iter=max_iter, tol=tol, armijo=armijo,
|
|
950
|
+
# # min_lam=min_lam, w_min=w_min
|
|
951
|
+
# # )
|
|
952
|
+
# #
|
|
953
|
+
# # # Time-seeded candidate (time continuation)
|
|
954
|
+
# # w_b, ok_b = fd_solve_w(
|
|
955
|
+
# # z_list[iz], t, a_coeffs, w_prev[iz],
|
|
956
|
+
# # max_iter=max_iter, tol=tol, armijo=armijo,
|
|
957
|
+
# # min_lam=min_lam, w_min=w_min
|
|
958
|
+
# # )
|
|
959
|
+
# #
|
|
960
|
+
# # if ok_a and ok_b:
|
|
961
|
+
# # da = abs(w_a - w_prev[iz])
|
|
962
|
+
# # db = abs(w_b - w_prev[iz])
|
|
963
|
+
# #
|
|
964
|
+
# # # Reject neighbor result if it drifted too far in one step
|
|
965
|
+
# # if da > time_rel_tol * (1.0 + abs(w_prev[iz])):
|
|
966
|
+
# # return w_b, True
|
|
967
|
+
# #
|
|
968
|
+
# # return (w_a, True) if (da <= db) else (w_b, True)
|
|
969
|
+
# #
|
|
970
|
+
# # if ok_a:
|
|
971
|
+
# # da = abs(w_a - w_prev[iz])
|
|
972
|
+
# # if da > time_rel_tol * (1.0 + abs(w_prev[iz])) and ok_b:
|
|
973
|
+
# # return w_b, True
|
|
974
|
+
# # return w_a, True
|
|
975
|
+
# #
|
|
976
|
+
# # if ok_b:
|
|
977
|
+
# # return w_b, True
|
|
978
|
+
# #
|
|
979
|
+
# # return w_a, False
|
|
980
|
+
# # ----------------------------------------
|
|
981
|
+
# # TEST
|
|
982
|
+
# # def solve_with_choice(iz, w_seed):
|
|
983
|
+
# # # candidate roots at this (t,z)
|
|
984
|
+
# # cands = fd_candidates_w(z_list[iz], t, a_coeffs, w_min=w_min)
|
|
985
|
+
# #
|
|
986
|
+
# # # ---------------------
|
|
987
|
+
# # # TEST
|
|
988
|
+
# # if iz in (0, nz//2, nz-1):
|
|
989
|
+
# # ims = [float(w.imag) for w in cands]
|
|
990
|
+
# # print(f" iz={iz} ncand={len(cands)} Im(cands) min/med/max="
|
|
991
|
+
# # f"{(min(ims) if ims else None)}/"
|
|
992
|
+
# # f"{(numpy.median(ims) if ims else None)}/"
|
|
993
|
+
# # f"{(max(ims) if ims else None)}")
|
|
994
|
+
# # # ---------------------
|
|
995
|
+
# #
|
|
996
|
+
# # if len(cands) == 0:
|
|
997
|
+
# # # fallback to your existing single-root solver
|
|
998
|
+
# # w, success = fd_solve_w(
|
|
999
|
+
# # z_list[iz], t, a_coeffs, w_prev[iz],
|
|
1000
|
+
# # max_iter=max_iter, tol=tol, armijo=armijo,
|
|
1001
|
+
# # min_lam=min_lam, w_min=w_min
|
|
1002
|
+
# # )
|
|
1003
|
+
# # return w, success
|
|
1004
|
+
# #
|
|
1005
|
+
# # # cost = spatial continuity + time continuity (tune weights if needed)
|
|
1006
|
+
# # w_time = w_prev[iz]
|
|
1007
|
+
# # w_space = w_seed
|
|
1008
|
+
# # best = None
|
|
1009
|
+
# # best_cost = None
|
|
1010
|
+
# #
|
|
1011
|
+
# # for w in cands:
|
|
1012
|
+
# # # TEST
|
|
1013
|
+
# # # cost = abs(w - w_space) + 0.25 * abs(w - w_time)
|
|
1014
|
+
# # # TEST
|
|
1015
|
+
# # # prefer continuity, but also prefer larger Im(w) to stay on the bulk branch
|
|
1016
|
+
# # cost = abs(w - w_space) + 0.25 * abs(w - w_time) - 5.0 * w.imag
|
|
1017
|
+
# # # --------------
|
|
1018
|
+
# #
|
|
1019
|
+
# # if (best_cost is None) or (cost < best_cost):
|
|
1020
|
+
# # best = w
|
|
1021
|
+
# # best_cost = cost
|
|
1022
|
+
# #
|
|
1023
|
+
# # return best, True
|
|
1024
|
+
# # ----------------------------------------
|
|
1025
|
+
# # TEST
|
|
1026
|
+
# def solve_with_choice(iz, w_neighbor):
|
|
1027
|
+
# # Neighbor-seeded Newton (spatial continuity).
|
|
1028
|
+
# w_a, ok_a = fd_solve_w(
|
|
1029
|
+
# z_list[iz], t, a_coeffs, w_neighbor,
|
|
1030
|
+
# max_iter=max_iter, tol=tol, armijo=armijo,
|
|
1031
|
+
# min_lam=min_lam, w_min=w_min
|
|
1032
|
+
# )
|
|
1033
|
+
#
|
|
1034
|
+
# # Time-seeded Newton (time continuity).
|
|
1035
|
+
# w_b, ok_b = fd_solve_w(
|
|
1036
|
+
# z_list[iz], t, a_coeffs, w_prev[iz],
|
|
1037
|
+
# max_iter=max_iter, tol=tol, armijo=armijo,
|
|
1038
|
+
# min_lam=min_lam, w_min=w_min
|
|
1039
|
+
# )
|
|
1040
|
+
#
|
|
1041
|
+
# z_here = z_list[iz]
|
|
1042
|
+
# w_asymp = -1.0 / z_here # mass=1 Stieltjes asymptote
|
|
1043
|
+
#
|
|
1044
|
+
# def score(w):
|
|
1045
|
+
# # prefer time continuity + correct asymptote (stronger for large |z|)
|
|
1046
|
+
# return (
|
|
1047
|
+
# abs(w - w_prev[iz])
|
|
1048
|
+
# + 0.2 * abs(z_here) * abs(w - w_asymp)
|
|
1049
|
+
# )
|
|
1050
|
+
#
|
|
1051
|
+
# if ok_a and ok_b:
|
|
1052
|
+
# # hard reject neighbor result if it jumped in time
|
|
1053
|
+
# da = abs(w_a - w_prev[iz])
|
|
1054
|
+
# if da > time_rel_tol * (1.0 + abs(w_prev[iz])):
|
|
1055
|
+
# return w_b, True
|
|
1056
|
+
#
|
|
1057
|
+
# return (w_a, True) if (score(w_a) <= score(w_b)) else (w_b, True)
|
|
1058
|
+
#
|
|
1059
|
+
# if ok_a:
|
|
1060
|
+
# # if only neighbor succeeded, still reject if it jumped badly
|
|
1061
|
+
# da = abs(w_a - w_prev[iz])
|
|
1062
|
+
# if da > time_rel_tol * (1.0 + abs(w_prev[iz])) and ok_b:
|
|
1063
|
+
# return w_b, True
|
|
1064
|
+
# return w_a, True
|
|
1065
|
+
#
|
|
1066
|
+
# if ok_b:
|
|
1067
|
+
# return w_b, True
|
|
1068
|
+
#
|
|
1069
|
+
# return w_a, False
|
|
1070
|
+
# # ----------------------------------------
|
|
1071
|
+
#
|
|
1072
|
+
# for it in range(1, nt):
|
|
1073
|
+
# t0 = float(t_grid[it - 1])
|
|
1074
|
+
# t1 = float(t_grid[it])
|
|
1075
|
+
# dt = t1 - t0
|
|
1076
|
+
# if dt <= 0.0:
|
|
1077
|
+
# raise ValueError("t_grid must be strictly increasing.")
|
|
1078
|
+
#
|
|
1079
|
+
# # Substep in time to keep continuation safe.
|
|
1080
|
+
# n_sub = int(numpy.ceil(dt / float(dt_max)))
|
|
1081
|
+
# if n_sub < 1:
|
|
1082
|
+
# n_sub = 1
|
|
1083
|
+
#
|
|
1084
|
+
# for ks in range(1, n_sub + 1):
|
|
1085
|
+
# t = t0 + dt * (ks / float(n_sub))
|
|
1086
|
+
#
|
|
1087
|
+
# w_row = numpy.empty(nz, dtype=complex)
|
|
1088
|
+
# ok_row = numpy.zeros(nz, dtype=bool)
|
|
1089
|
+
#
|
|
1090
|
+
# if not sweep:
|
|
1091
|
+
# # Independent solves: can miss edge activation in multi-bulk
|
|
1092
|
+
# # problems.
|
|
1093
|
+
# for iz in range(nz):
|
|
1094
|
+
# w, success = fd_solve_w(
|
|
1095
|
+
# z_list[iz], t, a_coeffs, w_prev[iz],
|
|
1096
|
+
# max_iter=max_iter, tol=tol, armijo=armijo,
|
|
1097
|
+
# min_lam=min_lam, w_min=w_min
|
|
1098
|
+
# )
|
|
1099
|
+
# w_row[iz] = w
|
|
1100
|
+
# ok_row[iz] = success
|
|
1101
|
+
#
|
|
1102
|
+
# w_prev = w_row
|
|
1103
|
+
# continue
|
|
1104
|
+
#
|
|
1105
|
+
# # Define "active" region from previous time: inside bulks
|
|
1106
|
+
# # Im(w_prev) is O(1), outside bulks Im(w_prev) is ~O(eta). Dilate
|
|
1107
|
+
# # by sweep_pad to allow edges to move.
|
|
1108
|
+
#
|
|
1109
|
+
# # ------------------------------
|
|
1110
|
+
# # TEST
|
|
1111
|
+
# # active = (numpy.abs(numpy.imag(w_prev)) > active_imag_eps)
|
|
1112
|
+
# # active_pad = active.copy()
|
|
1113
|
+
# # if sweep_pad > 0 and numpy.any(active):
|
|
1114
|
+
# # idx = numpy.flatnonzero(active)
|
|
1115
|
+
# # for i in idx:
|
|
1116
|
+
# # lo = 0 if (i - sweep_pad) < 0 else (i - sweep_pad)
|
|
1117
|
+
# # hi = \
|
|
1118
|
+
# # nz if (i + sweep_pad + 1) > nz else (i + sweep_pad + 1)
|
|
1119
|
+
# # active_pad[lo:hi] = True
|
|
1120
|
+
# # ------------------------------
|
|
1121
|
+
# # TEST
|
|
1122
|
+
# active = (numpy.abs(numpy.imag(w_prev)) > active_imag_eps)
|
|
1123
|
+
#
|
|
1124
|
+
# # Split active indices into contiguous blocks (bulks)
|
|
1125
|
+
# pad_label = -numpy.ones(nz, dtype=numpy.int64) # bulk id per index
|
|
1126
|
+
# active_pad = numpy.zeros(nz, dtype=bool)
|
|
1127
|
+
#
|
|
1128
|
+
# idx = numpy.flatnonzero(active)
|
|
1129
|
+
# if idx.size > 0:
|
|
1130
|
+
# cuts = numpy.where(numpy.diff(idx) > 1)[0]
|
|
1131
|
+
# blocks = numpy.split(idx, cuts + 1)
|
|
1132
|
+
#
|
|
1133
|
+
# # Build padded intervals + centers
|
|
1134
|
+
# centers = []
|
|
1135
|
+
# pads = []
|
|
1136
|
+
# for b in blocks:
|
|
1137
|
+
# centers.append(int((b[0] + b[-1]) // 2))
|
|
1138
|
+
# lo = int(max(0, b[0] - sweep_pad))
|
|
1139
|
+
# hi = int(min(nz - 1, b[-1] + sweep_pad))
|
|
1140
|
+
# pads.append((lo, hi))
|
|
1141
|
+
#
|
|
1142
|
+
# # Union of padded regions
|
|
1143
|
+
# for lo, hi in pads:
|
|
1144
|
+
# active_pad[lo:hi + 1] = True
|
|
1145
|
+
#
|
|
1146
|
+
# # Assign each padded index to the nearest bulk center (no overlap label)
|
|
1147
|
+
# idx_u = numpy.flatnonzero(active_pad)
|
|
1148
|
+
# c = numpy.asarray(centers, dtype=numpy.int64)
|
|
1149
|
+
# dist = numpy.abs(idx_u[:, None] - c[None, :])
|
|
1150
|
+
# winner = numpy.argmin(dist, axis=1).astype(numpy.int64)
|
|
1151
|
+
# pad_label[idx_u] = winner
|
|
1152
|
+
# # ------------------------------
|
|
1153
|
+
#
|
|
1154
|
+
# # ------------------------------
|
|
1155
|
+
# # TEST
|
|
1156
|
+
# def _ranges(idxs):
|
|
1157
|
+
# if idxs.size == 0:
|
|
1158
|
+
# return []
|
|
1159
|
+
# cuts = numpy.where(numpy.diff(idxs) > 1)[0]
|
|
1160
|
+
# blocks = numpy.split(idxs, cuts + 1)
|
|
1161
|
+
# return [(int(b[0]), int(b[-1])) for b in blocks]
|
|
1162
|
+
#
|
|
1163
|
+
# # print(" pad_label>=0 ranges:", _ranges(numpy.flatnonzero(pad_label >= 0)))
|
|
1164
|
+
# # print(" overlap(-2) ranges:", _ranges(numpy.flatnonzero(pad_label == -2)))
|
|
1165
|
+
# # ------------------------------
|
|
1166
|
+
#
|
|
1167
|
+
#
|
|
1168
|
+
#
|
|
1169
|
+
# # ----------------------------------------------
|
|
1170
|
+
#
|
|
1171
|
+
# # TEST
|
|
1172
|
+
# # eta = float(abs(z_list[0].imag))
|
|
1173
|
+
# #
|
|
1174
|
+
# # # Barrier: points that look like "gap" (tiny Im(w_prev))
|
|
1175
|
+
# # barrier_eps = 10.0 * eta # try 5*eta or 10*eta
|
|
1176
|
+
# # barrier = (numpy.abs(numpy.imag(w_prev)) <= barrier_eps)
|
|
1177
|
+
#
|
|
1178
|
+
#
|
|
1179
|
+
#
|
|
1180
|
+
# # TEST
|
|
1181
|
+
# # -------------------------
|
|
1182
|
+
# # --- diagnostics ---
|
|
1183
|
+
# active = (numpy.abs(numpy.imag(w_prev)) > active_imag_eps)
|
|
1184
|
+
# idx_active = numpy.flatnonzero(active)
|
|
1185
|
+
# idx_pad = numpy.flatnonzero(active_pad)
|
|
1186
|
+
#
|
|
1187
|
+
# def _ranges(idxs):
|
|
1188
|
+
# if idxs.size == 0:
|
|
1189
|
+
# return []
|
|
1190
|
+
# cuts = numpy.where(numpy.diff(idxs) > 1)[0]
|
|
1191
|
+
# blocks = numpy.split(idxs, cuts + 1)
|
|
1192
|
+
# return [(b[0], b[-1]) for b in blocks]
|
|
1193
|
+
#
|
|
1194
|
+
# # print(f"[t={t:.6g}] eta={z_list[0].imag:.2e} active_eps={active_imag_eps:.2e} "
|
|
1195
|
+
# # f"active_n={idx_active.size}/{nz} pad_n={idx_pad.size}/{nz} "
|
|
1196
|
+
# # f"active_ranges={_ranges(idx_active)} pad_ranges={_ranges(idx_pad)}")
|
|
1197
|
+
#
|
|
1198
|
+
# # Track the physical “gap” region around between bulks by looking at low Im(w_prev)
|
|
1199
|
+
# gap = numpy.abs(numpy.imag(w_prev)) <= 5.0 * z_list[0].imag
|
|
1200
|
+
# ig = numpy.flatnonzero(gap)
|
|
1201
|
+
# # if ig.size > 0:
|
|
1202
|
+
# # print(f" gap_ranges(Im<=5eta)={_ranges(ig)} "
|
|
1203
|
+
# # f"Im(w) min/med/max = {numpy.min(w_prev.imag):.3e}/"
|
|
1204
|
+
# # f"{numpy.median(w_prev.imag):.3e}/{numpy.max(w_prev.imag):.3e}")
|
|
1205
|
+
#
|
|
1206
|
+
# # ------------------
|
|
1207
|
+
#
|
|
1208
|
+
#
|
|
1209
|
+
#
|
|
1210
|
+
# # Left-to-right: use neighbor seed only within padded active
|
|
1211
|
+
# # regions, so we don't propagate a branch across the gap between
|
|
1212
|
+
# # bulks.
|
|
1213
|
+
# for iz in range(nz):
|
|
1214
|
+
# if iz == 0:
|
|
1215
|
+
# w_seed = w_prev[iz]
|
|
1216
|
+
# else:
|
|
1217
|
+
# # TEST
|
|
1218
|
+
# # if active_pad[iz] and active_pad[iz - 1]:
|
|
1219
|
+
# # w_seed = w_row[iz - 1]
|
|
1220
|
+
# # else:
|
|
1221
|
+
# # w_seed = w_prev[iz]
|
|
1222
|
+
# # TEST
|
|
1223
|
+
# # if (active_pad[iz] and active_pad[iz - 1] and
|
|
1224
|
+
# # (not barrier[iz]) and (not barrier[iz - 1])):
|
|
1225
|
+
# # w_seed = w_row[iz - 1]
|
|
1226
|
+
# # else:
|
|
1227
|
+
# # w_seed = w_prev[iz]
|
|
1228
|
+
# # ----------------------
|
|
1229
|
+
# # TEST
|
|
1230
|
+
# # if (active_pad[iz] and active_pad[iz - 1] and
|
|
1231
|
+
# # (pad_label[iz] == pad_label[iz - 1]) and
|
|
1232
|
+
# # (pad_label[iz] >= 0)):
|
|
1233
|
+
# # -----------------
|
|
1234
|
+
# # TEST
|
|
1235
|
+
# if (active_pad[iz] and active_pad[iz - 1] and
|
|
1236
|
+
# (pad_label[iz] == pad_label[iz - 1]) and
|
|
1237
|
+
# (pad_label[iz] >= 0)):
|
|
1238
|
+
# w_seed = w_row[iz - 1]
|
|
1239
|
+
# else:
|
|
1240
|
+
# w_seed = w_prev[iz]
|
|
1241
|
+
# # ----------------------
|
|
1242
|
+
#
|
|
1243
|
+
#
|
|
1244
|
+
# w_row[iz], ok_row[iz] = solve_with_choice(iz, w_seed)
|
|
1245
|
+
#
|
|
1246
|
+
# # Right-to-left refinement: helps stabilize left edges of bulks.
|
|
1247
|
+
# for iz in range(nz - 2, -1, -1):
|
|
1248
|
+
# # TEST
|
|
1249
|
+
# # if active_pad[iz] and active_pad[iz + 1]:
|
|
1250
|
+
# # TEST
|
|
1251
|
+
# # if (active_pad[iz] and active_pad[iz + 1] and
|
|
1252
|
+
# # (not barrier[iz]) and (not barrier[iz + 1])):
|
|
1253
|
+
# # TEST
|
|
1254
|
+
# # if (active_pad[iz] and active_pad[iz + 1] and
|
|
1255
|
+
# # (pad_label[iz] == pad_label[iz + 1]) and
|
|
1256
|
+
# # (pad_label[iz] >= 0)):
|
|
1257
|
+
# # TEST
|
|
1258
|
+
# if (active_pad[iz] and active_pad[iz + 1] and
|
|
1259
|
+
# (pad_label[iz] == pad_label[iz + 1]) and
|
|
1260
|
+
# (pad_label[iz] >= 0)):
|
|
1261
|
+
#
|
|
1262
|
+
# w_seed = w_row[iz + 1]
|
|
1263
|
+
# w_new, ok_new = solve_with_choice(iz, w_seed)
|
|
1264
|
+
# if ok_new:
|
|
1265
|
+
# # Keep the more time-consistent solution.
|
|
1266
|
+
# if (not ok_row[iz]) or (abs(w_new - w_prev[iz]) <
|
|
1267
|
+
# abs(w_row[iz] - w_prev[iz])):
|
|
1268
|
+
# w_row[iz] = w_new
|
|
1269
|
+
# ok_row[iz] = True
|
|
1270
|
+
#
|
|
1271
|
+
#
|
|
1272
|
+
#
|
|
1273
|
+
# # TEST
|
|
1274
|
+
# # print(f'solved_ok={ok_row.sum()}/{nz} (this substep)')
|
|
1275
|
+
#
|
|
1276
|
+
# w_prev = w_row
|
|
1277
|
+
#
|
|
1278
|
+
# W[it, :] = w_prev
|
|
1279
|
+
# ok[it, :] = ok_row
|
|
1280
|
+
#
|
|
1281
|
+
# return W, ok
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
def eval_row_by_z_homotopy(
|
|
1285
|
+
t,
|
|
1286
|
+
z_targets,
|
|
1287
|
+
w_seed_targets,
|
|
1288
|
+
R,
|
|
1289
|
+
a_coeffs,
|
|
1290
|
+
w_anchor,
|
|
1291
|
+
*,
|
|
1292
|
+
steps=80,
|
|
1293
|
+
max_iter=50,
|
|
1294
|
+
tol=1e-12,
|
|
1295
|
+
armijo=1e-4,
|
|
1296
|
+
min_lam=1e-6,
|
|
1297
|
+
w_min=1e-14,
|
|
1298
|
+
):
|
|
1299
|
+
"""
|
|
1300
|
+
Evaluate w(t,z) on z_targets in C^+ by z-homotopy from z0=iR,
|
|
1301
|
+
but anchored at the TRUE w(t,z0)=w_anchor (computed separately).
|
|
1302
|
+
|
|
1303
|
+
Path is 2-segment:
|
|
1304
|
+
z0=iR -> x+iR -> x+i*eta
|
|
1305
|
+
"""
|
|
1306
|
+
import numpy
|
|
1307
|
+
|
|
1308
|
+
z_targets = numpy.asarray(z_targets, dtype=numpy.complex128)
|
|
1309
|
+
w_seed_targets = numpy.asarray(w_seed_targets, dtype=numpy.complex128)
|
|
1310
|
+
|
|
1311
|
+
steps = int(steps)
|
|
1312
|
+
if steps < 2:
|
|
1313
|
+
steps = 2
|
|
1314
|
+
|
|
1315
|
+
z0 = 1j * float(R)
|
|
1316
|
+
eta_floor = float(abs(z_targets[0].imag))
|
|
1317
|
+
if eta_floor <= 0.0:
|
|
1318
|
+
eta_floor = 1e-6
|
|
1319
|
+
|
|
1320
|
+
w_out = numpy.empty(z_targets.size, dtype=numpy.complex128)
|
|
1321
|
+
ok_out = numpy.zeros(z_targets.size, dtype=bool)
|
|
1322
|
+
|
|
1323
|
+
def _pick(cands, z, w_ref):
|
|
1324
|
+
# Filter Herglotz for your convention: Im(w)>0 on C^+
|
|
1325
|
+
cpos = [u for u in cands if u.imag > 0.0]
|
|
1326
|
+
if cpos:
|
|
1327
|
+
cands = cpos
|
|
1328
|
+
|
|
1329
|
+
# Continuity + asymptotic-at-infinity preference (mass=1): w*z ~ -1
|
|
1330
|
+
# This is CRITICAL to avoid choosing the wrong Herglotz-looking sheet.
|
|
1331
|
+
best = None
|
|
1332
|
+
best_cost = None
|
|
1333
|
+
for u in cands:
|
|
1334
|
+
cost = abs(u - w_ref) + 1.0 * abs(u * z + 1.0)
|
|
1335
|
+
if (best_cost is None) or (cost < best_cost):
|
|
1336
|
+
best = u
|
|
1337
|
+
best_cost = cost
|
|
1338
|
+
return best
|
|
1339
|
+
|
|
1340
|
+
for k in range(z_targets.size):
|
|
1341
|
+
zT = z_targets[k]
|
|
1342
|
+
xT = float(zT.real)
|
|
1343
|
+
|
|
1344
|
+
zA = complex(xT, float(R)) # horizontal leg endpoint
|
|
1345
|
+
zB = complex(xT, eta_floor) # final point (vertical down)
|
|
1346
|
+
|
|
1347
|
+
w = w_anchor
|
|
1348
|
+
ok = True
|
|
1349
|
+
|
|
1350
|
+
# ---- segment 1: z0 -> zA (horizontal at imag=R) ----
|
|
1351
|
+
for j in range(1, steps + 1):
|
|
1352
|
+
s = j / float(steps)
|
|
1353
|
+
z = z0 + s * (zA - z0)
|
|
1354
|
+
|
|
1355
|
+
w_new, ok_new = fd_solve_w(
|
|
1356
|
+
z, t, a_coeffs, w,
|
|
1357
|
+
max_iter=max_iter, tol=tol, armijo=armijo,
|
|
1358
|
+
min_lam=min_lam, w_min=w_min
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
if not ok_new:
|
|
1362
|
+
cands = fd_candidates_w(z, t, a_coeffs, w_min=w_min)
|
|
1363
|
+
if cands:
|
|
1364
|
+
w_new = _pick(cands, z, w)
|
|
1365
|
+
ok_new = (w_new is not None)
|
|
1366
|
+
|
|
1367
|
+
if not ok_new:
|
|
1368
|
+
ok = False
|
|
1369
|
+
break
|
|
1370
|
+
w = w_new
|
|
1371
|
+
|
|
1372
|
+
# ---- segment 2: zA -> zB (vertical down at fixed real=xT) ----
|
|
1373
|
+
if ok:
|
|
1374
|
+
for j in range(1, steps + 1):
|
|
1375
|
+
s = j / float(steps)
|
|
1376
|
+
z = zA + s * (zB - zA)
|
|
1377
|
+
|
|
1378
|
+
w_new, ok_new = fd_solve_w(
|
|
1379
|
+
z, t, a_coeffs, w,
|
|
1380
|
+
max_iter=max_iter, tol=tol, armijo=armijo,
|
|
1381
|
+
min_lam=min_lam, w_min=w_min
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
if not ok_new:
|
|
1385
|
+
cands = fd_candidates_w(z, t, a_coeffs, w_min=w_min)
|
|
1386
|
+
if cands:
|
|
1387
|
+
w_new = _pick(cands, z, w)
|
|
1388
|
+
ok_new = (w_new is not None)
|
|
1389
|
+
|
|
1390
|
+
if not ok_new:
|
|
1391
|
+
ok = False
|
|
1392
|
+
break
|
|
1393
|
+
w = w_new
|
|
1394
|
+
|
|
1395
|
+
w_out[k] = w
|
|
1396
|
+
|
|
1397
|
+
if not ok:
|
|
1398
|
+
# fallback at zT: prefer continuity to the provided per-z time seed
|
|
1399
|
+
cands = fd_candidates_w(zT, t, a_coeffs, w_min=w_min)
|
|
1400
|
+
if cands:
|
|
1401
|
+
w_out[k] = _pick(cands, zT, w_seed_targets[k])
|
|
1402
|
+
ok_out[k] = (w_out[k] is not None)
|
|
1403
|
+
if not ok_out[k]:
|
|
1404
|
+
w_out[k] = w_seed_targets[k]
|
|
1405
|
+
else:
|
|
1406
|
+
w_out[k] = w_seed_targets[k]
|
|
1407
|
+
ok_out[k] = False
|
|
1408
|
+
else:
|
|
1409
|
+
ok_out[k] = True
|
|
1410
|
+
|
|
1411
|
+
return w_out, ok_out
|
|
1412
|
+
def decompress_newton(
|
|
1413
|
+
z_list,
|
|
1414
|
+
t_grid,
|
|
1415
|
+
a_coeffs,
|
|
1416
|
+
w0_list=None,
|
|
1417
|
+
*,
|
|
1418
|
+
R=400.0,
|
|
1419
|
+
z_hom_steps=160,
|
|
1420
|
+
eta_track=1e-3, # IMPORTANT: track branches at this safe height
|
|
1421
|
+
eta_steps=40, # vertical homotopy steps down to target imag
|
|
1422
|
+
max_iter=50,
|
|
1423
|
+
tol=1e-12,
|
|
1424
|
+
armijo=1e-4,
|
|
1425
|
+
min_lam=1e-6,
|
|
1426
|
+
w_min=1e-14,
|
|
1427
|
+
**_unused_kwargs,
|
|
1428
|
+
):
|
|
1429
|
+
"""
|
|
1430
|
+
Robust FD solver:
|
|
1431
|
+
(A) For each t, compute w(x+i*eta_track) by z-homotopy from z0=iR.
|
|
1432
|
+
(B) For each x, descend vertically from eta_track to target imag (typically 1e-5)
|
|
1433
|
+
using continuation in eta (vertical homotopy).
|
|
1434
|
+
This prevents multi-bulk cutoffs caused by trying to track directly at tiny eta.
|
|
1435
|
+
"""
|
|
1436
|
+
import numpy
|
|
1437
|
+
|
|
1438
|
+
z_list = numpy.asarray(z_list, dtype=numpy.complex128)
|
|
1439
|
+
t_grid = numpy.asarray(t_grid, dtype=float)
|
|
1440
|
+
nz = z_list.size
|
|
1441
|
+
nt = t_grid.size
|
|
1442
|
+
|
|
1443
|
+
if numpy.any(numpy.diff(t_grid) <= 0.0):
|
|
1444
|
+
raise ValueError("t_grid must be strictly increasing.")
|
|
1445
|
+
|
|
1446
|
+
x_list = z_list.real
|
|
1447
|
+
eta_target = float(z_list.imag.max()) # your z_query uses constant imag
|
|
1448
|
+
if eta_target <= 0.0:
|
|
1449
|
+
raise ValueError("This solver assumes z_list is in C^+ (imag>0).")
|
|
1450
|
+
|
|
1451
|
+
eta_track = float(max(eta_track, 10.0 * eta_target)) # ensure track height is above target
|
|
1452
|
+
|
|
1453
|
+
# -----------------
|
|
1454
|
+
# damped Newton solve
|
|
1455
|
+
# -----------------
|
|
1456
|
+
def solve_w_newton(z, t, w_init):
|
|
1457
|
+
z = complex(z)
|
|
1458
|
+
w = complex(w_init)
|
|
1459
|
+
|
|
1460
|
+
tau = float(numpy.exp(t))
|
|
1461
|
+
alpha = 1.0 - 1.0 / tau
|
|
1462
|
+
|
|
1463
|
+
# Herglotz for your convention: Im(w)>0 for z in C^+
|
|
1464
|
+
if w.imag <= 0.0:
|
|
1465
|
+
w = complex(w.real, max(1e-15, abs(w.imag)))
|
|
1466
|
+
|
|
1467
|
+
for _ in range(max_iter):
|
|
1468
|
+
if (not numpy.isfinite(w.real)) or (not numpy.isfinite(w.imag)):
|
|
1469
|
+
return w, False
|
|
1470
|
+
if abs(w) < w_min:
|
|
1471
|
+
return w, False
|
|
1472
|
+
if w.imag <= 0.0:
|
|
1473
|
+
return w, False
|
|
1474
|
+
|
|
1475
|
+
zeta = z + alpha / w
|
|
1476
|
+
y = tau * w
|
|
1477
|
+
F, Pz, Py = eval_P_partials(zeta, y, a_coeffs)
|
|
1478
|
+
F = complex(F)
|
|
1479
|
+
if abs(F) <= tol:
|
|
1480
|
+
return w, True
|
|
1481
|
+
|
|
1482
|
+
dF = (-alpha / (w * w)) * complex(Pz) + tau * complex(Py)
|
|
1483
|
+
if (dF == 0.0) or (not numpy.isfinite(dF.real)) or (not numpy.isfinite(dF.imag)):
|
|
1484
|
+
return w, False
|
|
1485
|
+
|
|
1486
|
+
step = -F / dF
|
|
1487
|
+
F_abs = abs(F)
|
|
1488
|
+
|
|
1489
|
+
lam = 1.0
|
|
1490
|
+
ok = False
|
|
1491
|
+
while lam >= min_lam:
|
|
1492
|
+
w_new = w + lam * step
|
|
1493
|
+
if (not numpy.isfinite(w_new.real)) or (not numpy.isfinite(w_new.imag)):
|
|
1494
|
+
lam *= 0.5
|
|
1495
|
+
continue
|
|
1496
|
+
if abs(w_new) < w_min:
|
|
1497
|
+
lam *= 0.5
|
|
1498
|
+
continue
|
|
1499
|
+
if w_new.imag <= 0.0:
|
|
1500
|
+
lam *= 0.5
|
|
1501
|
+
continue
|
|
1502
|
+
|
|
1503
|
+
zeta_new = z + alpha / w_new
|
|
1504
|
+
y_new = tau * w_new
|
|
1505
|
+
F_new = complex(eval_P_partials(zeta_new, y_new, a_coeffs)[0])
|
|
1506
|
+
|
|
1507
|
+
if abs(F_new) <= (1.0 - armijo * lam) * F_abs:
|
|
1508
|
+
w = w_new
|
|
1509
|
+
ok = True
|
|
1510
|
+
break
|
|
1511
|
+
lam *= 0.5
|
|
1512
|
+
|
|
1513
|
+
if not ok:
|
|
1514
|
+
return w, False
|
|
1515
|
+
|
|
1516
|
+
# accept if residual not crazy
|
|
1517
|
+
zeta = z + alpha / w
|
|
1518
|
+
y = tau * w
|
|
1519
|
+
F_end = complex(eval_P_partials(zeta, y, a_coeffs)[0])
|
|
1520
|
+
return w, (abs(F_end) <= 1e3 * tol)
|
|
1521
|
+
|
|
1522
|
+
# -----------------------
|
|
1523
|
+
# (A) z-homotopy at safe eta
|
|
1524
|
+
# -----------------------
|
|
1525
|
+
def row_by_z_homotopy_at_eta(t, w_anchor_prev):
|
|
1526
|
+
z0 = 1j * float(R)
|
|
1527
|
+
|
|
1528
|
+
# anchor solve at far point (use previous anchor in time)
|
|
1529
|
+
if w_anchor_prev is None:
|
|
1530
|
+
w0_seed = -1.0 / z0
|
|
1531
|
+
else:
|
|
1532
|
+
w0_seed = w_anchor_prev
|
|
1533
|
+
|
|
1534
|
+
w_anchor, ok_anchor = solve_w_newton(z0, t, w0_seed)
|
|
1535
|
+
if not ok_anchor:
|
|
1536
|
+
return None, None, False
|
|
1537
|
+
|
|
1538
|
+
w_row = numpy.empty(nz, dtype=numpy.complex128)
|
|
1539
|
+
ok_row = numpy.ones(nz, dtype=bool)
|
|
1540
|
+
|
|
1541
|
+
for iz in range(nz):
|
|
1542
|
+
zt = complex(x_list[iz], eta_track)
|
|
1543
|
+
dz = zt - z0
|
|
1544
|
+
w = w_anchor
|
|
1545
|
+
|
|
1546
|
+
for k in range(1, int(z_hom_steps) + 1):
|
|
1547
|
+
s = k / float(z_hom_steps)
|
|
1548
|
+
z = z0 + s * dz
|
|
1549
|
+
|
|
1550
|
+
# enforce imag floor at eta_track (never go near the real axis here)
|
|
1551
|
+
if z.imag < eta_track:
|
|
1552
|
+
z = complex(z.real, eta_track)
|
|
1553
|
+
|
|
1554
|
+
w, ok = solve_w_newton(z, t, w)
|
|
1555
|
+
if not ok:
|
|
1556
|
+
ok_row[iz] = False
|
|
1557
|
+
break
|
|
1558
|
+
|
|
1559
|
+
w_row[iz] = w if ok_row[iz] else (numpy.nan + 1j * numpy.nan)
|
|
1560
|
+
|
|
1561
|
+
return w_anchor, w_row, bool(ok_row.all())
|
|
1562
|
+
|
|
1563
|
+
# -----------------------
|
|
1564
|
+
# (B) vertical homotopy: eta_track -> eta_target
|
|
1565
|
+
# -----------------------
|
|
1566
|
+
def descend_in_eta(t, w_track_row):
|
|
1567
|
+
w_out = numpy.empty(nz, dtype=numpy.complex128)
|
|
1568
|
+
ok_out = numpy.ones(nz, dtype=bool)
|
|
1569
|
+
|
|
1570
|
+
if eta_steps <= 0 or eta_track <= eta_target:
|
|
1571
|
+
# no descent requested
|
|
1572
|
+
for iz in range(nz):
|
|
1573
|
+
# one final polish at target imag
|
|
1574
|
+
zt = complex(x_list[iz], eta_target)
|
|
1575
|
+
w, ok = solve_w_newton(zt, t, w_track_row[iz])
|
|
1576
|
+
w_out[iz] = w
|
|
1577
|
+
ok_out[iz] = ok
|
|
1578
|
+
return w_out, bool(ok_out.all())
|
|
1579
|
+
|
|
1580
|
+
for iz in range(nz):
|
|
1581
|
+
w = w_track_row[iz]
|
|
1582
|
+
ok = True
|
|
1583
|
+
|
|
1584
|
+
# linear schedule in imag
|
|
1585
|
+
for k in range(1, int(eta_steps) + 1):
|
|
1586
|
+
eta = eta_track + (eta_target - eta_track) * (k / float(eta_steps))
|
|
1587
|
+
z = complex(x_list[iz], eta)
|
|
1588
|
+
w, ok = solve_w_newton(z, t, w)
|
|
1589
|
+
if not ok:
|
|
1590
|
+
break
|
|
1591
|
+
|
|
1592
|
+
w_out[iz] = w
|
|
1593
|
+
ok_out[iz] = ok
|
|
1594
|
+
|
|
1595
|
+
return w_out, bool(ok_out.all())
|
|
1596
|
+
|
|
1597
|
+
# -----------------------
|
|
1598
|
+
# main time loop
|
|
1599
|
+
# -----------------------
|
|
1600
|
+
if w0_list is None:
|
|
1601
|
+
w_prev = -1.0 / z_list
|
|
1602
|
+
else:
|
|
1603
|
+
w_prev = numpy.asarray(w0_list, dtype=numpy.complex128).copy()
|
|
1604
|
+
|
|
1605
|
+
W = numpy.empty((nt, nz), dtype=numpy.complex128)
|
|
1606
|
+
OK = numpy.zeros((nt, nz), dtype=bool)
|
|
1607
|
+
|
|
1608
|
+
W[0, :] = w_prev
|
|
1609
|
+
OK[0, :] = True
|
|
1610
|
+
|
|
1611
|
+
w_anchor_prev = None
|
|
1612
|
+
|
|
1613
|
+
for it in range(1, nt):
|
|
1614
|
+
t = float(t_grid[it])
|
|
1615
|
+
|
|
1616
|
+
w_anchor_prev, w_track_row, ok_track = row_by_z_homotopy_at_eta(t, w_anchor_prev)
|
|
1617
|
+
if (not ok_track) or (w_track_row is None):
|
|
1618
|
+
# fallback: keep previous time
|
|
1619
|
+
W[it, :] = w_prev
|
|
1620
|
+
OK[it, :] = False
|
|
1621
|
+
continue
|
|
1622
|
+
|
|
1623
|
+
w_row, ok_row = descend_in_eta(t, w_track_row)
|
|
1624
|
+
|
|
1625
|
+
W[it, :] = w_row
|
|
1626
|
+
OK[it, :] = ok_row
|
|
1627
|
+
w_prev = w_row
|
|
1628
|
+
|
|
1629
|
+
return W, OK
|
|
1630
|
+
|
|
1631
|
+
|