freealg 0.5.4__py3-none-any.whl → 0.6.1__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/_sample.py CHANGED
@@ -15,7 +15,7 @@ from scipy.integrate import cumulative_trapezoid
15
15
  from scipy.interpolate import PchipInterpolator
16
16
  from scipy.stats import qmc
17
17
 
18
- __all__ = ['qmc_sample']
18
+ __all__ = ['sample']
19
19
 
20
20
 
21
21
  # =============
@@ -32,60 +32,75 @@ def _quantile_func(x, rho, clamp=1e-4, eps=1e-8):
32
32
  rho_clamp[rho < clamp] = eps
33
33
  cdf = cumulative_trapezoid(rho_clamp, x, initial=0)
34
34
  cdf /= cdf[-1]
35
+ cdf_inv = PchipInterpolator(cdf, x, extrapolate=False)
35
36
 
36
- return PchipInterpolator(cdf, x, extrapolate=False)
37
+ return cdf_inv
37
38
 
38
39
 
39
- # ==========
40
- # qmc sample
41
- # ==========
40
+ # ======
41
+ # sample
42
+ # ======
42
43
 
43
- def qmc_sample(x, rho, num_pts, seed=None):
44
+ def sample(x, rho, num_pts, method='qmc', seed=None):
44
45
  """
45
- Low-discrepancy sampling from a univariate density estimate using
46
- Quasi-Monte Carlo.
46
+ Low-discrepancy sampling from density estimate.
47
47
 
48
48
  Parameters
49
49
  ----------
50
50
 
51
- x : numpy.array, shape (n,)
52
- Sorted abscissae at which the density has been evaluated.
51
+ x : numpy.array
52
+ Sorted abscissae at which the density has been evaluated. Shape `(n,)`.
53
53
 
54
- rho : numpy.array, shape (n,)
54
+ rho : numpy.array
55
55
  Density values corresponding to `x`. Must be non-negative and define
56
56
  a valid probability density (i.e., integrate to 1 over the support).
57
+ Shape `(n,)`.
57
58
 
58
59
  num_pts : int
59
60
  Number of sample points to generate from the density estimate.
60
61
 
62
+ method : {``'mc'``, ``'qmc'``}, default= ``'qmc'``
63
+ Method of drawing samples from uniform distribution:
64
+
65
+ * ``'mc'``: Monte Carlo
66
+ * ``'qmc'``: Quasi Monte Carlo
67
+
61
68
  seed : int, default=None
62
69
  Seed for random number generator
63
70
 
64
71
  Returns
65
72
  -------
73
+
66
74
  samples : numpy.array, shape (num_pts,)
67
75
  Samples drawn from the estimated density using a one-dimensional Halton
68
76
  sequence mapped through the estimated quantile function.
69
77
 
70
78
  See Also
71
79
  --------
72
- scipy.stats.qmc.Halton
73
- Underlying Quasi-Monte Carlo engine used for generating low-discrepancy
74
- points.
80
+
81
+ freealg.supp
82
+ freealg.kde
83
+
84
+ Notes
85
+ -----
86
+
87
+ The underlying Quasi-Monte Carlo engine uses ``scipy.stats.qmc.Halton``
88
+ function for generating low-discrepancy points.
75
89
 
76
90
  Examples
77
91
  --------
78
92
 
79
93
  .. code-block:: python
94
+ :emphasize-lines: 8
80
95
 
81
96
  >>> import numpy
82
- >>> from freealg import qmc_sample
97
+ >>> from freealg import sample
83
98
 
84
99
  >>> # density of Beta(3,1) on [0,1]
85
100
  >>> x = numpy.linspace(0, 1, 200)
86
101
  >>> rho = 3 * x**2
87
102
 
88
- >>> samples = qmc_sample(x, rho, num_pts=1000)
103
+ >>> samples = sample(x, rho, num_pts=1000, method='qmc')
89
104
  >>> assert samples.shape == (1000,)
90
105
 
91
106
  >>> # Empirical mean should be close to 3/4
@@ -94,8 +109,17 @@ def qmc_sample(x, rho, num_pts, seed=None):
94
109
 
95
110
  rng = numpy.random.default_rng(seed)
96
111
  quantile = _quantile_func(x, rho)
97
- engine = qmc.Halton(d=1, rng=rng)
98
- u = engine.random(num_pts)
112
+
113
+ # Draw from uniform distribution
114
+ if method == 'mc':
115
+ u = rng.random(num_pts)
116
+ elif method == 'qmc':
117
+ engine = qmc.Halton(d=1, rng=rng)
118
+ u = engine.random(num_pts)
119
+ else:
120
+ raise NotImplementedError('"method" is invalid.')
121
+
122
+ # Draw from distribution by mapping from inverse CDF
99
123
  samples = quantile(u)
100
124
 
101
125
  return samples.ravel()
freealg/_series.py CHANGED
@@ -183,13 +183,13 @@ def wynn_rho(Sn, beta=0.0):
183
183
  -------
184
184
 
185
185
  S : numpy.ndarray
186
- A 1D array of shape ``(d,)`` giving the rhoaccelerated estimate
186
+ A 1D array of shape ``(d,)`` giving the rho-accelerated estimate
187
187
  of the series limit for each component.
188
188
 
189
189
  Notes
190
190
  -----
191
191
 
192
- Let ``S_n`` be the *n*‑th partial sum of the (possibly divergent)
192
+ Let ``S_n`` be the *n*-th partial sum of the (possibly divergent)
193
193
  sequence. Wynn's rho algorithm builds a triangular table
194
194
  ``rho[k, n]`` (row *k*, column *n*) as follows:
195
195
 
@@ -200,7 +200,7 @@ def wynn_rho(Sn, beta=0.0):
200
200
  (n + beta + k - 1) / (rho[k-1, n+1] - rho[k-1, n])
201
201
 
202
202
  Only even rows (k even) provide improved approximants. As with
203
- ``wynn_epsilon``, we apply the scalar recursion componentwise so that a
203
+ ``wynn_epsilon``, we apply the scalar recursion component-wise so that a
204
204
  slowly converging component does not stall the others.
205
205
  """
206
206
 
@@ -255,7 +255,7 @@ def wynn_rho(Sn, beta=0.0):
255
255
 
256
256
  def levin_u(Sn, omega=None, beta=0.0):
257
257
  """
258
- Levin utransform (vector form).
258
+ Levin u-transform (vector form).
259
259
 
260
260
  Parameters
261
261
  ----------
@@ -339,13 +339,13 @@ def weniger_delta(Sn):
339
339
  -------
340
340
 
341
341
  S : numpy.ndarray
342
- Array of shape (d,) giving the Δ²‑accelerated limit estimate for each
343
- component.
342
+ Array of shape (d,) giving the delta2 accelerated limit estimate for
343
+ each component.
344
344
  """
345
345
 
346
346
  N, d = Sn.shape
347
347
 
348
- # Need at least three partial sums to form Δ²
348
+ # Need at least three partial sums to form delta2
349
349
  if N < 3:
350
350
  return Sn[-1, :].copy()
351
351
 
@@ -384,14 +384,14 @@ def brezinski_theta(Sn):
384
384
  ----------
385
385
 
386
386
  Sn : numpy.ndarray
387
- A 2D array of the size ``(N, d)``, where `N` is the number of partial
387
+ A 2-D array of the size ``(N, d)``, where `N` is the number of partial
388
388
  sums and `d` is the vector size.
389
389
 
390
390
  Returns
391
391
  -------
392
392
 
393
393
  S : numpy.ndarray
394
- A 1D array of the size ``(d,)`` the thetaaccelerated estimate of
394
+ A 1-D array of the size ``(d,)``. The theta-accelerated estimate of
395
395
  the series limit in each vector component.
396
396
  """
397
397
 
freealg/_support.py CHANGED
@@ -14,7 +14,7 @@ import numpy
14
14
  import numba
15
15
  from scipy.stats import gaussian_kde
16
16
 
17
- __all__ = ['support_from_density', 'detect_support']
17
+ __all__ = ['support_from_density', 'supp']
18
18
 
19
19
 
20
20
  # ====================
@@ -34,26 +34,26 @@ def support_from_density(dx, density):
34
34
  n = density.shape[0]
35
35
  target = 1.0 / dx
36
36
 
37
- # 1) compute total_sum once
37
+ # compute total_sum once
38
38
  total_sum = 0.0
39
39
  for t in range(n):
40
40
  total_sum += density[t]
41
41
 
42
- # 2) set up our bestsofar trackers
42
+ # set up our "best-so-far" trackers
43
43
  large = 1e300
44
44
  best_nonneg_sum = large
45
45
  best_nonneg_idx = -1
46
46
  best_nonpos_sum = -large
47
47
  best_nonpos_idx = -1
48
48
 
49
- # 3) seed with first element (i.e. prefix_sum for k=1)
49
+ # seed with first element (i.e. prefix_sum for k=1)
50
50
  prefix_sum = density[0]
51
51
  if prefix_sum >= 0.0:
52
52
  best_nonneg_sum, best_nonneg_idx = prefix_sum, 1
53
53
  else:
54
54
  best_nonpos_sum, best_nonpos_idx = prefix_sum, 1
55
55
 
56
- # 4) sweep j from 2...n1, updating prefix_sum on the fly
56
+ # sweep j from 2, ..., n-1, updating prefix_sum on the fly
57
57
  optimal_i, optimal_j = 1, 2
58
58
  minimal_cost = large
59
59
 
@@ -88,7 +88,7 @@ def support_from_density(dx, density):
88
88
  minimal_cost = total_cost
89
89
  optimal_i, optimal_j = i_cand, j
90
90
 
91
- # update our prefixsum trackers
91
+ # update our prefix-sum trackers
92
92
  if prefix_sum >= 0.0:
93
93
  if prefix_sum < best_nonneg_sum:
94
94
  best_nonneg_sum, best_nonneg_idx = prefix_sum, j
@@ -99,36 +99,34 @@ def support_from_density(dx, density):
99
99
  return optimal_i, optimal_j
100
100
 
101
101
 
102
- # ==============
103
- # detect support
104
- # ==============
102
+ # ====
103
+ # supp
104
+ # ====
105
105
 
106
- def detect_support(eigs, method='asymp', k=None, p=0.001, **kwargs):
106
+ def supp(eigs, method='asymp', k=None, p=0.001):
107
107
  """
108
108
  Estimates the support of the eigenvalue density.
109
109
 
110
110
  Parameters
111
111
  ----------
112
112
 
113
- method : {``'range'``, ``'asymp'``, ``'jackknife'``, ``'regression'``,
114
- ``'interior'``, ``'interior_smooth'``}, \
115
- default= ``'asymp'``
113
+ method : {``'range'``, ``'asymp'``, ``'jackknife'``, ``'regression'``, \
114
+ ``'interior'``, ``'interior_smooth'``}, default= ``'asymp'``
116
115
  The method of support estimation:
117
116
 
118
117
  * ``'range'``: no estimation; the support is the range of the
119
- eigenvalues.
118
+ eigenvalues.
120
119
  * ``'asymp'``: assume the relative error in the min/max estimator is
121
- 1/n.
122
- * ``'jackknife'``: estimates the support using Quenouille's [1]
123
- jackknife estimator. Fast and simple, more accurate than the
124
- range.
120
+ :math:`1/n`.
121
+ * ``'jackknife'``: estimates the support using Quenouille's [1]_
122
+ jackknife estimator. Fast and simple, more accurate than the range.
125
123
  * ``'regression'``: estimates the support by performing a regression
126
- under the assumption that the edge behavior is of square-root
127
- type. Often most accurate.
124
+ under the assumption that the edge behavior is of square-root type.
125
+ Often most accurate.
128
126
  * ``'interior'``: estimates a support assuming the range overestimates;
129
- uses quantiles (p, 1-p).
127
+ uses quantiles :math:`(p, 1-p)`.
130
128
  * ``'interior_smooth'``: same as ``'interior'`` but using kernel
131
- density estimation, from [2]_.
129
+ density estimation, from [2]_.
132
130
 
133
131
  k : int, default = None
134
132
  Number of extreme order statistics to use for ``method='regression'``.
@@ -140,6 +138,21 @@ def detect_support(eigs, method='asymp', k=None, p=0.001, **kwargs):
140
138
  This value should be between 0 and 1, ideally a small number close to
141
139
  zero.
142
140
 
141
+ Returns
142
+ -------
143
+
144
+ lam_m : float
145
+ Lower end of support interval :math:`[\\lambda_{-}, \\lambda_{+}]`.
146
+
147
+ lam_p : float
148
+ Upper end of support interval :math:`[\\lambda_{-}, \\lambda_{+}]`.
149
+
150
+ See Also
151
+ --------
152
+
153
+ freealg.sample
154
+ freealg.kde
155
+
143
156
  References
144
157
  ----------
145
158
 
freealg/_util.py CHANGED
@@ -13,14 +13,60 @@
13
13
 
14
14
  import numpy
15
15
  import scipy
16
+ from scipy.stats import gaussian_kde
16
17
  from scipy.stats import beta
18
+ # from statsmodels.nonparametric.kde import KDEUnivariate
17
19
  from scipy.optimize import minimize
20
+ import matplotlib.pyplot as plt
21
+ import texplot
22
+ from ._plot_util import _auto_bins
18
23
 
19
24
  # Fallback to previous API
20
25
  if not hasattr(numpy, 'trapezoid'):
21
26
  numpy.trapezoid = numpy.trapz
22
27
 
23
- __all__ = ['compute_eig', 'beta_kde', 'force_density']
28
+ __all__ = ['resolve_complex_dtype', 'compute_eig', 'kde', 'force_density']
29
+
30
+
31
+ # =====================
32
+ # resolve complex dtype
33
+ # =====================
34
+
35
+ def resolve_complex_dtype(dtype):
36
+ """
37
+ Convert a user-supplied dtype name to a NumPy dtype object and fall back
38
+ safely if the requested precision is unavailable.
39
+ """
40
+
41
+ # Normalise the string
42
+ dtype = str(dtype).lower()
43
+
44
+ if not isinstance(numpy.dtype(dtype), numpy.dtype):
45
+ raise ValueError(f'{dtype} is not a recognized numpy dtype.')
46
+ elif not numpy.issubdtype(numpy.dtype(dtype), numpy.complexfloating):
47
+ raise ValueError(f'{dtype} is not a complex dtype.')
48
+
49
+ if dtype in {'complex128', '128'}:
50
+ cdtype = numpy.complex128
51
+
52
+ elif dtype in ['complex256', '256', 'longcomplex', 'clongcomplex']:
53
+
54
+ complex256_found = False
55
+ for name in ['complex256', 'clongcomplex']:
56
+ if hasattr(numpy, name):
57
+ cdtype = getattr(numpy, name)
58
+ complex256_found = True
59
+
60
+ if not complex256_found:
61
+ raise RuntimeWarning(
62
+ 'NumPy on this platform has no 256-bit complex type. ' +
63
+ 'Falling back to complex128.')
64
+ cdtype = numpy.complex128
65
+
66
+ else:
67
+ raise ValueError('Unsupported dtype.')
68
+
69
+ return cdtype
24
70
 
25
71
 
26
72
  # ===========
@@ -37,50 +83,107 @@ def compute_eig(A, lower=False):
37
83
  return eig
38
84
 
39
85
 
40
- # ========
41
- # beta kde
42
- # ========
86
+ # ===
87
+ # kde
88
+ # ===
43
89
 
44
- def beta_kde(eig, xs, lam_m, lam_p, h):
90
+ def kde(eig, xs, lam_m, lam_p, h, kernel='beta', plot=False):
45
91
  """
46
- Beta-kernel KDE with automatic guards against NaNs.
92
+ Kernel density estimation of eigenvalues.
47
93
 
48
94
  Parameters
49
95
  ----------
50
- eig : (n,) 1-D array of samples
51
- xs : evaluation grid (must lie within [lam_m, lam_p])
52
- lam_m, lam_p : float, support endpoints (lam_m < lam_p)
53
- h : bandwidth in rescaled units (0 < h < 1)
54
96
 
55
- Returns
56
- -------
57
- pdf : ndarray same length as xs
58
- """
97
+ eig : numpy.array
98
+ 1D array of samples of size `n`.
99
+
100
+ xs : numpy.array
101
+ 1D array of evaluation grid (must lie within ``[lam_m, lam_p]``)
102
+
103
+ lam_m : float
104
+ Lower end of the support endpoints with ``lam_m < lam_p``.
59
105
 
60
- span = lam_p - lam_m
61
- if span <= 0:
62
- raise ValueError("lam_p must be larger than lam_m")
106
+ lam_p : float
107
+ Upper end of the support endpoints with ``lam_m < lam_p``.
63
108
 
64
- # map samples and grid to [0, 1]
65
- u = (eig - lam_m) / span
66
- t = (xs - lam_m) / span
109
+ h : float
110
+ Kernel bandwidth in rescaled units where ``0 < h < 1``.
67
111
 
68
- if u.min() < 0 or u.max() > 1:
69
- mask = (u > 0) & (u < 1)
70
- u = u[mask]
112
+ kernel : {``'gaussian'``, ``'beta'``}, default= ``'beta'``
113
+ Kernel function using either Gaussian or Beta distribution.
71
114
 
72
- pdf = numpy.zeros_like(xs, dtype=float)
73
- n = len(u)
115
+ plot : bool, default=False
116
+ If `True`, the KDE is plotted.
74
117
 
75
- # tiny positive number to keep shape parameters >0
76
- eps = 1e-6
77
- for ui in u:
78
- a = max(ui / h + 1.0, eps)
79
- b = max((1.0 - ui) / h + 1.0, eps)
80
- pdf += beta.pdf(t, a, b)
118
+ Returns
119
+ -------
120
+
121
+ pdf : numpy.ndarray
122
+ Probability distribution function with the same length as ``xs``.
123
+
124
+ See Also
125
+ --------
126
+
127
+ freealg.supp
128
+ freealg.sample
129
+ """
81
130
 
82
- pdf /= n * span # renormalise
83
- pdf[(t < 0) | (t > 1)] = 0.0 # exact zeros outside
131
+ if kernel == 'gaussian':
132
+ pdf = gaussian_kde(eig, bw_method=h)(xs)
133
+
134
+ # Adaptive KDE
135
+ # k = KDEUnivariate(eig)
136
+ # k.fit(kernel='gau', bw='silverman', fft=False, weights=None,
137
+ # gridsize=1024, adaptive=True)
138
+ # pdf = k.evaluate(xs)
139
+
140
+ elif kernel == 'beta':
141
+
142
+ span = lam_p - lam_m
143
+ if span <= 0:
144
+ raise ValueError("lam_p must be larger than lam_m")
145
+
146
+ # map samples and grid to [0, 1]
147
+ u = (eig - lam_m) / span
148
+ t = (xs - lam_m) / span
149
+
150
+ if u.min() < 0 or u.max() > 1:
151
+ mask = (u > 0) & (u < 1)
152
+ u = u[mask]
153
+
154
+ pdf = numpy.zeros_like(xs, dtype=float)
155
+ n = len(u)
156
+
157
+ # tiny positive number to keep shape parameters > 0
158
+ 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)
163
+
164
+ pdf /= n * span # renormalise
165
+ pdf[(t < 0) | (t > 1)] = 0.0 # exact zeros outside
166
+
167
+ else:
168
+ raise NotImplementedError('"kernel" is invalid.')
169
+
170
+ if plot:
171
+ with texplot.theme(use_latex=False):
172
+ fig, ax = plt.subplots(figsize=(6, 4))
173
+
174
+ x_min = numpy.min(xs)
175
+ x_max = numpy.max(xs)
176
+ bins = numpy.linspace(x_min, x_max, _auto_bins(eig))
177
+ _ = ax.hist(eig, bins, density=True, color='silver',
178
+ edgecolor='none', label='Samples histogram')
179
+ ax.plot(xs, pdf, color='black', label='KDE')
180
+ ax.set_xlabel(r'$x$')
181
+ ax.set_ylabel(r'$\\rho(x)$')
182
+ ax.set_xlim([xs[0], xs[-1]])
183
+ ax.set_ylim(bottom=0)
184
+ ax.set_title('Kernel Density Estimation')
185
+ ax.legend(fontsize='x-small')
186
+ plt.show()
84
187
 
85
188
  return pdf
86
189
 
@@ -95,8 +198,8 @@ def force_density(psi0, support, density, grid, alpha=0.0, beta=0.0):
95
198
  min 0.5 ||psi - psi0||^2
96
199
  s.t. F_pos psi >= 0 (positivity on grid)
97
200
  psi[0] = psi0[0] (mass)
98
- f(lam_m)·psi = 0 (zero at left edge)
99
- f(lam_p)·psi = 0 (zero at right edge)
201
+ f(lam_m) psi = 0 (zero at left edge)
202
+ f(lam_p) psi = 0 (zero at right edge)
100
203
  """
101
204
 
102
205
  lam_m, lam_p = support
@@ -78,7 +78,7 @@ class KestenMcKay(object):
78
78
  ----------
79
79
 
80
80
  .. [1] Kesten, H. (1959). Symmetric random walks on groups. Transactions of
81
- the American Mathematical Society, 92(2), 336354.
81
+ the American Mathematical Society, 92(2), 336-354.
82
82
 
83
83
  .. [2] McKay, B. D. (1981). The expected eigenvalue distribution of a large
84
84
  regular graph. Linear Algebra and its Applications, 40, 203-216
@@ -290,7 +290,7 @@ class MarchenkoPastur(object):
290
290
  m1 = (-B + sqrtD) / (2 * A)
291
291
  m2 = (-B - sqrtD) / (2 * A)
292
292
 
293
- # pick correct branch only for nonmasked entries
293
+ # pick correct branch only for non-masked entries
294
294
  upper = z[not_mask].imag >= 0
295
295
  branch = numpy.empty_like(m1)
296
296
  branch[upper] = numpy.where(sign*m1[upper].imag > 0, m1[upper],
@@ -83,7 +83,7 @@ class Meixner(object):
83
83
 
84
84
  .. [1] Saitoh, N. & Yosnida, M. (2001). The infinite divisibility and
85
85
  orthogonal polynomials with a constant recursion formula in free
86
- probability theory. Probab. Math. Statist., 21, 159170.
86
+ probability theory. Probab. Math. Statist., 21, 159-170.
87
87
 
88
88
  Examples
89
89
  --------
@@ -315,7 +315,7 @@ class Meixner(object):
315
315
  m1 = (-B + sqrtD) / (2 * A)
316
316
  m2 = (-B - sqrtD) / (2 * A)
317
317
 
318
- # pick correct branch only for nonmasked entries
318
+ # pick correct branch only for non-masked entries
319
319
  upper = z.imag >= 0
320
320
  branch = numpy.empty_like(m1)
321
321
  branch[upper] = numpy.where(
@@ -290,7 +290,7 @@ class Wachter(object):
290
290
  m1 = (-B + sqrtD) / (2 * A)
291
291
  m2 = (-B - sqrtD) / (2 * A)
292
292
 
293
- # pick correct branch only for nonmasked entries
293
+ # pick correct branch only for non-masked entries
294
294
  upper = z.imag >= 0
295
295
  branch = numpy.empty_like(m1)
296
296
  branch[upper] = numpy.where(sign*m1[upper].imag > 0, m1[upper],