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,363 @@
|
|
|
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
|
+
# Imports
|
|
11
|
+
# =======
|
|
12
|
+
|
|
13
|
+
import numpy
|
|
14
|
+
|
|
15
|
+
__all__ = ['decompress_newton']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ==========================
|
|
19
|
+
# polynomial helpers: P, dP
|
|
20
|
+
# ==========================
|
|
21
|
+
|
|
22
|
+
def _poly_powers(z, deg):
|
|
23
|
+
z = numpy.asarray(z, dtype=complex).ravel()
|
|
24
|
+
n = z.size
|
|
25
|
+
zp = numpy.ones((n, deg + 1), dtype=complex)
|
|
26
|
+
for k in range(1, deg + 1):
|
|
27
|
+
zp[:, k] = zp[:, k - 1] * z
|
|
28
|
+
return zp
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _eval_P_dP(z, m, a_coeffs):
|
|
32
|
+
"""
|
|
33
|
+
Evaluate P(z,m), dP/dz, dP/dm for polynomial coefficients a_coeffs.
|
|
34
|
+
|
|
35
|
+
a_coeffs has shape (deg_z+1, s+1), where
|
|
36
|
+
P(z,m) = sum_{j=0}^s a_j(z) m^j
|
|
37
|
+
a_j(z) = sum_{i=0}^{deg_z} a_coeffs[i,j] z^i
|
|
38
|
+
"""
|
|
39
|
+
z = numpy.asarray(z, dtype=complex).ravel()
|
|
40
|
+
m = numpy.asarray(m, dtype=complex).ravel()
|
|
41
|
+
|
|
42
|
+
deg_z = int(a_coeffs.shape[0] - 1)
|
|
43
|
+
s = int(a_coeffs.shape[1] - 1)
|
|
44
|
+
|
|
45
|
+
zp = _poly_powers(z, deg_z) # (n, deg_z+1)
|
|
46
|
+
# a_j(z) for all j
|
|
47
|
+
a = zp @ a_coeffs # (n, s+1)
|
|
48
|
+
|
|
49
|
+
# derivative a_j'(z)
|
|
50
|
+
if deg_z >= 1:
|
|
51
|
+
# coeffs multiplied by power index
|
|
52
|
+
idx = numpy.arange(deg_z + 1, dtype=float)
|
|
53
|
+
a_coeffs_dz = a_coeffs * idx[:, None]
|
|
54
|
+
# powers z^{i-1}: shift zp right
|
|
55
|
+
zp_m1 = numpy.zeros_like(zp)
|
|
56
|
+
zp_m1[:, 0] = 0.0
|
|
57
|
+
zp_m1[:, 1:] = zp[:, :-1]
|
|
58
|
+
a_dz = zp_m1 @ a_coeffs_dz # (n, s+1)
|
|
59
|
+
else:
|
|
60
|
+
a_dz = numpy.zeros_like(a)
|
|
61
|
+
|
|
62
|
+
# powers of m
|
|
63
|
+
mp = numpy.ones((m.size, s + 1), dtype=complex)
|
|
64
|
+
for j in range(1, s + 1):
|
|
65
|
+
mp[:, j] = mp[:, j - 1] * m
|
|
66
|
+
|
|
67
|
+
P = numpy.sum(a * mp, axis=1)
|
|
68
|
+
|
|
69
|
+
# dP/dz = sum_j a_j'(z) m^j
|
|
70
|
+
Pz = numpy.sum(a_dz * mp, axis=1)
|
|
71
|
+
|
|
72
|
+
# dP/dm = sum_{j>=1} j a_j(z) m^{j-1}
|
|
73
|
+
if s >= 1:
|
|
74
|
+
jm = numpy.arange(s + 1, dtype=float)
|
|
75
|
+
# m^{j-1}: shift mp left
|
|
76
|
+
mp_m1 = numpy.zeros_like(mp)
|
|
77
|
+
mp_m1[:, 0] = 0.0
|
|
78
|
+
mp_m1[:, 1:] = mp[:, :-1]
|
|
79
|
+
Pm = numpy.sum((a * jm[None, :]) * mp_m1, axis=1)
|
|
80
|
+
else:
|
|
81
|
+
Pm = numpy.zeros_like(P)
|
|
82
|
+
|
|
83
|
+
return P, Pz, Pm
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ===========================
|
|
87
|
+
# 2x2 complex Newton (correct)
|
|
88
|
+
# ===========================
|
|
89
|
+
|
|
90
|
+
def _newton_corrector(z_fixed, tau, a_coeffs, zeta0, y0,
|
|
91
|
+
max_iter=50, tol=1e-12,
|
|
92
|
+
armijo=1e-4, min_lam=1e-6):
|
|
93
|
+
"""
|
|
94
|
+
Solve for (zeta, y) in:
|
|
95
|
+
F1 = P(zeta, y) = 0
|
|
96
|
+
F2 = zeta - (tau-1)/y - z_fixed = 0
|
|
97
|
+
using damped Newton in C^2 (2 complex unknowns).
|
|
98
|
+
"""
|
|
99
|
+
zeta = zeta0
|
|
100
|
+
y = y0
|
|
101
|
+
|
|
102
|
+
# A tiny stabilizer for division by y
|
|
103
|
+
eps_y = 0.0
|
|
104
|
+
|
|
105
|
+
for it in range(int(max_iter)):
|
|
106
|
+
P, Pz, Py = _eval_P_dP(numpy.array([zeta]), numpy.array([y]), a_coeffs)
|
|
107
|
+
F1 = P[0]
|
|
108
|
+
F2 = zeta - (tau - 1.0) / (y + eps_y) - z_fixed
|
|
109
|
+
|
|
110
|
+
# Convergence test on infinity norm
|
|
111
|
+
if max(abs(F1), abs(F2)) <= tol:
|
|
112
|
+
return zeta, y, True, it
|
|
113
|
+
|
|
114
|
+
# Jacobian (complex 2x2)
|
|
115
|
+
# dF1/dzeta = Pz, dF1/dy = Py
|
|
116
|
+
# dF2/dzeta = 1, dF2/dy = (tau-1)/y^2
|
|
117
|
+
J11 = Pz[0]
|
|
118
|
+
J12 = Py[0]
|
|
119
|
+
J21 = 1.0 + 0.0j
|
|
120
|
+
J22 = (tau - 1.0) / ((y + eps_y) * (y + eps_y))
|
|
121
|
+
|
|
122
|
+
# Solve J * delta = -F
|
|
123
|
+
try:
|
|
124
|
+
delta = numpy.linalg.solve(
|
|
125
|
+
numpy.array([[J11, J12], [J21, J22]], dtype=complex),
|
|
126
|
+
numpy.array([-F1, -F2], dtype=complex)
|
|
127
|
+
)
|
|
128
|
+
except numpy.linalg.LinAlgError:
|
|
129
|
+
return zeta, y, False, it
|
|
130
|
+
|
|
131
|
+
dzeta = delta[0]
|
|
132
|
+
dy = delta[1]
|
|
133
|
+
|
|
134
|
+
# Damped update (Armijo on ||F||_inf)
|
|
135
|
+
lam = 1.0
|
|
136
|
+
norm0 = max(abs(F1), abs(F2))
|
|
137
|
+
|
|
138
|
+
while lam >= float(min_lam):
|
|
139
|
+
zeta_try = zeta + lam * dzeta
|
|
140
|
+
y_try = y + lam * dy
|
|
141
|
+
|
|
142
|
+
P_try, _, _ = _eval_P_dP(numpy.array([zeta_try]), numpy.array([y_try]), a_coeffs)
|
|
143
|
+
F1_try = P_try[0]
|
|
144
|
+
F2_try = zeta_try - (tau - 1.0) / (y_try + eps_y) - z_fixed
|
|
145
|
+
norm_try = max(abs(F1_try), abs(F2_try))
|
|
146
|
+
|
|
147
|
+
# Armijo-like sufficient decrease
|
|
148
|
+
if norm_try <= (1.0 - float(armijo) * lam) * norm0 or norm_try < norm0:
|
|
149
|
+
zeta = zeta_try
|
|
150
|
+
y = y_try
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
lam *= 0.5
|
|
154
|
+
|
|
155
|
+
else:
|
|
156
|
+
# failed to find a decreasing step
|
|
157
|
+
return zeta, y, False, it
|
|
158
|
+
|
|
159
|
+
return zeta, y, False, int(max_iter)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ===========================
|
|
163
|
+
# predictor step (tangent ODE)
|
|
164
|
+
# ===========================
|
|
165
|
+
|
|
166
|
+
def _predictor_step(z_fixed, tau0, tau1, a_coeffs, zeta0, y0):
|
|
167
|
+
"""
|
|
168
|
+
One explicit Euler predictor from tau0 to tau1 along the sheet.
|
|
169
|
+
|
|
170
|
+
Uses:
|
|
171
|
+
dy/dzeta = -Pz/Py from P(zeta,y)=0
|
|
172
|
+
zeta' = (1/y) / ( 1 + (tau-1)*(dy/dzeta)/y^2 )
|
|
173
|
+
"""
|
|
174
|
+
dtau = float(tau1 - tau0)
|
|
175
|
+
if dtau == 0.0:
|
|
176
|
+
return zeta0, y0
|
|
177
|
+
|
|
178
|
+
P, Pz, Py = _eval_P_dP(numpy.array([zeta0]), numpy.array([y0]), a_coeffs)
|
|
179
|
+
Pz = Pz[0]
|
|
180
|
+
Py = Py[0]
|
|
181
|
+
|
|
182
|
+
# dy/dzeta on curve (avoid division by zero)
|
|
183
|
+
if Py == 0.0:
|
|
184
|
+
D = 0.0 + 0.0j
|
|
185
|
+
else:
|
|
186
|
+
D = -Pz / Py
|
|
187
|
+
|
|
188
|
+
den = 1.0 + (tau0 - 1.0) * D / (y0 * y0)
|
|
189
|
+
if den == 0.0:
|
|
190
|
+
den = 1.0 + 0.0j
|
|
191
|
+
|
|
192
|
+
zeta_prime = (1.0 / y0) / den
|
|
193
|
+
y_prime = D * zeta_prime
|
|
194
|
+
|
|
195
|
+
zeta1 = zeta0 + dtau * zeta_prime
|
|
196
|
+
y1 = y0 + dtau * y_prime
|
|
197
|
+
|
|
198
|
+
return zeta1, y1
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# =================
|
|
202
|
+
# decompress newton
|
|
203
|
+
# =================
|
|
204
|
+
|
|
205
|
+
def decompress_newton(z_query, t, a_coeffs, w0_list=None,
|
|
206
|
+
max_iter=50, tol=1e-12,
|
|
207
|
+
armijo=1e-4, min_lam=1e-6,
|
|
208
|
+
w_min=1e-14,
|
|
209
|
+
sweep=True,
|
|
210
|
+
max_substeps=8,
|
|
211
|
+
**kwargs):
|
|
212
|
+
"""
|
|
213
|
+
Free decompression via characteristic continuation on the algebraic curve
|
|
214
|
+
P(z,m)=0 using predictor-corrector in tau = exp(t).
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
z_query : array_like (n_z,)
|
|
219
|
+
Query points z = x + i*delta (typically slightly above real axis).
|
|
220
|
+
t : array_like (n_t,)
|
|
221
|
+
Time grid (must be sorted increasing; should include 0).
|
|
222
|
+
a_coeffs : ndarray
|
|
223
|
+
Polynomial coefficients of P(z,m)=0 as in AlgebraicForm.fit.
|
|
224
|
+
w0_list : array_like (n_z,)
|
|
225
|
+
Initial condition m(t=0, z_query) on the physical branch.
|
|
226
|
+
max_iter, tol, armijo, min_lam : float/int
|
|
227
|
+
Newton corrector parameters.
|
|
228
|
+
w_min : float
|
|
229
|
+
Lower bound on |w| used to guard divisions (rare).
|
|
230
|
+
sweep : bool
|
|
231
|
+
If True, advance in time using previous solution as initial guess.
|
|
232
|
+
max_substeps : int
|
|
233
|
+
If corrector fails on a time step, internally bisect in tau and try
|
|
234
|
+
smaller predictor steps (without changing returned t grid).
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
W : ndarray (n_t, n_z), complex
|
|
239
|
+
m(t,z) for all t and z_query.
|
|
240
|
+
ok : ndarray (n_t, n_z), bool
|
|
241
|
+
Per-point success flag.
|
|
242
|
+
"""
|
|
243
|
+
z_query = numpy.asarray(z_query, dtype=complex).ravel()
|
|
244
|
+
t = numpy.asarray(t, dtype=float).ravel()
|
|
245
|
+
|
|
246
|
+
if t.size == 0:
|
|
247
|
+
return numpy.empty((0, z_query.size), dtype=complex), \
|
|
248
|
+
numpy.empty((0, z_query.size), dtype=bool)
|
|
249
|
+
|
|
250
|
+
# Ensure sorted increasing
|
|
251
|
+
if numpy.any(numpy.diff(t) < 0.0):
|
|
252
|
+
raise ValueError('t must be sorted increasing.')
|
|
253
|
+
|
|
254
|
+
tau = numpy.exp(t)
|
|
255
|
+
|
|
256
|
+
n_t = t.size
|
|
257
|
+
n_z = z_query.size
|
|
258
|
+
|
|
259
|
+
W = numpy.full((n_t, n_z), numpy.nan + 1j * numpy.nan, dtype=complex)
|
|
260
|
+
ok = numpy.zeros((n_t, n_z), dtype=bool)
|
|
261
|
+
|
|
262
|
+
if w0_list is None:
|
|
263
|
+
raise ValueError('w0_list must be provided (physical branch at t=0).')
|
|
264
|
+
w0_list = numpy.asarray(w0_list, dtype=complex).ravel()
|
|
265
|
+
if w0_list.size != n_z:
|
|
266
|
+
raise ValueError('w0_list must have the same size as z_query.')
|
|
267
|
+
|
|
268
|
+
# Initialize at tau=1 (t=0)
|
|
269
|
+
# We do not assume t[0] == 0, but if not, we still use w0_list as seed.
|
|
270
|
+
for j in range(n_z):
|
|
271
|
+
W[0, j] = w0_list[j]
|
|
272
|
+
ok[0, j] = numpy.isfinite(W[0, j])
|
|
273
|
+
|
|
274
|
+
# Per-point continuation state in (zeta, y=tau*w)
|
|
275
|
+
zeta_state = z_query.copy()
|
|
276
|
+
y_state = (tau[0] * w0_list).copy()
|
|
277
|
+
|
|
278
|
+
# Ensure no tiny y to avoid division in F2
|
|
279
|
+
tiny = numpy.abs(y_state) < float(w_min)
|
|
280
|
+
if numpy.any(tiny):
|
|
281
|
+
y_state[tiny] = y_state[tiny] + (float(w_min) + 0.0j)
|
|
282
|
+
|
|
283
|
+
# Time sweep
|
|
284
|
+
for k in range(1, n_t):
|
|
285
|
+
tau_prev = float(tau[k - 1])
|
|
286
|
+
tau_next = float(tau[k])
|
|
287
|
+
|
|
288
|
+
# Allow internal substepping if needed
|
|
289
|
+
for j in range(n_z):
|
|
290
|
+
if not sweep and k > 1:
|
|
291
|
+
# If sweep=False, restart from t=0 each time
|
|
292
|
+
zeta0 = z_query[j]
|
|
293
|
+
y0 = (tau_prev * w0_list[j])
|
|
294
|
+
else:
|
|
295
|
+
zeta0 = zeta_state[j]
|
|
296
|
+
y0 = y_state[j]
|
|
297
|
+
|
|
298
|
+
# If previous was not ok, fall back to trivial seed
|
|
299
|
+
if not numpy.isfinite(zeta0) or not numpy.isfinite(y0):
|
|
300
|
+
zeta0 = z_query[j]
|
|
301
|
+
y0 = tau_prev * w0_list[j]
|
|
302
|
+
|
|
303
|
+
# Internal bisection in tau (predictor-corrector)
|
|
304
|
+
success = False
|
|
305
|
+
zeta_sol = zeta0
|
|
306
|
+
y_sol = y0
|
|
307
|
+
|
|
308
|
+
# Create a queue of tau intervals to try (depth-first)
|
|
309
|
+
intervals = [(tau_prev, tau_next, zeta0, y0, 0)]
|
|
310
|
+
while intervals:
|
|
311
|
+
a_tau, b_tau, a_zeta, a_y, depth = intervals.pop()
|
|
312
|
+
|
|
313
|
+
# Predictor from a_tau to b_tau
|
|
314
|
+
zeta_pred, y_pred = _predictor_step(
|
|
315
|
+
z_query[j], a_tau, b_tau, a_coeffs, a_zeta, a_y
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Guard tiny y_pred
|
|
319
|
+
if abs(y_pred) < float(w_min):
|
|
320
|
+
y_pred = y_pred + (float(w_min) + 0.0j)
|
|
321
|
+
|
|
322
|
+
# Corrector at b_tau
|
|
323
|
+
zeta_corr, y_corr, ok1, _ = _newton_corrector(
|
|
324
|
+
z_fixed=z_query[j], tau=b_tau, a_coeffs=a_coeffs,
|
|
325
|
+
zeta0=zeta_pred, y0=y_pred,
|
|
326
|
+
max_iter=max_iter, tol=tol, armijo=armijo, min_lam=min_lam
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if ok1 and numpy.isfinite(zeta_corr) and numpy.isfinite(y_corr):
|
|
330
|
+
# Accept this segment; if it ends at tau_next, done
|
|
331
|
+
if b_tau == tau_next:
|
|
332
|
+
success = True
|
|
333
|
+
zeta_sol = zeta_corr
|
|
334
|
+
y_sol = y_corr
|
|
335
|
+
break
|
|
336
|
+
else:
|
|
337
|
+
# Continue from this intermediate point to tau_next
|
|
338
|
+
intervals.append((b_tau, tau_next, zeta_corr, y_corr, depth))
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
# If failed and we can subdivide further, split interval
|
|
342
|
+
if depth < int(max_substeps):
|
|
343
|
+
mid = 0.5 * (a_tau + b_tau)
|
|
344
|
+
# Subdivide: first do [a, mid] then [mid, b]
|
|
345
|
+
intervals.append((mid, b_tau, a_zeta, a_y, depth + 1))
|
|
346
|
+
intervals.append((a_tau, mid, a_zeta, a_y, depth + 1))
|
|
347
|
+
# else give up
|
|
348
|
+
|
|
349
|
+
if success:
|
|
350
|
+
w = y_sol / tau_next
|
|
351
|
+
W[k, j] = w
|
|
352
|
+
ok[k, j] = True
|
|
353
|
+
|
|
354
|
+
# update state
|
|
355
|
+
zeta_state[j] = zeta_sol
|
|
356
|
+
y_state[j] = y_sol
|
|
357
|
+
else:
|
|
358
|
+
# keep previous (or NaN) and mark fail
|
|
359
|
+
W[k, j] = numpy.nan + 1j * numpy.nan
|
|
360
|
+
ok[k, j] = False
|
|
361
|
+
# do not update state; but keep last good if any
|
|
362
|
+
|
|
363
|
+
return W, ok
|