freealg 0.5.1__py3-none-any.whl → 0.5.3__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.5.1"
1
+ __version__ = "0.5.3"
freealg/_chebyshev.py CHANGED
@@ -13,10 +13,11 @@
13
13
 
14
14
  import numpy
15
15
  from scipy.special import eval_chebyu
16
- from ._series import partial_sum, wynn_epsilon
16
+ from ._series import partial_sum, wynn_epsilon, wynn_rho, levin_u, \
17
+ weniger_delta, brezinski_theta
17
18
 
18
19
  __all__ = ['chebyshev_sample_proj', 'chebyshev_kernel_proj',
19
- 'chebyshev_approx', 'chebyshev_stieltjes']
20
+ 'chebyshev_density', 'chebyshev_stieltjes']
20
21
 
21
22
 
22
23
  # =====================
@@ -115,11 +116,11 @@ def chebyshev_kernel_proj(xs, pdf, support, K=10, reg=0.0):
115
116
  return psi
116
117
 
117
118
 
118
- # ================
119
- # chebyshev approx
120
- # ================
119
+ # =================
120
+ # chebyshev density
121
+ # =================
121
122
 
122
- def chebyshev_approx(x, psi, support):
123
+ def chebyshev_density(x, psi, support):
123
124
  """
124
125
  Given \\psi_k, evaluate the approximate density \\rho(x).
125
126
 
@@ -164,7 +165,7 @@ def chebyshev_approx(x, psi, support):
164
165
  # chebushev stieltjes
165
166
  # ===================
166
167
 
167
- def chebyshev_stieltjes(z, psi, support, use_wynn_epsilon=False):
168
+ def chebyshev_stieltjes(z, psi, support, continuation='pade'):
168
169
  """
169
170
  Compute the Stieltjes transform m(z) for a Chebyshev‐II expansion
170
171
 
@@ -197,8 +198,8 @@ def chebyshev_stieltjes(z, psi, support, use_wynn_epsilon=False):
197
198
  support : tuple
198
199
  The support interval of the original density.
199
200
 
200
- use_wynn_epsilon : bool, default=False
201
- Use Wynn epsilon, otherwise assumes Pade is used.
201
+ continuation : str, default= ``'pade'``
202
+ Methof of analytiv continuation.
202
203
 
203
204
  Returns
204
205
  -------
@@ -225,14 +226,7 @@ def chebyshev_stieltjes(z, psi, support, use_wynn_epsilon=False):
225
226
  J = numpy.where(Jp.imag > 0, Jm, Jp)
226
227
 
227
228
  # This depends on the method of analytic continuation
228
- if use_wynn_epsilon:
229
- # Flatten J before passing to Wynn method.
230
- psi_zero = numpy.concatenate([[0], psi])
231
- Sn = partial_sum(psi_zero, J.ravel())
232
- S = wynn_epsilon(Sn)
233
- S = S.reshape(J.shape)
234
-
235
- else:
229
+ if continuation == 'pade':
236
230
  # Build powers J^(k+1) for k = 0, ..., K
237
231
  K = len(psi) - 1
238
232
  Jpow = J[..., None] ** numpy.arange(1, K+2) # shape: (..., K+1)
@@ -240,6 +234,26 @@ def chebyshev_stieltjes(z, psi, support, use_wynn_epsilon=False):
240
234
  # Summing psi_k * J^(k+1)
241
235
  S = numpy.sum(psi * Jpow, axis=-1)
242
236
 
237
+ else:
238
+ # Flatten J before passing to Wynn method.
239
+ psi_zero = numpy.concatenate([[0], psi])
240
+ Sn = partial_sum(psi_zero, J.ravel(), p=0)
241
+
242
+ if continuation == 'wynn-eps':
243
+ S = wynn_epsilon(Sn)
244
+ elif continuation == 'wynn-rho':
245
+ S = wynn_rho(Sn)
246
+ elif continuation == 'levin':
247
+ S = levin_u(Sn)
248
+ elif continuation == 'weniger':
249
+ S = weniger_delta(Sn)
250
+ elif continuation == 'brezinski':
251
+ S = brezinski_theta(Sn)
252
+ else:
253
+ raise NotImplementedError('"continuation" is invalid.')
254
+
255
+ S = S.reshape(J.shape)
256
+
243
257
  # Assemble m(z)
244
258
  m_z = -(2.0 / span) * numpy.pi * S
245
259
 
freealg/_jacobi.py CHANGED
@@ -14,8 +14,10 @@
14
14
  import numpy
15
15
  from scipy.special import eval_jacobi, roots_jacobi
16
16
  from scipy.special import gammaln, beta as Beta
17
+ from ._series import wynn_epsilon, wynn_rho, levin_u, weniger_delta, \
18
+ brezinski_theta
17
19
 
18
- __all__ = ['jacobi_sample_proj', 'jacobi_kernel_proj', 'jacobi_approx',
20
+ __all__ = ['jacobi_sample_proj', 'jacobi_kernel_proj', 'jacobi_density',
19
21
  'jacobi_stieltjes']
20
22
 
21
23
 
@@ -108,13 +110,13 @@ def jacobi_kernel_proj(xs, pdf, support, K=10, alpha=0.0, beta=0.0, reg=0.0):
108
110
  return psi
109
111
 
110
112
 
111
- # =============
112
- # jacobi approx
113
- # =============
113
+ # ==============
114
+ # jacobi density
115
+ # ==============
114
116
 
115
- def jacobi_approx(x, psi, support, alpha=0.0, beta=0.0):
117
+ def jacobi_density(x, psi, support, alpha=0.0, beta=0.0):
116
118
  """
117
- Reconstruct Jacobi approximation.
119
+ Reconstruct Jacobi approximation of density.
118
120
 
119
121
  Parameters
120
122
  ----------
@@ -144,8 +146,8 @@ def jacobi_approx(x, psi, support, alpha=0.0, beta=0.0):
144
146
  w = (1 - t)**alpha * (1 + t)**beta
145
147
  P = numpy.vstack([eval_jacobi(k, alpha, beta, t) for k in range(len(psi))])
146
148
 
147
- rho_t = w * (psi @ P) # density in tvariable
148
- rho_x = rho_t * (2.0 / (lam_p - lam_m)) # back to xvariable
149
+ rho_t = w * (psi @ P) # density in t-variable
150
+ rho_x = rho_t * (2.0 / (lam_p - lam_m)) # back to x-variable
149
151
 
150
152
  return rho_x
151
153
 
@@ -154,7 +156,8 @@ def jacobi_approx(x, psi, support, alpha=0.0, beta=0.0):
154
156
  # jacobi stieltjes
155
157
  # ================
156
158
 
157
- def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40):
159
+ def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40,
160
+ continuation='pade'):
158
161
  """
159
162
  Compute m(z) = sum_k psi_k * m_k(z) where
160
163
 
@@ -178,22 +181,33 @@ def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40):
178
181
  Minimum quadrature size. For degree-k polynomial we use
179
182
  n_quad = max(n_base, k+1).
180
183
 
184
+ continuation : str, default= ``'pade'``
185
+ Methof of analytiv continuation.
186
+
181
187
  Returns
182
188
  -------
183
189
 
184
- m1 : ndarray (same shape as z)
190
+ m1 : ndarray
191
+ Same shape as z
185
192
 
186
- m12 : ndarray (same shape as z)
193
+ m2 : ndarray
194
+ Same shape as z
187
195
  """
188
196
 
189
197
  z = numpy.asarray(z, dtype=numpy.complex128)
190
198
  lam_minus, lam_plus = support
191
199
  span = lam_plus - lam_minus
192
200
  centre = 0.5 * (lam_plus + lam_minus)
193
- u_z = (2.0 / span) * (z - centre) # map z -> u
201
+
202
+ # Map z -> u in the standard [-1,1] domain
203
+ u = (2.0 / span) * (z - centre)
194
204
 
195
205
  m_total = numpy.zeros_like(z, dtype=numpy.complex128)
196
206
 
207
+ if continuation != 'pade':
208
+ # Stores m with the ravel size of z.
209
+ m_partial = numpy.zeros((psi.size, z.size), dtype=numpy.complex128)
210
+
197
211
  for k, psi_k in enumerate(psi):
198
212
  # Select quadrature size tailored to this P_k
199
213
  n_quad = max(n_base, k + 1)
@@ -205,14 +219,54 @@ def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40):
205
219
  # Integrand values at nodes: w_nodes already include the weight
206
220
  integrand = w_nodes * P_k_nodes # (n_quad,)
207
221
 
208
- # Broadcast over z: shape (n_quad, ...) / ...
209
- diff = u_z[None, ...] - t_nodes[:, None, None] # (n_quad, Ny, Nx)
210
- m_k = (integrand[:, None, None] / diff).sum(axis=0)
222
+ # Evaluate jacobi polynomals of the second kind, Q_k using quadrature
223
+ diff = t_nodes[:, None, None] - u[None, ...] # (n_quad, Ny, Nx)
224
+ Q_k = (integrand[:, None, None] / diff).sum(axis=0)
225
+
226
+ # Principal branch
227
+ m_k = (2.0 / span) * Q_k
228
+
229
+ # Compute secondary branch from the principal branch
230
+ if continuation != 'pade':
231
+
232
+ # Compute analytic extension of rho(z) to lower-half plane for
233
+ # when rho is just the k-th Jacobi basis: w(z) P_k(z). FOr this,
234
+ # we create a psi array (called unit_psi_j), with all zeros, except
235
+ # its k-th element is one. Ten we call jacobi_density.
236
+ unit_psi_k = numpy.zeros_like(psi)
237
+ unit_psi_k[k] = 1.0
238
+
239
+ # Only lower-half plane
240
+ mask_m = z.imag <= 0
241
+ z_m = z[mask_m]
242
+
243
+ # Dnesity here is rho = w(z) P_k
244
+ rho_k = jacobi_density(z_m.ravel(), unit_psi_k, support,
245
+ alpha=alpha, beta=beta).reshape(z_m.shape)
246
+
247
+ # Secondary branch is principal branch + 2 \pi i rho, using Plemelj
248
+ # (in fact, Riemann-Hirbert jump).
249
+ m_k[mask_m] = m_k[mask_m] + 2.0 * numpy.pi * 1j * rho_k
211
250
 
212
251
  # Accumulate with factor 2/span
213
- m_total += psi_k * (2.0 / span) * m_k
252
+ m_total += psi_k * m_k
253
+
254
+ if continuation != 'pade':
255
+ m_partial[k, :] = m_total.ravel()
256
+
257
+ if continuation != 'pade':
258
+
259
+ if continuation == 'wynn-eps':
260
+ S = wynn_epsilon(m_partial)
261
+ elif continuation == 'wynn-rho':
262
+ S = wynn_rho(m_partial)
263
+ elif continuation == 'levin':
264
+ S = levin_u(m_partial)
265
+ elif continuation == 'weniger':
266
+ S = weniger_delta(m_partial)
267
+ elif continuation == 'brezinski':
268
+ S = brezinski_theta(m_partial)
214
269
 
215
- # We use a negative sign convention
216
- m_total = -m_total
270
+ m_total = S.reshape(z.shape)
217
271
 
218
272
  return m_total
freealg/_series.py CHANGED
@@ -12,30 +12,35 @@
12
12
 
13
13
  import numpy
14
14
 
15
- __all__ = ['partial_sum', 'wynn_epsilon']
15
+ __all__ = ['partial_sum', 'wynn_epsilon', 'wynn_rho', 'levin_u',
16
+ 'weniger_delta', 'brezinski_theta']
16
17
 
17
18
 
18
19
  # ===========
19
20
  # partial sum
20
21
  # ===========
21
22
 
22
- def partial_sum(coeffs, x):
23
+ def partial_sum(coeffs, x, p=0.0):
23
24
  """
24
25
  Compute partial sum:
25
26
 
26
27
  .. math::
27
28
 
28
- S_n(x) = \\sum_{n=0}^{N} coeffs[n] * x^n.
29
+ S_n(x) = \\sum_{n=0}^{N-1} coeffs[n] * x^{n+p}.
29
30
 
30
31
  Parameters
31
32
  ----------
32
33
 
33
34
  coeffs : array_like
34
- Coefficients [a0, a1, a2, ...] of the power series of the size N.
35
+ Coefficients [a_0, a_1, a_2, ..., a_{N-1}] of the power series of the
36
+ size N.
35
37
 
36
38
  x : numpy.array
37
39
  A flattened array of the size d.
38
40
 
41
+ d : float, default=0.0
42
+ Offset power.
43
+
39
44
  Returns
40
45
  -------
41
46
 
@@ -44,21 +49,26 @@ def partial_sum(coeffs, x):
44
49
  partial sum.
45
50
  """
46
51
 
47
- xn = x.ravel()
52
+ x_ = x.ravel()
48
53
  N = len(coeffs)
49
- d = xn.size
54
+ d = x_.size
50
55
 
51
56
  # Forming partial sum via Horner method
52
57
  Sn = numpy.zeros((N, d), dtype=x.dtype)
53
58
  sum_ = numpy.zeros((d,), dtype=x.dtype)
54
59
  pow_x = numpy.ones((d,), dtype=x.dtype)
55
60
 
61
+ if p == 1:
62
+ pow_x *= x_
63
+ elif p != 0:
64
+ pow_x *= x_**p
65
+
56
66
  for n in range(N):
57
67
  sum_ += coeffs[n] * pow_x
58
68
  Sn[n, :] = sum_
59
69
 
60
70
  if n < N-1:
61
- pow_x *= xn
71
+ pow_x *= x_
62
72
 
63
73
  return Sn
64
74
 
@@ -84,40 +94,361 @@ def wynn_epsilon(Sn):
84
94
  S : numpy.array
85
95
  A 1D array of the size (d,) which is the accelerated value of the
86
96
  series at each vector element.
97
+
98
+ Notes
99
+ -----
100
+
101
+ Given a series of vectors:
102
+
103
+ .. math::
104
+
105
+ (S_n)_{n=1}^N = (S1, \\dots, S_n)
106
+
107
+ this function finds the limit S = \\lim_{n \\to infty} S_n.
108
+
109
+ Each :math:`S_i \\in \\mathbb{C}^d` is a vector. However, instead of using
110
+ the vector version of the Wynn's epsilon algorithm, we use the scalar
111
+ version on each component of the vector. The reason for this is that in our
112
+ dataset, each component has its own convergence rate. The convergence rate
113
+ of vector version of the algorithm is bounded by the worse point, and this
114
+ potentially stall convergence for all points. As such, vector version is
115
+ avoided.
116
+
117
+ In our dataset, the series is indeed divergent. The Wynn's accelerated
118
+ method computes the principal value of the convergence series.
87
119
  """
88
120
 
89
121
  # N: number of partial sums, d: vector size
90
122
  N, d = Sn.shape
91
123
 
92
- eps = numpy.zeros((N, N, d), dtype=Sn.dtype)
93
- eps[0, :, :] = Sn
124
+ # Epsilons of stage k-1 and k-2
125
+ eps_prev = Sn.copy() # row k-1
126
+ eps_pprev = None # row k-2
94
127
 
95
- tol = numpy.finfo(float).eps
128
+ tol = numpy.finfo(Sn.dtype).eps
96
129
 
97
- # Wynn's epsilon algorithm
130
+ # Wynn's epsilon triangle table
98
131
  for k in range(1, N):
99
132
  Nk = N - k
100
133
 
101
- delta = eps[k-1, 1:N-k+1, :] - eps[k-1, :Nk, :]
134
+ delta = eps_prev[1:Nk+1, :] - eps_prev[:Nk, :]
135
+ small = numpy.abs(delta) <= \
136
+ tol * numpy.maximum(1.0, numpy.abs(eps_prev[1:Nk+1, :]))
102
137
 
103
138
  # Reciprocal of delta
104
139
  rec_delta = numpy.empty_like(delta)
140
+ rec_delta[small] = 0.0j
141
+ rec_delta[~small] = 1.0 / delta[~small]
105
142
 
106
- # Avoid division by zero error
107
- mask_inf = numpy.abs(delta) < tol
108
- rec_delta[mask_inf] = numpy.inf
109
- rec_delta[~mask_inf] = 1.0 / delta[~mask_inf]
143
+ # Current epsilon of row k
144
+ eps_curr = rec_delta
145
+ if k > 1:
146
+ eps_curr += eps_pprev[1:Nk+1, :]
110
147
 
111
- mask_zero = numpy.logical_or(numpy.isinf(delta),
112
- numpy.isnan(delta))
113
- rec_delta[mask_zero] = 0.0
148
+ # Roll rows
149
+ eps_pprev = eps_prev
150
+ eps_prev = eps_curr
114
151
 
115
- eps[k, :Nk, :] = rec_delta
152
+ # Last even row
153
+ if (N - 1) % 2 == 0:
154
+ # N is odd, so use step k-1
155
+ S = eps_prev[0, :]
156
+ else:
157
+ # N is even, so use k-2
158
+ S = eps_pprev[0, :]
116
159
 
117
- if k > 1:
118
- eps[k, :Nk, :] += eps[k-2, 1:Nk+1, :]
160
+ return S
161
+
162
+
163
+ # ========
164
+ # wynn rho
165
+ # ========
166
+
167
+ def wynn_rho(Sn, beta=0.0):
168
+ """
169
+ Accelerate convergence of a series using Wynn's rho algorithm.
170
+
171
+ Parameters
172
+ ----------
173
+
174
+ Sn : numpy.ndarray
175
+ A 2D array of shape ``(N, d)``, where *N* is the number of partial
176
+ sums and *d* is the vector size.
177
+
178
+ beta : float, default=0.0
179
+ Shift parameter in the rho recursion, usually chosen in the range
180
+ ``0 < beta <= 1``.
181
+
182
+ Returns
183
+ -------
184
+
185
+ S : numpy.ndarray
186
+ A 1D array of shape ``(d,)`` giving the rho‑accelerated estimate
187
+ of the series limit for each component.
188
+
189
+ Notes
190
+ -----
191
+
192
+ Let ``S_n`` be the *n*‑th partial sum of the (possibly divergent)
193
+ sequence. Wynn's rho algorithm builds a triangular table
194
+ ``rho[k, n]`` (row *k*, column *n*) as follows:
195
+
196
+ rho[-1, n] = 0
197
+ rho[ 0, n] = S_n
198
+
199
+ rho[k, n] = rho[k-2, n+1] +
200
+ (n + beta + k - 1) / (rho[k-1, n+1] - rho[k-1, n])
201
+
202
+ Only even rows (k even) provide improved approximants. As with
203
+ ``wynn_epsilon``, we apply the scalar recursion component‑wise so that a
204
+ slowly converging component does not stall the others.
205
+ """
206
+
207
+ # N: number of partial sums, d: vector size
208
+ N, d = Sn.shape
209
+
210
+ # Rho of stage k-1 and k-2
211
+ rho_prev = Sn.copy() # row k-1
212
+ rho_pprev = None # row k-2
213
+
214
+ tol = numpy.finfo(Sn.dtype).eps
215
+
216
+ # Wynn's rho triangule table
217
+ for k in range(1, N):
218
+ Nk = N - k
219
+
220
+ delta = rho_prev[1:Nk+1, :] - rho_prev[:Nk, :]
221
+ small = numpy.abs(delta) <= \
222
+ tol * numpy.maximum(1.0, numpy.abs(rho_prev[1:Nk+1, :]))
223
+
224
+ coef = (beta + (k - 1) + numpy.arange(Nk))[:, None] # (Nk, 1)
225
+ coef = numpy.repeat(coef, d, axis=1) # (Nk, d)
226
+
227
+ # Current rho of row k
228
+ rho_curr = numpy.empty_like(delta)
229
+ rho_curr[small] = 0.0j # treat near-zero denominator
230
+
231
+ if k == 1:
232
+ rho_curr[~small] = coef[~small] / delta[~small]
233
+ else:
234
+ rho_curr[~small] = rho_pprev[1:Nk+1][~small] + \
235
+ coef[~small] / delta[~small]
236
+
237
+ # Roll rows
238
+ rho_pprev = rho_prev
239
+ rho_prev = rho_curr
240
+
241
+ # Last even row
242
+ if (N - 1) % 2 == 0:
243
+ # N is odd, so use step k-1
244
+ S = rho_prev[0, :]
245
+ else:
246
+ # N is even, so use k-2
247
+ S = rho_pprev[0, :]
248
+
249
+ return S
250
+
251
+
252
+ # ========
253
+ # levin u
254
+ # ========
255
+
256
+ def levin_u(Sn, omega=None, beta=0.0):
257
+ """
258
+ Levin u‑transform (vector form).
259
+
260
+ Parameters
261
+ ----------
262
+ Sn : ndarray, shape (N, d)
263
+ First N partial sums of a vector series.
264
+ omega : None or ndarray, shape (N-1, d), optional
265
+ Remainder estimate. If None, uses omega_n = S_{n+1} - S_n.
266
+ beta : float, optional
267
+ Levin shift parameter (default 0.0).
268
+
269
+ Returns
270
+ -------
271
+ S : ndarray, shape (d,)
272
+ Accelerated sum / antilimit.
273
+ """
274
+
275
+ Sn = numpy.asarray(Sn)
276
+ N, d = Sn.shape
277
+ if N < 3:
278
+ raise ValueError("Need at least 3 partial sums for Levin u.")
279
+
280
+ # default omega_n = forward difference
281
+ if omega is None:
282
+ omega = Sn[1:, :] - Sn[:-1, :]
283
+ else:
284
+ omega = numpy.asarray(omega)
285
+ if omega.shape != (N - 1, d):
286
+ raise ValueError("omega must have shape (N-1, d).")
287
+
288
+ tol = numpy.finfo(Sn.dtype).eps
289
+ m = N - 2 # highest possible order
290
+
291
+ # binomial coefficients with alternating sign
292
+ Cmk = numpy.empty(m + 1, dtype=Sn.dtype)
293
+ Cmk[0] = 1.0
294
+ for k in range(1, m + 1):
295
+ Cmk[k] = Cmk[k - 1] * (m - k + 1) / k
296
+ Cmk *= (-1.0) ** numpy.arange(m + 1)
297
+
298
+ # powers (k + beta)^(m-1)
299
+ if m == 1:
300
+ Pk = numpy.ones(m + 1, dtype=Sn.dtype)
301
+ else:
302
+ Pk = (numpy.arange(m + 1, dtype=Sn.dtype) + beta) ** (m - 1)
303
+
304
+ numer = numpy.zeros(d, dtype=Sn.dtype)
305
+ denom = numpy.zeros(d, dtype=Sn.dtype)
306
+
307
+ for k in range(m + 1):
308
+ idx = k
309
+ w = omega[idx, :]
310
+
311
+ inv_w = numpy.empty_like(w)
312
+ mask = numpy.abs(w) < tol
313
+ inv_w[mask] = 0.0
314
+ inv_w[~mask] = 1.0 / w[~mask]
315
+
316
+ coeff = Cmk[k] * Pk[k]
317
+ numer += coeff * Sn[idx, :] * inv_w
318
+ denom += coeff * inv_w
319
+
320
+ return numer / denom
321
+
322
+
323
+ # =============
324
+ # weniger delta
325
+ # =============
326
+
327
+ def weniger_delta(Sn):
328
+ """
329
+ Weniger's nonlinear delta^2 sequence transformation.
330
+
331
+ Parameters
332
+ ----------
333
+
334
+ Sn : numpy.ndarray
335
+ Array of shape (N, d) containing the first N partial sums of the
336
+ series.
337
+
338
+ Returns
339
+ -------
340
+
341
+ S : numpy.ndarray
342
+ Array of shape (d,) giving the Δ²‑accelerated limit estimate for each
343
+ component.
344
+ """
345
+
346
+ N, d = Sn.shape
347
+
348
+ # Need at least three partial sums to form Δ²
349
+ if N < 3:
350
+ return Sn[-1, :].copy()
351
+
352
+ # First and second forward differences
353
+ delta1 = Sn[1:] - Sn[:-1] # shape (N-1, d)
354
+ delta2 = delta1[1:] - delta1[:-1] # shape (N-2, d)
355
+
356
+ tol = numpy.finfo(Sn.real.dtype).eps
357
+
358
+ # Safe reciprocal of delta2
359
+ small = numpy.abs(delta2) <= tol * numpy.maximum(
360
+ 1.0, numpy.abs(delta1[:-1]))
361
+
362
+ rec_delta2 = numpy.empty_like(delta2)
363
+ rec_delta2[small] = 0.0j
364
+ rec_delta2[~small] = 1.0 / delta2[~small]
365
+
366
+ # Delta sequence, length N-2
367
+ delta_sq = Sn[:-2] - (delta1[:-1] ** 2) * rec_delta2
368
+
369
+ # Return the last Delta2 term as the accelerated estimate
370
+ S = delta_sq[-1, :]
371
+
372
+ return S
373
+
374
+
375
+ # ===============
376
+ # brezinski theta
377
+ # ===============
378
+
379
+ def brezinski_theta(Sn):
380
+ """
381
+ Accelerate convergence of a series using Brezinski's theta algorithm.
382
+
383
+ Parameters
384
+ ----------
385
+
386
+ Sn : numpy.ndarray
387
+ A 2‑D array of the size ``(N, d)``, where `N` is the number of partial
388
+ sums and `d` is the vector size.
389
+
390
+ Returns
391
+ -------
392
+
393
+ S : numpy.ndarray
394
+ A 1‑D array of the size ``(d,)`` – the theta‑accelerated estimate of
395
+ the series limit in each vector component.
396
+ """
397
+
398
+ N, d = Sn.shape
399
+
400
+ theta_prev = Sn.copy() # row k-1
401
+ theta_pprev = numpy.zeros_like(theta_prev) # row k-2
402
+
403
+ tol = numpy.finfo(Sn.dtype).eps
404
+
405
+ for k in range(1, N):
406
+ L_prev = theta_prev.shape[0] # current row length
407
+
408
+ if k % 2 == 1:
409
+
410
+ # Odd row 2m+1
411
+ if L_prev < 2:
412
+ break
413
+
414
+ delta = theta_prev[1:] - theta_prev[:-1] # len = L
415
+ theta_pp = theta_pprev[1:L_prev] # len = L
416
+
417
+ small = numpy.abs(delta) <= \
418
+ tol * numpy.maximum(1.0, numpy.abs(theta_prev[1:]))
419
+
420
+ theta_curr = numpy.empty_like(delta)
421
+ theta_curr[small] = 0.0j
422
+ theta_curr[~small] = theta_pp[~small] + 1.0 / delta[~small]
423
+
424
+ else:
425
+
426
+ # Even row 2m+2
427
+ if L_prev < 3:
428
+ break
429
+
430
+ delta_even = theta_pprev[2:L_prev] - theta_pprev[1:L_prev-1]
431
+ delta_odd = theta_prev[1:L_prev-1] - theta_prev[:L_prev-2]
432
+ delta2_odd = (theta_prev[2:L_prev] - 2.0 * theta_prev[1:L_prev-1]
433
+ + theta_prev[:L_prev-2])
434
+
435
+ small = numpy.abs(delta2_odd) <= tol * numpy.maximum(
436
+ 1.0, numpy.abs(theta_prev[1:L_prev-1]))
437
+
438
+ theta_curr = numpy.empty_like(delta_odd)
439
+ theta_curr[small] = theta_pprev[1:L_prev-1][small]
440
+ theta_curr[~small] = (
441
+ theta_pprev[1:L_prev-1][~small] +
442
+ (delta_even[~small] * delta_odd[~small]) /
443
+ delta2_odd[~small])
444
+
445
+ # roll rows
446
+ theta_pprev = theta_prev
447
+ theta_prev = theta_curr
119
448
 
120
- k_even = 2 * ((N - 1) // 2)
121
- series = eps[k_even, 0, :]
449
+ if (N - 1) % 2 == 0:
450
+ S = theta_prev[0]
451
+ else:
452
+ S = theta_pprev[0]
122
453
 
123
- return series
454
+ return S
freealg/_util.py CHANGED
@@ -89,7 +89,7 @@ def beta_kde(eig, xs, lam_m, lam_p, h):
89
89
  # force density
90
90
  # =============
91
91
 
92
- def force_density(psi0, support, approx, grid, alpha=0.0, beta=0.0):
92
+ def force_density(psi0, support, density, grid, alpha=0.0, beta=0.0):
93
93
  """
94
94
  Starting from psi0 (raw projection), solve
95
95
  min 0.5 ||psi - psi0||^2
@@ -113,26 +113,26 @@ def force_density(psi0, support, approx, grid, alpha=0.0, beta=0.0):
113
113
 
114
114
  # Enforce positivity
115
115
  constraints.append({'type': 'ineq',
116
- 'fun': lambda psi: approx(grid, psi)})
116
+ 'fun': lambda psi: density(grid, psi)})
117
117
 
118
118
  # Enforce unit mass
119
119
  constraints.append({
120
120
  'type': 'eq',
121
- 'fun': lambda psi: numpy.trapz(approx(grid, psi), grid) - 1.0
121
+ 'fun': lambda psi: numpy.trapz(density(grid, psi), grid) - 1.0
122
122
  })
123
123
 
124
124
  # Enforce zero at left edge
125
125
  if beta <= 0.0 and beta > -0.5:
126
126
  constraints.append({
127
127
  'type': 'eq',
128
- 'fun': lambda psi: approx(numpy.array([lam_m]), psi)[0]
128
+ 'fun': lambda psi: density(numpy.array([lam_m]), psi)[0]
129
129
  })
130
130
 
131
131
  # Enforce zero at right edge
132
132
  if alpha <= 0.0 and alpha > -0.5:
133
133
  constraints.append({
134
134
  'type': 'eq',
135
- 'fun': lambda psi: approx(numpy.array([lam_p]), psi)[0]
135
+ 'fun': lambda psi: density(numpy.array([lam_p]), psi)[0]
136
136
  })
137
137
 
138
138
  # Solve a small quadratic programming
@@ -146,7 +146,7 @@ def force_density(psi0, support, approx, grid, alpha=0.0, beta=0.0):
146
146
 
147
147
  # Normalize first mode to unit mass
148
148
  x = numpy.linspace(lam_m, lam_p, 1000)
149
- rho = approx(x, psi)
149
+ rho = density(x, psi)
150
150
  mass = numpy.trapezoid(rho, x)
151
151
  psi[0] = psi[0] / mass
152
152
 
freealg/freeform.py CHANGED
@@ -16,10 +16,10 @@ from scipy.stats import gaussian_kde
16
16
  # from statsmodels.nonparametric.kde import KDEUnivariate
17
17
  from functools import partial
18
18
  from ._util import compute_eig, beta_kde, force_density
19
- from ._jacobi import jacobi_sample_proj, jacobi_kernel_proj, jacobi_approx, \
19
+ from ._jacobi import jacobi_sample_proj, jacobi_kernel_proj, jacobi_density, \
20
20
  jacobi_stieltjes
21
21
  from ._chebyshev import chebyshev_sample_proj, chebyshev_kernel_proj, \
22
- chebyshev_approx, chebyshev_stieltjes
22
+ chebyshev_density, chebyshev_stieltjes
23
23
  from ._damp import jackson_damping, lanczos_damping, fejer_damping, \
24
24
  exponential_damping, parzen_damping
25
25
  from ._plot_util import plot_fit, plot_density, plot_hilbert, plot_stieltjes
@@ -234,13 +234,18 @@ class FreeForm(object):
234
234
  If `True`, it forces the density to have unit mass and to be
235
235
  strictly positive.
236
236
 
237
- continuation : {``'pade'``, ``'wynn'``}, default= ``'pade'``
237
+ continuation : {``'pade'``, ``'wynn-eps'``, ``'wynn-rho'``,
238
+ ``'levin'``, ``'weniger'``, ``'brezinski'``}, default= ``'pade'``
238
239
  Method of analytic continuation to construct the second branch of
239
240
  Steltjes transform in the lower-half complex plane:
240
241
 
241
242
  * ``'pade'``: using Riemann-Hilbert problem with Pade
242
243
  approximation.
243
- * ``'wynn'``: Wynn's epsilon algorithm.
244
+ * ``'wynn-eps'``: Wynn's :math:`\\epsilon` algorithm.
245
+ * ``'wynn-rho'``: Wynn's :math:`\\rho` algorithm.
246
+ * ``'levin'``: Levin's :math:`u` transform.
247
+ * ``'weniger'``: Weniger's :math:`\\delta^2` algorithm.
248
+ * ``'brezinski'``: Brezinski's :math:`\\theta` algorithm.
244
249
 
245
250
  pade_p : int, default=0
246
251
  Degree of polynomial :math:`P(z)` is :math:`q+p` where :math:`p`
@@ -401,15 +406,15 @@ class FreeForm(object):
401
406
  grid = numpy.linspace(self.lam_m, self.lam_p, 500)
402
407
 
403
408
  if method == 'jacobi':
404
- approx = partial(jacobi_approx, support=self.support,
405
- alpha=alpha, beta=beta)
409
+ density = partial(jacobi_density, support=self.support,
410
+ alpha=alpha, beta=beta)
406
411
  elif method == 'chebyshev':
407
- approx = partial(chebyshev_approx, support=self.support)
412
+ density = partial(chebyshev_density, support=self.support)
408
413
  else:
409
414
  raise RuntimeError('"method" is invalid.')
410
415
 
411
416
  # Enforce positivity, unit mass, and zero at edges
412
- psi = force_density(psi, support=self.support, approx=approx,
417
+ psi = force_density(psi, support=self.support, density=density,
413
418
  grid=grid, alpha=alpha, beta=beta)
414
419
 
415
420
  # Update attributes
@@ -419,7 +424,8 @@ class FreeForm(object):
419
424
  self.beta = beta
420
425
 
421
426
  # Analytic continuation
422
- if continuation not in ['pade', 'wynn']:
427
+ if continuation not in ['pade', 'wynn-eps', 'wynn-rho', 'levin',
428
+ 'weniger', 'brezinski']:
423
429
  raise NotImplementedError('"continuation" method is invalid.')
424
430
 
425
431
  self.continuation = continuation
@@ -438,12 +444,6 @@ class FreeForm(object):
438
444
  # Do nothing. Make sure _pade_sol is still None
439
445
  self._pade_sol = None
440
446
 
441
- if method != 'chebyshev':
442
- raise NotImplementedError(
443
- 'Up to the current version, the analytic continuation ' +
444
- 'using "wynn" is only implemented for "chebyshev" ' +
445
- 'estimation method.')
446
-
447
447
  if plot:
448
448
  if self._pade_sol is not None:
449
449
  g_supp_approx = eval_pade(x_supp[None, :],
@@ -539,10 +539,10 @@ class FreeForm(object):
539
539
  mask = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
540
540
 
541
541
  if self.method == 'jacobi':
542
- rho[mask] = jacobi_approx(x[mask], self.psi, self.support,
543
- self.alpha, self.beta)
542
+ rho[mask] = jacobi_density(x[mask], self.psi, self.support,
543
+ self.alpha, self.beta)
544
544
  elif self.method == 'chebyshev':
545
- rho[mask] = chebyshev_approx(x[mask], self.psi, self.support)
545
+ rho[mask] = chebyshev_density(x[mask], self.psi, self.support)
546
546
  else:
547
547
  raise RuntimeError('"method" is invalid.')
548
548
 
@@ -825,18 +825,13 @@ class FreeForm(object):
825
825
  if self.method == 'jacobi':
826
826
  stieltjes = partial(jacobi_stieltjes, psi=self.psi,
827
827
  support=self.support, alpha=self.alpha,
828
- beta=self.beta) # n_base = n_base
828
+ beta=self.beta, continuation=self.continuation)
829
+ # n_base = n_base
829
830
 
830
831
  elif self.method == 'chebyshev':
831
-
832
- if self.continuation == 'wynn':
833
- use_wynn_epsilon = True
834
- else:
835
- use_wynn_epsilon = False
836
-
837
832
  stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
838
833
  support=self.support,
839
- use_wynn_epsilon=use_wynn_epsilon)
834
+ continuation=self.continuation)
840
835
 
841
836
  mask_p = z.imag >= 0.0
842
837
  mask_m = z.imag < 0.0
@@ -857,7 +852,8 @@ class FreeForm(object):
857
852
  m2[mask_m] = -m1[mask_m] + self._glue(
858
853
  z[mask_m].reshape(-1, 1)).ravel()
859
854
 
860
- elif self.continuation == 'wynn':
855
+ elif self.continuation in ['wynn-eps', 'wynn-rho', 'levin', 'weniger',
856
+ 'brezinski']:
861
857
  m2[:] = stieltjes(z.reshape(-1, 1)).reshape(*m2.shape)
862
858
  if branches:
863
859
  m1[mask_p] = m2[mask_p]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Free probability for large matrices
5
5
  Home-page: https://github.com/ameli/freealg
6
6
  Download-URL: https://github.com/ameli/freealg/archive/main.zip
@@ -26,7 +26,7 @@ Classifier: Intended Audience :: Science/Research
26
26
  Classifier: Intended Audience :: Developers
27
27
  Classifier: Topic :: Software Development
28
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
- Requires-Python: >=3.9
29
+ Requires-Python: >=3.10
30
30
  Description-Content-Type: text/x-rst
31
31
  License-File: LICENSE.txt
32
32
  License-File: AUTHORS.txt
@@ -1,26 +1,26 @@
1
1
  freealg/__init__.py,sha256=KfzqG7qig6ZZCKqgHcC2ApTg4rTLkrbJsvVoJd8UFG8,625
2
- freealg/__version__.py,sha256=eZ1bOun1DDVV0YLOBW4wj2FP1ajReLjbIrGmzN7ASBw,22
3
- freealg/_chebyshev.py,sha256=-XiMKPra9pjkvMLjLusPo8n4NfjcJd9K0tAm5TF8PdA,6311
2
+ freealg/__version__.py,sha256=tgzuqHKcEdKBaP57F5oXxq4XlW2n9J4Fj8ZGu7nGOZg,22
3
+ freealg/_chebyshev.py,sha256=UHU9e9LyiTJcs_01h3oIbX8_QQ9wH0P8GRmA3phvWhU,6780
4
4
  freealg/_damp.py,sha256=k2vtBtWOxQBf4qXaWu_En81lQBXbEO4QbxxWpvuVhdE,1802
5
5
  freealg/_decompress.py,sha256=RKYnuLKMx1V6-XrFjYX1UkGMRbUD9Gmf9XTyS4ffPSc,31970
6
- freealg/_jacobi.py,sha256=AT4ONSHGGDxVKE3MGMLyMR8uDFiO-e9u3x5udYfdJJk,5635
6
+ freealg/_jacobi.py,sha256=k1zYZtvoA3_gcanqepIVSoA0RJrExtluQzDamkRz_Uk,7570
7
7
  freealg/_linalg.py,sha256=khfRwdQIDtf45aOIq3QSeihAEtUCL-HgwEYtEs0YELc,13137
8
8
  freealg/_pade.py,sha256=Ecm373RvBWznAaf8a5D9qtjcRA8O_8NQsePwto-dsP4,13656
9
9
  freealg/_plot_util.py,sha256=GKvmc1wjVGeqoomrULPbzBEt6P86FdoR2idBLYh5EDY,20068
10
10
  freealg/_sample.py,sha256=MUY9ZkJbdhbC5thoy5_JMRWOHz_OfqMfaH0g2VhKPhg,2550
11
- freealg/_series.py,sha256=yCDPct6SQsGQ7ku9lBBRf_esuz_XdoTiwE9x9nZ8uLY,2605
11
+ freealg/_series.py,sha256=MV8yDHentc06aqPuPlqrIQUNgG0-WiviD8ZWMmLWC0I,11490
12
12
  freealg/_support.py,sha256=ZfU0T7CCDaTXMdEHSbwOL-rMnCPvJWsRb6JtqUMsrzY,6441
13
- freealg/_util.py,sha256=yerqBCedkglc9soWmRgxcm7HTnJGKeiDq9xxfKcE9WY,3780
14
- freealg/freeform.py,sha256=7uXc_y-wy4qDVfD3HgJcYaQmO7jzkWj_s4MPwpoDJf0,42287
13
+ freealg/_util.py,sha256=xKhtPgbW4vHidNAG6F5c5aQhTaG4MmaUqCsxwjBznfU,3786
14
+ freealg/freeform.py,sha256=i09DcRikLL6dlsu4JzoC1AQ7EHTyacx2fgyEz3hFakI,42432
15
15
  freealg/distributions/__init__.py,sha256=t_yZyEkW_W_tSV9IvgYXtVASxD2BEdiNVXcV2ebMy8M,579
16
16
  freealg/distributions/_kesten_mckay.py,sha256=4EvW0seBZUv-cZjuLzfVCredmrCiOJcQ_hJM0mJDl6g,19899
17
17
  freealg/distributions/_marchenko_pastur.py,sha256=r-jmz8Wq5vG0-x7rB0qvVemqvu0Oq6S-BN67pqH0uiw,16992
18
18
  freealg/distributions/_meixner.py,sha256=bZv-x4mwS4yjisKoYSZAffHXtvDEKEis-74BeAjK4_s,17530
19
19
  freealg/distributions/_wachter.py,sha256=rkAmrBLqD2zvpBh0F32aQBscjXv8earXDH9_BITgFF0,16900
20
20
  freealg/distributions/_wigner.py,sha256=w8OlZL9pSfGnXVSSB6A4KBiImr0Zz4iH2PDLCHFfpaY,15877
21
- freealg-0.5.1.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
22
- freealg-0.5.1.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
23
- freealg-0.5.1.dist-info/METADATA,sha256=dxEIFw2EgtZzak_XLxVW6HPfB3IZt-XFUdcn2ozzXPI,5529
24
- freealg-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- freealg-0.5.1.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
26
- freealg-0.5.1.dist-info/RECORD,,
21
+ freealg-0.5.3.dist-info/licenses/AUTHORS.txt,sha256=0b67Nz4_JgIzUupHJTAZxu5QdSUM_HRM_X_w4xCb17o,30
22
+ freealg-0.5.3.dist-info/licenses/LICENSE.txt,sha256=J-EEYEtxb3VVf_Bn1TYfWnpY5lMFIM15iLDDcnaDTPA,1443
23
+ freealg-0.5.3.dist-info/METADATA,sha256=p8NU7kItll4WbsBnavnrJQDOdvE5SxKsoQtEHLOL_1w,5530
24
+ freealg-0.5.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ freealg-0.5.3.dist-info/top_level.txt,sha256=eR2wrgYwDdnnJ9Zf5PruPqe4kQav0GMvRsqct6y00Q8,8
26
+ freealg-0.5.3.dist-info/RECORD,,