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.
Files changed (28) hide show
  1. freealg/__version__.py +1 -1
  2. freealg/_algebraic_form/_cusp.py +357 -0
  3. freealg/_algebraic_form/_cusp_wrap.py +268 -0
  4. freealg/_algebraic_form/_decompress2.py +2 -0
  5. freealg/_algebraic_form/_decompress4.py +739 -0
  6. freealg/_algebraic_form/_decompress5.py +738 -0
  7. freealg/_algebraic_form/_decompress6.py +492 -0
  8. freealg/_algebraic_form/_decompress7.py +355 -0
  9. freealg/_algebraic_form/_decompress8.py +369 -0
  10. freealg/_algebraic_form/_decompress9.py +363 -0
  11. freealg/_algebraic_form/_decompress_new.py +431 -0
  12. freealg/_algebraic_form/_decompress_new_2.py +1631 -0
  13. freealg/_algebraic_form/_decompress_util.py +172 -0
  14. freealg/_algebraic_form/_homotopy2.py +289 -0
  15. freealg/_algebraic_form/_homotopy3.py +215 -0
  16. freealg/_algebraic_form/_homotopy4.py +320 -0
  17. freealg/_algebraic_form/_homotopy5.py +185 -0
  18. freealg/_algebraic_form/_moments.py +0 -1
  19. freealg/_algebraic_form/_support.py +132 -177
  20. freealg/_algebraic_form/algebraic_form.py +21 -2
  21. freealg/distributions/_compound_poisson.py +481 -0
  22. freealg/distributions/_deformed_marchenko_pastur.py +6 -7
  23. {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/METADATA +1 -1
  24. {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/RECORD +28 -12
  25. {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/WHEEL +0 -0
  26. {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/licenses/AUTHORS.txt +0 -0
  27. {freealg-0.7.12.dist-info → freealg-0.7.15.dist-info}/licenses/LICENSE.txt +0 -0
  28. {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