freealg 0.7.11__py3-none-any.whl → 0.7.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. freealg/__init__.py +2 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +2 -1
  4. freealg/_algebraic_form/_constraints.py +53 -12
  5. freealg/_algebraic_form/_cusp.py +357 -0
  6. freealg/_algebraic_form/_cusp_wrap.py +268 -0
  7. freealg/_algebraic_form/_decompress.py +330 -381
  8. freealg/_algebraic_form/_decompress2.py +120 -0
  9. freealg/_algebraic_form/_decompress4.py +739 -0
  10. freealg/_algebraic_form/_decompress5.py +738 -0
  11. freealg/_algebraic_form/_decompress6.py +492 -0
  12. freealg/_algebraic_form/_decompress7.py +355 -0
  13. freealg/_algebraic_form/_decompress8.py +369 -0
  14. freealg/_algebraic_form/_decompress9.py +363 -0
  15. freealg/_algebraic_form/_decompress_new.py +431 -0
  16. freealg/_algebraic_form/_decompress_new_2.py +1631 -0
  17. freealg/_algebraic_form/_decompress_util.py +172 -0
  18. freealg/_algebraic_form/_edge.py +46 -68
  19. freealg/_algebraic_form/_homotopy.py +62 -30
  20. freealg/_algebraic_form/_homotopy2.py +289 -0
  21. freealg/_algebraic_form/_homotopy3.py +215 -0
  22. freealg/_algebraic_form/_homotopy4.py +320 -0
  23. freealg/_algebraic_form/_homotopy5.py +185 -0
  24. freealg/_algebraic_form/_moments.py +43 -57
  25. freealg/_algebraic_form/_support.py +132 -177
  26. freealg/_algebraic_form/algebraic_form.py +163 -30
  27. freealg/distributions/__init__.py +3 -1
  28. freealg/distributions/_compound_poisson.py +464 -0
  29. freealg/distributions/_deformed_marchenko_pastur.py +51 -0
  30. freealg/distributions/_deformed_wigner.py +44 -0
  31. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/METADATA +2 -1
  32. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/RECORD +36 -20
  33. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/WHEEL +1 -1
  34. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/AUTHORS.txt +0 -0
  35. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/LICENSE.txt +0 -0
  36. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,172 @@
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', 'eval_P_partials']
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)
@@ -13,7 +13,7 @@
13
13
 
14
14
  import numpy
15
15
  from ._continuation_algebraic import eval_roots
16
- from ._decompress import eval_P_partials
16
+ from ._decompress_util import eval_P_partials
17
17
 
18
18
  __all__ = ['evolve_edges', 'merge_edges']
19
19
 
@@ -129,65 +129,28 @@ def _init_edge_point_from_support(x_edge, a_coeffs, eta=1e-3):
129
129
  # evolve edges
130
130
  # ============
131
131
 
132
- def evolve_edges(t_grid, a_coeffs, support=None, eta=1e-3, dt_max=0.1,
133
- max_iter=30, tol=1e-12):
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):
134
141
  """
135
- Evolve spectral edges under free decompression using the fitted
136
- polynomial P.
137
-
138
- At time t, edges are computed as critical values of the FD map restricted
139
- to the spectral curve P(zeta,y)=0. We solve for (zeta(t), y(t)):
142
+ Evolve spectral edges under free decompression using the fitted polynomial
143
+ P.
140
144
 
145
+ Solves for (zeta(t), y(t)) on the spectral curve:
141
146
  P(zeta,y) = 0,
142
147
  y^2 * Py(zeta,y) - (exp(t)-1) * Pzeta(zeta,y) = 0,
143
148
 
144
- then map to the physical coordinate:
149
+ then maps to physical coordinate:
145
150
  z_edge(t) = zeta - (exp(t)-1)/y.
146
151
 
147
- Parameters
148
- ----------
149
- t_grid : array_like of float
150
- Strictly increasing time grid.
151
- a_coeffs : ndarray
152
- Coefficients defining P(zeta,y).
153
- support : list of (float, float), optional
154
- List of intervals [(a1,b1),...,(ak,bk)] at t=0. If provided, these
155
- endpoints are used as labels/initial guesses and all are tracked.
156
- If omitted, this function currently raises ValueError (auto-detection
157
- is intentionally not implemented here to avoid fragile heuristics).
158
- eta : float, optional
159
- Small imaginary part used only to pick an initial physical root near
160
- each endpoint at t=0.
161
- dt_max : float, optional
162
- Maximum internal time step used for substepping in t.
163
- max_iter : int, optional
164
- Newton iterations per time step.
165
- tol : float, optional
166
- Tolerance for the 2x2 Newton solve.
167
-
168
- Returns
169
- -------
170
- edges : ndarray, shape (len(t_grid), 2*k)
171
- Tracked edges in the order [a1,b1,a2,b2,...] for each time.
172
- ok : ndarray of bool, same shape as edges
173
- Flags indicating whether each edge solve succeeded.
174
-
175
- Notes
176
- -----
177
- The solve is done by continuation in time. If two edges merge, the Newton
178
- system may become ill-conditioned near the merge time.
179
-
180
- Examples
181
- --------
182
- .. code-block:: python
183
-
184
- t_grid = numpy.linspace(0.0, 3.0, 61)
185
- support = [(a1,b1)]
186
- edges, ok = fd_evolve_edges(t_grid, a_coeffs, support=support,
187
- eta=1e-3)
188
-
189
- a_t = edges[:, 0]
190
- b_t = edges[:, 1]
152
+ If return_preimage=True, also returns zeta_hist and y_hist of shape
153
+ (nt, 2k).
191
154
  """
192
155
 
193
156
  t_grid = numpy.asarray(t_grid, dtype=float).ravel()
@@ -197,32 +160,43 @@ def evolve_edges(t_grid, a_coeffs, support=None, eta=1e-3, dt_max=0.1,
197
160
  raise ValueError("t_grid must be strictly increasing.")
198
161
 
199
162
  if support is None:
200
- raise ValueError(
201
- "support must be provided (auto-detection not implemented).")
163
+ raise ValueError("support must be provided (auto-detection not " +
164
+ "implemented).")
202
165
 
203
- # Flatten endpoints in the order [a1,b1,a2,b2,...]
166
+ # Flatten endpoints in fixed order [a1,b1,a2,b2,...]
204
167
  endpoints0 = []
205
168
  for a, b in support:
206
169
  endpoints0.append(float(a))
207
170
  endpoints0.append(float(b))
208
171
 
209
172
  m = len(endpoints0)
210
- edges = numpy.empty((t_grid.size, m), dtype=float)
173
+ complex_edges = numpy.empty((t_grid.size, m), dtype=numpy.complex128)
211
174
  ok = numpy.zeros((t_grid.size, m), dtype=bool)
212
175
 
213
- # Initialize spectral points (zeta,y) at t=0 for each endpoint
214
- zeta = numpy.empty(m, dtype=complex)
215
- y = numpy.empty(m, dtype=complex)
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)
216
186
 
217
187
  for j in range(m):
218
188
  z0, y0, ok0 = _init_edge_point_from_support(endpoints0[j], a_coeffs,
219
189
  eta=eta)
220
190
  zeta[j] = z0
221
191
  y[j] = y0
222
- edges[0, j] = float(numpy.real(z0)) # at t=0, z_edge = zeta
223
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
224
198
 
225
- # Time continuation
199
+ # Time stepping
226
200
  for it in range(1, t_grid.size):
227
201
  t0 = float(t_grid[it - 1])
228
202
  t1 = float(t_grid[it])
@@ -236,19 +210,22 @@ def evolve_edges(t_grid, a_coeffs, support=None, eta=1e-3, dt_max=0.1,
236
210
  t = t0 + dt * (ks / float(n_sub))
237
211
  for j in range(m):
238
212
  zeta[j], y[j], okj = _edge_newton_step(
239
- t, zeta[j], y[j], a_coeffs,
240
- max_iter=max_iter, tol=tol
213
+ t, zeta[j], y[j], a_coeffs, max_iter=max_iter, tol=tol
241
214
  )
242
215
  ok[it, j] = okj
243
216
 
244
217
  tau = float(numpy.exp(t1))
245
218
  c = tau - 1.0
246
- z_edge = zeta - c / y
219
+ complex_edges[it, :] = zeta - c / y
247
220
 
248
- edges[it, :] = numpy.real(z_edge)
249
- # ok[it,:] already set in last substep loop
221
+ if return_preimage:
222
+ zeta_hist[it, :] = zeta
223
+ y_hist[it, :] = y
250
224
 
251
- return edges, ok
225
+ if return_preimage:
226
+ return complex_edges, ok, zeta_hist, y_hist
227
+
228
+ return complex_edges, ok
252
229
 
253
230
 
254
231
  # ===========
@@ -282,6 +259,7 @@ def merge_edges(edges, tol=0.0):
282
259
  active_k : ndarray, shape (nt,)
283
260
  Number of remaining bulks (connected components) at each time.
284
261
  """
262
+
285
263
  edges = numpy.asarray(edges, dtype=float)
286
264
  nt, m = edges.shape
287
265
  if m % 2 != 0:
@@ -5,6 +5,7 @@
5
5
  import numpy
6
6
  from ._moments import AlgebraicStieltjesMoments
7
7
  from tqdm import tqdm
8
+ from math import comb
8
9
 
9
10
  __all__ = ['StieltjesPoly']
10
11
 
@@ -86,6 +87,8 @@ class StieltjesPoly(object):
86
87
  Coefficient matrix defining P(z, m) in the monomial basis. For fixed
87
88
  z, the coefficients of the polynomial in m are assembled from powers
88
89
  of z.
90
+ mom : callable, optional
91
+ A callable providing raw moments ``m_k = mom(k)``
89
92
  eps : float or None, optional
90
93
  If Im(z) == 0, use z + i*eps as the boundary evaluation point.
91
94
  If None and Im(z) == 0, eps is set to 1e-8 * max(1, |z|).
@@ -114,7 +117,7 @@ class StieltjesPoly(object):
114
117
  None, eps is chosen per element as 1e-8 * max(1, |z|).
115
118
  """
116
119
 
117
- def __init__(self, a, eps=None, height=2.0, steps=100, order=15):
120
+ def __init__(self, a, mom=None, eps=None, height=2.0, steps=100, order=15):
118
121
  a = numpy.asarray(a)
119
122
  if a.ndim != 2:
120
123
  raise ValueError("a must be a 2D array.")
@@ -125,13 +128,28 @@ class StieltjesPoly(object):
125
128
  self.height = height
126
129
  self.steps = steps
127
130
  self.order = order
131
+ if order < 3:
132
+ raise RuntimeError("order is too small, choose a larger value.")
128
133
 
129
- self.mom = AlgebraicStieltjesMoments(a)
130
- self.rad = 1.0 + self.height * self.mom.radius(self.order)
134
+ if mom is None:
135
+ self.mom = AlgebraicStieltjesMoments(a)
136
+ else:
137
+ self.mom = mom
138
+ self.mu = numpy.array([self.mom(j) for j in range(self.order+1)])
139
+ self.rad = max([numpy.abs(self.mu[j] / self.mu[j-1])
140
+ for j in range(2, self.order+1)])
141
+ self.rad = 1.0 + self.height * self.rad
131
142
  self.z0_p = 1j * self.rad
132
- self.m0_p = self.mom.stieltjes(self.z0_p, self.order)
143
+ self.m0_p = self._moment_est(self.z0_p)
133
144
  self.z0_m = -1j * self.rad
134
- self.m0_m = self.mom.stieltjes(self.z0_m, self.order)
145
+ self.m0_m = self._moment_est(self.z0_m)
146
+
147
+ def _moment_est(self, z):
148
+ # Estimate Stieltjes transform (root) using moment
149
+ # expansion
150
+ z = numpy.asarray(z)
151
+ pows = z[..., numpy.newaxis]**(-numpy.arange(self.order+1)-1)
152
+ return -numpy.sum(pows * self.mu, axis=-1)
135
153
 
136
154
  def _poly_coeffs_m(self, z_val):
137
155
  z_powers = z_val ** numpy.arange(self.a_l)
@@ -142,7 +160,8 @@ class StieltjesPoly(object):
142
160
  dtype=numpy.complex128)
143
161
  return numpy.roots(coeffs)
144
162
 
145
- def evaluate(self, z, eps=None, height=2.0, steps=100, order=15):
163
+ def evaluate(self, z, eps=None, height=2.0, steps=100, order=15, extrap=2,
164
+ num_angles=1):
146
165
  """
147
166
  Evaluate the Stieltjes-branch solution m(z) at a single point.
148
167
 
@@ -168,33 +187,46 @@ class StieltjesPoly(object):
168
187
  if half_sign == 0.0:
169
188
  half_sign = 1.0
170
189
 
171
- # # If z is outside radius of convergence, no homotopy
172
- # # necessary
173
- # if numpy.abs(z) > self.rad:
174
- # target = self.mom.stieltjes(z, self.order)
175
- # return select_root(self._poly_roots(z), z, target)
176
-
177
- if half_sign > 0.0:
178
- z0 = self.z0_p
179
- target = self.m0_p
180
- else:
181
- z0 = self.z0_m
182
- target = self.m0_m
190
+ # If z is outside radius of convergence, no homotopy
191
+ # necessary
192
+ if numpy.abs(z) > self.rad:
193
+ target = self._moment_est(z)
194
+ return select_root(self._poly_roots(z), z, target)
195
+
196
+ # z0 = z.real
197
+ # z0 = z0 + 1j*numpy.sqrt(self.rad**2 - z0**2)
198
+ # target = self._moment_est(z0)
199
+ # if half_sign > 0.0:
200
+ # z0 = self.z0_p
201
+ # target = self.m0_p
202
+ # else:
203
+ # z0 = self.z0_m
204
+ # target = self.m0_m
183
205
 
184
206
  # Initialize at z0
185
- w_prev = select_root(self._poly_roots(z0), z0, target)
186
-
187
- # Straight-line homotopy continuation
188
- for tau in numpy.linspace(0.0, 1.0, int(self.steps) + 1)[1:]:
189
- z_tau = z0 + tau * (z_eval - z0)
190
- w_prev = select_root(self._poly_roots(z_tau), z_tau, w_prev)
191
-
192
- return w_prev
193
-
194
- def __call__(self, z, progress=False):
207
+ res = 0
208
+ for theta in numpy.linspace(0, numpy.pi, num_angles+2)[1:-1]:
209
+ z0 = self.rad * numpy.exp(1j * theta) * half_sign
210
+ target = self._moment_est(z0)
211
+ coeffs = numpy.array([(-1)**k * comb(extrap, k + 1)
212
+ for k in range(extrap)])
213
+ w_prev = numpy.ones(extrap) * \
214
+ select_root(self._poly_roots(z0), z0, target)
215
+
216
+ # Straight-line homotopy continuation
217
+ for tau in numpy.linspace(0.0, 1.0, int(self.steps) + 1)[1:]:
218
+ z_tau = z0 + tau * (z_eval - z0)
219
+ target = numpy.dot(coeffs, w_prev)
220
+ w_prev[1:] = w_prev[0:-1]
221
+ w_prev[0] = select_root(self._poly_roots(z_tau), z_tau, target)
222
+ res += w_prev[0]
223
+
224
+ return res / num_angles
225
+
226
+ def __call__(self, z, progress=False, num_angles=1):
195
227
  # Scalar fast-path
196
228
  if numpy.isscalar(z):
197
- return self.evaluate(z)
229
+ return self.evaluate(z, num_angles=num_angles)
198
230
 
199
231
  # Array-like: evaluate elementwise, preserving shape
200
232
  z_arr = numpy.asarray(z)
@@ -206,7 +238,7 @@ class StieltjesPoly(object):
206
238
  else:
207
239
  indices = numpy.ndindex(z_arr.shape)
208
240
  for idx in indices:
209
- out[idx] = self.evaluate(z_arr[idx])
241
+ out[idx] = self.evaluate(z_arr[idx], num_angles=num_angles)
210
242
 
211
243
  return out
212
244