freealg 0.1.11__py3-none-any.whl → 0.7.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freealg/__init__.py +8 -2
- freealg/__version__.py +1 -1
- freealg/_algebraic_form/__init__.py +12 -0
- freealg/_algebraic_form/_branch_points.py +288 -0
- freealg/_algebraic_form/_constraints.py +139 -0
- freealg/_algebraic_form/_continuation_algebraic.py +706 -0
- freealg/_algebraic_form/_decompress.py +641 -0
- freealg/_algebraic_form/_decompress2.py +204 -0
- freealg/_algebraic_form/_edge.py +330 -0
- freealg/_algebraic_form/_homotopy.py +323 -0
- freealg/_algebraic_form/_moments.py +448 -0
- freealg/_algebraic_form/_sheets_util.py +145 -0
- freealg/_algebraic_form/_support.py +309 -0
- freealg/_algebraic_form/algebraic_form.py +1232 -0
- freealg/_free_form/__init__.py +16 -0
- freealg/{_chebyshev.py → _free_form/_chebyshev.py} +75 -43
- freealg/_free_form/_decompress.py +993 -0
- freealg/_free_form/_density_util.py +243 -0
- freealg/_free_form/_jacobi.py +359 -0
- freealg/_free_form/_linalg.py +508 -0
- freealg/{_pade.py → _free_form/_pade.py} +42 -208
- freealg/{_plot_util.py → _free_form/_plot_util.py} +37 -22
- freealg/{_sample.py → _free_form/_sample.py} +58 -22
- freealg/_free_form/_series.py +454 -0
- freealg/_free_form/_support.py +214 -0
- freealg/_free_form/free_form.py +1362 -0
- freealg/_geometric_form/__init__.py +13 -0
- freealg/_geometric_form/_continuation_genus0.py +175 -0
- freealg/_geometric_form/_continuation_genus1.py +275 -0
- freealg/_geometric_form/_elliptic_functions.py +174 -0
- freealg/_geometric_form/_sphere_maps.py +63 -0
- freealg/_geometric_form/_torus_maps.py +118 -0
- freealg/_geometric_form/geometric_form.py +1094 -0
- freealg/_util.py +56 -110
- freealg/distributions/__init__.py +7 -1
- freealg/distributions/_chiral_block.py +494 -0
- freealg/distributions/_deformed_marchenko_pastur.py +726 -0
- freealg/distributions/_deformed_wigner.py +386 -0
- freealg/distributions/_kesten_mckay.py +29 -15
- freealg/distributions/_marchenko_pastur.py +224 -95
- freealg/distributions/_meixner.py +47 -37
- freealg/distributions/_wachter.py +29 -17
- freealg/distributions/_wigner.py +27 -14
- freealg/visualization/__init__.py +12 -0
- freealg/visualization/_glue_util.py +32 -0
- freealg/visualization/_rgb_hsv.py +125 -0
- freealg-0.7.12.dist-info/METADATA +172 -0
- freealg-0.7.12.dist-info/RECORD +53 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/WHEEL +1 -1
- freealg/_decompress.py +0 -180
- freealg/_jacobi.py +0 -218
- freealg/_support.py +0 -85
- freealg/freeform.py +0 -967
- freealg-0.1.11.dist-info/METADATA +0 -140
- freealg-0.1.11.dist-info/RECORD +0 -24
- /freealg/{_damp.py → _free_form/_damp.py} +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/AUTHORS.txt +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/licenses/LICENSE.txt +0 -0
- {freealg-0.1.11.dist-info → freealg-0.7.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# =======
|
|
2
|
+
# Imports
|
|
3
|
+
# =======
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from scipy.special import comb
|
|
8
|
+
import texplot
|
|
9
|
+
from ._continuation_algebraic import _normalize_coefficients
|
|
10
|
+
|
|
11
|
+
__all__ = ['decompress_coeffs']
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# =================
|
|
15
|
+
# decompress_coeffs
|
|
16
|
+
# =================
|
|
17
|
+
|
|
18
|
+
def decompress_coeffs(a, t, normalize=True):
|
|
19
|
+
"""
|
|
20
|
+
Compute the decompressed coefficients A[r, s](t) induced by
|
|
21
|
+
the transform Q_t(z, m) = m^L P(z + (1 - e^{-t}) / m, e^t m).
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
a : array_like of float, shape (L+1, K+1)
|
|
26
|
+
Coefficients defining P(z, m) in the monomial basis:
|
|
27
|
+
P(z, m) = sum_{j=0..L} sum_{k=0..K} a[j, k] z^j m^k.
|
|
28
|
+
t : float
|
|
29
|
+
Time parameter.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
A : ndarray, shape (L+1, L+K+1)
|
|
34
|
+
Coefficients A[r, s](t) such that
|
|
35
|
+
sum_{r=0..L} sum_{s=0..L+K} A[r, s](t) z^r m^s = 0,
|
|
36
|
+
normalized by normalize_coefficients.
|
|
37
|
+
"""
|
|
38
|
+
a = numpy.asarray(a)
|
|
39
|
+
a[-1, 0] = 0.0
|
|
40
|
+
if a.ndim != 2:
|
|
41
|
+
raise ValueError("a must be a 2D array-like of shape (L+1, K+1).")
|
|
42
|
+
|
|
43
|
+
l_degree = a.shape[0] - 1
|
|
44
|
+
k_degree = a.shape[1] - 1
|
|
45
|
+
|
|
46
|
+
c = 1.0 - numpy.exp(-t)
|
|
47
|
+
|
|
48
|
+
# Scale columns of a by e^{t k}: scaled[j, k] = a[j, k] e^{t k}.
|
|
49
|
+
exp_factors = numpy.exp(numpy.arange(k_degree + 1) * t)
|
|
50
|
+
scaled = a * exp_factors
|
|
51
|
+
|
|
52
|
+
# Output coefficients.
|
|
53
|
+
out_dtype = numpy.result_type(a, float)
|
|
54
|
+
a_out = numpy.zeros((l_degree + 1, l_degree + k_degree + 1),
|
|
55
|
+
dtype=out_dtype)
|
|
56
|
+
|
|
57
|
+
# Precompute binomial(j, r) * c^{j-r} for all j, r (lower-triangular).
|
|
58
|
+
j_inds = numpy.arange(l_degree + 1)[:, None]
|
|
59
|
+
r_inds = numpy.arange(l_degree + 1)[None, :]
|
|
60
|
+
mask = r_inds <= j_inds
|
|
61
|
+
|
|
62
|
+
binom_weights = numpy.zeros((l_degree + 1, l_degree + 1), dtype=float)
|
|
63
|
+
binom_weights[mask] = comb(j_inds, r_inds, exact=False)[mask]
|
|
64
|
+
binom_weights[mask] *= (c ** (j_inds - r_inds))[mask]
|
|
65
|
+
|
|
66
|
+
# Main accumulation:
|
|
67
|
+
# For fixed j and r, add:
|
|
68
|
+
# A[r, (L - j + r) + k] += binom_weights[j, r] * scaled[j, k],
|
|
69
|
+
# for k = 0..K.
|
|
70
|
+
for j in range(l_degree + 1):
|
|
71
|
+
row_scaled = scaled[j]
|
|
72
|
+
if numpy.all(row_scaled == 0):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
base0 = l_degree - j
|
|
76
|
+
row_b = binom_weights[j]
|
|
77
|
+
|
|
78
|
+
for r in range(j + 1):
|
|
79
|
+
coeff = row_b[r]
|
|
80
|
+
if coeff == 0:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
start = base0 + r
|
|
84
|
+
a_out[r, start:start + (k_degree + 1)] += coeff * row_scaled
|
|
85
|
+
|
|
86
|
+
if normalize:
|
|
87
|
+
return _normalize_coefficients(a_out)
|
|
88
|
+
|
|
89
|
+
return a_out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def plot_candidates(a, x, delta=1e-4, size=None, latex=False, verbose=False):
|
|
93
|
+
"""
|
|
94
|
+
Visualize candidate densities implied by an algebraic Stieltjes-transform
|
|
95
|
+
relation:
|
|
96
|
+
P(z, m) = sum_{i=0..I} sum_{j=0..J} a[i, j] z^i m^j,
|
|
97
|
+
where m(z) is defined implicitly by P(z, m(z)) = 0.
|
|
98
|
+
|
|
99
|
+
For each grid point x_k, set z = x_k + i * delta, form the polynomial in m
|
|
100
|
+
given by P(z, m) = 0, solve for its roots, and plot the cloud of candidate
|
|
101
|
+
densities:
|
|
102
|
+
(1 / pi) * Im(m_root),
|
|
103
|
+
keeping only roots with Im(m_root) > 0 (roots are not tracked/paired across
|
|
104
|
+
x-values).
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
a : array_like of complex or float, shape (I+1, J+1)
|
|
109
|
+
Coefficients defining P(z, m) in the monomial basis:
|
|
110
|
+
P(z, m) = sum_{i=0..I} sum_{j=0..J} a[i, j] z^i m^j.
|
|
111
|
+
x : array_like of float, shape (N,)
|
|
112
|
+
1D array of real x-values (evaluation grid).
|
|
113
|
+
delta : float, optional
|
|
114
|
+
Small positive imaginary offset used to evaluate m(x + i * delta).
|
|
115
|
+
size : integer, optional
|
|
116
|
+
For labelling purposes, the size of the corresponding matrix can
|
|
117
|
+
be provided.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
fig : matplotlib.figure.Figure
|
|
122
|
+
The created figure.
|
|
123
|
+
ax : matplotlib.axes.Axes
|
|
124
|
+
The axes the scatter plot was drawn on.
|
|
125
|
+
"""
|
|
126
|
+
if not (isinstance(delta, (float, int)) and delta > 0):
|
|
127
|
+
raise ValueError("delta must be a positive scalar.")
|
|
128
|
+
|
|
129
|
+
x = numpy.asarray(x)
|
|
130
|
+
if x.ndim != 1:
|
|
131
|
+
raise ValueError("x must be a 1D NumPy array.")
|
|
132
|
+
|
|
133
|
+
a = numpy.asarray(a)
|
|
134
|
+
if a.ndim != 2:
|
|
135
|
+
raise ValueError("a must be a 2D NumPy array with a[i, j] coefficients.")
|
|
136
|
+
if not numpy.issubdtype(a.dtype, numpy.number):
|
|
137
|
+
raise ValueError("a must be numeric.")
|
|
138
|
+
|
|
139
|
+
i_degree = a.shape[0] - 1
|
|
140
|
+
|
|
141
|
+
xs = []
|
|
142
|
+
ys = []
|
|
143
|
+
max_ys = numpy.zeros_like(x)
|
|
144
|
+
|
|
145
|
+
# Precompute i-powers indices to avoid repeated arange creation.
|
|
146
|
+
i_idx = numpy.arange(i_degree + 1)
|
|
147
|
+
|
|
148
|
+
for idx, xk in enumerate(x):
|
|
149
|
+
z = complex(float(xk), float(delta)) # x + i * delta
|
|
150
|
+
|
|
151
|
+
# b[j] = sum_i a[i, j] * z^i => polynomial in m:
|
|
152
|
+
# sum_{j=0..J} b[j] m^j = 0
|
|
153
|
+
z_pows = z ** i_idx # length I+1
|
|
154
|
+
b = (z_pows[:, None] * a).sum(axis=0) # length J+1, low->high in m
|
|
155
|
+
|
|
156
|
+
# Trim trailing (highest-degree) coefficients near zero to avoid
|
|
157
|
+
# numerical issues in numpy.roots. b is low->high, so trim from end.
|
|
158
|
+
tol = 1e-14
|
|
159
|
+
b_trim = b.copy()
|
|
160
|
+
while b_trim.size > 1 and abs(b_trim[-1]) < tol:
|
|
161
|
+
b_trim = b_trim[:-1]
|
|
162
|
+
|
|
163
|
+
# If constant polynomial (no roots), skip.
|
|
164
|
+
if b_trim.size <= 1:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# numpy.roots expects highest degree first.
|
|
168
|
+
coeffs_high_to_low = b_trim[::-1]
|
|
169
|
+
roots = numpy.roots(coeffs_high_to_low)
|
|
170
|
+
|
|
171
|
+
# Keep only roots with positive imaginary part.
|
|
172
|
+
im = numpy.imag(roots)
|
|
173
|
+
mask = im > 0
|
|
174
|
+
if numpy.any(mask):
|
|
175
|
+
xs.append(numpy.full(mask.sum(), float(xk)))
|
|
176
|
+
ys.append(im[mask] / numpy.pi)
|
|
177
|
+
max_ys[idx] = max(ys[-1])
|
|
178
|
+
|
|
179
|
+
if verbose:
|
|
180
|
+
max_density = numpy.trapezoid(max_ys, x)
|
|
181
|
+
print("Max density: {}".format(max_density))
|
|
182
|
+
|
|
183
|
+
if xs:
|
|
184
|
+
xs = numpy.concatenate(xs)
|
|
185
|
+
ys = numpy.concatenate(ys)
|
|
186
|
+
else:
|
|
187
|
+
xs = numpy.array([], dtype=float)
|
|
188
|
+
ys = numpy.array([], dtype=float)
|
|
189
|
+
|
|
190
|
+
with texplot.theme(use_latex=latex):
|
|
191
|
+
fig, ax = plt.subplots(figsize=(6, 2.7))
|
|
192
|
+
ax.scatter(xs, ys, s=8, alpha=1, linewidths=0, c='k')
|
|
193
|
+
|
|
194
|
+
ax.set_xlabel(r'$\lambda$')
|
|
195
|
+
ax.set_ylabel(r'$\rho(\lambda)$''')
|
|
196
|
+
ax.set_title("Candidate Density Cloud")
|
|
197
|
+
if size is not None:
|
|
198
|
+
ax.set_title("Candidate Density Cloud (size = {})".format(size))
|
|
199
|
+
ax.grid(True, alpha=1)
|
|
200
|
+
save_status = False
|
|
201
|
+
save_filename = ''
|
|
202
|
+
texplot.show_or_save_plot(plt, default_filename=save_filename,
|
|
203
|
+
transparent_background=True, dpi=400,
|
|
204
|
+
show_and_save=save_status, verbose=True)
|
|
@@ -0,0 +1,330 @@
|
|
|
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 eval_roots
|
|
16
|
+
from ._decompress_util import eval_P_partials
|
|
17
|
+
|
|
18
|
+
__all__ = ['evolve_edges', 'merge_edges']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ================
|
|
22
|
+
# edge newton step
|
|
23
|
+
# ================
|
|
24
|
+
|
|
25
|
+
def _edge_newton_step(t, zeta, y, a_coeffs, max_iter=30, tol=1e-12):
|
|
26
|
+
"""
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
tau = float(numpy.exp(t))
|
|
30
|
+
c = tau - 1.0
|
|
31
|
+
|
|
32
|
+
for _ in range(max_iter):
|
|
33
|
+
P, Pz, Py = eval_P_partials(zeta, y, a_coeffs)
|
|
34
|
+
|
|
35
|
+
# F1 = P(zeta,y)
|
|
36
|
+
F1 = complex(P)
|
|
37
|
+
|
|
38
|
+
# F2 = y^2 Py - c Pz
|
|
39
|
+
F2 = complex((y * y) * Py - c * Pz)
|
|
40
|
+
|
|
41
|
+
if max(abs(F1), abs(F2)) <= tol:
|
|
42
|
+
return zeta, y, True
|
|
43
|
+
|
|
44
|
+
# Numerical Jacobian (2x2) in (zeta,y)
|
|
45
|
+
eps_z = 1e-8 * (1.0 + abs(zeta))
|
|
46
|
+
eps_y = 1e-8 * (1.0 + abs(y))
|
|
47
|
+
|
|
48
|
+
Pp, Pzp, Pyp = eval_P_partials(zeta + eps_z, y, a_coeffs)
|
|
49
|
+
F1_zp = (complex(Pp) - F1) / eps_z
|
|
50
|
+
F2_zp = (complex((y * y) * Pyp - c * Pzp) - F2) / eps_z
|
|
51
|
+
|
|
52
|
+
Pp, Pzp, Pyp = eval_P_partials(zeta, y + eps_y, a_coeffs)
|
|
53
|
+
F1_yp = (complex(Pp) - F1) / eps_y
|
|
54
|
+
F2_yp = (complex(((y + eps_y) * (y + eps_y)) * Pyp - c * Pzp) - F2) / \
|
|
55
|
+
eps_y
|
|
56
|
+
|
|
57
|
+
# Solve J * [dz, dy] = -F
|
|
58
|
+
det = F1_zp * F2_yp - F1_yp * F2_zp
|
|
59
|
+
if det == 0.0:
|
|
60
|
+
return zeta, y, False
|
|
61
|
+
|
|
62
|
+
dz = (-F1 * F2_yp + F1_yp * F2) / det
|
|
63
|
+
dy = (-F1_zp * F2 + F1 * F2_zp) / det
|
|
64
|
+
|
|
65
|
+
# Mild damping if update is huge
|
|
66
|
+
lam = 1.0
|
|
67
|
+
if abs(dz) + abs(dy) > 10.0 * (1.0 + abs(zeta) + abs(y)):
|
|
68
|
+
lam = 0.2
|
|
69
|
+
|
|
70
|
+
zeta = zeta + lam * dz
|
|
71
|
+
y = y + lam * dy
|
|
72
|
+
|
|
73
|
+
return zeta, y, False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ==================
|
|
77
|
+
# pick physical root
|
|
78
|
+
# ==================
|
|
79
|
+
|
|
80
|
+
def _pick_physical_root(z, roots):
|
|
81
|
+
"""
|
|
82
|
+
Pick the Herglotz/physical root at a point z in C+.
|
|
83
|
+
|
|
84
|
+
Heuristic: choose the root with maximal Im(root) when Im(z)>0,
|
|
85
|
+
then enforce Im(root)>0. Falls back to closest-to -1/z if needed.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
r = numpy.asarray(roots, dtype=complex).ravel()
|
|
89
|
+
if r.size == 0:
|
|
90
|
+
return numpy.nan + 1j * numpy.nan
|
|
91
|
+
|
|
92
|
+
if z.imag > 0.0:
|
|
93
|
+
pos = r[numpy.imag(r) > 0.0]
|
|
94
|
+
if pos.size > 0:
|
|
95
|
+
return pos[numpy.argmax(numpy.imag(pos))]
|
|
96
|
+
|
|
97
|
+
target = -1.0 / z
|
|
98
|
+
return r[numpy.argmin(numpy.abs(r - target))]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ============================
|
|
102
|
+
# init edge point from support
|
|
103
|
+
# ============================
|
|
104
|
+
|
|
105
|
+
def _init_edge_point_from_support(x_edge, a_coeffs, eta=1e-3):
|
|
106
|
+
"""
|
|
107
|
+
Initialize (zeta,y) at t=0 for an edge near x_edge.
|
|
108
|
+
|
|
109
|
+
Uses z = x_edge + i*eta, picks physical root y, then refines zeta on real
|
|
110
|
+
axis.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
z = complex(x_edge + 1j * eta)
|
|
114
|
+
roots = eval_roots(numpy.array([z]), a_coeffs)[0]
|
|
115
|
+
y = _pick_physical_root(z, roots)
|
|
116
|
+
|
|
117
|
+
# Move zeta to real axis as initial guess
|
|
118
|
+
zeta = complex(x_edge)
|
|
119
|
+
|
|
120
|
+
# Refine zeta,y to satisfy P=0 and Py=0 at t=0 (branch point)
|
|
121
|
+
# This uses the same Newton system with c=0, i.e. F2 = y^2 Py.
|
|
122
|
+
zeta, y, ok = _edge_newton_step(0.0, zeta, y, a_coeffs, max_iter=50,
|
|
123
|
+
tol=1e-10)
|
|
124
|
+
|
|
125
|
+
return zeta, y, ok
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ============
|
|
129
|
+
# evolve edges
|
|
130
|
+
# ============
|
|
131
|
+
|
|
132
|
+
def evolve_edges(
|
|
133
|
+
t_grid,
|
|
134
|
+
a_coeffs,
|
|
135
|
+
support=None,
|
|
136
|
+
eta=1e-3,
|
|
137
|
+
dt_max=0.1,
|
|
138
|
+
max_iter=30,
|
|
139
|
+
tol=1e-12,
|
|
140
|
+
return_preimage=False):
|
|
141
|
+
"""
|
|
142
|
+
Evolve spectral edges under free decompression using the fitted polynomial
|
|
143
|
+
P.
|
|
144
|
+
|
|
145
|
+
Solves for (zeta(t), y(t)) on the spectral curve:
|
|
146
|
+
P(zeta,y) = 0,
|
|
147
|
+
y^2 * Py(zeta,y) - (exp(t)-1) * Pzeta(zeta,y) = 0,
|
|
148
|
+
|
|
149
|
+
then maps to physical coordinate:
|
|
150
|
+
z_edge(t) = zeta - (exp(t)-1)/y.
|
|
151
|
+
|
|
152
|
+
If return_preimage=True, also returns zeta_hist and y_hist of shape
|
|
153
|
+
(nt, 2k).
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
t_grid = numpy.asarray(t_grid, dtype=float).ravel()
|
|
157
|
+
if t_grid.size < 1:
|
|
158
|
+
raise ValueError("t_grid must be non-empty.")
|
|
159
|
+
if numpy.any(numpy.diff(t_grid) <= 0.0):
|
|
160
|
+
raise ValueError("t_grid must be strictly increasing.")
|
|
161
|
+
|
|
162
|
+
if support is None:
|
|
163
|
+
raise ValueError("support must be provided (auto-detection not " +
|
|
164
|
+
"implemented).")
|
|
165
|
+
|
|
166
|
+
# Flatten endpoints in fixed order [a1,b1,a2,b2,...]
|
|
167
|
+
endpoints0 = []
|
|
168
|
+
for a, b in support:
|
|
169
|
+
endpoints0.append(float(a))
|
|
170
|
+
endpoints0.append(float(b))
|
|
171
|
+
|
|
172
|
+
m = len(endpoints0)
|
|
173
|
+
complex_edges = numpy.empty((t_grid.size, m), dtype=numpy.complex128)
|
|
174
|
+
ok = numpy.zeros((t_grid.size, m), dtype=bool)
|
|
175
|
+
|
|
176
|
+
if return_preimage:
|
|
177
|
+
zeta_hist = numpy.empty((t_grid.size, m), dtype=numpy.complex128)
|
|
178
|
+
y_hist = numpy.empty((t_grid.size, m), dtype=numpy.complex128)
|
|
179
|
+
else:
|
|
180
|
+
zeta_hist = None
|
|
181
|
+
y_hist = None
|
|
182
|
+
|
|
183
|
+
# Initialize (zeta,y) at t=0 from support endpoints
|
|
184
|
+
zeta = numpy.empty(m, dtype=numpy.complex128)
|
|
185
|
+
y = numpy.empty(m, dtype=numpy.complex128)
|
|
186
|
+
|
|
187
|
+
for j in range(m):
|
|
188
|
+
z0, y0, ok0 = _init_edge_point_from_support(endpoints0[j], a_coeffs,
|
|
189
|
+
eta=eta)
|
|
190
|
+
zeta[j] = z0
|
|
191
|
+
y[j] = y0
|
|
192
|
+
ok[0, j] = ok0
|
|
193
|
+
complex_edges[0, j] = z0 # at t=0, tau-1 = 0 => z_edge = zeta
|
|
194
|
+
|
|
195
|
+
if return_preimage:
|
|
196
|
+
zeta_hist[0, :] = zeta
|
|
197
|
+
y_hist[0, :] = y
|
|
198
|
+
|
|
199
|
+
# Time stepping
|
|
200
|
+
for it in range(1, t_grid.size):
|
|
201
|
+
t0 = float(t_grid[it - 1])
|
|
202
|
+
t1 = float(t_grid[it])
|
|
203
|
+
dt = t1 - t0
|
|
204
|
+
|
|
205
|
+
n_sub = int(numpy.ceil(dt / float(dt_max)))
|
|
206
|
+
if n_sub < 1:
|
|
207
|
+
n_sub = 1
|
|
208
|
+
|
|
209
|
+
for ks in range(1, n_sub + 1):
|
|
210
|
+
t = t0 + dt * (ks / float(n_sub))
|
|
211
|
+
for j in range(m):
|
|
212
|
+
zeta[j], y[j], okj = _edge_newton_step(
|
|
213
|
+
t, zeta[j], y[j], a_coeffs, max_iter=max_iter, tol=tol
|
|
214
|
+
)
|
|
215
|
+
ok[it, j] = okj
|
|
216
|
+
|
|
217
|
+
tau = float(numpy.exp(t1))
|
|
218
|
+
c = tau - 1.0
|
|
219
|
+
complex_edges[it, :] = zeta - c / y
|
|
220
|
+
|
|
221
|
+
if return_preimage:
|
|
222
|
+
zeta_hist[it, :] = zeta
|
|
223
|
+
y_hist[it, :] = y
|
|
224
|
+
|
|
225
|
+
if return_preimage:
|
|
226
|
+
return complex_edges, ok, zeta_hist, y_hist
|
|
227
|
+
|
|
228
|
+
return complex_edges, ok
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ===========
|
|
232
|
+
# merge edges
|
|
233
|
+
# ===========
|
|
234
|
+
|
|
235
|
+
def merge_edges(edges, tol=0.0):
|
|
236
|
+
"""
|
|
237
|
+
Merge bulks when inner edges cross, without shifting columns.
|
|
238
|
+
|
|
239
|
+
Columns are fixed as [a1,b1,a2,b2,...,ak,bk]. When the gap between bulk j
|
|
240
|
+
and bulk j+1 closes (b_j >= a_{j+1} - tol), we annihilate the two inner
|
|
241
|
+
edges by setting b_j and a_{j+1} to NaN. All other columns remain in place.
|
|
242
|
+
|
|
243
|
+
This preserves smooth plotting per original edge index (e.g. b2 stays in
|
|
244
|
+
the same column for all t). The number of active bulks is computed as the
|
|
245
|
+
number of connected components after merges.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
edges : ndarray, shape (nt, 2k)
|
|
250
|
+
Edge trajectories [a1,b1,a2,b2,...].
|
|
251
|
+
tol : float
|
|
252
|
+
Merge tolerance in x-units.
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
edges2 : ndarray, shape (nt, 2k)
|
|
257
|
+
Same shape as input. Inner merged edges are NaN. No columns are
|
|
258
|
+
shifted.
|
|
259
|
+
active_k : ndarray, shape (nt,)
|
|
260
|
+
Number of remaining bulks (connected components) at each time.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
edges = numpy.asarray(edges, dtype=float)
|
|
264
|
+
nt, m = edges.shape
|
|
265
|
+
if m % 2 != 0:
|
|
266
|
+
raise ValueError("edges must have even number of columns.")
|
|
267
|
+
k0 = m // 2
|
|
268
|
+
|
|
269
|
+
edges2 = edges.copy()
|
|
270
|
+
active_k = numpy.zeros(nt, dtype=int)
|
|
271
|
+
|
|
272
|
+
for it in range(nt):
|
|
273
|
+
row = edges2[it, :].copy()
|
|
274
|
+
a = row[0::2].copy()
|
|
275
|
+
b = row[1::2].copy()
|
|
276
|
+
|
|
277
|
+
# Initialize blocks as list of (L_index, R_index) in bulk indices.
|
|
278
|
+
blocks = []
|
|
279
|
+
for j in range(k0):
|
|
280
|
+
if numpy.isfinite(a[j]) and numpy.isfinite(b[j]) and (b[j] > a[j]):
|
|
281
|
+
blocks.append([j, j])
|
|
282
|
+
|
|
283
|
+
if len(blocks) == 0:
|
|
284
|
+
active_k[it] = 0
|
|
285
|
+
edges2[it, :] = row
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Helper to get current left/right edge value of a block.
|
|
289
|
+
def left_edge(block):
|
|
290
|
+
return a[block[0]]
|
|
291
|
+
|
|
292
|
+
def right_edge(block):
|
|
293
|
+
return b[block[1]]
|
|
294
|
+
|
|
295
|
+
# Iteratively merge adjacent blocks when they overlap / touch.
|
|
296
|
+
merged = True
|
|
297
|
+
while merged and (len(blocks) > 1):
|
|
298
|
+
merged = False
|
|
299
|
+
new_blocks = [blocks[0]]
|
|
300
|
+
for blk in blocks[1:]:
|
|
301
|
+
prev = new_blocks[-1]
|
|
302
|
+
# If right(prev) crosses left(blk), merge.
|
|
303
|
+
if numpy.isfinite(right_edge(prev)) and \
|
|
304
|
+
numpy.isfinite(left_edge(blk)) and \
|
|
305
|
+
(right_edge(prev) >= left_edge(blk) - float(tol)):
|
|
306
|
+
|
|
307
|
+
# Annihilate inner boundary edges in fixed columns:
|
|
308
|
+
# b_{prev.right_bulk} and a_{blk.left_bulk}
|
|
309
|
+
bj = prev[1]
|
|
310
|
+
aj = blk[0]
|
|
311
|
+
b[bj] = numpy.nan
|
|
312
|
+
a[aj] = numpy.nan
|
|
313
|
+
|
|
314
|
+
# Merge block indices: left stays prev.left, right becomes
|
|
315
|
+
# blk.right
|
|
316
|
+
prev[1] = blk[1]
|
|
317
|
+
merged = True
|
|
318
|
+
else:
|
|
319
|
+
new_blocks.append(blk)
|
|
320
|
+
blocks = new_blocks
|
|
321
|
+
|
|
322
|
+
active_k[it] = len(blocks)
|
|
323
|
+
|
|
324
|
+
# Write back modified a,b into the row without shifting any columns.
|
|
325
|
+
row2 = row.copy()
|
|
326
|
+
row2[0::2] = a
|
|
327
|
+
row2[1::2] = b
|
|
328
|
+
edges2[it, :] = row2
|
|
329
|
+
|
|
330
|
+
return edges2, active_k
|