freealg 0.6.0__tar.gz → 0.6.2__tar.gz

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 (37) hide show
  1. {freealg-0.6.0 → freealg-0.6.2}/PKG-INFO +1 -1
  2. freealg-0.6.2/freealg/__version__.py +1 -0
  3. {freealg-0.6.0 → freealg-0.6.2}/freealg/_chebyshev.py +8 -8
  4. {freealg-0.6.0 → freealg-0.6.2}/freealg/_jacobi.py +123 -51
  5. {freealg-0.6.0 → freealg-0.6.2}/freealg/_pade.py +33 -151
  6. {freealg-0.6.0 → freealg-0.6.2}/freealg/_sample.py +7 -2
  7. {freealg-0.6.0 → freealg-0.6.2}/freealg/_util.py +45 -13
  8. {freealg-0.6.0 → freealg-0.6.2}/freealg/distributions/_kesten_mckay.py +8 -2
  9. {freealg-0.6.0 → freealg-0.6.2}/freealg/distributions/_marchenko_pastur.py +8 -2
  10. {freealg-0.6.0 → freealg-0.6.2}/freealg/distributions/_meixner.py +28 -26
  11. {freealg-0.6.0 → freealg-0.6.2}/freealg/distributions/_wachter.py +8 -2
  12. {freealg-0.6.0 → freealg-0.6.2}/freealg/distributions/_wigner.py +8 -2
  13. {freealg-0.6.0 → freealg-0.6.2}/freealg/freeform.py +56 -28
  14. {freealg-0.6.0 → freealg-0.6.2}/freealg.egg-info/PKG-INFO +1 -1
  15. freealg-0.6.0/freealg/__version__.py +0 -1
  16. {freealg-0.6.0 → freealg-0.6.2}/AUTHORS.txt +0 -0
  17. {freealg-0.6.0 → freealg-0.6.2}/CHANGELOG.rst +0 -0
  18. {freealg-0.6.0 → freealg-0.6.2}/LICENSE.txt +0 -0
  19. {freealg-0.6.0 → freealg-0.6.2}/MANIFEST.in +0 -0
  20. {freealg-0.6.0 → freealg-0.6.2}/README.rst +0 -0
  21. {freealg-0.6.0 → freealg-0.6.2}/freealg/__init__.py +0 -0
  22. {freealg-0.6.0 → freealg-0.6.2}/freealg/_damp.py +0 -0
  23. {freealg-0.6.0 → freealg-0.6.2}/freealg/_decompress.py +0 -0
  24. {freealg-0.6.0 → freealg-0.6.2}/freealg/_linalg.py +0 -0
  25. {freealg-0.6.0 → freealg-0.6.2}/freealg/_plot_util.py +0 -0
  26. {freealg-0.6.0 → freealg-0.6.2}/freealg/_series.py +0 -0
  27. {freealg-0.6.0 → freealg-0.6.2}/freealg/_support.py +0 -0
  28. {freealg-0.6.0 → freealg-0.6.2}/freealg/distributions/__init__.py +0 -0
  29. {freealg-0.6.0 → freealg-0.6.2}/freealg.egg-info/SOURCES.txt +0 -0
  30. {freealg-0.6.0 → freealg-0.6.2}/freealg.egg-info/dependency_links.txt +0 -0
  31. {freealg-0.6.0 → freealg-0.6.2}/freealg.egg-info/not-zip-safe +0 -0
  32. {freealg-0.6.0 → freealg-0.6.2}/freealg.egg-info/requires.txt +0 -0
  33. {freealg-0.6.0 → freealg-0.6.2}/freealg.egg-info/top_level.txt +0 -0
  34. {freealg-0.6.0 → freealg-0.6.2}/pyproject.toml +0 -0
  35. {freealg-0.6.0 → freealg-0.6.2}/requirements.txt +0 -0
  36. {freealg-0.6.0 → freealg-0.6.2}/setup.cfg +0 -0
  37. {freealg-0.6.0 → freealg-0.6.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.6.0
3
+ Version: 0.6.2
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
@@ -0,0 +1 @@
1
+ __version__ = "0.6.2"
@@ -203,7 +203,7 @@ def chebyshev_stieltjes(z, psi, support, continuation='pade',
203
203
  Methof of analytiv continuation.
204
204
 
205
205
  dtype : numpy.type, default=numpy.complex128
206
- Data type for compelx arrays. This might enhance series acceleration.
206
+ Data type for complex arrays. This might enhance series acceleration.
207
207
 
208
208
  Returns
209
209
  -------
@@ -218,7 +218,7 @@ def chebyshev_stieltjes(z, psi, support, continuation='pade',
218
218
  span = lam_p - lam_m
219
219
  center = 0.5 * (lam_m + lam_p)
220
220
 
221
- # Map z -> u in the standard [-1,1] domain
221
+ # Map z to u in the standard [-1,1] domain
222
222
  u = (2.0 * (z - center)) / span
223
223
 
224
224
  # Inverse-Joukowski: pick branch sqrt with +Im
@@ -232,15 +232,15 @@ def chebyshev_stieltjes(z, psi, support, continuation='pade',
232
232
 
233
233
  # This depends on the method of analytic continuation
234
234
  if continuation == 'pade':
235
- # Build powers J^(k+1) for k = 0, ..., K
235
+ # Horner summation for S0(J) = sum_{k=0}^K psi_k * J**k
236
236
  K = len(psi) - 1
237
- Jpow = J[..., None] ** numpy.arange(1, K+2) # shape: (..., K+1)
238
-
239
- # Summing psi_k * J^(k+1)
240
- S = numpy.sum(psi * Jpow, axis=-1)
237
+ S0 = numpy.zeros_like(J)
238
+ for k in range(K, -1, -1):
239
+ S0 = psi[k] + J * S0
240
+ S = J * S0
241
241
 
242
242
  else:
243
- # Flatten J before passing to Wynn method.
243
+ # Flatten J before passing to any of the acceleration methods.
244
244
  psi_zero = numpy.concatenate([[0.0], psi])
245
245
  Sn = partial_sum(psi_zero, J.ravel(), p=0)
246
246
 
@@ -97,7 +97,7 @@ def jacobi_kernel_proj(xs, pdf, support, K=10, alpha=0.0, beta=0.0, reg=0.0):
97
97
  Pk = eval_jacobi(k, alpha, beta, t)
98
98
  N_k = jacobi_sq_norm(k, alpha, beta)
99
99
 
100
- # \int P_k(t) w(t) \rho(t) dt. w(t) cancels with pdf already being rho
100
+ # \int P_k(t) w(t) \rho(t) dt. w(t) cancels with pdf already being rho
101
101
  moment = numpy.trapz(Pk * pdf, xs)
102
102
 
103
103
  if k == 0:
@@ -168,36 +168,41 @@ def jacobi_density(x, psi, support, alpha=0.0, beta=0.0):
168
168
  # jacobi stieltjes
169
169
  # ================
170
170
 
171
- def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40,
171
+ def jacobi_stieltjes(z, cache, psi, support, alpha=0.0, beta=0.0, n_quad=None,
172
172
  continuation='pade', dtype=numpy.complex128):
173
173
  """
174
174
  Compute m(z) = sum_k psi_k * m_k(z) where
175
175
 
176
- m_k(z) = \\int w^{(alpha, beta)}(t) P_k^{(alpha, beta)}(t) / (u(z)-t) dt
176
+ .. math::
177
+
178
+ m_k(z) = \\int \\frac{w^{(alpha, beta)}(t) P_k^{(alpha, beta)}(t)}{
179
+ (u(z)-t)} \\mathrm{d} t
177
180
 
178
181
  Each m_k is evaluated *separately* with a Gauss-Jacobi rule sized
179
- for that k. This follows the user's request: 1 quadrature rule per P_k.
182
+ for that k. This follows the user's request: 1 quadrature rule per P_k.
180
183
 
181
184
  Parameters
182
185
  ----------
183
186
 
184
187
  z : complex or ndarray
185
188
 
189
+ cache : dict
190
+ Pass a dict to enable cross-call caching.
191
+
186
192
  psi : (K+1,) array_like
187
193
 
188
194
  support : (lambda_minus, lambda_plus)
189
195
 
190
196
  alpha, beta : float
191
197
 
192
- n_base : int
193
- Minimum quadrature size. For degree-k polynomial we use
194
- n_quad = max(n_base, k+1).
198
+ n_quad : int, default=None
199
+ Number of Gauss-Jacobi quadrature points.
195
200
 
196
201
  continuation : str, default= ``'pade'``
197
- Methof of analytiv continuation.
202
+ Method of analytic continuation.
198
203
 
199
204
  dtype : numpy.type, default=numpy.complex128
200
- Data type for compelx arrays. This might enhance series acceleration.
205
+ Data type for complex arrays. This might enhance series acceleration.
201
206
 
202
207
  Returns
203
208
  -------
@@ -209,67 +214,132 @@ def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40,
209
214
  Same shape as z
210
215
  """
211
216
 
217
+ if not isinstance(cache, dict):
218
+ raise TypeError('"cache" must be a dict; pass a persistent dict '
219
+ '(e.g., self.cache).')
220
+
221
+ # Number of quadratures
222
+ if 'n_quad' not in cache:
223
+ if n_quad is None:
224
+ # Set number of quadratures based on Bernstein ellipse. Here using
225
+ # an evaluation point a with distance delta from support, to
226
+ # achieve the quadrature error below tol.
227
+ tol = 1e-16
228
+ delta = 1e-2
229
+ n_quad = int(-numpy.log(tol) / (2.0 * numpy.sqrt(delta)))
230
+ n_quad = max(n_quad, psi.size)
231
+ cache['n_quad'] = n_quad
232
+ else:
233
+ n_quad = cache['n_quad']
234
+
235
+ # Quadrature nodes and weights
236
+ if ('t_nodes' not in cache) or ('w_nodes' not in cache):
237
+ t_nodes, w_nodes = roots_jacobi(n_quad, alpha, beta) # (n_quad,)
238
+ cache['t_nodes'] = t_nodes
239
+ cache['w_nodes'] = w_nodes
240
+ else:
241
+ t_nodes = cache['t_nodes']
242
+ w_nodes = cache['w_nodes']
243
+
212
244
  z = numpy.asarray(z, dtype=dtype)
213
245
  lam_minus, lam_plus = support
214
246
  span = lam_plus - lam_minus
215
247
  centre = 0.5 * (lam_plus + lam_minus)
216
248
 
217
- # Map z -> u in the standard [-1,1] domain
249
+ # Map z to u in the standard [-1,1] domain
218
250
  u = (2.0 / span) * (z - centre)
219
251
 
220
- m_total = numpy.zeros_like(z, dtype=dtype)
252
+ # Cauchy Kernel (flattened for all z)
253
+ u_flat = u.ravel()
254
+ ker = (1.0 / (t_nodes[:, None] - u_flat[None, :])).astype(
255
+ dtype, copy=False) # (n_quad, Ny*Nx)
256
+
257
+ if continuation == 'pade':
258
+
259
+ if 'integrand_nodes' not in cache:
260
+
261
+ # Compute sum_k psi_k P_k (call it s_node)
262
+ s_nodes = numpy.zeros_like(t_nodes, dtype=dtype)
263
+ for k, psi_k in enumerate(psi):
264
+
265
+ # Evaluate P_k at the quadrature nodes
266
+ P_k_nodes = eval_jacobi(k, alpha, beta, t_nodes) # (n_quad,)
267
+ s_nodes += psi_k * P_k_nodes
268
+
269
+ integrand_nodes = (2.0 / span) * (w_nodes * s_nodes).astype(dtype)
270
+ cache['integrand_nodes'] = integrand_nodes
221
271
 
222
- if continuation != 'pade':
223
- # Stores m with the ravel size of z.
224
- m_partial = numpy.zeros((psi.size, z.size), dtype=dtype)
272
+ else:
273
+ integrand_nodes = cache['integrand_nodes']
274
+
275
+ Q_flat = (integrand_nodes[:, None] * ker).sum(axis=0)
276
+ m_total = Q_flat.reshape(z.shape)
277
+
278
+ return m_total
279
+
280
+ else:
225
281
 
226
- for k, psi_k in enumerate(psi):
227
- # Select quadrature size tailored to this P_k
228
- n_quad = max(n_base, k + 1)
229
- t_nodes, w_nodes = roots_jacobi(n_quad, alpha, beta) # (n_quad,)
282
+ # Continuation is not Pade. This is one of Wynn, Levin, etc. These
283
+ # methods need the series for m for 1, ..., k.
230
284
 
231
- # Evaluate P_k at the quadrature nodes
232
- P_k_nodes = eval_jacobi(k, alpha, beta, t_nodes) # (n_quad,)
285
+ if 'B' not in cache:
286
+ # All P_k at quadrature nodes (real), row-scale by weights
287
+ P_nodes = numpy.empty((psi.size, n_quad), dtype=w_nodes.dtype)
288
+ for k in range(psi.size):
289
+ P_nodes[k, :] = eval_jacobi(k, alpha, beta, t_nodes)
233
290
 
234
- # Integrand values at nodes: w_nodes already include the weight
235
- integrand = w_nodes * P_k_nodes # (n_quad,)
291
+ # All P_k * w shape (K+1, n_quad)
292
+ B = (2.0 / span) * (P_nodes * w_nodes[None, :]).astype(
293
+ dtype, copy=False)
294
+ cache['B'] = B
236
295
 
237
- # Evaluate jacobi polynomals of the second kind, Q_k using quadrature
238
- diff = t_nodes[:, None, None] - u[None, ...] # (n_quad, Ny, Nx)
239
- Q_k = (integrand[:, None, None] / diff).sum(axis=0).astype(dtype)
296
+ else:
297
+ B = cache['B']
298
+
299
+ # Principal branch. 2D matrix for all k
300
+ m_k_all = B @ ker
240
301
 
241
- # Principal branch
242
- m_k = (2.0 / span) * Q_k
302
+ # Compute m on secondary branch from the principal branch, which is
303
+ # m_k = m_k + 2 \pi i rho_k(z), and rho(z) is the analytic extension of
304
+ # rho_k(x) using the k-th basis. Basically, rho_k(z) is w * P_k(z).
243
305
 
244
- # Compute secondary branch from the principal branch
245
- if continuation != 'pade':
306
+ # Lower-half-plane jump for ALL k at once (vectorized)
307
+ mask_m = (z.imag <= 0)
308
+ if numpy.any(mask_m):
309
+ idx = numpy.flatnonzero(mask_m.ravel())
310
+ u_m = u_flat[idx].astype(dtype, copy=False) # complex
246
311
 
247
- # Compute analytic extension of rho(z) to lower-half plane for
248
- # when rho is just the k-th Jacobi basis: w(z) P_k(z). FOr this,
249
- # we create a psi array (called unit_psi_j), with all zeros, except
250
- # its k-th element is one. Ten we call jacobi_density.
251
- unit_psi_k = numpy.zeros_like(psi)
252
- unit_psi_k[k] = 1.0
312
+ # Scipy's eval_jacobi tops out at complex128 type. If u_m is
313
+ # complex256, downcast to complex128.
314
+ if u_m.dtype.itemsize > numpy.dtype(numpy.complex128).itemsize:
315
+ u_m_eval = u_m.astype(numpy.complex128, copy=False)
316
+ down_cast = True
317
+ else:
318
+ u_m_eval = u_m
319
+ down_cast = False
253
320
 
254
- # Only lower-half plane
255
- mask_m = z.imag <= 0
256
- z_m = z[mask_m]
321
+ # P_k at complex u_m (all means for all k = 1,...,K)
322
+ P_all_m = numpy.empty((psi.size, u_m.size), dtype=dtype)
323
+ for k in range(psi.size):
324
+ P_all_m[k, :] = eval_jacobi(k, alpha, beta, u_m_eval)
257
325
 
258
- # Dnesity here is rho = w(z) P_k
259
- rho_k = jacobi_density(z_m.ravel(), unit_psi_k, support,
260
- alpha=alpha, beta=beta).reshape(z_m.shape)
326
+ # Jacobi weight. Must match jacobi_density's branch
327
+ w_m = numpy.power(1.0 - u_m, alpha) * numpy.power(1.0 + u_m, beta)
261
328
 
262
- # Secondary branch is principal branch + 2 \pi i rho, using Plemelj
263
- # (in fact, Riemann-Hirbert jump).
264
- m_k[mask_m] = m_k[mask_m] + 2.0 * numpy.pi * 1j * rho_k
329
+ # rho_k(z) in x-units is (2/span) * w(u) * P_k(u)
330
+ rho_all = ((2.0 / span) * w_m[None, :] * P_all_m).astype(
331
+ dtype, copy=False)
265
332
 
266
- # Accumulate with factor 2/span
267
- m_total += psi_k * m_k
333
+ if down_cast:
334
+ rho_all = rho_all.astype(dtype)
268
335
 
269
- if continuation != 'pade':
270
- m_partial[k, :] = m_total.ravel()
336
+ # compute analytic extension of rho(z) to lower-half plane for when
337
+ # rho is just the k-th Jacobi basis: w(z) P_k(z). For this, we
338
+ m_k_all[:, idx] = m_k_all[:, idx] + (2.0 * numpy.pi * 1j) * rho_all
271
339
 
272
- if continuation != 'pade':
340
+ # Partial sums S_k = sum_{j<=k} psi_j * m_j
341
+ WQ = (psi[:, None].astype(dtype, copy=False) * m_k_all)
342
+ m_partial = numpy.cumsum(WQ, axis=0)
273
343
 
274
344
  if continuation == 'wynn-eps':
275
345
  S = wynn_epsilon(m_partial)
@@ -281,7 +351,9 @@ def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40,
281
351
  S = weniger_delta(m_partial)
282
352
  elif continuation == 'brezinski':
283
353
  S = brezinski_theta(m_partial)
354
+ else:
355
+ # No acceleration (likely diverges in the lower-half plane)
356
+ S = m_partial[-1, :]
284
357
 
285
358
  m_total = S.reshape(z.shape)
286
-
287
- return m_total
359
+ return m_total
@@ -108,32 +108,35 @@ def _decode_poles(s, lam_m, lam_p):
108
108
  # inner ls
109
109
  # ========
110
110
 
111
- def _inner_ls(x, f, poles, p=1, pade_reg=0.0):
111
+ def _inner_ls(x, f, poles, dpq=1, pade_reg=0.0):
112
112
  """
113
113
  This is the inner least square (blazing fast).
114
+
115
+ dqp is the difference between the order of P (numerator) and Q
116
+ (denominator).
114
117
  """
115
118
 
116
- if poles.size == 0 and p == -1:
119
+ if poles.size == 0 and dpq == -1:
117
120
  return 0.0, 0.0, numpy.empty(0)
118
121
 
119
122
  if poles.size == 0: # q = 0
120
123
  # A = numpy.column_stack((numpy.ones_like(x), x))
121
- cols = [numpy.ones_like(x)] if p >= 0 else []
122
- if p == 1:
124
+ cols = [numpy.ones_like(x)] if dpq >= 0 else []
125
+ if dpq == 1:
123
126
  cols.append(x)
124
127
  A = numpy.column_stack(cols)
125
128
  # ---
126
129
  theta, *_ = lstsq(A, f, rcond=None)
127
130
  # c, D = theta # TEST
128
- if p == -1:
131
+ if dpq == -1:
129
132
  c = 0.0
130
133
  D = 0.0
131
134
  resid = numpy.empty(0)
132
- elif p == 0:
135
+ elif dpq == 0:
133
136
  c = theta[0]
134
137
  D = 0.0
135
138
  resid = numpy.empty(0)
136
- else: # p == 1
139
+ else: # dpq == 1
137
140
  c, D = theta
138
141
  resid = numpy.empty(0)
139
142
  else:
@@ -142,28 +145,28 @@ def _inner_ls(x, f, poles, p=1, pade_reg=0.0):
142
145
  # # theta, *_ = lstsq(A, f, rcond=None)
143
146
  # # c, D, resid = theta[0], theta[1], theta[2:]
144
147
  # phi = 1.0 / (x[:, None] - poles[None, :])
145
- # cols = [numpy.ones_like(x)] if p >= 0 else []
146
- # if p == 1:
148
+ # cols = [numpy.ones_like(x)] if dpq >= 0 else []
149
+ # if dpq == 1:
147
150
  # cols.append(x)
148
151
  # cols.append(phi)
149
152
  # A = numpy.column_stack(cols)
150
153
  # theta, *_ = lstsq(A, f, rcond=None)
151
- # if p == -1:
154
+ # if dpq == -1:
152
155
  # c = 0.0
153
156
  # D = 0.0
154
157
  # resid = theta
155
- # elif p == 0:
158
+ # elif dpq == 0:
156
159
  # c = theta[0]
157
160
  # D = 0.0
158
161
  # resid = theta[1:]
159
- # else: # p == 1
162
+ # else: # dpq == 1
160
163
  # c = theta[0]
161
164
  # D = theta[1]
162
165
  # resid = theta[2:]
163
166
 
164
167
  phi = 1.0 / (x[:, None] - poles[None, :])
165
- cols = [numpy.ones_like(x)] if p >= 0 else []
166
- if p == 1:
168
+ cols = [numpy.ones_like(x)] if dpq >= 0 else []
169
+ if dpq == 1:
167
170
  cols.append(x)
168
171
  cols.append(phi)
169
172
 
@@ -179,9 +182,9 @@ def _inner_ls(x, f, poles, p=1, pade_reg=0.0):
179
182
  # theta = numpy.linalg.solve(ATA, ATf)
180
183
 
181
184
  # figure out how many elements to skip
182
- if p == 1:
185
+ if dpq == 1:
183
186
  skip = 2 # skip c and D
184
- elif p == 0:
187
+ elif dpq == 0:
185
188
  skip = 1 # skip c only
186
189
  else:
187
190
  skip = 0 # all entries are residues
@@ -198,11 +201,11 @@ def _inner_ls(x, f, poles, p=1, pade_reg=0.0):
198
201
  else:
199
202
  theta, *_ = lstsq(A, f, rcond=None)
200
203
 
201
- if p == -1:
204
+ if dpq == -1:
202
205
  c, D, resid = 0.0, 0.0, theta
203
- elif p == 0:
206
+ elif dpq == 0:
204
207
  c, D, resid = theta[0], 0.0, theta[1:]
205
- else: # p == 1
208
+ else: # dpq == 1
206
209
  c, D, resid = theta[0], theta[1], theta[2:]
207
210
 
208
211
  return c, D, resid
@@ -240,7 +243,7 @@ def _eval_rational(z, c, D, poles, resid):
240
243
  # fit pade
241
244
  # ========
242
245
 
243
- def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
246
+ def fit_pade(x, f, lam_m, lam_p, p=2, q=2, odd_side='left', pade_reg=0.0,
244
247
  safety=1.0, max_outer=40, xtol=1e-12, ftol=1e-12, optimizer='ls',
245
248
  verbose=0):
246
249
  """
@@ -251,16 +254,19 @@ def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
251
254
  if not (odd_side in ['left', 'right']):
252
255
  raise ValueError('"odd_side" can only be "left" or "right".')
253
256
 
254
- if not (p in [-1, 0, 1]):
255
- raise ValueError('"pade_p" can only be -1, 0, or 1.')
257
+ # Difference between the degrees of numerator and denominator
258
+ dpq = p - q
259
+ if not (dpq in [-1, 0, 1]):
260
+ raise ValueError('"pade_p" and "pade_q" can only differ by "+1", ' +
261
+ '"0", or "-1".')
256
262
 
257
263
  x = numpy.asarray(x, float)
258
264
  f = numpy.asarray(f, float)
259
265
 
260
266
  poles0 = _default_poles(q, lam_m, lam_p, safety=safety, odd_side=odd_side)
261
- if q == 0 and p <= 0:
267
+ if q == 0 and dpq <= 0:
262
268
  # c, D, resid = _inner_ls(x, f, poles0, pade_reg=pade_reg) # TEST
263
- c, D, resid = _inner_ls(x, f, poles0, p, pade_reg=pade_reg)
269
+ c, D, resid = _inner_ls(x, f, poles0, dpq, pade_reg=pade_reg)
264
270
  pade_sol = {
265
271
  'c': c, 'D': D, 'poles': poles0, 'resid': resid,
266
272
  'outer_iters': 0
@@ -274,10 +280,10 @@ def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
274
280
  # residual
275
281
  # --------
276
282
 
277
- def residual(s, p=p):
283
+ def residual(s, dpq=dpq):
278
284
  poles = _decode_poles(s, lam_m, lam_p)
279
285
  # c, D, resid = _inner_ls(x, f, poles, pade_reg=pade_reg) # TEST
280
- c, D, resid = _inner_ls(x, f, poles, p, pade_reg=pade_reg)
286
+ c, D, resid = _inner_ls(x, f, poles, dpq, pade_reg=pade_reg)
281
287
  return _eval_rational(x, c, D, poles, resid) - f
282
288
 
283
289
  # ----------------
@@ -324,7 +330,7 @@ def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
324
330
 
325
331
  poles = _decode_poles(res.x, lam_m, lam_p)
326
332
  # c, D, resid = _inner_ls(x, f, poles, pade_reg=pade_reg) # TEST
327
- c, D, resid = _inner_ls(x, f, poles, p, pade_reg=pade_reg)
333
+ c, D, resid = _inner_ls(x, f, poles, dpq, pade_reg=pade_reg)
328
334
 
329
335
  pade_sol = {
330
336
  'c': c, 'D': D, 'poles': poles, 'resid': resid,
@@ -364,127 +370,3 @@ def eval_pade(z, pade_sol):
364
370
  for bj, rj in zip(poles, resid):
365
371
  out += rj/(z - bj) # each is an (N,) op, no N*q temp
366
372
  return out
367
-
368
-
369
- # ============
370
- # fit pade old
371
- # ============
372
-
373
- def fit_pade_old(x, f, lam_m, lam_p, p, q, delta=1e-8, B=numpy.inf,
374
- S=numpy.inf, B_default=10.0, S_factor=2.0, maxiter_de=200):
375
- """
376
- Deprecated.
377
-
378
- Fit a [p/q] rational P/Q of the form:
379
- P(x) = s * prod_{i=0..p-1}(x - a_i)
380
- Q(x) = prod_{j=0..q-1}(x - b_j)
381
-
382
- Constraints:
383
- a_i in [lam_m, lam_p]
384
- b_j in (-infty, lam_m - delta] cup [lam_p + delta, infty)
385
-
386
- Approach:
387
- - Brute-force all 2^q left/right assignments for denominator roots
388
- - Global search with differential_evolution, fallback to zeros if needed
389
- - Local refinement with least_squares
390
-
391
- Returns a dict with keys:
392
- 's' : optimal scale factor
393
- 'a' : array of p numerator roots (in [lam_m, lam_p])
394
- 'b' : array of q denominator roots (outside the interval)
395
- 'resid' : final residual norm
396
- 'signs' : tuple indicating left/right pattern for each b_j
397
- """
398
-
399
- # Determine finite bounds for DE
400
- if not numpy.isfinite(B):
401
- B_eff = B_default
402
- else:
403
- B_eff = B
404
- if not numpy.isfinite(S):
405
- # scale bound: S_factor * max|f| * interval width + safety
406
- S_eff = S_factor * numpy.max(numpy.abs(f)) * (lam_p - lam_m) + 1.0
407
- if S_eff <= 0:
408
- S_eff = 1.0
409
- else:
410
- S_eff = S
411
-
412
- def map_roots(signs, b):
413
- """Map unconstrained b_j -> real root outside the interval."""
414
- out = numpy.empty_like(b)
415
- for j, (s_val, bj) in enumerate(zip(signs, b)):
416
- if s_val > 0:
417
- out[j] = lam_p + delta + numpy.exp(bj)
418
- else:
419
- out[j] = lam_m - delta - numpy.exp(bj)
420
- return out
421
-
422
- best = {'resid': numpy.inf}
423
-
424
- # Enumerate all left/right sign patterns
425
- for signs in product([-1, 1], repeat=q):
426
- # Residual vector for current pattern
427
- def resid_vec(z):
428
- s_val = z[0]
429
- a = z[1:1+p]
430
- b = z[1+p:]
431
- P = s_val * numpy.prod(x[:, None] - a[None, :], axis=1)
432
- roots_Q = map_roots(signs, b)
433
- Q = numpy.prod(x[:, None] - roots_Q[None, :], axis=1)
434
- return P - f * Q
435
-
436
- def obj(z):
437
- r = resid_vec(z)
438
- return r.dot(r)
439
-
440
- # Build bounds for DE
441
- bounds = []
442
- bounds.append((-S_eff, S_eff)) # s
443
- bounds += [(lam_m, lam_p)] * p # a_i
444
- bounds += [(-B_eff, B_eff)] * q # b_j
445
-
446
- # 1) Global search
447
- try:
448
- de = differential_evolution(obj, bounds,
449
- maxiter=maxiter_de,
450
- polish=False)
451
- z0 = de.x
452
- except ValueError:
453
- # fallback: start at zeros
454
- z0 = numpy.zeros(1 + p + q)
455
-
456
- # 2) Local refinement
457
- ls = least_squares(resid_vec, z0, xtol=1e-12, ftol=1e-12)
458
-
459
- rnorm = numpy.linalg.norm(resid_vec(ls.x))
460
- if rnorm < best['resid']:
461
- best.update(resid=rnorm, signs=signs, x=ls.x.copy())
462
-
463
- # Unpack best solution
464
- z_best = best['x']
465
- s_opt = z_best[0]
466
- a_opt = z_best[1:1+p]
467
- b_opt = map_roots(best['signs'], z_best[1+p:])
468
-
469
- return {
470
- 's': s_opt,
471
- 'a': a_opt,
472
- 'b': b_opt,
473
- 'resid': best['resid'],
474
- 'signs': best['signs'],
475
- }
476
-
477
-
478
- # =============
479
- # eval pade old
480
- # =============
481
-
482
- def eval_pade_old(z, s, a, b):
483
- """
484
- Deprecated.
485
- """
486
-
487
- Pz = s * numpy.prod([z - aj for aj in a], axis=0)
488
- Qz = numpy.prod([z - bj for bj in b], axis=0)
489
-
490
- return Pz / Qz
@@ -113,9 +113,14 @@ def sample(x, rho, num_pts, method='qmc', seed=None):
113
113
  # Draw from uniform distribution
114
114
  if method == 'mc':
115
115
  u = rng.random(num_pts)
116
+
116
117
  elif method == 'qmc':
117
- engine = qmc.Halton(d=1, rng=rng)
118
- u = engine.random(num_pts)
118
+ try:
119
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
120
+ except TypeError:
121
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
122
+ u = engine.random(num_pts).ravel()
123
+
119
124
  else:
120
125
  raise NotImplementedError('"method" is invalid.')
121
126
 
@@ -126,6 +126,27 @@ def kde(eig, xs, lam_m, lam_p, h, kernel='beta', plot=False):
126
126
 
127
127
  freealg.supp
128
128
  freealg.sample
129
+
130
+ Notes
131
+ -----
132
+
133
+ In Beta kernel density estimation, the shape parameters "a" and "b" of the
134
+ Beta(a, b)) distribution are computed for each data point "u" as:
135
+
136
+ a = (u / h) + 1.0
137
+ b = ((1.0 - u) / h) + 1.0
138
+
139
+ This is a standard way of using Beta kernel (see R-package documentation:
140
+ https://search.r-project.org/CRAN/refmans/DELTD/html/Beta.html
141
+
142
+ These equations are derived from "moment matching" method, where
143
+
144
+ Mean(Beta(a,b)) = u
145
+ Var(Beta(a,b)) = (1-u) u h
146
+
147
+ Solving these two equations for "a" and "b" yields the relations above.
148
+ See paper (page 134)
149
+ https://www.songxichen.com/Uploads/Files/Publication/Chen-CSD-99.pdf
129
150
  """
130
151
 
131
152
  if kernel == 'gaussian':
@@ -141,28 +162,39 @@ def kde(eig, xs, lam_m, lam_p, h, kernel='beta', plot=False):
141
162
 
142
163
  span = lam_p - lam_m
143
164
  if span <= 0:
144
- raise ValueError("lam_p must be larger than lam_m")
165
+ raise ValueError('"lam_p" must be larger than "lam_m".')
145
166
 
146
167
  # map samples and grid to [0, 1]
147
168
  u = (eig - lam_m) / span
148
169
  t = (xs - lam_m) / span
149
170
 
150
- if u.min() < 0 or u.max() > 1:
151
- mask = (u > 0) & (u < 1)
152
- u = u[mask]
171
+ # keep only samples strictly inside (0,1)
172
+ if (u.min() < 0) or (u.max() > 1):
173
+ u = u[(u > 0) & (u < 1)]
174
+
175
+ n = u.size
176
+ if n == 0:
177
+ return numpy.zeros_like(xs, dtype=float)
153
178
 
154
- pdf = numpy.zeros_like(xs, dtype=float)
155
- n = len(u)
179
+ # Shape parameters "a" and "b" or the kernel Beta(a, b), which is
180
+ # computed for each data point "u" (see notes above). These are
181
+ # vectorized.
182
+ a = (u / h) + 1.0
183
+ b = ((1.0 - u) / h) + 1.0
156
184
 
157
- # tiny positive number to keep shape parameters > 0
185
+ # # tiny positive number to keep shape parameters > 0
158
186
  eps = 1e-6
159
- for ui in u:
160
- a = max(ui / h + 1.0, eps)
161
- b = max((1.0 - ui) / h + 1.0, eps)
162
- pdf += beta.pdf(t, a, b)
187
+ a = numpy.clip(a, eps, None)
188
+ b = numpy.clip(b, eps, None)
189
+
190
+ # Beta kernel
191
+ pdf_matrix = beta.pdf(t[None, :], a[:, None], b[:, None])
192
+
193
+ # Average and re-normalize back to x variable
194
+ pdf = pdf_matrix.sum(axis=0) / (n * span)
163
195
 
164
- pdf /= n * span # renormalise
165
- pdf[(t < 0) | (t > 1)] = 0.0 # exact zeros outside
196
+ # Exact zeros outside [lam_m, lam_p]
197
+ pdf[(t < 0) | (t > 1)] = 0.0
166
198
 
167
199
  else:
168
200
  raise NotImplementedError('"kernel" is invalid.')
@@ -526,9 +526,15 @@ class KestenMcKay(object):
526
526
  # Draw from uniform distribution
527
527
  if method == 'mc':
528
528
  u = rng.random(size)
529
+
529
530
  elif method == 'qmc':
530
- engine = qmc.Halton(d=1, rng=rng)
531
- u = engine.random(size)
531
+ try:
532
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
533
+ except TypeError:
534
+ # Older scipy versions
535
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
536
+ u = engine.random(size).ravel()
537
+
532
538
  else:
533
539
  raise NotImplementedError('"method" is invalid.')
534
540
 
@@ -533,9 +533,15 @@ class MarchenkoPastur(object):
533
533
  # Draw from uniform distribution
534
534
  if method == 'mc':
535
535
  u = rng.random(size)
536
+
536
537
  elif method == 'qmc':
537
- engine = qmc.Halton(d=1, rng=rng)
538
- u = engine.random(size)
538
+ try:
539
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
540
+ except TypeError:
541
+ # Older scipy versions
542
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
543
+ u = engine.random(size).ravel()
544
+
539
545
  else:
540
546
  raise NotImplementedError('"method" is invalid.')
541
547
 
@@ -177,18 +177,12 @@ class Meixner(object):
177
177
  rho = numpy.zeros_like(x)
178
178
  mask = numpy.logical_and(x > self.lam_m, x < self.lam_p)
179
179
 
180
- # rho[mask] = \
181
- # numpy.sqrt(4.0 * (1.0 + self.b) - (x[mask] - self.a)**2) / \
182
- # (2.0 * numpy.pi * (self.b * x[mask]**2 + self.a * x[mask] + 1))
183
-
184
180
  numer = numpy.zeros_like(x)
185
181
  denom = numpy.ones_like(x)
186
182
  numer[mask] = self.c * numpy.sqrt(4.0 * self.b - (x[mask] - self.a)**2)
187
- denom[mask] = (1 - self.c)*(x[mask] - self.a)**2
188
- denom[mask] += self.a * (2 - self.c)*(x[mask] - self.a)
189
- denom[mask] += self.a**2 + self.b * self.c**2
190
- denom[mask] *= 2 * numpy.pi
191
-
183
+ denom[mask] = 2.0 * numpy.pi * (
184
+ (1.0 - self.c) * x[mask]**2 + self.a * self.c * x[mask] +
185
+ self.b * self.c**2)
192
186
  rho[mask] = numer[mask] / denom[mask]
193
187
 
194
188
  if plot:
@@ -260,14 +254,14 @@ class Meixner(object):
260
254
  def _P(x):
261
255
  # denom = 1.0 + self.b
262
256
  # return ((1.0 + 2.0 * self.b) * x + self.a) / denom
263
- P = ((self.c - 2.0) * x - self.a * self.c) / 2.0
257
+ P = (self.c - 2.0) * x - self.a * self.c
264
258
  return P
265
259
 
266
260
  def _Q(x):
267
261
  # denom = 1.0 + self.b
268
262
  # return (self.b * x**2 + self.a * x + 1.0) / denom
269
- Q = ((1.0 - self.c) * x**2 + self.a * self.c * x +
270
- self.b * self.c**2) / 4.0
263
+ Q = (1.0 - self.c) * x**2 + self.a * self.c * x + \
264
+ self.b * self.c**2
271
265
  return Q
272
266
 
273
267
  P = _P(x)
@@ -277,9 +271,6 @@ class Meixner(object):
277
271
  sign = numpy.sign(P)
278
272
  hilb = (P - sign * Delta) / (2.0 * Q)
279
273
 
280
- # using negative sign convention
281
- hilb = -hilb
282
-
283
274
  if plot:
284
275
  plot_hilbert(x, hilb, support=self.support, latex=latex, save=save)
285
276
 
@@ -299,21 +290,26 @@ class Meixner(object):
299
290
  # denom = 1.0 + self.b
300
291
  # A = (self.b * z**2 + self.a * z + 1.0) / denom
301
292
  # B = ((1.0 + 2.0 * self.b) * z + self.a) / denom
302
- A = ((1.0 - self.c) * z**2 + self.a * self.c * z +
303
- self.b * self.c**2) / 4.0
304
- B = ((self.c - 2.0) * z - self.a * self.c) / 2.0
293
+ # A = ((1.0 - self.c) * z**2 + self.a * self.c * z +
294
+ # self.b * self.c**2) / 4.0
295
+ # B = ((self.c - 2.0) * z - self.a * self.c) / 2.0
296
+
297
+ Q = (1.0 - self.c) * z**2 + self.a * self.c * z + \
298
+ self.b * self.c**2
299
+ P = (self.c - 2.0) * z - self.a * self.c
305
300
 
306
301
  # D = B**2 - 4 * A
307
302
  # sqrtD = numpy.sqrt(D)
308
303
 
309
304
  # Avoid numpy picking the wrong branch
310
- d = 2 * numpy.sqrt(1.0 + self.b)
311
- r_min = self.a - d
312
- r_max = self.a + d
313
- sqrtD = numpy.sqrt(z - r_min) * numpy.sqrt(z - r_max)
305
+ # d = 2 * numpy.sqrt(1.0 + self.b)
306
+ # r_min = self.a - d
307
+ # r_max = self.a + d
308
+ # sqrtD = numpy.sqrt(z - r_min) * numpy.sqrt(z - r_max)
309
+ sqrtD = numpy.sqrt(P**2 - 4.0 * Q)
314
310
 
315
- m1 = (-B + sqrtD) / (2 * A)
316
- m2 = (-B - sqrtD) / (2 * A)
311
+ m1 = (P + sqrtD) / (2 * Q)
312
+ m2 = (P - sqrtD) / (2 * Q)
317
313
 
318
314
  # pick correct branch only for non-masked entries
319
315
  upper = z.imag >= 0
@@ -558,9 +554,15 @@ class Meixner(object):
558
554
  # Draw from uniform distribution
559
555
  if method == 'mc':
560
556
  u = rng.random(size)
557
+
561
558
  elif method == 'qmc':
562
- engine = qmc.Halton(d=1, rng=rng)
563
- u = engine.random(size)
559
+ try:
560
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
561
+ except TypeError:
562
+ # Older scipy versions
563
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
564
+ u = engine.random(size).ravel()
565
+
564
566
  else:
565
567
  raise NotImplementedError('"method" is invalid.')
566
568
 
@@ -533,9 +533,15 @@ class Wachter(object):
533
533
  # Draw from uniform distribution
534
534
  if method == 'mc':
535
535
  u = rng.random(size)
536
+
536
537
  elif method == 'qmc':
537
- engine = qmc.Halton(d=1, rng=rng)
538
- u = engine.random(size)
538
+ try:
539
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
540
+ except TypeError:
541
+ # Older scipy versions
542
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
543
+ u = engine.random(size).ravel()
544
+
539
545
  else:
540
546
  raise NotImplementedError('"method" is invalid.')
541
547
 
@@ -510,9 +510,15 @@ class Wigner(object):
510
510
  # Draw from uniform distribution
511
511
  if method == 'mc':
512
512
  u = rng.random(size)
513
+
513
514
  elif method == 'qmc':
514
- engine = qmc.Halton(d=1, rng=rng)
515
- u = engine.random(size)
515
+ try:
516
+ engine = qmc.Halton(d=1, scramble=True, rng=rng)
517
+ except TypeError:
518
+ # Older scipy versions
519
+ engine = qmc.Halton(d=1, scramble=True, seed=rng)
520
+ u = engine.random(size).ravel()
521
+
516
522
  else:
517
523
  raise NotImplementedError('"method" is invalid.')
518
524
 
@@ -178,10 +178,13 @@ class FreeForm(object):
178
178
  # Detect support
179
179
  self.lam_m, self.lam_p = supp(self.eig, **kwargs)
180
180
  else:
181
- self.lam_m = support[0]
182
- self.lam_p = support[1]
181
+ self.lam_m = float(support[0])
182
+ self.lam_p = float(support[1])
183
183
  self.support = (self.lam_m, self.lam_p)
184
184
 
185
+ # Number of quadrature points to evaluate Stieltjes using Gauss-Jacobi
186
+ self.n_quad = None
187
+
185
188
  # Initialize
186
189
  self.method = None # fitting rho: jacobi, chebyshev
187
190
  self.continuation = None # analytic continuation: pade, wynn
@@ -189,15 +192,17 @@ class FreeForm(object):
189
192
  self.psi = None # coefficients of estimating rho
190
193
  self.alpha = None # Jacobi polynomials alpha parameter
191
194
  self.beta = None # Jacobi polynomials beta parameter
195
+ self.cache = {} # Cache inner-computations
192
196
 
193
197
  # ===
194
198
  # fit
195
199
  # ===
196
200
 
197
- def fit(self, method='jacobi', K=10, alpha=0.0, beta=0.0, reg=0.0,
198
- projection='gaussian', kernel_bw=0.001, damp=None, force=False,
199
- continuation='pade', pade_p=0, pade_q=1, odd_side='left',
200
- pade_reg=0.0, optimizer='ls', plot=False, latex=False, save=False):
201
+ def fit(self, method='jacobi', K=10, alpha=0.0, beta=0.0, n_quad=60,
202
+ reg=0.0, projection='gaussian', kernel_bw=0.001, damp=None,
203
+ force=False, continuation='pade', pade_p=1, pade_q=1,
204
+ odd_side='left', pade_reg=0.0, optimizer='ls', plot=False,
205
+ latex=False, save=False):
201
206
  """
202
207
  Fit model to eigenvalues.
203
208
 
@@ -221,6 +226,11 @@ class FreeForm(object):
221
226
  fitting model on the left side of interval. This should be greater
222
227
  then -1. This option is only applicable when ``method='jacobi'``.
223
228
 
229
+ n_quad : int, default=60
230
+ Number of quadrature points to evaluate Stieltjes transform later
231
+ on (when :func:`decompress` is called) using Gauss-Jacob
232
+ quadrature. This option is relevant only if ``method='jacobi'``.
233
+
224
234
  reg : float, default=0.0
225
235
  Tikhonov regularization coefficient.
226
236
 
@@ -265,14 +275,15 @@ class FreeForm(object):
265
275
  * ``'brezinski'``: Brezinski's :math:`\\theta` algorithm
266
276
  (`experimental`).
267
277
 
268
- pade_p : int, default=0
269
- Degree of polynomial :math:`P(z)` is :math:`q+p` where :math:`p`
270
- can only be ``-1``, ``0``, or ``1``. See notes below. This option
278
+ pade_p : int, default=1
279
+ Degree of polynomial :math:`P(z)` is :math:`p` where :math:`p` can
280
+ only be ``q-1``, ``q``, or ``q+1``. See notes below. This option
271
281
  is applicable if ``continuation='pade'``.
272
282
 
273
283
  pade_q : int, default=1
274
- Degree of polynomial :math:`Q(z)` is :math:`q`. See notes below.
275
- This option is applicable if ``continuation='pade'``.
284
+ Degree of polynomial :math:`Q(z)` is :math:`q` where :math:`q` can
285
+ only be ``p-1``, ``p``, or ``p+1``. See notes below. This option
286
+ is applicable if ``continuation='pade'``.
276
287
 
277
288
  odd_side : {``'left'``, ``'right'``}, default= ``'left'``
278
289
  In case of odd number of poles (when :math:`q` is odd), the extra
@@ -336,6 +347,10 @@ class FreeForm(object):
336
347
  >>> from freealg import FreeForm
337
348
  """
338
349
 
350
+ # Very important: reset cache whenever this function is called. This
351
+ # also empties all references holdign a cache copy.
352
+ self.cache.clear()
353
+
339
354
  if alpha <= -1:
340
355
  raise ValueError('"alpha" should be greater then "-1".')
341
356
 
@@ -351,6 +366,10 @@ class FreeForm(object):
351
366
  # Project eigenvalues to Jacobi polynomials basis
352
367
  if method == 'jacobi':
353
368
 
369
+ # Set number of Gauss-Jacobi quadratures. This is not used in this
370
+ # function (used later when decompress is called)
371
+ self.n_quad = n_quad
372
+
354
373
  if projection == 'sample':
355
374
  psi = jacobi_sample_proj(self.eig, support=self.support, K=K,
356
375
  alpha=alpha, beta=beta, reg=reg)
@@ -726,6 +745,7 @@ class FreeForm(object):
726
745
 
727
746
  See Also
728
747
  --------
748
+
729
749
  density
730
750
  hilbert
731
751
 
@@ -757,7 +777,7 @@ class FreeForm(object):
757
777
 
758
778
  # Create y if not given
759
779
  if (plot is False) and (y is None):
760
- # Do no tuse a Cartesian grid. Create a 1D array z slightly above
780
+ # Do not use a Cartesian grid. Create a 1D array z slightly above
761
781
  # the real line.
762
782
  y = self.delta * 1j
763
783
  z = x.astype(complex) + y # shape (Nx,)
@@ -809,25 +829,27 @@ class FreeForm(object):
809
829
  if self.psi is None:
810
830
  raise RuntimeError('"fit" the model first.')
811
831
 
812
- # Allow for arbitrary input shapes
813
832
  z = numpy.asarray(z)
814
- shape = z.shape
815
- if len(shape) == 0:
816
- shape = (1,)
817
- z = z.reshape(-1, 1)
818
-
819
- # # Set the number of bases as the number of x points insides support
820
- # mask_sup = numpy.logical_and(z.real >= self.lam_m,
821
- # z.real <= self.lam_p)
822
- # n_base = 2 * numpy.sum(mask_sup)
823
833
 
824
834
  # Stieltjes function
825
835
  if self.method == 'jacobi':
826
- stieltjes = partial(jacobi_stieltjes, psi=self.psi,
827
- support=self.support, alpha=self.alpha,
828
- beta=self.beta, continuation=self.continuation,
829
- dtype=self.dtype)
830
- # n_base = n_base
836
+
837
+ # Number of quadrature points
838
+ if z.ndim == 2:
839
+ # set to twice num x points inside support. This oversampling
840
+ # avoids anti-aliasing when visualizing.
841
+ x = z[0, :].real
842
+ mask_sup = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
843
+ n_quad = 2 * numpy.sum(mask_sup)
844
+ else:
845
+ # If this is None, the calling function will handle it.
846
+ n_quad = self.n_quad
847
+
848
+ stieltjes = partial(jacobi_stieltjes, cache=self.cache,
849
+ psi=self.psi, support=self.support,
850
+ alpha=self.alpha, beta=self.beta,
851
+ continuation=self.continuation,
852
+ dtype=self.dtype, n_quad=n_quad)
831
853
 
832
854
  elif self.method == 'chebyshev':
833
855
  stieltjes = partial(chebyshev_stieltjes, psi=self.psi,
@@ -835,6 +857,12 @@ class FreeForm(object):
835
857
  continuation=self.continuation,
836
858
  dtype=self.dtype)
837
859
 
860
+ # Allow for arbitrary input shapes
861
+ shape = z.shape
862
+ if len(shape) == 0:
863
+ shape = (1,)
864
+ z = z.reshape(-1, 1)
865
+
838
866
  mask_p = z.imag >= 0.0
839
867
  mask_m = z.imag < 0.0
840
868
 
@@ -930,7 +958,7 @@ class FreeForm(object):
930
958
  Estimated spectral density at locations x. ``rho`` can be a 1D or
931
959
  2D array output:
932
960
 
933
- * If ``size`` is a scalar, ``rho`` is a 1D array od the same size
961
+ * If ``size`` is a scalar, ``rho`` is a 1D array of the same size
934
962
  as ``x``.
935
963
  * If ``size`` is an array of size `n`, ``rho`` is a 2D array with
936
964
  `n` rows, where each row corresponds to decompression to a size.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.6.0
3
+ Version: 0.6.2
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
@@ -1 +0,0 @@
1
- __version__ = "0.6.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes