freealg 0.6.3__py3-none-any.whl → 0.7.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.
Files changed (48) hide show
  1. freealg/__init__.py +8 -7
  2. freealg/__version__.py +1 -1
  3. freealg/_algebraic_form/__init__.py +11 -0
  4. freealg/_algebraic_form/_continuation_algebraic.py +503 -0
  5. freealg/_algebraic_form/_decompress.py +648 -0
  6. freealg/_algebraic_form/_edge.py +352 -0
  7. freealg/_algebraic_form/_sheets_util.py +145 -0
  8. freealg/_algebraic_form/algebraic_form.py +987 -0
  9. freealg/_freeform/__init__.py +16 -0
  10. freealg/_freeform/_density_util.py +243 -0
  11. freealg/{_linalg.py → _freeform/_linalg.py} +1 -1
  12. freealg/{freeform.py → _freeform/freeform.py} +2 -1
  13. freealg/_geometric_form/__init__.py +13 -0
  14. freealg/_geometric_form/_continuation_genus0.py +175 -0
  15. freealg/_geometric_form/_continuation_genus1.py +275 -0
  16. freealg/_geometric_form/_elliptic_functions.py +174 -0
  17. freealg/_geometric_form/_sphere_maps.py +63 -0
  18. freealg/_geometric_form/_torus_maps.py +118 -0
  19. freealg/_geometric_form/geometric_form.py +1094 -0
  20. freealg/_util.py +1 -228
  21. freealg/distributions/__init__.py +5 -1
  22. freealg/distributions/_chiral_block.py +440 -0
  23. freealg/distributions/_deformed_marchenko_pastur.py +617 -0
  24. freealg/distributions/_deformed_wigner.py +312 -0
  25. freealg/distributions/_kesten_mckay.py +2 -2
  26. freealg/distributions/_marchenko_pastur.py +199 -82
  27. freealg/distributions/_meixner.py +2 -2
  28. freealg/distributions/_wachter.py +2 -2
  29. freealg/distributions/_wigner.py +2 -2
  30. freealg/visualization/__init__.py +12 -0
  31. freealg/visualization/_glue_util.py +32 -0
  32. freealg/visualization/_rgb_hsv.py +125 -0
  33. {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/METADATA +1 -1
  34. freealg-0.7.1.dist-info/RECORD +47 -0
  35. freealg-0.6.3.dist-info/RECORD +0 -26
  36. /freealg/{_chebyshev.py → _freeform/_chebyshev.py} +0 -0
  37. /freealg/{_damp.py → _freeform/_damp.py} +0 -0
  38. /freealg/{_decompress.py → _freeform/_decompress.py} +0 -0
  39. /freealg/{_jacobi.py → _freeform/_jacobi.py} +0 -0
  40. /freealg/{_pade.py → _freeform/_pade.py} +0 -0
  41. /freealg/{_plot_util.py → _freeform/_plot_util.py} +0 -0
  42. /freealg/{_sample.py → _freeform/_sample.py} +0 -0
  43. /freealg/{_series.py → _freeform/_series.py} +0 -0
  44. /freealg/{_support.py → _freeform/_support.py} +0 -0
  45. {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/WHEEL +0 -0
  46. {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/licenses/AUTHORS.txt +0 -0
  47. {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/licenses/LICENSE.txt +0 -0
  48. {freealg-0.6.3.dist-info → freealg-0.7.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,16 @@
1
+ # SPDX-FileCopyrightText: Copyright 2025, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the license found in the LICENSE.txt file in the root
7
+ # directory of this source tree.
8
+
9
+ from .freeform import FreeForm
10
+ from ._linalg import eigvalsh, cond, norm, trace, slogdet
11
+ from ._support import supp
12
+ from ._sample import sample
13
+ from ._density_util import kde
14
+
15
+ __all__ = ['FreeForm', 'eigvalsh', 'cond', 'norm', 'trace', 'slogdet', 'supp',
16
+ 'sample', 'kde']
@@ -0,0 +1,243 @@
1
+ # SPDX-FileCopyrightText: Copyright 2025, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it under
6
+ # the terms of the license found in the LICENSE.txt file in the root directory
7
+ # of this source tree.
8
+
9
+
10
+ # =======
11
+ # Imports
12
+ # =======
13
+
14
+ import numpy
15
+ from scipy.stats import gaussian_kde
16
+ from scipy.stats import beta
17
+ # from statsmodels.nonparametric.kde import KDEUnivariate
18
+ from scipy.optimize import minimize
19
+ import matplotlib.pyplot as plt
20
+ import texplot
21
+ from ._plot_util import _auto_bins
22
+
23
+ # Fallback to previous API
24
+ if not hasattr(numpy, 'trapezoid'):
25
+ numpy.trapezoid = numpy.trapz
26
+
27
+ __all__ = ['kde', 'force_density']
28
+
29
+
30
+ # ===
31
+ # kde
32
+ # ===
33
+
34
+ def kde(eig, xs, lam_m, lam_p, h, kernel='beta', plot=False):
35
+ """
36
+ Kernel density estimation of eigenvalues.
37
+
38
+ Parameters
39
+ ----------
40
+
41
+ eig : numpy.array
42
+ 1D array of samples of size `n`.
43
+
44
+ xs : numpy.array
45
+ 1D array of evaluation grid (must lie within ``[lam_m, lam_p]``)
46
+
47
+ lam_m : float
48
+ Lower end of the support endpoints with ``lam_m < lam_p``.
49
+
50
+ lam_p : float
51
+ Upper end of the support endpoints with ``lam_m < lam_p``.
52
+
53
+ h : float
54
+ Kernel bandwidth in rescaled units where ``0 < h < 1``.
55
+
56
+ kernel : {``'gaussian'``, ``'beta'``}, default= ``'beta'``
57
+ Kernel function using either Gaussian or Beta distribution.
58
+
59
+ plot : bool, default=False
60
+ If `True`, the KDE is plotted.
61
+
62
+ Returns
63
+ -------
64
+
65
+ pdf : numpy.ndarray
66
+ Probability distribution function with the same length as ``xs``.
67
+
68
+ See Also
69
+ --------
70
+
71
+ freealg.supp
72
+ freealg.sample
73
+
74
+ References
75
+ ----------
76
+
77
+ .. [1] `R-package documentation for Beta kernel
78
+ <https://search.r-project.org/CRAN/refmans/DELTD/html/Beta.html>`__
79
+
80
+ .. [2] Chen, S. X. (1999). Beta Kernel estimators for density functions.
81
+ *Computational Statistics and Data Analysis* 31 p. 131--145.
82
+
83
+ Notes
84
+ -----
85
+
86
+ In Beta kernel density estimation, the shape parameters :math:`a` and
87
+ :math:`b` of the :math:`\\mathrm{Beta}(a, b)` distribution are computed
88
+ for each data point :math:`u` as:
89
+
90
+ .. math::
91
+
92
+ a = (u / h) + 1.0
93
+ b = ((1.0 - u) / h) + 1.0
94
+
95
+ This is a standard way of using Beta kernel (see R-package documentation
96
+ [1]_). These equations are derived from *moment matching* method, where
97
+
98
+ .. math::
99
+
100
+ \\mathrm{Mean}(\\mathrm{Beta}(a,b)) = u
101
+ \\mathrm{Var}(\\mathrm{Beta}(a,b)) = (1-u) u h
102
+
103
+ Solving these two equations for :math:`a` and :math:`b` yields the
104
+ relations above. See [2]_ (page 134).
105
+ """
106
+
107
+ if kernel == 'gaussian':
108
+ pdf = gaussian_kde(eig, bw_method=h)(xs)
109
+
110
+ # Adaptive KDE
111
+ # k = KDEUnivariate(eig)
112
+ # k.fit(kernel='gau', bw='silverman', fft=False, weights=None,
113
+ # gridsize=1024, adaptive=True)
114
+ # pdf = k.evaluate(xs)
115
+
116
+ elif kernel == 'beta':
117
+
118
+ span = lam_p - lam_m
119
+ if span <= 0:
120
+ raise ValueError('"lam_p" must be larger than "lam_m".')
121
+
122
+ # map samples and grid to [0, 1]
123
+ u = (eig - lam_m) / span
124
+ t = (xs - lam_m) / span
125
+
126
+ # keep only samples strictly inside (0,1)
127
+ if (u.min() < 0) or (u.max() > 1):
128
+ u = u[(u > 0) & (u < 1)]
129
+
130
+ n = u.size
131
+ if n == 0:
132
+ return numpy.zeros_like(xs, dtype=float)
133
+
134
+ # Shape parameters "a" and "b" or the kernel Beta(a, b), which is
135
+ # computed for each data point "u" (see notes above). These are
136
+ # vectorized.
137
+ a = (u / h) + 1.0
138
+ b = ((1.0 - u) / h) + 1.0
139
+
140
+ # # tiny positive number to keep shape parameters > 0
141
+ eps = 1e-6
142
+ a = numpy.clip(a, eps, None)
143
+ b = numpy.clip(b, eps, None)
144
+
145
+ # Beta kernel
146
+ pdf_matrix = beta.pdf(t[None, :], a[:, None], b[:, None])
147
+
148
+ # Average and re-normalize back to x variable
149
+ pdf = pdf_matrix.sum(axis=0) / (n * span)
150
+
151
+ # Exact zeros outside [lam_m, lam_p]
152
+ pdf[(t < 0) | (t > 1)] = 0.0
153
+
154
+ else:
155
+ raise NotImplementedError('"kernel" is invalid.')
156
+
157
+ if plot:
158
+ with texplot.theme(use_latex=False):
159
+ fig, ax = plt.subplots(figsize=(6, 4))
160
+
161
+ x_min = numpy.min(xs)
162
+ x_max = numpy.max(xs)
163
+ bins = numpy.linspace(x_min, x_max, _auto_bins(eig))
164
+ _ = ax.hist(eig, bins, density=True, color='silver',
165
+ edgecolor='none', label='Samples histogram')
166
+ ax.plot(xs, pdf, color='black', label='KDE')
167
+ ax.set_xlabel(r'$x$')
168
+ ax.set_ylabel(r'$\\rho(x)$')
169
+ ax.set_xlim([xs[0], xs[-1]])
170
+ ax.set_ylim(bottom=0)
171
+ ax.set_title('Kernel Density Estimation')
172
+ ax.legend(fontsize='x-small')
173
+ plt.show()
174
+
175
+ return pdf
176
+
177
+
178
+ # =============
179
+ # force density
180
+ # =============
181
+
182
+ def force_density(psi0, support, density, grid, alpha=0.0, beta=0.0):
183
+ """
184
+ Starting from psi0 (raw projection), solve
185
+ min 0.5 ||psi - psi0||^2
186
+ s.t. F_pos psi >= 0 (positivity on grid)
187
+ psi[0] = psi0[0] (mass)
188
+ f(lam_m) psi = 0 (zero at left edge)
189
+ f(lam_p) psi = 0 (zero at right edge)
190
+ """
191
+
192
+ lam_m, lam_p = support
193
+
194
+ # Objective and gradient
195
+ def fun(psi):
196
+ return 0.5 * numpy.dot(psi-psi0, psi-psi0)
197
+
198
+ def grad(psi):
199
+ return psi - psi0
200
+
201
+ # Constraints:
202
+ constraints = []
203
+
204
+ # Enforce positivity
205
+ constraints.append({'type': 'ineq',
206
+ 'fun': lambda psi: density(grid, psi)})
207
+
208
+ # Enforce unit mass
209
+ constraints.append({
210
+ 'type': 'eq',
211
+ 'fun': lambda psi: numpy.trapz(density(grid, psi), grid) - 1.0
212
+ })
213
+
214
+ # Enforce zero at left edge
215
+ if beta <= 0.0 and beta > -0.5:
216
+ constraints.append({
217
+ 'type': 'eq',
218
+ 'fun': lambda psi: density(numpy.array([lam_m]), psi)[0]
219
+ })
220
+
221
+ # Enforce zero at right edge
222
+ if alpha <= 0.0 and alpha > -0.5:
223
+ constraints.append({
224
+ 'type': 'eq',
225
+ 'fun': lambda psi: density(numpy.array([lam_p]), psi)[0]
226
+ })
227
+
228
+ # Solve a small quadratic programming
229
+ res = minimize(fun, psi0, jac=grad,
230
+ constraints=constraints,
231
+ # method='trust-constr',
232
+ method='SLSQP',
233
+ options={'maxiter': 1000, 'ftol': 1e-9, 'eps': 1e-8})
234
+
235
+ psi = res.x
236
+
237
+ # Normalize first mode to unit mass
238
+ x = numpy.linspace(lam_m, lam_p, 1000)
239
+ rho = density(x, psi)
240
+ mass = numpy.trapezoid(rho, x)
241
+ psi[0] = psi[0] / mass
242
+
243
+ return psi
@@ -11,7 +11,7 @@
11
11
  # =======
12
12
 
13
13
  import numpy
14
- from ._util import compute_eig
14
+ from .._util import compute_eig
15
15
  from .freeform import FreeForm
16
16
 
17
17
  __all__ = ['eigvalsh', 'cond', 'norm', 'trace', 'slogdet']
@@ -13,7 +13,8 @@
13
13
 
14
14
  import numpy
15
15
  from functools import partial
16
- from ._util import resolve_complex_dtype, compute_eig, kde, force_density
16
+ from .._util import resolve_complex_dtype, compute_eig
17
+ from ._density_util import kde, force_density
17
18
  from ._jacobi import jacobi_sample_proj, jacobi_kernel_proj, jacobi_density, \
18
19
  jacobi_stieltjes
19
20
  from ._chebyshev import chebyshev_sample_proj, chebyshev_kernel_proj, \
@@ -0,0 +1,13 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the license found in the LICENSE.txt file in the root
7
+ # directory of this source tree.
8
+
9
+ from .geometric_form import GeometricForm
10
+ # from ._elliptic_functions import ellipj
11
+ # from ._continuation_genus1 import mobius_z
12
+
13
+ __all__ = ['GeometricForm']
@@ -0,0 +1,175 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026, Siavash Ameli <sameli@berkeley.edu>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # SPDX-FileType: SOURCE
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the license found in the LICENSE.txt file in the root
7
+ # directory of this source tree.
8
+
9
+
10
+ # =======
11
+ # Imports
12
+ # =======
13
+
14
+ import numpy
15
+
16
+ __all__ = ['joukowski_z', 'joukowski_w', 'fit_pade', 'eval_pade',
17
+ 'generate_pade']
18
+
19
+
20
+ # ===========
21
+ # joukowski z
22
+ # ===========
23
+
24
+ def joukowski_z(w, a, b):
25
+
26
+ c = 0.5 * (a + b)
27
+ d = 0.5 * (b - a)
28
+ z = c + 0.5 * d * (w + 1.0 / w)
29
+
30
+ return z
31
+
32
+
33
+ # ===========
34
+ # joukowski w
35
+ # ===========
36
+
37
+ def joukowski_w(z, a, b):
38
+
39
+ c = 0.5 * (a + b)
40
+ d = 0.5 * (b - a)
41
+ xi = (z - c) / d
42
+ s = numpy.sqrt(xi * xi - 1.0)
43
+
44
+ # Stabilize sqrt branch: make s have same sign as xi (helps continuity)
45
+ s = numpy.where(numpy.real(xi) < 0.0, -s, s)
46
+
47
+ w1 = xi + s
48
+ w2 = xi - s
49
+ w = numpy.where(numpy.abs(w1) >= 1.0, w1, w2)
50
+
51
+ return w
52
+
53
+
54
+ # ========
55
+ # fit pade
56
+ # ========
57
+
58
+ def fit_pade(w, m1, deg_p=12, deg_q=12, ridge_lambda=0.0):
59
+ """
60
+ Fit m1 on w-plane using Pade rational approximation.
61
+ """
62
+
63
+ n_samples = m1.size
64
+
65
+ wp = numpy.ones((n_samples, deg_p + 1), dtype=complex)
66
+ for k in range(1, deg_p + 1):
67
+ wp[:, k] = wp[:, k - 1] * w
68
+
69
+ wq = numpy.ones((n_samples, deg_q + 1), dtype=complex)
70
+ for k in range(1, deg_q + 1):
71
+ wq[:, k] = wq[:, k - 1] * w
72
+
73
+ A = numpy.hstack([wp, -m1[:, None] * wq[:, 1:]])
74
+ bvec = m1
75
+
76
+ # Scale columns for better conditioning on LS
77
+ s = numpy.linalg.norm(A, axis=0)
78
+ s[s == 0] = 1.0
79
+ As = A / s[None, :]
80
+
81
+ if ridge_lambda is None:
82
+ ridge_lambda = 0.0
83
+
84
+ if ridge_lambda > 0.0:
85
+ # Scale ridge by average diagonal magnitude of AhA
86
+ # Since columns of As have unit norm, this is typically ~1.
87
+ alpha = ridge_lambda
88
+
89
+ # Solving augmented least square
90
+ n_coef = As.shape[1]
91
+ A_aug = numpy.vstack([As, numpy.sqrt(alpha) * numpy.eye(
92
+ n_coef, dtype=complex)])
93
+ b_aug = numpy.concatenate([bvec, numpy.zeros(n_coef, dtype=complex)])
94
+
95
+ coef, _, _, _ = numpy.linalg.lstsq(A_aug, b_aug, rcond=None)
96
+ else:
97
+ coef, _, _, _ = numpy.linalg.lstsq(As, bvec, rcond=None)
98
+
99
+ coef = coef / s
100
+
101
+ p = coef[:deg_p + 1]
102
+ q = numpy.zeros(deg_q + 1, dtype=complex)
103
+ q[0] = 1.0
104
+ q[1:] = coef[deg_p + 1:]
105
+
106
+ return p, q
107
+
108
+
109
+ # =========
110
+ # eval pade
111
+ # =========
112
+
113
+ def eval_pade(w, p, q):
114
+
115
+ num = numpy.zeros_like(w, dtype=complex)
116
+ den = numpy.zeros_like(w, dtype=complex)
117
+
118
+ for k in range(len(p) - 1, -1, -1):
119
+ num = num * w + p[k]
120
+
121
+ for k in range(len(q) - 1, -1, -1):
122
+ den = den * w + q[k]
123
+
124
+ return num / den
125
+
126
+
127
+ # =============
128
+ # generate pade
129
+ # =============
130
+
131
+ def generate_pade(m1_fn, a, b, deg_p=12, deg_q=12, n_samples=4096, r=1.2,
132
+ n_r=1, r_min=None, ridge_lambda=0.0):
133
+
134
+ if r_min is None:
135
+ r_min = 1.0 + 0.05 * (r - 1.0) if r > 1.0 else 1.0
136
+
137
+ if n_r is None or n_r < 1:
138
+ n_r = 1
139
+
140
+ if n_samples % 2 != 0:
141
+ raise ValueError('n_samples should be even.')
142
+
143
+ if n_r == 1:
144
+ rs = numpy.array([r], dtype=float)
145
+ else:
146
+ rs = numpy.linspace(r_min, r, n_r)
147
+
148
+ W_list = []
149
+ M_list = []
150
+
151
+ n_half = n_samples // 2
152
+
153
+ for r_i in rs:
154
+
155
+ # Generate sample points along theta
156
+ theta = numpy.pi * (numpy.arange(n_half) + 0.5) / n_half
157
+ w = r_i * numpy.exp(1j * theta)
158
+ z = joukowski_z(w, a, b)
159
+ m1 = m1_fn(z)
160
+
161
+ W_list.append(w)
162
+ M_list.append(m1)
163
+
164
+ # Add conjugate points which enforces Schwarz reflection
165
+ W_list.append(numpy.conjugate(w))
166
+ M_list.append(numpy.conjugate(m1))
167
+
168
+ w_all = numpy.concatenate(W_list)
169
+ m1_all = numpy.concatenate(M_list)
170
+
171
+ # Fit on the sample data from m1
172
+ p, q = fit_pade(w_all, m1_all, deg_p=deg_p, deg_q=deg_q,
173
+ ridge_lambda=ridge_lambda)
174
+
175
+ return p, q