freealg 0.7.3__tar.gz → 0.7.5__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 (60) hide show
  1. {freealg-0.7.3 → freealg-0.7.5}/PKG-INFO +1 -1
  2. freealg-0.7.5/freealg/__version__.py +1 -0
  3. {freealg-0.7.3 → freealg-0.7.5}/freealg/_algebraic_form/_continuation_algebraic.py +88 -7
  4. freealg-0.7.5/freealg/_algebraic_form/_decompress2.py +86 -0
  5. freealg-0.7.5/freealg/_algebraic_form/_homotopy.py +138 -0
  6. {freealg-0.7.3 → freealg-0.7.5}/freealg/_algebraic_form/algebraic_form.py +198 -124
  7. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_deformed_wigner.py +7 -19
  8. {freealg-0.7.3 → freealg-0.7.5}/freealg.egg-info/PKG-INFO +1 -1
  9. {freealg-0.7.3 → freealg-0.7.5}/freealg.egg-info/SOURCES.txt +2 -0
  10. freealg-0.7.3/freealg/__version__.py +0 -1
  11. {freealg-0.7.3 → freealg-0.7.5}/AUTHORS.txt +0 -0
  12. {freealg-0.7.3 → freealg-0.7.5}/CHANGELOG.rst +0 -0
  13. {freealg-0.7.3 → freealg-0.7.5}/LICENSE.txt +0 -0
  14. {freealg-0.7.3 → freealg-0.7.5}/MANIFEST.in +0 -0
  15. {freealg-0.7.3 → freealg-0.7.5}/README.rst +0 -0
  16. {freealg-0.7.3 → freealg-0.7.5}/freealg/__init__.py +0 -0
  17. {freealg-0.7.3 → freealg-0.7.5}/freealg/_algebraic_form/__init__.py +0 -0
  18. {freealg-0.7.3 → freealg-0.7.5}/freealg/_algebraic_form/_decompress.py +0 -0
  19. {freealg-0.7.3 → freealg-0.7.5}/freealg/_algebraic_form/_edge.py +0 -0
  20. {freealg-0.7.3 → freealg-0.7.5}/freealg/_algebraic_form/_sheets_util.py +0 -0
  21. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/__init__.py +0 -0
  22. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_chebyshev.py +0 -0
  23. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_damp.py +0 -0
  24. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_decompress.py +0 -0
  25. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_density_util.py +0 -0
  26. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_jacobi.py +0 -0
  27. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_linalg.py +0 -0
  28. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_pade.py +0 -0
  29. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_plot_util.py +0 -0
  30. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_sample.py +0 -0
  31. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_series.py +0 -0
  32. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/_support.py +0 -0
  33. {freealg-0.7.3 → freealg-0.7.5}/freealg/_free_form/free_form.py +0 -0
  34. {freealg-0.7.3 → freealg-0.7.5}/freealg/_geometric_form/__init__.py +0 -0
  35. {freealg-0.7.3 → freealg-0.7.5}/freealg/_geometric_form/_continuation_genus0.py +0 -0
  36. {freealg-0.7.3 → freealg-0.7.5}/freealg/_geometric_form/_continuation_genus1.py +0 -0
  37. {freealg-0.7.3 → freealg-0.7.5}/freealg/_geometric_form/_elliptic_functions.py +0 -0
  38. {freealg-0.7.3 → freealg-0.7.5}/freealg/_geometric_form/_sphere_maps.py +0 -0
  39. {freealg-0.7.3 → freealg-0.7.5}/freealg/_geometric_form/_torus_maps.py +0 -0
  40. {freealg-0.7.3 → freealg-0.7.5}/freealg/_geometric_form/geometric_form.py +0 -0
  41. {freealg-0.7.3 → freealg-0.7.5}/freealg/_util.py +0 -0
  42. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/__init__.py +0 -0
  43. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_chiral_block.py +0 -0
  44. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_deformed_marchenko_pastur.py +0 -0
  45. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_kesten_mckay.py +0 -0
  46. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_marchenko_pastur.py +0 -0
  47. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_meixner.py +0 -0
  48. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_wachter.py +0 -0
  49. {freealg-0.7.3 → freealg-0.7.5}/freealg/distributions/_wigner.py +0 -0
  50. {freealg-0.7.3 → freealg-0.7.5}/freealg/visualization/__init__.py +0 -0
  51. {freealg-0.7.3 → freealg-0.7.5}/freealg/visualization/_glue_util.py +0 -0
  52. {freealg-0.7.3 → freealg-0.7.5}/freealg/visualization/_rgb_hsv.py +0 -0
  53. {freealg-0.7.3 → freealg-0.7.5}/freealg.egg-info/dependency_links.txt +0 -0
  54. {freealg-0.7.3 → freealg-0.7.5}/freealg.egg-info/not-zip-safe +0 -0
  55. {freealg-0.7.3 → freealg-0.7.5}/freealg.egg-info/requires.txt +0 -0
  56. {freealg-0.7.3 → freealg-0.7.5}/freealg.egg-info/top_level.txt +0 -0
  57. {freealg-0.7.3 → freealg-0.7.5}/pyproject.toml +0 -0
  58. {freealg-0.7.3 → freealg-0.7.5}/requirements.txt +0 -0
  59. {freealg-0.7.3 → freealg-0.7.5}/setup.cfg +0 -0
  60. {freealg-0.7.3 → freealg-0.7.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.7.3
3
+ Version: 0.7.5
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.7.5"
@@ -15,20 +15,22 @@ import numpy
15
15
  from .._geometric_form._continuation_genus0 import joukowski_z
16
16
 
17
17
  __all__ = ['sample_z_joukowski', 'filter_z_away_from_cuts', 'powers',
18
- 'fit_polynomial_relation', 'eval_P', 'eval_roots',
19
- 'build_sheets_from_roots']
18
+ 'fit_polynomial_relation', 'sanity_check_stieltjes_branch',
19
+ 'eval_P', 'eval_roots', 'build_sheets_from_roots']
20
20
 
21
21
 
22
22
  # ======================
23
23
  # normalize coefficients
24
24
  # ======================
25
25
 
26
- def _normalize_coefficients(a):
26
+ def _normalize_coefficients(arr):
27
27
  """
28
28
  Trim rows and columns on the sides (equivalent to factorizing or reducing
29
29
  degree) and normalize so that the sum of the first column is one.
30
30
  """
31
31
 
32
+ a = numpy.asarray(arr).copy()
33
+
32
34
  if a.size == 0:
33
35
  return a
34
36
 
@@ -51,7 +53,7 @@ def _normalize_coefficients(a):
51
53
  a = a[:, first_col:last_col]
52
54
 
53
55
  # --- Normalize so first column sums to 1 ---
54
- col_sum = numpy.sum(a[:, 0])
56
+ col_sum = numpy.sum(numpy.abs(a[:, 0]))
55
57
  if col_sum != 0:
56
58
  a = a / col_sum
57
59
 
@@ -189,12 +191,15 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
189
191
  if w is not None:
190
192
  A = A * w[:, None]
191
193
 
192
- s_col = numpy.max(numpy.abs(A), axis=0)
194
+ # Enforce real coefficients by solving: Re(A) c = 0 and Im(A) c = 0
195
+ Ar = numpy.vstack([A.real, A.imag])
196
+
197
+ s_col = numpy.max(numpy.abs(Ar), axis=0)
193
198
  s_col[s_col == 0.0] = 1.0
194
- As = A / s_col[None, :]
199
+ As = Ar / s_col[None, :]
195
200
 
196
201
  if ridge_lambda > 0.0:
197
- L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=complex)
202
+ L = numpy.sqrt(ridge_lambda) * numpy.eye(n_coef, dtype=float)
198
203
  As = numpy.vstack([As, L])
199
204
 
200
205
  _, _, vh = numpy.linalg.svd(As, full_matrices=False)
@@ -211,6 +216,77 @@ def fit_polynomial_relation(z, m, s, deg_z, ridge_lambda=0.0, weights=None,
211
216
  return full
212
217
 
213
218
 
219
+ # =============================
220
+ # sanity check stieltjes branch
221
+ # =============================
222
+
223
+ def sanity_check_stieltjes_branch(a_coeffs, x_min, x_max, eta=0.1,
224
+ n_x=64, y0=None, max_bad_frac=0.05):
225
+ """
226
+ Quick sanity check: does P(z,m)=0 admit a continuously trackable root with
227
+ Im(m)>0 along z=x+i*eta.
228
+ """
229
+
230
+ x_min = float(x_min)
231
+ x_max = float(x_max)
232
+ eta = float(eta)
233
+ n_x = int(n_x)
234
+ if n_x < 4:
235
+ n_x = 4
236
+
237
+ if y0 is None:
238
+ y0 = 10.0 * max(1.0, abs(x_min), abs(x_max))
239
+ y0 = float(y0)
240
+
241
+ z0 = 1j * y0
242
+ m0_target = -1.0 / z0
243
+
244
+ c0 = _poly_coef_in_m(numpy.array([z0]), a_coeffs)[0]
245
+ r0 = numpy.roots(c0[::-1])
246
+ if r0.size == 0:
247
+ return {'ok': False, 'frac_bad': 1.0, 'n_test': 0, 'n_bad': 0}
248
+
249
+ k0 = int(numpy.argmin(numpy.abs(r0 - m0_target)))
250
+ m_prev = r0[k0]
251
+
252
+ xs = numpy.linspace(x_min, x_max, n_x)
253
+ zs = xs + 1j * eta
254
+
255
+ n_bad = 0
256
+ n_ok = 0
257
+
258
+ for z in zs:
259
+ c = _poly_coef_in_m(numpy.array([z]), a_coeffs)[0]
260
+ r = numpy.roots(c[::-1])
261
+ if r.size == 0 or not numpy.all(numpy.isfinite(r)):
262
+ n_bad += 1
263
+ continue
264
+
265
+ k = int(numpy.argmin(numpy.abs(r - m_prev)))
266
+ m_sel = r[k]
267
+ m_prev = m_sel
268
+ n_ok += 1
269
+
270
+ if not numpy.isfinite(m_sel) or (m_sel.imag <= 0.0):
271
+ n_bad += 1
272
+
273
+ n_test = n_ok + (n_bad - (n_x - n_ok))
274
+ if n_test <= 0:
275
+ n_test = n_x
276
+
277
+ frac_bad = float(n_bad) / float(n_x)
278
+ ok = frac_bad <= float(max_bad_frac)
279
+
280
+ status = {
281
+ 'ok': ok,
282
+ 'frac_bad': frac_bad,
283
+ 'n_test': n_x,
284
+ 'n_bad': n_bad
285
+ }
286
+
287
+ return status
288
+
289
+
214
290
  # ======
215
291
  # eval P
216
292
  # ======
@@ -359,6 +435,10 @@ def eval_roots(z, a_coeffs):
359
435
  # =======================
360
436
 
361
437
  def track_one_sheet_on_grid(z, roots, sheet_seed, cuts=None, i0=None, j0=None):
438
+ """
439
+ This is mostly used for visualization of the sheets.
440
+ """
441
+
362
442
  z = numpy.asarray(z)
363
443
  n_y, n_x = z.shape
364
444
  s = roots.shape[1]
@@ -465,6 +545,7 @@ def track_one_sheet_on_grid(z, roots, sheet_seed, cuts=None, i0=None, j0=None):
465
545
  # =======================
466
546
 
467
547
  def build_sheets_from_roots(z, roots, m1, cuts=None, i0=None, j0=None):
548
+
468
549
  z = numpy.asarray(z)
469
550
  m1 = numpy.asarray(m1)
470
551
 
@@ -0,0 +1,86 @@
1
+ # =======
2
+ # Imports
3
+ # =======
4
+
5
+ import numpy
6
+ from scipy.special import comb
7
+ from ._continuation_algebraic import _normalize_coefficients
8
+
9
+ __all__ = ['decompress_coeffs']
10
+
11
+
12
+ # =================
13
+ # decompress_coeffs
14
+ # =================
15
+
16
+ def decompress_coeffs(a, t, normalize=True):
17
+ """
18
+ Compute the decompressed coefficients A[r, s](t) induced by
19
+ the transform Q_t(z, m) = m^L P(z + (1 - e^{-t}) / m, e^t m).
20
+
21
+ Parameters
22
+ ----------
23
+ a : array_like of float, shape (L+1, K+1)
24
+ Coefficients defining P(z, m) in the monomial basis:
25
+ P(z, m) = sum_{j=0..L} sum_{k=0..K} a[j, k] z^j m^k.
26
+ t : float
27
+ Time parameter.
28
+
29
+ Returns
30
+ -------
31
+ A : ndarray, shape (L+1, L+K+1)
32
+ Coefficients A[r, s](t) such that
33
+ sum_{r=0..L} sum_{s=0..L+K} A[r, s](t) z^r m^s = 0,
34
+ normalized by normalize_coefficients.
35
+ """
36
+ a = numpy.asarray(a)
37
+ if a.ndim != 2:
38
+ raise ValueError("a must be a 2D array-like of shape (L+1, K+1).")
39
+
40
+ l_degree = a.shape[0] - 1
41
+ k_degree = a.shape[1] - 1
42
+
43
+ c = 1.0 - numpy.exp(-t)
44
+
45
+ # Scale columns of a by e^{t k}: scaled[j, k] = a[j, k] e^{t k}.
46
+ exp_factors = numpy.exp(numpy.arange(k_degree + 1) * t)
47
+ scaled = a * exp_factors
48
+
49
+ # Output coefficients.
50
+ out_dtype = numpy.result_type(a, float)
51
+ a_out = numpy.zeros((l_degree + 1, l_degree + k_degree + 1),
52
+ dtype=out_dtype)
53
+
54
+ # Precompute binomial(j, r) * c^{j-r} for all j, r (lower-triangular).
55
+ j_inds = numpy.arange(l_degree + 1)[:, None]
56
+ r_inds = numpy.arange(l_degree + 1)[None, :]
57
+ mask = r_inds <= j_inds
58
+
59
+ binom_weights = numpy.zeros((l_degree + 1, l_degree + 1), dtype=float)
60
+ binom_weights[mask] = comb(j_inds, r_inds, exact=False)[mask]
61
+ binom_weights[mask] *= (c ** (j_inds - r_inds))[mask]
62
+
63
+ # Main accumulation:
64
+ # For fixed j and r, add:
65
+ # A[r, (L - j + r) + k] += binom_weights[j, r] * scaled[j, k],
66
+ # for k = 0..K.
67
+ for j in range(l_degree + 1):
68
+ row_scaled = scaled[j]
69
+ if numpy.all(row_scaled == 0):
70
+ continue
71
+
72
+ base0 = l_degree - j
73
+ row_b = binom_weights[j]
74
+
75
+ for r in range(j + 1):
76
+ coeff = row_b[r]
77
+ if coeff == 0:
78
+ continue
79
+
80
+ start = base0 + r
81
+ a_out[r, start:start + (k_degree + 1)] += coeff * row_scaled
82
+
83
+ if normalize:
84
+ return _normalize_coefficients(a_out)
85
+
86
+ return a_out
@@ -0,0 +1,138 @@
1
+ # =======
2
+ # Imports
3
+ # =======
4
+
5
+ import numpy
6
+
7
+ __all__ = ['stieltjes_poly']
8
+
9
+
10
+ # =====================
11
+ # stieltjes select root
12
+ # =====================
13
+
14
+ def stieltjes_select_root(roots, z, w_prev=None):
15
+ """
16
+ Select the Stieltjes-branch root among candidates at a given z.
17
+
18
+ Parameters
19
+ ----------
20
+ roots : array_like of complex
21
+ Candidate roots for m at the given z.
22
+ z : complex
23
+ Evaluation point. The Stieltjes/Herglotz branch satisfies
24
+ sign(Im(m)) = sign(Im(z)) away from the real axis.
25
+ w_prev : complex or None, optional
26
+ Previous continuation value used to enforce continuity. If None,
27
+ the asymptotic target -1/z is used.
28
+
29
+ Returns
30
+ -------
31
+ w : complex
32
+ Selected root corresponding to the Stieltjes branch.
33
+ """
34
+
35
+ z = complex(z)
36
+ roots = numpy.asarray(roots, dtype=numpy.complex128).ravel()
37
+
38
+ if roots.size == 0:
39
+ raise ValueError("roots must contain at least one candidate root.")
40
+
41
+ desired_sign = numpy.sign(z.imag)
42
+
43
+ if w_prev is None:
44
+ target = -1.0 / z
45
+ else:
46
+ target = complex(w_prev)
47
+
48
+ # Apply a soft Herglotz sign filter: prefer roots with Im(w) having the
49
+ # same sign as Im(z), allowing tiny numerical violations near the axis.
50
+ imag_roots = numpy.imag(roots)
51
+
52
+ good = roots[numpy.sign(imag_roots) == desired_sign]
53
+ if good.size == 0:
54
+ good = roots[(imag_roots * desired_sign) > -1e-12]
55
+
56
+ candidates = good if good.size > 0 else roots
57
+ idx = int(numpy.argmin(numpy.abs(candidates - target)))
58
+ return candidates[idx]
59
+
60
+
61
+ # ==============
62
+ # stieltjes poly
63
+ # ==============
64
+
65
+ def stieltjes_poly(z, a, eps=None, height=1e+4, steps=100):
66
+ """
67
+ Evaluate the Stieltjes-branch solution m(z) of an algebraic equation.
68
+
69
+ The coefficients `a` define a polynomial relation
70
+ P(z, m) = 0,
71
+ where P is a polynomial in z and m with monomial-basis coefficients
72
+ arranged so that for fixed z, the coefficients of the polynomial in m
73
+ can be assembled from powers of z.
74
+
75
+ Parameters
76
+ ----------
77
+ z : complex
78
+ Evaluation point. Must be a single value.
79
+ a : ndarray, shape (L, K)
80
+ Coefficient matrix defining P(z, m) in the monomial basis.
81
+ eps : float or None, optional
82
+ If Im(z) == 0, use z + i*eps as the boundary evaluation point.
83
+ If None and Im(z) == 0, eps is set to 1e-8 * max(1, |z|).
84
+ height : float, optional
85
+ Imaginary height used for the starting point z0 in the same
86
+ half-plane as the evaluation point.
87
+ steps : int, optional
88
+ Number of continuation steps along the homotopy path.
89
+
90
+ Returns
91
+ -------
92
+ w : complex
93
+ Value of the Stieltjes-branch solution m(z) (or m(z+i*eps) if z is
94
+ real).
95
+ """
96
+
97
+ z = complex(z)
98
+ a = numpy.asarray(a)
99
+
100
+ if a.ndim != 2:
101
+ raise ValueError('a must be a 2D array.')
102
+
103
+ if steps < 1:
104
+ raise ValueError("steps must be a positive integer.")
105
+
106
+ a_l, _ = a.shape
107
+
108
+ def poly_coeffs_m(z_val):
109
+ z_powers = z_val ** numpy.arange(a_l)
110
+ return (z_powers @ a)[::-1]
111
+
112
+ def poly_roots(z_val):
113
+ coeffs = numpy.asarray(poly_coeffs_m(z_val), dtype=numpy.complex128)
114
+ return numpy.roots(coeffs)
115
+
116
+ # If user asked for a real-axis value, interpret as boundary value from C+.
117
+ if z.imag == 0.0:
118
+ if eps is None:
119
+ eps = 1e-8 * max(1.0, abs(z))
120
+ z_eval = z + 1j * float(eps)
121
+ else:
122
+ z_eval = z
123
+
124
+ half_sign = numpy.sign(z_eval.imag)
125
+ if half_sign == 0.0:
126
+ half_sign = 1.0
127
+
128
+ z0 = 1j * float(half_sign) * float(height)
129
+
130
+ # Initialize at z0 via asymptotic / Im-sign selection.
131
+ w_prev = stieltjes_select_root(poly_roots(z0), z0, w_prev=None)
132
+
133
+ # Straight-line homotopy from z0 to z_eval.
134
+ for tau in numpy.linspace(0.0, 1.0, int(steps) + 1)[1:]:
135
+ z_tau = z0 + tau * (z_eval - z0)
136
+ w_prev = stieltjes_select_root(poly_roots(z_tau), z_tau, w_prev=w_prev)
137
+
138
+ return w_prev
@@ -12,12 +12,17 @@
12
12
  # =======
13
13
 
14
14
  import numpy
15
- from .._util import resolve_complex_dtype
15
+ from .._util import resolve_complex_dtype, compute_eig
16
16
  # from .._util import compute_eig
17
17
  from ._continuation_algebraic import sample_z_joukowski, \
18
- filter_z_away_from_cuts, fit_polynomial_relation, eval_P
18
+ filter_z_away_from_cuts, fit_polynomial_relation, \
19
+ sanity_check_stieltjes_branch, eval_P
19
20
  from ._edge import evolve_edges, merge_edges
20
21
  from ._decompress import decompress_newton
22
+ from ._decompress2 import decompress_coeffs
23
+ from ._homotopy import stieltjes_poly
24
+ from .._free_form._support import supp
25
+ from .._free_form._plot_util import plot_density
21
26
 
22
27
  # Fallback to previous numpy API
23
28
  if not hasattr(numpy, 'trapezoid'):
@@ -129,46 +134,65 @@ class AlgebraicForm(object):
129
134
  # def __init__(self, A, support=None, delta=1e-6, dtype='complex128',
130
135
  # **kwargs):
131
136
 
132
- def __init__(self, stieltjes, support=None, delta=1e-5, dtype='complex128',
137
+ def __init__(self, A, support=None, delta=1e-5, dtype='complex128',
133
138
  **kwargs):
134
139
  """
135
140
  Initialization.
136
141
  """
137
142
 
138
- # self.A = None
139
- # self.eig = None
140
- self.stieltjes = stieltjes
143
+ self.A = None
144
+ self.eig = None
145
+ self.stieltjes = None
141
146
  self.support = support
142
147
  self.delta = delta # Offset above real axis to apply Plemelj formula
143
148
 
144
149
  # Data type for complex arrays
145
150
  self.dtype = resolve_complex_dtype(dtype)
146
151
 
147
- # # Eigenvalues
148
- # if A.ndim == 1:
149
- # # When A is a 1D array, it is assumed A is the eigenvalue array.
150
- # self.eig = A
151
- # self.n = len(A)
152
- # elif A.ndim == 2:
153
- # # When A is a 2D array, it is assumed A is the actual array,
154
- # # and its eigenvalues will be computed.
155
- # self.A = A
156
- # self.n = A.shape[0]
157
- # assert A.shape[0] == A.shape[1], \
158
- # 'Only square matrices are permitted.'
159
- # self.eig = compute_eig(A)
152
+ if hasattr(A, 'stieltjes') and callable(getattr(A, 'stieltjes', None)):
153
+ # This is one of the distribution objects, like MarchenkoPastur
154
+ self.stieltjes = A.stieltjes
155
+ self.n = 1
156
+
157
+ elif callable(A):
158
+ # This is a custom function
159
+ self.stieltjes = A
160
+ self.n = 1
161
+
162
+ else:
163
+ # Eigenvalues
164
+ if A.ndim == 1:
165
+ # If A is a 1D array, it is assumed A is the eigenvalues array.
166
+ self.eig = A
167
+ self.n = len(A)
168
+ elif A.ndim == 2:
169
+ # When A is a 2D array, it is assumed A is the actual array,
170
+ # and its eigenvalues will be computed.
171
+ self.A = A
172
+ self.n = A.shape[0]
173
+ assert A.shape[0] == A.shape[1], \
174
+ 'Only square matrices are permitted.'
175
+ self.eig = compute_eig(A)
176
+
177
+ # Use empirical Stieltjes function
178
+ self.stieltjes = lambda z: \
179
+ numpy.mean(1.0/(self.eig-z[:, numpy.newaxis]), axis=-1)
160
180
 
161
181
  # Support
162
- # if support is None:
163
- # # Detect support
164
- # self.lam_m, self.lam_p = supp(self.eig, **kwargs)
165
- # else:
166
- # self.lam_m = float(support[0])
167
- # self.lam_p = float(support[1])
168
- # self.support = (self.lam_m, self.lam_p)
182
+ if support is None:
183
+ if self.eig is None:
184
+ raise RuntimeError("Support must be provided without data")
185
+ # Detect support
186
+ self.lam_m, self.lam_p = supp(self.eig, **kwargs)
187
+ self.support = [(self.lam_m, self.lam_p)]
188
+ self.broad_support = self.support[0]
189
+ else:
190
+ self.support = support
191
+ self.lam_m = min([s[0] for s in self.support])
192
+ self.lam_p = max([s[1] for s in self.support])
193
+ self.broad_support = (self.lam_m, self.lam_p)
169
194
 
170
195
  # Initialize
171
- # self.method = None # fitting rho: jacobi, chebyshev
172
196
  self.a_coeffs = None # Polynomial coefficients
173
197
  self.cache = {} # Cache inner-computations
174
198
 
@@ -190,11 +214,9 @@ class AlgebraicForm(object):
190
214
  """
191
215
 
192
216
  # Very important: reset cache whenever this function is called. This
193
- # also empties all references holdign a cache copy.
217
+ # also empties all references holding a cache copy.
194
218
  # self.cache.clear()
195
219
 
196
- # return self.a_coeffs
197
-
198
220
  z_fits = []
199
221
  for sup in self.support:
200
222
  a, b = sup
@@ -205,12 +227,10 @@ class AlgebraicForm(object):
205
227
 
206
228
  z_fit = numpy.concatenate(z_fits)
207
229
 
208
- # Remove points too close to ANY cut
230
+ # Remove points too close to any cut
209
231
  z_fit = filter_z_away_from_cuts(z_fit, self.support, y_eps=y_eps,
210
232
  x_pad=x_pad)
211
233
 
212
- # ---------
213
-
214
234
  m1_fit = self.stieltjes(z_fit)
215
235
  a_coeffs = fit_polynomial_relation(z_fit, m1_fit, s=deg_m, deg_z=deg_z,
216
236
  ridge_lambda=reg,
@@ -219,46 +239,66 @@ class AlgebraicForm(object):
219
239
 
220
240
  self.a_coeffs = a_coeffs
221
241
 
242
+ # Reporting error
243
+ P_res = numpy.abs(eval_P(z_fit, m1_fit, a_coeffs))
244
+ res_max = numpy.max(P_res[numpy.isfinite(P_res)])
245
+ res_99_9 = numpy.quantile(P_res[numpy.isfinite(P_res)], 0.999)
246
+
247
+ # Check polynomial has Stieltjes root
248
+ x_min = self.lam_m - 1.0
249
+ x_max = self.lam_p + 1.0
250
+ status = sanity_check_stieltjes_branch(a_coeffs, x_min, x_max,
251
+ eta=max(y_eps, 1e-2), n_x=128,
252
+ max_bad_frac=0.05)
253
+
254
+ status['res_max'] = res_max
255
+ status['res_99_9'] = res_99_9
256
+
222
257
  if verbose:
223
- P_res = numpy.abs(eval_P(z_fit, m1_fit, a_coeffs))
224
- print("fit residual max:", numpy.max(P_res[numpy.isfinite(P_res)]))
225
- print("fit residual 99.9%:",
226
- numpy.quantile(P_res[numpy.isfinite(P_res)], 0.999))
258
+ print(f'fit residual max : {res_max:>0.4e}')
259
+ print(f'fit residual 99.9%: {res_99_9:>0.4e}')
227
260
 
228
- print('\nCoefficinets')
229
- with numpy.printoptions(precision=4, suppress=True):
261
+ print('\nCoefficients (real)')
262
+ with numpy.printoptions(precision=8, suppress=True):
230
263
  for i in range(a_coeffs.shape[0]):
231
264
  for j in range(a_coeffs.shape[1]):
232
265
  v = a_coeffs[i, j]
233
- print(f"{v.real:>+0.4f}{v.imag:>+0.4f}j", end=" ")
266
+ print(f'{v.real:>+0.8f}', end=' ')
234
267
  print('')
235
268
 
236
- print('\nCoefficients mangitudes')
237
- with numpy.printoptions(precision=6, suppress=True):
238
- print(numpy.abs(a_coeffs))
269
+ a_coeffs_img_norm = numpy.linalg.norm(a_coeffs.imag, ord='fro')
270
+ print(f'\nCoefficients (imag) norm: {a_coeffs_img_norm:>0.4e}')
239
271
 
240
- return a_coeffs
272
+ if not status['ok']:
273
+ print("\nWARNING: sanity check failed:\n" +
274
+ f"\tfrac_bad: {status['frac_bad']:>0.3f}\n" +
275
+ f"\tn_bad : {status['n_bad']}\n" +
276
+ f"\tn_test : {status['n_test']}")
277
+ else:
278
+ print('\nStieltjes sanity check: OK')
279
+
280
+ return a_coeffs, status
241
281
 
242
282
  # =============
243
283
  # generate grid
244
284
  # =============
245
285
 
246
- # def _generate_grid(self, scale, extend=1.0, N=500):
247
- # """
248
- # Generate a grid of points to evaluate density / Hilbert / Stieltjes
249
- # transforms.
250
- # """
251
- #
252
- # radius = 0.5 * (self.lam_p - self.lam_m)
253
- # center = 0.5 * (self.lam_p + self.lam_m)
254
- #
255
- # x_min = numpy.floor(extend * (center - extend * radius * scale))
256
- # x_max = numpy.ceil(extend * (center + extend * radius * scale))
257
- #
258
- # x_min /= extend
259
- # x_max /= extend
260
- #
261
- # return numpy.linspace(x_min, x_max, N)
286
+ def _generate_grid(self, scale, extend=1.0, N=500):
287
+ """
288
+ Generate a grid of points to evaluate density / Hilbert / Stieltjes
289
+ transforms.
290
+ """
291
+
292
+ radius = 0.5 * (self.lam_p - self.lam_m)
293
+ center = 0.5 * (self.lam_p + self.lam_m)
294
+
295
+ x_min = numpy.floor(extend * (center - extend * radius * scale))
296
+ x_max = numpy.ceil(extend * (center + extend * radius * scale))
297
+
298
+ x_min /= extend
299
+ x_max /= extend
300
+
301
+ return numpy.linspace(x_min, x_max, N)
262
302
 
263
303
  # =======
264
304
  # density
@@ -312,16 +352,19 @@ class AlgebraicForm(object):
312
352
  raise RuntimeError('The model needs to be fit using the .fit() ' +
313
353
  'function.')
314
354
 
315
- # # Create x if not given
316
- # if x is None:
317
- # x = self._generate_grid(1.25)
318
- #
319
- # # Preallocate density to zero
320
- # rho = numpy.zeros_like(x)
321
- #
322
- # # Compute density only inside support
323
- # mask = numpy.logical_and(x >= self.lam_m, x <= self.lam_p)
324
- #
355
+ # Create x if not given
356
+ if x is None:
357
+ x = self._generate_grid(1.25)
358
+
359
+ # Preallocate density to zero
360
+ rho = numpy.zeros_like(x)
361
+
362
+ for idx, x_i in enumerate(x):
363
+ m_i = stieltjes_poly(x_i, self.a_coeffs)
364
+ rho[idx] = m_i.imag
365
+
366
+ rho = rho / numpy.pi
367
+
325
368
  # if self.method == 'jacobi':
326
369
  # rho[mask] = jacobi_density(x[mask], self.psi, self.support,
327
370
  # self.alpha, self.beta)
@@ -341,12 +384,12 @@ class AlgebraicForm(object):
341
384
  # if min_rho < 0.0 - 1e-3:
342
385
  # print(f'"rho" is not positive. min_rho: {min_rho:>0.3f}. Set ' +
343
386
  # r'"force=True".')
344
- #
345
- # if plot:
346
- # plot_density(x, rho, eig=self.eig, support=self.support,
347
- # label='Estimate', latex=latex, save=save)
348
- #
349
- # return rho
387
+
388
+ if plot:
389
+ plot_density(x, rho, eig=self.eig, support=self.broad_support,
390
+ label='Estimate', latex=latex, save=save)
391
+
392
+ return rho
350
393
 
351
394
  # =======
352
395
  # hilbert
@@ -568,68 +611,99 @@ class AlgebraicForm(object):
568
611
  # decompress
569
612
  # ==========
570
613
 
571
- def decompress(self, x, t,
572
- max_iter=50,
573
- tol=1e-12,
574
- armijo=1e-4,
575
- min_lam=1e-6,
576
- w_min=1e-14,
577
- sweep=True,
578
- verbose=False):
614
+ def decompress(self, size, x=None, method='one', plot=False, latex=False,
615
+ save=False, verbose=False, newton_opt={
616
+ 'max_iter': 50, 'tol': 1e-12, 'armijo': 1e-4,
617
+ 'min_lam': 1e-6, 'w_min': 1e-14, 'sweep': True}):
579
618
  """
580
619
  Free decompression of spectral density.
581
620
  """
582
621
 
583
622
  # Check size argument
584
- # if numpy.isscalar(size):
585
- # size = int(size)
586
- # else:
587
- # # Check monotonic increment (either all increasing or decreasing)
588
- # diff = numpy.diff(size)
589
- # if not (numpy.all(diff >= 0) or numpy.all(diff <= 0)):
590
- # raise ValueError('"size" increment should be monotonic.')
623
+ if numpy.isscalar(size):
624
+ size = int(size)
625
+ else:
626
+ # Check monotonic increment (either all increasing or decreasing)
627
+ diff = numpy.diff(size)
628
+ if not (numpy.all(diff >= 0) or numpy.all(diff <= 0)):
629
+ raise ValueError('"size" increment should be monotonic.')
591
630
 
592
631
  # Decompression ratio equal to e^{t}.
593
- # alpha = numpy.atleast_1d(size) / self.n
632
+ alpha = numpy.atleast_1d(size) / self.n
594
633
 
595
- # # If the input size was only a scalar, return a 1D rho, otherwise 2D.
596
- # if numpy.isscalar(size):
597
- # rho = numpy.squeeze(rho)
598
- #
599
- # # Plot only the last size
600
- # if plot:
601
- # if numpy.isscalar(size):
602
- # rho_last = rho
603
- # else:
604
- # rho_last = rho[-1, :]
605
- # plot_density(x, rho_last, support=(lb, ub),
606
- # label='Decompression', latex=latex, save=save)
607
- #
608
- # return rho, x
634
+ def m_fn(z):
635
+ return stieltjes_poly(z, self.a_coeffs)
609
636
 
610
- # Query grid on the real axis + a small imaginary buffer
611
- z_query = x + 1j * self.delta
637
+ # Lower and upper bound on new support
638
+ hilb_lb = (1.0 / m_fn(self.lam_m + self.delta * 1j).item()).real
639
+ hilb_ub = (1.0 / m_fn(self.lam_p + self.delta * 1j).item()).real
640
+ lb = self.lam_m - (numpy.max(alpha) - 1) * hilb_lb
641
+ ub = self.lam_p - (numpy.max(alpha) - 1) * hilb_ub
612
642
 
613
- # Initial condition at t=0 (physical branch)
614
- w0_list = self.stieltjes(z_query)
643
+ # Create x if not given
644
+ if x is None:
645
+ radius = 0.5 * (ub - lb)
646
+ center = 0.5 * (ub + lb)
647
+ scale = 1.25
648
+ x_min = numpy.floor(center - radius * scale)
649
+ x_max = numpy.ceil(center + radius * scale)
650
+ x = numpy.linspace(x_min, x_max, 200)
651
+ else:
652
+ x = numpy.asarray(x)
615
653
 
616
- # Evolve
617
- W, ok = decompress_newton(
618
- z_query, t, self.a_coeffs,
619
- w0_list=w0_list,
620
- max_iter=max_iter,
621
- tol=tol,
622
- armijo=armijo,
623
- min_lam=min_lam,
624
- w_min=w_min,
625
- sweep=sweep)
654
+ if method == 'one':
626
655
 
627
- rho = W.imag / numpy.pi
656
+ # Query grid on the real axis + a small imaginary buffer
657
+ z_query = x + 1j * self.delta
628
658
 
629
- if verbose:
630
- print("success rate per t:", ok.mean(axis=1))
659
+ # Initial condition at t=0 (physical branch)
660
+ w0_list = self.stieltjes(z_query)
631
661
 
632
- return rho
662
+ # Times
663
+ t = numpy.log(alpha)
664
+
665
+ # Evolve
666
+ W, ok = decompress_newton(
667
+ z_query, t, self.a_coeffs,
668
+ w0_list=w0_list, **newton_opt)
669
+
670
+ rho = W.imag / numpy.pi
671
+
672
+ if verbose:
673
+ print("success rate per t:", ok.mean(axis=1))
674
+
675
+ elif method == 'two':
676
+
677
+ # Preallocate density to zero
678
+ rho = numpy.zeros((alpha.size, x.size), dtype=float)
679
+
680
+ # Decompress to each alpha
681
+ for i in range(alpha.size):
682
+ coeffs_i = decompress_coeffs(self.a_coeffs,
683
+ numpy.log(alpha[i]))
684
+ for j, x_j in enumerate(x):
685
+ m_j = stieltjes_poly(x_j, coeffs_i)
686
+ rho[i, j] = m_j.imag
687
+
688
+ rho = rho / numpy.pi
689
+
690
+ else:
691
+ raise ValueError('"method" is invalid.')
692
+
693
+ # If the input size was only a scalar, return a 1D rho, otherwise 2D.
694
+ if numpy.isscalar(size):
695
+ rho = numpy.squeeze(rho)
696
+
697
+ # Plot only the last size
698
+ if plot:
699
+ if numpy.isscalar(size):
700
+ rho_last = rho
701
+ else:
702
+ rho_last = rho[-1, :]
703
+ plot_density(x, rho_last, support=(lb, ub),
704
+ label='Decompression', latex=latex, save=save)
705
+
706
+ return rho, x
633
707
 
634
708
  # ====
635
709
  # edge
@@ -290,26 +290,14 @@ class DeformedWigner(object):
290
290
  A : numpy.ndarray
291
291
  A matrix of the size :math:`n \\times n`.
292
292
 
293
- Parameters
294
- ----------
295
- size : int
296
- Size n of the matrix.
297
-
298
- seed : int, default=None
299
- Seed for random number generator.
300
-
301
- Returns
302
- -------
303
- A : numpy.ndarray
304
- Symmetric matrix of shape (n, n).
305
-
306
293
  Notes
307
294
  -----
308
295
 
309
- Generate an :math:`n x n` matrix :math:`\\mathbf{A} = \\mathbf{T} +
310
- \\sigma \\mathbf{W}` whose ESD converges to
311
- :math:`H \\boxplus SC_{\\sigma^2}`, where
312
- :math:`H = w_1 \\delta_{t_1} + (1-w_1) \\delta_{t_2}`.
296
+ Generate an :math:`n \\times n` matrix
297
+ :math:`\\mathbf{A} = \\mathbf{T} + \\sigma \\mathbf{W}`
298
+ whose ESD converges to
299
+ :math:`H \\boxplus \\mathrm{SC}_{\\sigma^2}`, where
300
+ :math:`H = w_1 \\delta_{t_1} + (1 - w_1) \\delta_{t_2}`.
313
301
 
314
302
  Examples
315
303
  --------
@@ -317,8 +305,8 @@ class DeformedWigner(object):
317
305
  .. code-block::python
318
306
 
319
307
  >>> from freealg.distributions import DeformedWigner
320
- >>> mp = DeformedWigner(1/50)
321
- >>> A = mp.matrix(2000)
308
+ >>> dwg = DeformedWigner(1/50)
309
+ >>> A = dwg.matrix(2000)
322
310
  """
323
311
 
324
312
  n = int(size)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: freealg
3
- Version: 0.7.3
3
+ Version: 0.7.5
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
@@ -19,7 +19,9 @@ freealg.egg-info/top_level.txt
19
19
  freealg/_algebraic_form/__init__.py
20
20
  freealg/_algebraic_form/_continuation_algebraic.py
21
21
  freealg/_algebraic_form/_decompress.py
22
+ freealg/_algebraic_form/_decompress2.py
22
23
  freealg/_algebraic_form/_edge.py
24
+ freealg/_algebraic_form/_homotopy.py
23
25
  freealg/_algebraic_form/_sheets_util.py
24
26
  freealg/_algebraic_form/algebraic_form.py
25
27
  freealg/_free_form/__init__.py
@@ -1 +0,0 @@
1
- __version__ = "0.7.3"
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