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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. freealg/__init__.py +2 -2
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +2 -1
  4. freealg/_algebraic_form/_constraints.py +53 -12
  5. freealg/_algebraic_form/_cusp.py +357 -0
  6. freealg/_algebraic_form/_cusp_wrap.py +268 -0
  7. freealg/_algebraic_form/_decompress.py +330 -381
  8. freealg/_algebraic_form/_decompress2.py +120 -0
  9. freealg/_algebraic_form/_decompress4.py +739 -0
  10. freealg/_algebraic_form/_decompress5.py +738 -0
  11. freealg/_algebraic_form/_decompress6.py +492 -0
  12. freealg/_algebraic_form/_decompress7.py +355 -0
  13. freealg/_algebraic_form/_decompress8.py +369 -0
  14. freealg/_algebraic_form/_decompress9.py +363 -0
  15. freealg/_algebraic_form/_decompress_new.py +431 -0
  16. freealg/_algebraic_form/_decompress_new_2.py +1631 -0
  17. freealg/_algebraic_form/_decompress_util.py +172 -0
  18. freealg/_algebraic_form/_edge.py +46 -68
  19. freealg/_algebraic_form/_homotopy.py +62 -30
  20. freealg/_algebraic_form/_homotopy2.py +289 -0
  21. freealg/_algebraic_form/_homotopy3.py +215 -0
  22. freealg/_algebraic_form/_homotopy4.py +320 -0
  23. freealg/_algebraic_form/_homotopy5.py +185 -0
  24. freealg/_algebraic_form/_moments.py +43 -57
  25. freealg/_algebraic_form/_support.py +132 -177
  26. freealg/_algebraic_form/algebraic_form.py +163 -30
  27. freealg/distributions/__init__.py +3 -1
  28. freealg/distributions/_compound_poisson.py +464 -0
  29. freealg/distributions/_deformed_marchenko_pastur.py +51 -0
  30. freealg/distributions/_deformed_wigner.py +44 -0
  31. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/METADATA +2 -1
  32. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/RECORD +36 -20
  33. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/WHEEL +1 -1
  34. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/AUTHORS.txt +0 -0
  35. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/licenses/LICENSE.txt +0 -0
  36. {freealg-0.7.11.dist-info → freealg-0.7.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,320 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # Robust Stieltjes branch evaluation for algebraic P(z,m)=0.
6
+ #
7
+ # This version is tailored for empirical polynomial fits where spurious
8
+ # small-Im roots can appear outside the true support and create fake bulks
9
+ # when using rho = Im m(x+i*eta)/pi.
10
+ #
11
+ # Core idea: pick the physical branch by combining
12
+ # (i) Herglotz half-plane constraint,
13
+ # (ii) asymptotic constraint z*m ~ -1,
14
+ # (iii) 1D continuity along x (Viterbi),
15
+ # and DO NOT globally reward large |Im(m)| (which can fabricate density).
16
+
17
+ import numpy
18
+
19
+ __all__ = ["StieltjesPoly"]
20
+
21
+
22
+ # =========================
23
+ # Poly -> roots in m for z
24
+ # =========================
25
+
26
+ def _poly_m_coeffs(a_coeffs, z):
27
+ """Return coefficients b_j for \sum_j b_j m^j = 0 at fixed z.
28
+
29
+ a_coeffs[i,j] is coeff of z^i m^j.
30
+ Returns b of length (deg_m+1) with b[j] = \sum_i a[i,j] z^i.
31
+ """
32
+ a = numpy.asarray(a_coeffs, dtype=numpy.complex128)
33
+ deg_z = a.shape[0] - 1
34
+ deg_m = a.shape[1] - 1
35
+
36
+ # Horner in z for each m-power j
37
+ z = complex(z)
38
+ b = numpy.zeros((deg_m + 1,), dtype=numpy.complex128)
39
+ # b[j] = a[0,j] + a[1,j] z + ... + a[deg_z,j] z^{deg_z}
40
+ # do Horner: (((a[deg_z,j] z + a[deg_z-1,j]) z + ...) z + a[0,j])
41
+ for j in range(deg_m + 1):
42
+ acc = 0.0 + 0.0j
43
+ for i in range(deg_z, -1, -1):
44
+ acc = acc * z + a[i, j]
45
+ b[j] = acc
46
+
47
+ return b
48
+
49
+
50
+ def _roots_m(a_coeffs, z):
51
+ """All algebraic roots in m at fixed z."""
52
+ b = _poly_m_coeffs(a_coeffs, z)
53
+
54
+ # Drop leading zeros in highest power to keep numpy.roots stable
55
+ # numpy.roots expects highest degree first.
56
+ coeffs = b.copy()
57
+ # find highest nonzero index
58
+ nz = numpy.flatnonzero(numpy.abs(coeffs) > 0.0)
59
+ if nz.size == 0:
60
+ return numpy.array([], dtype=numpy.complex128)
61
+ j_max = int(nz.max())
62
+ coeffs = coeffs[: j_max + 1]
63
+
64
+ # reverse to highest-first
65
+ return numpy.roots(coeffs[::-1])
66
+
67
+
68
+ # ==============================
69
+ # Physical root selection scalar
70
+ # ==============================
71
+
72
+ def _pick_physical_root_scalar(z, roots, target=None, tol_im=1e-12):
73
+ """Pick the physical root among candidates for a single z.
74
+
75
+ Rules:
76
+ 1) Prefer roots with sign(Im m) = sign(Im z) (Herglotz).
77
+ 2) Break ties by asymptotic closeness to -1/z.
78
+ 3) If target provided, also enforce continuity by closeness to target.
79
+
80
+ Returns complex root.
81
+ """
82
+ z = complex(z)
83
+ roots = numpy.asarray(roots, dtype=numpy.complex128).ravel()
84
+ if roots.size == 0:
85
+ return numpy.nan + 1j * numpy.nan
86
+
87
+ s = numpy.sign(z.imag)
88
+ if s == 0.0:
89
+ s = 1.0
90
+
91
+ im_s = numpy.imag(roots) * s
92
+ cand = roots[im_s > -tol_im]
93
+ if cand.size == 0:
94
+ cand = roots
95
+
96
+ # asymptotic target
97
+ m_asym = -1.0 / z
98
+
99
+ if target is None:
100
+ score = numpy.abs(cand - m_asym)
101
+ else:
102
+ t = complex(target)
103
+ # continuity dominates, asymptotic helps in ambiguous regions
104
+ score = numpy.abs(cand - t) + 0.15 * numpy.abs(cand - m_asym)
105
+
106
+ return cand[int(numpy.argmin(score))]
107
+
108
+
109
+ # =====================
110
+ # Viterbi along a line
111
+ # =====================
112
+
113
+ def _viterbi_track(z_list, roots_list, mL, mR, *,
114
+ lam_space=1.0,
115
+ lam_edge=20.0,
116
+ lam_asym=0.25,
117
+ lam_time=0.0,
118
+ # Hinge penalty against "tiny-im" traps ONLY
119
+ lam_tiny_im=0.0,
120
+ tiny_im=1e-7,
121
+ tol_im=1e-12,
122
+ m_prev=None):
123
+ """Choose one root per z via dynamic programming.
124
+
125
+ z_list: (nz,)
126
+ roots_list: list of arrays of candidate roots, each (k_i,)
127
+ mL,mR: boundary anchors (complex)
128
+ m_prev: optional previous-time chosen path, shape (nz,)
129
+
130
+ Returns m_path (nz,) and ok (nz,) boolean indicating finite.
131
+ """
132
+ z_list = numpy.asarray(z_list, dtype=numpy.complex128).ravel()
133
+ nz = z_list.size
134
+
135
+ # Determine a fixed K by padding with NaNs (max roots)
136
+ K = max((r.size for r in roots_list), default=0)
137
+ if K == 0:
138
+ return (numpy.full((nz,), numpy.nan + 1j * numpy.nan, dtype=numpy.complex128),
139
+ numpy.zeros((nz,), dtype=bool))
140
+
141
+ R = numpy.full((nz, K), numpy.nan + 1j * numpy.nan, dtype=numpy.complex128)
142
+ for i, r in enumerate(roots_list):
143
+ if r.size:
144
+ R[i, : r.size] = r
145
+
146
+ # feasible mask: finite and Herglotz half-plane (soft)
147
+ s = numpy.sign(z_list.imag)
148
+ s[s == 0.0] = 1.0
149
+ IM = (numpy.imag(R) * s[:, None])
150
+ feasible = numpy.isfinite(R) & (IM > -tol_im)
151
+
152
+ # unary cost
153
+ unary = numpy.full((nz, K), numpy.inf, dtype=numpy.float64)
154
+ # continuity to asymptotic (-1/z) discourages fake branches outside support
155
+ m_asym = -1.0 / z_list
156
+
157
+ for i in range(nz):
158
+ zi = z_list[i]
159
+ mi = m_asym[i]
160
+ for k in range(K):
161
+ if not feasible[i, k]:
162
+ continue
163
+ w = R[i, k]
164
+ c = 0.0
165
+
166
+ # asymptotic penalty (small weight)
167
+ if lam_asym != 0.0:
168
+ c += float(lam_asym) * float(numpy.abs(zi * w + 1.0))
169
+
170
+ # optional hinge against tiny imag (ONLY below threshold)
171
+ if lam_tiny_im != 0.0:
172
+ im = abs(w.imag)
173
+ floor = max(float(tiny_im), 0.25 * abs(zi.imag))
174
+ if im < floor:
175
+ c += float(lam_tiny_im) * float(((floor / max(im, 1e-16)) - 1.0) ** 2)
176
+
177
+ # optional time consistency
178
+ if (lam_time != 0.0) and (m_prev is not None) and numpy.isfinite(m_prev[i]):
179
+ c += float(lam_time) * float(numpy.abs(w - m_prev[i]))
180
+
181
+ unary[i, k] = c
182
+
183
+ # boundary anchors
184
+ if numpy.isfinite(mL):
185
+ unary[0, :] += float(lam_edge) * numpy.abs(R[0, :] - mL)
186
+ if numpy.isfinite(mR):
187
+ unary[-1, :] += float(lam_edge) * numpy.abs(R[-1, :] - mR)
188
+
189
+ # pairwise cost
190
+ dp = numpy.full((nz, K), numpy.inf, dtype=numpy.float64)
191
+ prev = numpy.full((nz, K), -1, dtype=numpy.int64)
192
+
193
+ dp[0, :] = unary[0, :]
194
+
195
+ for i in range(1, nz):
196
+ for k in range(K):
197
+ if not numpy.isfinite(unary[i, k]):
198
+ continue
199
+ # transition from any j
200
+ best_val = numpy.inf
201
+ best_j = -1
202
+ wk = R[i, k]
203
+ for j in range(K):
204
+ if not numpy.isfinite(dp[i - 1, j]):
205
+ continue
206
+ wj = R[i - 1, j]
207
+ if not numpy.isfinite(wj):
208
+ continue
209
+ val = dp[i - 1, j] + float(lam_space) * float(numpy.abs(wk - wj))
210
+ if val < best_val:
211
+ best_val = val
212
+ best_j = j
213
+ if best_j >= 0:
214
+ dp[i, k] = best_val + unary[i, k]
215
+ prev[i, k] = best_j
216
+
217
+ # backtrack
218
+ k_end = int(numpy.argmin(dp[-1, :]))
219
+ m_path = numpy.full((nz,), numpy.nan + 1j * numpy.nan, dtype=numpy.complex128)
220
+ if not numpy.isfinite(dp[-1, k_end]):
221
+ return m_path, numpy.zeros((nz,), dtype=bool)
222
+
223
+ k = k_end
224
+ for i in range(nz - 1, -1, -1):
225
+ m_path[i] = R[i, k]
226
+ k = prev[i, k]
227
+ if (i > 0) and (k < 0):
228
+ # cannot continue
229
+ break
230
+
231
+ ok = numpy.isfinite(m_path)
232
+ return m_path, ok
233
+
234
+
235
+ # ============
236
+ # StieltjesPoly
237
+ # ============
238
+
239
+ class StieltjesPoly(object):
240
+ """Callable m(z) for P(z,m)=0 using robust branch selection."""
241
+
242
+ def __init__(self, a_coeffs, *,
243
+ viterbi_opt=None):
244
+ self.a_coeffs = numpy.asarray(a_coeffs, dtype=numpy.complex128)
245
+ self.viterbi_opt = dict(viterbi_opt or {})
246
+
247
+ # ----------
248
+ # scalar eval
249
+ # ----------
250
+
251
+ def evaluate_scalar(self, z, target=None):
252
+ r = _roots_m(self.a_coeffs, z)
253
+ return _pick_physical_root_scalar(z, r, target=target)
254
+
255
+ # ---------------
256
+ # vectorized eval
257
+ # ---------------
258
+
259
+ def __call__(self, z):
260
+ z = numpy.asarray(z, dtype=numpy.complex128)
261
+ scalar = (z.ndim == 0)
262
+ if scalar:
263
+ z = z.reshape((1,))
264
+
265
+ # If 1D, do Viterbi tracking in the given order
266
+ if z.ndim == 1:
267
+ z_list = z.ravel()
268
+
269
+ # roots for each point
270
+ roots_list = [_roots_m(self.a_coeffs, zi) for zi in z_list]
271
+
272
+ # boundary anchors via scalar selection
273
+ mL = self.evaluate_scalar(z_list[0])
274
+ mR = self.evaluate_scalar(z_list[-1])
275
+
276
+ opt = {
277
+ "lam_space": 1.0,
278
+ "lam_edge": 20.0,
279
+ "lam_asym": 0.25,
280
+ "lam_time": 0.0,
281
+ "lam_tiny_im": 0.0,
282
+ "tiny_im": 1e-7,
283
+ "tol_im": 1e-12,
284
+ }
285
+ opt.update(self.viterbi_opt)
286
+
287
+ m_path, ok = _viterbi_track(
288
+ z_list,
289
+ roots_list,
290
+ mL,
291
+ mR,
292
+ lam_space=opt["lam_space"],
293
+ lam_edge=opt["lam_edge"],
294
+ lam_asym=opt["lam_asym"],
295
+ lam_time=opt["lam_time"],
296
+ lam_tiny_im=opt["lam_tiny_im"],
297
+ tiny_im=opt["tiny_im"],
298
+ tol_im=opt["tol_im"],
299
+ m_prev=None,
300
+ )
301
+
302
+ # fallback pointwise for any failures
303
+ if not numpy.all(ok):
304
+ out = m_path.copy()
305
+ for i in numpy.flatnonzero(~ok):
306
+ out[i] = self.evaluate_scalar(z_list[i], target=None)
307
+ m_path = out
308
+
309
+ out = m_path.reshape(z.shape)
310
+ return out.reshape(()) if scalar else out
311
+
312
+ # For nd>1: evaluate pointwise with asymptotic tie-break
313
+ out = numpy.empty(z.size, dtype=numpy.complex128)
314
+ zf = z.ravel()
315
+ prev = None
316
+ for i in range(zf.size):
317
+ out[i] = self.evaluate_scalar(zf[i], target=prev)
318
+ prev = out[i]
319
+ out = out.reshape(z.shape)
320
+ return out.reshape(()) if scalar else out
@@ -0,0 +1,185 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # Robust Stieltjes branch evaluation for algebraic P(z,m)=0 using
6
+ # global 1D dynamic programming (Viterbi) along a complex line.
7
+
8
+ import numpy
9
+
10
+
11
+ def _poly_coeffs_in_m(a_coeffs, z):
12
+ a = a_coeffs
13
+ dz = a.shape[0] - 1
14
+ s = a.shape[1] - 1
15
+ zp = numpy.array([z**i for i in range(dz + 1)], dtype=numpy.complex128)
16
+ coeff_m = numpy.empty(s + 1, dtype=numpy.complex128)
17
+ for j in range(s + 1):
18
+ coeff_m[j] = numpy.dot(a[:, j], zp)
19
+ return coeff_m
20
+
21
+
22
+ def _roots_m(a_coeffs, z):
23
+ coeff_m = _poly_coeffs_in_m(a_coeffs, z)
24
+ c = coeff_m[::-1]
25
+ while c.size > 1 and numpy.abs(c[0]) == 0:
26
+ c = c[1:]
27
+ if c.size <= 1:
28
+ return numpy.array([], dtype=numpy.complex128)
29
+ return numpy.roots(c)
30
+
31
+
32
+ def _pick_anchor(z, roots, tol_im, lam_asym):
33
+ if roots.size == 0:
34
+ return numpy.nan + 1j * numpy.nan
35
+ sgn = 1.0 if numpy.imag(z) >= 0 else -1.0
36
+ ok = (sgn * numpy.imag(roots) > tol_im)
37
+ if numpy.any(ok):
38
+ cand = roots[ok]
39
+ else:
40
+ cand = roots
41
+ cost = lam_asym * numpy.abs(z * cand + 1.0)
42
+ return cand[int(numpy.argmin(cost))]
43
+
44
+
45
+ def _viterbi_1d(z_list, roots_all, *, lam_space, lam_asym,
46
+ lam_tiny_im, tiny_im, tol_im):
47
+ n, s = roots_all.shape
48
+ big = 1.0e300
49
+
50
+ cost0 = numpy.zeros((n, s), dtype=float)
51
+ back = numpy.zeros((n, s), dtype=numpy.int64)
52
+ dp = numpy.full((n, s), big, dtype=float)
53
+
54
+ for k in range(n):
55
+ z = z_list[k]
56
+ r = roots_all[k]
57
+ sgn = 1.0 if numpy.imag(z) >= 0 else -1.0
58
+
59
+ ok = (sgn * numpy.imag(r) > tol_im)
60
+ cost0[k, ~ok] += big * 1.0e-6
61
+
62
+ if lam_tiny_im != 0.0 and tiny_im is not None:
63
+ imabs = numpy.abs(numpy.imag(r))
64
+ hing = numpy.maximum(0.0, float(tiny_im) - imabs)
65
+ cost0[k] += lam_tiny_im * hing
66
+
67
+ if lam_asym != 0.0:
68
+ cost0[k] += lam_asym * numpy.abs(z * r + 1.0)
69
+
70
+ m0 = _pick_anchor(z_list[0], roots_all[0], tol_im, lam_asym)
71
+ mN = _pick_anchor(z_list[-1], roots_all[-1], tol_im, lam_asym)
72
+
73
+ init = cost0[0] + lam_space * numpy.abs(roots_all[0] - m0)
74
+ dp[0] = init
75
+
76
+ for k in range(1, n):
77
+ r = roots_all[k]
78
+ rp = roots_all[k - 1]
79
+ for j in range(s):
80
+ trans = dp[k - 1] + lam_space * numpy.abs(r[j] - rp)
81
+ idx = int(numpy.argmin(trans))
82
+ dp[k, j] = trans[idx] + cost0[k, j]
83
+ back[k, j] = idx
84
+
85
+ last = dp[-1] + lam_space * numpy.abs(roots_all[-1] - mN)
86
+ jn = int(numpy.argmin(last))
87
+
88
+ path = numpy.empty(n, dtype=numpy.complex128)
89
+ for k in range(n - 1, -1, -1):
90
+ path[k] = roots_all[k, jn]
91
+ if k > 0:
92
+ jn = int(back[k, jn])
93
+ return path
94
+
95
+
96
+ class StieltjesPoly(object):
97
+ """Callable m(z) for P(z,m)=0 using robust branch selection."""
98
+
99
+ def __init__(self, a_coeffs, *, viterbi_opt=None):
100
+ self.a_coeffs = numpy.asarray(a_coeffs, dtype=numpy.complex128)
101
+ self.viterbi_opt = dict(viterbi_opt or {})
102
+
103
+ def evaluate_scalar(self, z, target=None):
104
+ r = _roots_m(self.a_coeffs, z)
105
+ if r.size == 0:
106
+ return numpy.nan + 1j * numpy.nan
107
+ tol_im = float(self.viterbi_opt.get("tol_im", 1e-14))
108
+ lam_asym = float(self.viterbi_opt.get("lam_asym", 1.0))
109
+ sgn = 1.0 if numpy.imag(z) >= 0 else -1.0
110
+ ok = (sgn * numpy.imag(r) > tol_im)
111
+ cand = r[ok] if numpy.any(ok) else r
112
+ cost = lam_asym * numpy.abs(z * cand + 1.0)
113
+ if target is not None and numpy.isfinite(target):
114
+ lam_space = float(self.viterbi_opt.get("lam_space", 1.0))
115
+ cost = cost + lam_space * numpy.abs(cand - target)
116
+ return cand[int(numpy.argmin(cost))]
117
+
118
+ def __call__(self, z):
119
+ z = numpy.asarray(z, dtype=numpy.complex128)
120
+ scalar = (z.ndim == 0)
121
+ if scalar:
122
+ z = z.reshape((1,))
123
+
124
+ if z.ndim == 1 and z.size >= 2:
125
+ z_list = z.ravel()
126
+ s = self.a_coeffs.shape[1] - 1
127
+ roots_all = numpy.empty((z_list.size, s), dtype=numpy.complex128)
128
+ ok_all = numpy.ones(z_list.size, dtype=bool)
129
+ for k in range(z_list.size):
130
+ r = _roots_m(self.a_coeffs, z_list[k])
131
+ if r.size != s:
132
+ ok_all[k] = False
133
+ if r.size == 0:
134
+ roots_all[k] = numpy.nan + 1j * numpy.nan
135
+ elif r.size < s:
136
+ rr = numpy.empty(s, dtype=numpy.complex128)
137
+ rr[:] = numpy.nan + 1j * numpy.nan
138
+ rr[:r.size] = r
139
+ roots_all[k] = rr
140
+ else:
141
+ roots_all[k] = r[:s]
142
+ else:
143
+ roots_all[k] = r
144
+
145
+ opt = {
146
+ "lam_space": 1.0,
147
+ "lam_asym": 1.0,
148
+ "lam_tiny_im": 200.0,
149
+ "tiny_im": None,
150
+ "tol_im": 1e-14,
151
+ }
152
+ opt.update(self.viterbi_opt)
153
+
154
+ if opt["tiny_im"] is None:
155
+ opt["tiny_im"] = 0.5 * numpy.abs(numpy.imag(z_list[0]))
156
+
157
+ m_path = _viterbi_1d(
158
+ z_list, roots_all,
159
+ lam_space=float(opt["lam_space"]),
160
+ lam_asym=float(opt["lam_asym"]),
161
+ lam_tiny_im=float(opt["lam_tiny_im"]),
162
+ tiny_im=float(opt["tiny_im"]),
163
+ tol_im=float(opt["tol_im"]),
164
+ )
165
+
166
+ if not numpy.all(ok_all):
167
+ out = m_path.copy()
168
+ prev = None
169
+ for i in range(z_list.size):
170
+ if not ok_all[i] or not numpy.isfinite(out[i]):
171
+ out[i] = self.evaluate_scalar(z_list[i], target=prev)
172
+ prev = out[i]
173
+ m_path = out
174
+
175
+ out = m_path.reshape(z.shape)
176
+ return out.reshape(()) if scalar else out
177
+
178
+ out = numpy.empty(z.size, dtype=numpy.complex128)
179
+ zf = z.ravel()
180
+ prev = None
181
+ for i in range(zf.size):
182
+ out[i] = self.evaluate_scalar(zf[i], target=prev)
183
+ prev = out[i]
184
+ out = out.reshape(z.shape)
185
+ return out.reshape(()) if scalar else out
@@ -9,7 +9,7 @@ import numpy
9
9
  # Moments
10
10
  # =======
11
11
 
12
- class MomentsESD(object):
12
+ class Moments(object):
13
13
  """
14
14
  Moments :math:`\\mu_n(t)` generated from eigenvalues, under
15
15
  free decompression, where
@@ -23,15 +23,21 @@ class MomentsESD(object):
23
23
  Parameters
24
24
  ----------
25
25
 
26
- eig : array_like
27
- 1D array of eigenvalues (or samples). Internally it is converted to a
28
- floating-point :class:`numpy.ndarray`.
26
+ source : array_like or callable
27
+ Either
28
+
29
+ * a 1D array of eigenvalues (or samples), or
30
+ * a callable returning the raw moments at zero, ``source(n) = m_n``.
31
+
32
+ If an array is provided, moments are estimated via sample averages.
33
+ If a callable is provided, it is assumed to return exact values of
34
+ :math:`m_n`.
29
35
 
30
36
  Attributes
31
37
  ----------
32
38
 
33
- eig : numpy.ndarray
34
- Eigenvalue samples.
39
+ eig : numpy.ndarray or None
40
+ Eigenvalue samples, if provided.
35
41
 
36
42
  Methods
37
43
  -------
@@ -56,31 +62,23 @@ class MomentsESD(object):
56
62
 
57
63
  The coefficient row :math:`a_n` is computed using an intermediate quantity
58
64
  :math:`R_{n,k}` formed via discrete convolutions of previous rows.
59
-
60
- Examples
61
- --------
62
-
63
- .. code-block:: python
64
-
65
- >>> import numpy as np
66
- >>> eig = np.array([1.0, 2.0, 3.0])
67
- >>> mu = Moments(eig)
68
- >>> mu(3, t=0.0) # equals m_3
69
- 12.0
70
- >>> mu(3, t=0.1)
71
- 14.203...
72
65
  """
73
66
 
74
67
  # ====
75
68
  # init
76
69
  # ====
77
70
 
78
- def __init__(self, eig):
71
+ def __init__(self, source):
79
72
  """
80
73
  Initialization.
81
74
  """
75
+ self.eig = None
76
+ self._moment_fn = None
82
77
 
83
- self.eig = numpy.asarray(eig, dtype=float)
78
+ if callable(source):
79
+ self._moment_fn = source
80
+ else:
81
+ self.eig = numpy.asarray(source, dtype=float)
84
82
 
85
83
  # Memoized moments m_n
86
84
  self._m = {0: 1.0}
@@ -107,12 +105,25 @@ class MomentsESD(object):
107
105
  -------
108
106
 
109
107
  m_n : float
110
- The raw moment :math:`m_n = \\mathbb{E}[\\lambda^n]`, estimated by
111
- the sample mean of ``eig**n``.
108
+ The raw moment :math:`m_n = \\mathbb{E}[\\lambda^n]`.
109
+
110
+ Notes
111
+ -----
112
+
113
+ If the instance was initialized with eigenvalue samples, the moment is
114
+ estimated by the sample mean of ``eig**n``. If initialized with a
115
+ callable, the callable is used directly.
112
116
  """
117
+ n = int(n)
118
+ if n < 0:
119
+ raise ValueError("Moment order n must be >= 0.")
113
120
 
114
121
  if n not in self._m:
115
- self._m[n] = numpy.mean(self.eig ** n)
122
+ if self._moment_fn is not None:
123
+ self._m[n] = float(self._moment_fn(n))
124
+ else:
125
+ self._m[n] = float(numpy.mean(self.eig ** n))
126
+
116
127
  return self._m[n]
117
128
 
118
129
  # ======
@@ -136,6 +147,9 @@ class MomentsESD(object):
136
147
  Array of shape ``(n,)`` containing :math:`(a_{n,0},
137
148
  \\dots, a_{n,n-1})`.
138
149
  """
150
+ n = int(n)
151
+ if n < 0:
152
+ raise ValueError("Order n must be >= 0.")
139
153
 
140
154
  if n in self._a:
141
155
  return self._a[n]
@@ -161,36 +175,7 @@ class MomentsESD(object):
161
175
 
162
176
  n : int
163
177
  Row index to compute.
164
-
165
- Notes
166
- -----
167
-
168
- For :math:`n=1`, the row is
169
-
170
- .. math::
171
-
172
- a_{1,0} = m_1.
173
-
174
- For :math:`n \\ge 2`, let :math:`R_n` be a length ``n-1`` array defined
175
- by convolution of previous rows:
176
-
177
- .. math::
178
-
179
- R_n = \\sum_{i=1}^{n-1} (a_i * a_{n-i})\\big|_{0:(n-2)}.
180
-
181
- Then for :math:`k = 0, \\dots, n-2`,
182
-
183
- .. math::
184
-
185
- a_{n,k} = \\frac{1 + k/2}{(n-1-k)} R_{n,k},
186
-
187
- and the last coefficient is chosen so that :math:`\\mu_n(0)=m_n`:
188
-
189
- .. math::
190
-
191
- a_{n,n-1} = m_n - \\sum_{k=0}^{n-2} a_{n,k}.
192
178
  """
193
-
194
179
  if n in self._a:
195
180
  return
196
181
 
@@ -205,8 +190,7 @@ class MomentsESD(object):
205
190
 
206
191
  a_n = numpy.zeros(n, dtype=float)
207
192
 
208
- # Compute R_{n,k} via convolutions:
209
- # R_n = sum_{i=1}^{n-1} convolve(a[i], a[n-i]) truncated to length n-1
193
+ # Compute R_{n,k} via convolutions
210
194
  R = numpy.zeros(n - 1, dtype=float)
211
195
  for i in range(1, n):
212
196
  conv = numpy.convolve(self._a[i], self._a[n - i])
@@ -255,13 +239,15 @@ class MomentsESD(object):
255
239
 
256
240
  For ``n == 0``, it returns ``1.0``.
257
241
  """
258
-
242
+ n = int(n)
243
+ if n < 0:
244
+ raise ValueError("Order n must be >= 0.")
259
245
  if n == 0:
260
246
  return 1.0
261
247
 
262
248
  a_n = self.coeffs(n)
263
249
  k = numpy.arange(n, dtype=float)
264
- return numpy.dot(a_n, numpy.exp(k * t))
250
+ return float(numpy.dot(a_n, numpy.exp(k * t)))
265
251
 
266
252
 
267
253
  # ===========================