freealg 0.1.12__py3-none-any.whl → 0.1.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.
freealg/__init__.py CHANGED
@@ -6,7 +6,8 @@
6
6
  # under the terms of the license found in the LICENSE.txt file in the root
7
7
  # directory of this source tree.
8
8
 
9
- from .freeform import FreeForm, eigfree
9
+ from .freeform import FreeForm
10
+ from .eigfree import eigfree
10
11
  from . import distributions
11
12
 
12
13
  __all__ = ['FreeForm', 'distributions', 'eigfree']
freealg/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.12"
1
+ __version__ = "0.1.14"
freealg/_chebyshev.py CHANGED
@@ -58,7 +58,6 @@ def chebyshev_sample_proj(eig, support, K=10, reg=0.0):
58
58
 
59
59
  # Map to [–1,1] interval
60
60
  t = (2 * eig - (lam_m + lam_p)) / (lam_p - lam_m)
61
- N = eig.size
62
61
 
63
62
  # Inner‐product norm of each U_k under w(t) = sqrt{1–t^2} is \\pi/2
64
63
  norm = numpy.pi / 2
@@ -104,7 +103,7 @@ def chebyshev_kernel_proj(xs, pdf, support, K=10, reg=0.0):
104
103
 
105
104
  for k in range(K + 1):
106
105
  Pk = eval_chebyu(k, t) # U_k(t) on the grid
107
- moment = numpy.trapezoid(Pk * pdf, xs) # \int U_k(t) \rho(x) dx
106
+ moment = numpy.trapezoid(Pk * pdf, xs) # \int U_k(t) \rho(x) dx
108
107
 
109
108
  if k == 0:
110
109
  penalty = 0
@@ -226,12 +225,12 @@ def chebyshev_stieltjes(z, psi, support):
226
225
  S = wynn_pade(psi_zero, J)
227
226
 
228
227
  # build powers J^(k+1) for k=0..K
229
- #K = len(psi) - 1
228
+ # K = len(psi) - 1
230
229
  # shape: (..., K+1)
231
- #Jpow = J[..., None] ** numpy.arange(1, K+2)
230
+ # Jpow = J[..., None] ** numpy.arange(1, K+2)
232
231
 
233
232
  # sum psi_k * J^(k+1)
234
- #S = numpy.sum(psi * Jpow, axis=-1)
233
+ # S = numpy.sum(psi * Jpow, axis=-1)
235
234
 
236
235
  # assemble m(z)
237
236
  m_z = -2 / span * numpy.pi * S
freealg/_decompress.py CHANGED
@@ -11,17 +11,198 @@
11
11
  # =======
12
12
 
13
13
  import numpy
14
- # from scipy.integrate import solve_ivp
15
14
 
16
15
  __all__ = ['decompress', 'reverse_characteristics']
17
16
 
18
17
 
18
+ # =============
19
+ # secant method
20
+ # =============
21
+
22
+
23
+ def secant_complex(f, z0, z1, a=0+0j, tol=1e-12, max_iter=100,
24
+ alpha=0.5, max_bt=2, eps=1e-30, step_factor=5.0,
25
+ post_smooth=True, jump_tol=10.0, verbose=False):
26
+ """
27
+ Solves :math:``f(z) = a`` for many starting points simultaneously
28
+ using the secant method in the complex plane.
29
+
30
+ Parameters
31
+ ----------
32
+ f : callable
33
+ Function that accepts and returns complex `ndarray`s.
34
+
35
+ z0, z1 : array_like
36
+ Two initial guesses. ``z1`` may be broadcast to ``z0``.
37
+
38
+ a : complex or array_like, optional
39
+ Right‑hand‑side targets (broadcasted to ``z0``). Defaults to ``0+0j``.
40
+
41
+ tol : float, optional
42
+ Convergence criterion on ``|f(z) - a|``. Defaults to ``1e-12``.
43
+
44
+ max_iter : int, optional
45
+ Maximum number of secant iterations. Defaults to ``100``.
46
+
47
+ alpha : float, optional
48
+ Back‑tracking shrink factor (``0 < alpha < 1``). Defaults to ``0.5``.
49
+
50
+ max_bt : int, optional
51
+ Maximum back‑tracking trials per iteration. Defaults to ``0``.
52
+
53
+ eps : float, optional
54
+ Safeguard added to tiny denominators. Defaults to ``1e-30``.
55
+
56
+ post_smooth : bool, optional
57
+ If True (default) run a single vectorised clean-up pass that
58
+ re-solves points whose final root differs from the *nearest*
59
+ neighbour by more than ``jump_tol`` times the local median jump.
60
+
61
+ jump_tol : float, optional
62
+ Sensitivity of the clean-up pass; larger tolerance implies fewer
63
+ re-solves.
64
+
65
+ verbose : bool, optional
66
+ If *True*, prints progress every 10 iterations.
67
+
68
+ Returns
69
+ -------
70
+ roots : ndarray
71
+ Estimated roots, shaped like the broadcast inputs.
72
+ residuals : ndarray
73
+ Final residuals ``|f(root) - a|``.
74
+ iterations : ndarray
75
+ Iteration count for each point.
76
+ """
77
+
78
+ # Broadcast inputs
79
+ z0, z1, a = numpy.broadcast_arrays(
80
+ numpy.asarray(z0, numpy.complex128),
81
+ numpy.asarray(z1, numpy.complex128),
82
+ numpy.asarray(a, numpy.complex128),
83
+ )
84
+ orig_shape = z0.shape
85
+ z0, z1, a = (x.ravel() for x in (z0, z1, a))
86
+
87
+ n_points = z0.size
88
+ roots = z1.copy()
89
+ iterations = numpy.zeros(n_points, dtype=int)
90
+
91
+ f0 = f(z0) - a
92
+ f1 = f(z1) - a
93
+ residuals = numpy.abs(f1)
94
+ converged = residuals < tol
95
+
96
+ # Entering main loop
97
+ for k in range(max_iter):
98
+ active = ~converged
99
+ if not active.any():
100
+ break
101
+
102
+ # Secant step
103
+ denom = f1 - f0
104
+ denom = numpy.where(numpy.abs(denom) < eps, denom + eps, denom)
105
+ dz = (z1 - z0) * f1 / denom
106
+
107
+ # Step-size limiter
108
+ prev_step = numpy.maximum(numpy.abs(z1 - z0), eps)
109
+ max_step = step_factor * prev_step
110
+ big = numpy.abs(dz) > max_step
111
+ dz[big] *= max_step[big] / numpy.abs(dz[big])
112
+
113
+ z2 = z1 - dz
114
+ f2 = f(z2) - a
115
+
116
+ # Line search by backtracking
117
+ worse = (numpy.abs(f2) >= numpy.abs(f1)) & active
118
+ if worse.any():
119
+ shrink = numpy.ones_like(dz)
120
+ for _ in range(max_bt):
121
+ shrink[worse] *= alpha
122
+ z_try = z1[worse] - shrink[worse] * dz[worse]
123
+ f_try = f(z_try) - a[worse]
124
+
125
+ improved = numpy.abs(f_try) < numpy.abs(f1[worse])
126
+ if not improved.any():
127
+ continue
128
+
129
+ idx = numpy.flatnonzero(worse)[improved]
130
+ z2[idx], f2[idx] = z_try[improved], f_try[improved]
131
+ worse[idx] = False
132
+ if not worse.any():
133
+ break
134
+
135
+ # Book‑keeping
136
+ newly_conv = (numpy.abs(f2) < tol) & active
137
+ converged[newly_conv] = True
138
+ iterations[newly_conv] = k + 1
139
+ roots[newly_conv] = z2[newly_conv]
140
+ residuals[newly_conv] = numpy.abs(f2[newly_conv])
141
+
142
+ still = active & ~newly_conv
143
+ z0[still], z1[still] = z1[still], z2[still]
144
+ f0[still], f1[still] = f1[still], f2[still]
145
+
146
+ if verbose and k % 10 == 0:
147
+ print(f"Iter {k}: {converged.sum()} / {n_points} converged")
148
+
149
+ # Non‑converged points
150
+ remaining = ~converged
151
+ roots[remaining] = z1[remaining]
152
+ residuals[remaining] = numpy.abs(f1[remaining])
153
+ iterations[remaining] = max_iter
154
+
155
+ # Optional clean-up pass
156
+ if post_smooth and n_points > 2:
157
+ # absolute jump to *nearest* neighbour (left or right)
158
+ diff_left = numpy.empty_like(roots)
159
+ diff_right = numpy.empty_like(roots)
160
+ diff_left[1:] = numpy.abs(roots[1:] - roots[:-1])
161
+ diff_right[:-1] = numpy.abs(roots[:-1] - roots[1:])
162
+ jump = numpy.minimum(diff_left, diff_right)
163
+
164
+ # ignore unconverged points
165
+ median_jump = numpy.median(jump[~remaining])
166
+ bad = (jump > jump_tol * median_jump) & ~remaining
167
+
168
+ if bad.any():
169
+ z_first_all = numpy.where(bad & (diff_left <= diff_right),
170
+ roots - diff_left,
171
+ roots + diff_right)
172
+
173
+ # keep only the offending indices
174
+ z_first = z_first_all[bad]
175
+ z_second = z_first + (roots[bad] - z_first) * 1e-2
176
+
177
+ # re-solve just the outliers in one vector call
178
+ new_root, new_res, new_iter = secant_complex(
179
+ f, z_first, z_second, a[bad],
180
+ tol=tol, max_iter=max_iter,
181
+ alpha=alpha, max_bt=max_bt,
182
+ eps=eps, step_factor=step_factor,
183
+ post_smooth=False, # avoid recursion
184
+ )
185
+ roots[bad] = new_root
186
+ residuals[bad] = new_res
187
+ iterations[bad] = iterations[bad] + new_iter
188
+
189
+ if verbose:
190
+ print(f"Clean-up: re-solved {bad.sum()} outliers")
191
+
192
+ return (
193
+ roots.reshape(orig_shape),
194
+ residuals.reshape(orig_shape),
195
+ iterations.reshape(orig_shape),
196
+ )
197
+
198
+
19
199
  # ==========
20
200
  # decompress
21
201
  # ==========
22
202
 
23
- def decompress(freeform, size, x=None, delta=1e-6, iterations=500,
24
- step_size=0.1, tolerance=1e-9):
203
+
204
+ def decompress(freeform, size, x=None, delta=1e-4, max_iter=500,
205
+ tolerance=1e-8):
25
206
  """
26
207
  Free decompression of spectral density.
27
208
 
@@ -42,15 +223,11 @@ def decompress(freeform, size, x=None, delta=1e-6, iterations=500,
42
223
  Size of the perturbation into the upper half plane for Plemelj's
43
224
  formula.
44
225
 
45
- iterations: int, default=500
46
- Maximum number of Newton iterations.
226
+ max_iter: int, default=500
227
+ Maximum number of secant method iterations.
47
228
 
48
- step_size: float, default=0.1
49
- Step size for Newton iterations.
50
-
51
- tolerance: float, default=1e-4
52
- Tolerance for the solution obtained by the Newton solver. Also
53
- used for the finite difference approximation to the derivative.
229
+ tolerance: float, default=1e-12
230
+ Tolerance for the solution obtained by the secant method solver.
54
231
 
55
232
  Returns
56
233
  -------
@@ -85,56 +262,55 @@ def decompress(freeform, size, x=None, delta=1e-6, iterations=500,
85
262
  alpha = size / freeform.n
86
263
  m = freeform._eval_stieltjes
87
264
  # Lower and upper bound on new support
88
- hilb_lb = (1 / m(freeform.lam_m + delta * 1j)[1]).real
89
- hilb_ub = (1 / m(freeform.lam_p + delta * 1j)[1]).real
265
+ hilb_lb = (1 / m(freeform.lam_m + delta * 1j)).real
266
+ hilb_ub = (1 / m(freeform.lam_p + delta * 1j)).real
90
267
  lb = freeform.lam_m - (alpha - 1) * hilb_lb
91
268
  ub = freeform.lam_p - (alpha - 1) * hilb_ub
92
269
 
93
270
  # Create x if not given
94
- if x is None:
271
+ on_grid = (x is None)
272
+ if on_grid:
95
273
  radius = 0.5 * (ub - lb)
96
274
  center = 0.5 * (ub + lb)
97
275
  scale = 1.25
98
276
  x_min = numpy.floor(center - radius * scale)
99
277
  x_max = numpy.ceil(center + radius * scale)
100
278
  x = numpy.linspace(x_min, x_max, 500)
101
-
102
- def _char_z(z):
103
- return z + (1 / m(z)[1]) * (1 - alpha)
104
-
105
- # Ensure that input is an array
106
- x = numpy.asarray(x)
279
+ else:
280
+ x = numpy.asarray(x)
107
281
 
108
282
  target = x + delta * 1j
283
+ if numpy.isclose(alpha, 1.0):
284
+ return freeform.density(x), x, freeform.support
109
285
 
110
- z = numpy.full(target.shape, numpy.mean(freeform.support) - .1j,
111
- dtype=numpy.complex128)
112
-
113
- # Broken Newton steps can produce a lot of warnings. Removing them
114
- # for now.
115
- with numpy.errstate(all='ignore'):
116
- for _ in range(iterations):
117
- objective = _char_z(z) - target
118
- mask = numpy.abs(objective) >= tolerance
119
- if not numpy.any(mask):
120
- break
121
- z_m = z[mask]
286
+ # Characteristic curve map
287
+ def _char_z(z):
288
+ return z + (1 / m(z)) * (1 - alpha)
122
289
 
123
- # Perform finite difference approximation
124
- dfdz = _char_z(z_m+tolerance) - _char_z(z_m-tolerance)
125
- dfdz /= 2*tolerance
126
- dfdz[dfdz == 0] = 1.0
290
+ z0 = numpy.full(target.shape, numpy.mean(freeform.support) + .1j,
291
+ dtype=numpy.complex128)
292
+ z1 = z0 - .2j
127
293
 
128
- # Perform Newton step
129
- z[mask] = z_m - step_size * objective[mask] / dfdz
294
+ roots, _, _ = secant_complex(
295
+ _char_z, z0, z1,
296
+ a=target,
297
+ tol=tolerance,
298
+ max_iter=max_iter
299
+ )
130
300
 
131
301
  # Plemelj's formula
132
- char_s = m(z)[1] / alpha
302
+ z = roots
303
+ char_s = m(z) / alpha
133
304
  rho = numpy.maximum(0, char_s.imag / numpy.pi)
134
305
  rho[numpy.isnan(rho) | numpy.isinf(rho)] = 0
135
- rho = rho.reshape(*x.shape)
306
+ if on_grid:
307
+ x, rho = x.ravel(), rho.ravel()
308
+ # dx = x[1] - x[0]
309
+ # left_idx, right_idx = support_from_density(dx, rho)
310
+ # x, rho = x[left_idx-1:right_idx+1], rho[left_idx-1:right_idx+1]
311
+ rho = rho / numpy.trapezoid(rho, x)
136
312
 
137
- return rho, x, (lb, ub)
313
+ return rho.reshape(*x.shape), x, (lb, ub)
138
314
 
139
315
 
140
316
  # =======================
@@ -152,7 +328,7 @@ def reverse_characteristics(freeform, z_inits, T, iterations=500,
152
328
  m = freeform._eval_stieltjes
153
329
 
154
330
  def _char_z(z, t):
155
- return z + (1 / m(z)[1]) * (1 - numpy.exp(t))
331
+ return z + (1 / m(z)) * (1 - numpy.exp(t))
156
332
 
157
333
  target_z, target_t = numpy.meshgrid(z_inits, t_eval)
158
334
 
freealg/_pade.py CHANGED
@@ -236,9 +236,10 @@ def _eval_rational(z, c, D, poles, resid):
236
236
 
237
237
  return c + D * z + term
238
238
 
239
- # ========
240
- # Wynn epsilon algorithm for Pade
241
- # ========
239
+
240
+ # =========
241
+ # Wynn pade
242
+ # =========
242
243
 
243
244
  @numba.jit(nopython=True, parallel=True)
244
245
  def wynn_pade(coeffs, x):
@@ -248,48 +249,57 @@ def wynn_pade(coeffs, x):
248
249
  returns a function handle that computes the Pade approximant at any x
249
250
  using Wynn's epsilon algorithm.
250
251
 
251
- Parameters:
252
- coeffs (list or array): Coefficients [a0, a1, a2, ...] of the power series.
252
+ Parameters
253
+ ----------
254
+
255
+ coeffs (list or array):
256
+ Coefficients [a0, a1, a2, ...] of the power series.
253
257
 
254
- Returns:
255
- function: A function approximant(x) that returns the approximated value f(x).
258
+ Returns
259
+ -------
260
+
261
+ function:
262
+ A function approximant(x) that returns the approximated value f(x).
256
263
  """
264
+
257
265
  # Number of coefficients
258
266
  xn = x.ravel()
259
267
  d = len(xn)
260
268
  N = len(coeffs)
261
-
269
+
262
270
  # Compute the partial sums s_n = sum_{i=0}^n a_i * x^i for n=0,...,N-1
263
271
  eps = numpy.zeros((N+1, N, d), dtype=numpy.complex128)
264
272
  for i in numba.prange(d):
265
273
  partial_sum = 0.0
266
274
  for n in range(N):
267
275
  partial_sum += coeffs[n] * (xn[i] ** n)
268
- eps[0,n,i] = partial_sum
276
+ eps[0, n, i] = partial_sum
269
277
 
270
278
  for i in numba.prange(d):
271
279
  for k in range(1, N+1):
272
280
  for j in range(N - k):
273
- delta = eps[k-1, j+1,i] - eps[k-1, j,i]
281
+ delta = eps[k-1, j+1, i] - eps[k-1, j, i]
274
282
  if delta == 0:
275
283
  rec_delta = numpy.inf
276
284
  elif numpy.isinf(delta) or numpy.isnan(delta):
277
285
  rec_delta = 0.0
278
286
  else:
279
287
  rec_delta = 1.0 / delta
280
- eps[k,j,i] = rec_delta
288
+ eps[k, j, i] = rec_delta
281
289
  if k > 1:
282
- eps[k,j,i] += eps[k-2,j+1,i]
290
+ eps[k, j, i] += eps[k-2, j+1, i]
283
291
 
284
292
  if (N % 2) == 0:
285
293
  N -= 1
286
-
294
+
287
295
  return eps[N-1, 0, :].reshape(x.shape)
288
296
 
297
+
289
298
  # ========
290
299
  # fit pade
291
300
  # ========
292
301
 
302
+
293
303
  def fit_pade(x, f, lam_m, lam_p, p=1, q=2, odd_side='left', pade_reg=0.0,
294
304
  safety=1.0, max_outer=40, xtol=1e-12, ftol=1e-12, optimizer='ls',
295
305
  verbose=0):
freealg/_plot_util.py CHANGED
@@ -139,7 +139,7 @@ def _auto_bins(array, method='scott', factor=5):
139
139
  # ============
140
140
 
141
141
  def plot_density(x, rho, eig=None, support=None, label='',
142
- title='Spectral density', latex=False, save=False):
142
+ title='Spectral Density', latex=False, save=False):
143
143
  """
144
144
  """
145
145
 
@@ -147,8 +147,11 @@ def plot_density(x, rho, eig=None, support=None, label='',
147
147
 
148
148
  fig, ax = plt.subplots(figsize=(6, 2.7))
149
149
 
150
- if (support is not None) and (eig is not None):
151
- lam_m, lam_p = support
150
+ if eig is not None:
151
+ if support is not None:
152
+ lam_m, lam_p = support
153
+ else:
154
+ lam_m, lam_p = min(eig), max(eig)
152
155
  bins = numpy.linspace(lam_m, lam_p, _auto_bins(eig))
153
156
  _ = ax.hist(eig, bins, density=True, color='silver',
154
157
  edgecolor='none', label='Histogram')
freealg/_support.py CHANGED
@@ -1,31 +1,126 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+ # SPDX-FileType: SOURCE
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it under
5
+ # the terms of the license found in the LICENSE.txt file in the root directory
6
+ # of this source tree.
7
+
8
+
9
+ # =======
10
+ # Imports
11
+ # =======
12
+
1
13
  import numpy
14
+ import numba
2
15
  from scipy.stats import gaussian_kde
3
16
 
4
- def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs):
17
+
18
+ @numba.njit(numba.types.UniTuple(numba.types.int64, 2)(
19
+ numba.types.float64,
20
+ numba.types.float64[::1]
21
+ ))
22
+ def support_from_density(dx, density):
23
+ """
24
+ Estimates the support from a collection of noisy observations of a
25
+ density over a grid of x-values with mesh spacing dx.
26
+ """
27
+ n = density.shape[0]
28
+ target = 1.0 / dx
29
+
30
+ # 1) compute total_sum once
31
+ total_sum = 0.0
32
+ for t in range(n):
33
+ total_sum += density[t]
34
+
35
+ # 2) set up our “best‐so‐far” trackers
36
+ large = 1e300
37
+ best_nonneg_sum = large
38
+ best_nonneg_idx = -1
39
+ best_nonpos_sum = -large
40
+ best_nonpos_idx = -1
41
+
42
+ # 3) seed with first element (i.e. prefix_sum for k=1)
43
+ prefix_sum = density[0]
44
+ if prefix_sum >= 0.0:
45
+ best_nonneg_sum, best_nonneg_idx = prefix_sum, 1
46
+ else:
47
+ best_nonpos_sum, best_nonpos_idx = prefix_sum, 1
48
+
49
+ # 4) sweep j from 2...n–1, updating prefix_sum on the fly
50
+ optimal_i, optimal_j = 1, 2
51
+ minimal_cost = large
52
+
53
+ for j in range(2, n):
54
+ # extend prefix_sum to cover density[0]...density[j-1]
55
+ prefix_sum += density[j-1]
56
+
57
+ # cost for [0...i], [i...j]
58
+ diff_mid = prefix_sum - target
59
+ if diff_mid >= 0.0 and best_nonneg_sum <= diff_mid:
60
+ cost12 = diff_mid
61
+ i_cand = best_nonneg_idx
62
+ elif diff_mid < 0.0 and best_nonpos_sum >= diff_mid:
63
+ cost12 = -diff_mid
64
+ i_cand = best_nonpos_idx
65
+ else:
66
+ cost_using_nonpos = diff_mid - 2.0 * best_nonpos_sum
67
+ cost_using_nonneg = 2.0 * best_nonneg_sum - diff_mid
68
+ if cost_using_nonpos < cost_using_nonneg:
69
+ cost12, i_cand = cost_using_nonpos, best_nonpos_idx
70
+ else:
71
+ cost12, i_cand = cost_using_nonneg, best_nonneg_idx
72
+
73
+ # cost for [j...n]
74
+ cost3 = total_sum - prefix_sum
75
+ if cost3 < 0.0:
76
+ cost3 = -cost3
77
+
78
+ # total and maybe update best split
79
+ total_cost = cost12 + cost3
80
+ if total_cost < minimal_cost:
81
+ minimal_cost = total_cost
82
+ optimal_i, optimal_j = i_cand, j
83
+
84
+ # update our prefix‐sum trackers
85
+ if prefix_sum >= 0.0:
86
+ if prefix_sum < best_nonneg_sum:
87
+ best_nonneg_sum, best_nonneg_idx = prefix_sum, j
88
+ else:
89
+ if prefix_sum > best_nonpos_sum:
90
+ best_nonpos_sum, best_nonpos_idx = prefix_sum, j
91
+
92
+ return optimal_i, optimal_j
93
+
94
+
95
+ def detect_support(eigs, method='asymp', k=None, p=0.001, **kwargs):
5
96
  """
6
97
  Estimates the support of the eigenvalue density.
7
98
 
8
99
  Parameters
9
100
  ----------
10
- method : {``'range'``, ``'jackknife'``, ``'regression'``, ``'interior'``,
11
- ``'interior_smooth'``}, \
12
- default= ``'jackknife'``
101
+ method : {``'range'``, ``'asymp'``, ``'jackknife'``, ``'regression'``,
102
+ ``'interior'``, ``'interior_smooth'``}, \
103
+ default= ``'asymp'``
13
104
  The method of support estimation:
14
105
 
15
- * ``'range'``: no estimation; the support is the range of the eigenvalues
106
+ * ``'range'``: no estimation; the support is the range of the
107
+ eigenvalues.
108
+ * ``'asymp'``: assume the relative error in the min/max estimator is
109
+ 1/n.
16
110
  * ``'jackknife'``: estimates the support using Quenouille's [1]
17
- jackknife estimator. Fast and simple, more accurate than the range.
18
- * ``'regression'``: estimates the support by performing a regression under
19
- the assumption that the edge behavior is of square-root type. Often
20
- most accurate.
111
+ jackknife estimator. Fast and simple, more accurate than the
112
+ range.
113
+ * ``'regression'``: estimates the support by performing a regression
114
+ under the assumption that the edge behavior is of square-root
115
+ type. Often most accurate.
21
116
  * ``'interior'``: estimates a support assuming the range overestimates;
22
117
  uses quantiles (p, 1-p).
23
- * ``'interior_smooth'``: same as ``'interior'`` but using kernel density
24
- estimation.
118
+ * ``'interior_smooth'``: same as ``'interior'`` but using kernel
119
+ density estimation.
25
120
 
26
121
  k : int, default = None
27
- Number of extreme order statistics to use for ``method='regression'``.
28
-
122
+ Number of extreme order statistics to use for ``method='regression'``.
123
+
29
124
  p : float, default=0.001
30
125
  The edges of the support of the distribution is detected by the
31
126
  :math:`p`-quantile on the left and :math:`(1-p)`-quantile on the right
@@ -36,28 +131,33 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
36
131
  References
37
132
  ----------
38
133
 
39
- .. [1] Quenouille, M. H. (1949, July). Approximate tests of correlation in time-series.
40
- In Mathematical Proceedings of the Cambridge Philosophical Society (Vol. 45, No. 3,
41
- pp. 483-484). Cambridge University Press.
134
+ .. [1] Quenouille, M. H. (1949, July). Approximate tests of correlation in
135
+ time-series. In Mathematical Proceedings of the Cambridge
136
+ Philosophical Society (Vol. 45, No. 3, pp. 483-484). Cambridge
137
+ University Press.
42
138
  """
43
139
 
44
- if method=='range':
140
+ if method == 'range':
45
141
  lam_m = eigs.min()
46
142
  lam_p = eigs.max()
47
143
 
48
- elif method=='jackknife':
144
+ elif method == 'asymp':
145
+ lam_m = eigs.min() - abs(eigs.min()) / len(eigs)
146
+ lam_p = eigs.max() + abs(eigs.max()) / len(eigs)
147
+
148
+ elif method == 'jackknife':
49
149
  x, n = numpy.sort(eigs), len(eigs)
50
- lam_m = x[0] - (n - 1)/n * (x[1] - x[0])
150
+ lam_m = x[0] - (n - 1)/n * (x[1] - x[0])
51
151
  lam_p = x[-1] + (n - 1)/n * (x[-1] - x[-2])
52
152
 
53
- elif method=='regression':
153
+ elif method == 'regression':
54
154
  x, n = numpy.sort(eigs), len(eigs)
55
155
  if k is None:
56
156
  k = int(round(n ** (2/3)))
57
157
  k = max(5, min(k, n // 2))
58
158
 
59
159
  # The theoretical cdf near the edge behaves like const*(x - a)^{3/2},
60
- # so (i/n) (x - a)^{3/2} x a + const*(i/n)^{2/3}.
160
+ # so (i/n) ~ (x - a)^{3/2} -> x ~ a + const*(i/n)^{2/3}.
61
161
  y = ((numpy.arange(1, k + 1) - 0.5) / n) ** (2 / 3)
62
162
 
63
163
  # Left edge: regress x_{(i)} on y
@@ -66,10 +166,10 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
66
166
  # Right edge: regress x_{(n-i+1)} on y
67
167
  _, lam_p = numpy.polyfit(y, x[-k:][::-1], 1)
68
168
 
69
- elif method=='interior':
169
+ elif method == 'interior':
70
170
  lam_m, lam_p = numpy.quantile(eigs, [p, 1-p])
71
-
72
- elif method=='interior_smooth':
171
+
172
+ elif method == 'interior_smooth':
73
173
  kde = gaussian_kde(eigs)
74
174
  xs = numpy.linspace(eigs.min(), eigs.max(), 1000)
75
175
  fs = kde(xs)
@@ -79,6 +179,7 @@ def detect_support(eigs, method='interior_smooth', k = None, p = 0.001, **kwargs
79
179
 
80
180
  lam_m = numpy.interp(p, cdf, xs)
81
181
  lam_p = numpy.interp(1-p, cdf, xs)
182
+
82
183
  else:
83
184
  raise NotImplementedError("Unknown method")
84
185