freealg 0.0.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/__init__.py ADDED
@@ -0,0 +1,13 @@
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
+
11
+ __all__ = ['FreeForm']
12
+
13
+ from .__version__ import __version__ # noqa: F401 E402
freealg/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
freealg/_chebyshev.py ADDED
@@ -0,0 +1,201 @@
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.special import eval_chebyu
16
+
17
+ __all__ = ['chebyshev_proj', 'chebyshev_approx', 'chebyshev_stieltjes']
18
+
19
+
20
+ # ==============
21
+ # chebyshev proj
22
+ # ==============
23
+
24
+ def chebyshev_proj(eig, support, K=10, reg=0.0):
25
+ """
26
+ Estimate the coefficients \\psi_k in
27
+
28
+ \\rho(x) = w(t) \\sum_{k=0}^K \\psi_k U_k(t),
29
+
30
+ where t = (2x–(\\lambda_{-} + \\lambda_{+}))/ (\\lambda_{+} - \\lambda_{-})
31
+ in [-1, 1] and w(t) = \\sqrt{(1 - t^2}.
32
+
33
+ Parameters
34
+ ----------
35
+
36
+ eig : array_like, shape (N,)
37
+ The raw eigenvalues x_i.
38
+
39
+ support : tuple
40
+ The assumed compact support of rho.
41
+
42
+ K : int
43
+ Highest Chebyshev‐II order.
44
+
45
+ reg : float
46
+ Tikhonov‐style ridge on each coefficient (defaults to 0).
47
+
48
+ Returns
49
+ -------
50
+
51
+ psi : ndarray, shape (K+1,)
52
+ The projected coefficients \\psi_k.
53
+ """
54
+
55
+ lam_m, lam_p = support
56
+
57
+ # Map to [–1,1] interval
58
+ t = (2 * eig - (lam_m + lam_p)) / (lam_p - lam_m)
59
+ N = eig.size
60
+
61
+ # Inner‐product norm of each U_k under w(t) = sqrt{1–t^2} is \\pi/2
62
+ norm = numpy.pi / 2
63
+
64
+ psi = numpy.empty(K+1)
65
+ for k in range(K+1):
66
+
67
+ # empirical moment M_k = (1/N) \\sum U_k(t_i)
68
+ M_k = numpy.sum(eval_chebyu(k, t)) / N
69
+
70
+ # Regularization
71
+ if k == 0:
72
+ # Do not penalize at k=0, as this keeps unit mass.
73
+ # k=0 has unit mass, while k>0 has zero mass by orthogonality.
74
+ penalty = 0
75
+ else:
76
+ penalty = reg * (k / (K + 1))**2
77
+
78
+ # Add regularization on the diagonal
79
+ psi[k] = M_k / (norm + penalty)
80
+
81
+ return psi
82
+
83
+
84
+ # ================
85
+ # chebyshev approx
86
+ # ================
87
+
88
+ def chebyshev_approx(x, psi, support):
89
+ """
90
+ Given \\psi_k, evaluate the approximate density \\rho(x).
91
+
92
+ Parameters
93
+ ----------
94
+
95
+ x : array_like
96
+ Points at which to evaluate \\rho.
97
+
98
+ psi : array_like, shape (K+1,)
99
+ Coefficients from chebyshev_proj.
100
+
101
+ support : tuple
102
+ Same support used for projection.
103
+
104
+ Returns
105
+ -------
106
+
107
+ rho_x : ndarray, same shape as x
108
+ Approximated spectral density on the original x‐axis.
109
+ """
110
+
111
+ lam_m, lam_p = support
112
+
113
+ # Map to [–1,1] interval
114
+ t = (2 * numpy.asarray(x) - (lam_m + lam_p)) / (lam_p - lam_m)
115
+
116
+ # Weight sqrt{1–t^2} (clip for numerical safety)
117
+ w = numpy.sqrt(numpy.clip(1 - t**2, a_min=0, a_max=None))
118
+
119
+ # Summation approximation
120
+ U = numpy.vstack([eval_chebyu(k, t) for k in range(len(psi))]).T
121
+ rho_t = w * (U @ psi)
122
+
123
+ # Adjust for dt to dx transformation
124
+ rho_x = rho_t * (2.0 / (lam_p - lam_m))
125
+
126
+ return rho_x
127
+
128
+
129
+ # ===================
130
+ # chebushev stieltjes
131
+ # ===================
132
+
133
+ def chebyshev_stieltjes(z, psi, support):
134
+ """
135
+ Compute the Stieltjes transform m(z) for a Chebyshev‐II expansion
136
+
137
+ rho(x) = (2/(lam_p - lam_m)) * sqrt(1−t(x)^2) * sum_{k=0}^K psi_k U_k(t(x))
138
+
139
+ via the closed‐form
140
+
141
+ \\int_{-1}^1 U_k(t) sqrt(1−t^2)/(u - t) dt = \\pi J(u)^(k+1),
142
+
143
+ where
144
+
145
+ u = (2(z−center))/span,
146
+ center = (lam_p + lam_m)/2,
147
+ span = lam_p - lam_m,
148
+ J(u) = u − sqrt(u^2−1)
149
+
150
+ and then
151
+
152
+ m(z) = - (2/ span) * \\sum{k=0}^K \\psi_k * [ \\pi J(u)^(k+1) ].
153
+
154
+ Parameters
155
+ ----------
156
+
157
+ z : complex or array_like of complex
158
+ Points in the complex plane.
159
+
160
+ psi : array_like, shape (K+1,)
161
+ Chebyshev‐II coefficients \\psi.
162
+
163
+ support : tuple
164
+ The support interval of the original density.
165
+
166
+ Returns
167
+ -------
168
+
169
+ m_z : ndarray of complex
170
+ The Stieltjes transform m(z) on the same shape as z.
171
+ """
172
+
173
+ z = numpy.asarray(z, dtype=numpy.complex128)
174
+ lam_m, lam_p = support
175
+ span = lam_p - lam_m
176
+ center = 0.5 * (lam_m + lam_p)
177
+
178
+ # map z -> u in the standard [-1,1] domain
179
+ u = (2.0 * (z - center)) / span
180
+
181
+ # inverse-Joukowski: pick branch sqrt with +Im
182
+ root = numpy.sqrt(u*u - 1)
183
+ Jm = u - root
184
+ Jp = u + root
185
+
186
+ # Make sure J is Herglotz
187
+ J = numpy.zeros_like(Jp)
188
+ J = numpy.where(Jp.imag > 0, Jm, Jp)
189
+
190
+ # build powers J^(k+1) for k=0..K
191
+ K = len(psi) - 1
192
+ # shape: (..., K+1)
193
+ Jpow = J[..., None] ** numpy.arange(1, K+2)
194
+
195
+ # sum psi_k * J^(k+1)
196
+ S = numpy.sum(psi * Jpow, axis=-1)
197
+
198
+ # assemble m(z)
199
+ m_z = - (2.0 / span) * numpy.pi * S
200
+
201
+ return m_z
freealg/_damp.py ADDED
@@ -0,0 +1,88 @@
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
+
16
+ __all__ = ['jackson_damping', 'lanczos_damping', 'fejer_damping',
17
+ 'exponential_damping', 'parzen_damping']
18
+
19
+
20
+ # ===============
21
+ # jackson damping
22
+ # ===============
23
+
24
+ def jackson_damping(K):
25
+ """
26
+ Compute Jackson damping coefficients for orders k = 0, 1, ..., K-1.
27
+ """
28
+
29
+ k = numpy.arange(K)
30
+ g = ((K - k + 1) * numpy.cos(numpy.pi * k / (K + 1)) +
31
+ numpy.sin(numpy.pi * k / (K + 1)) / numpy.tan(numpy.pi / (K + 1))) \
32
+ / (K + 1)
33
+
34
+ return g
35
+
36
+
37
+ # ===============
38
+ # lanczos damping
39
+ # ===============
40
+
41
+ def lanczos_damping(K):
42
+ """
43
+ Compute Lanczos damping coefficients for orders k = 0, 1, ..., K-1.
44
+ """
45
+
46
+ k = numpy.arange(K)
47
+ sigma = numpy.sinc(k / K)
48
+
49
+ return sigma
50
+
51
+
52
+ # =============
53
+ # fejer damping
54
+ # =============
55
+
56
+ def fejer_damping(K):
57
+ """
58
+ Compute Fejer damping coefficients for orders k = 0, 1, ..., K-1.
59
+ """
60
+
61
+ k = numpy.arange(K)
62
+ return 1 - k / K
63
+
64
+
65
+ # ===================
66
+ # exponential damping
67
+ # ===================
68
+
69
+ def exponential_damping(K, alpha=6):
70
+ """
71
+ Compute exponential damping coefficients for orders k = 0, 1, ..., K-1.
72
+ """
73
+
74
+ k = numpy.arange(K)
75
+ return numpy.exp(-alpha * (k / K)**2)
76
+
77
+
78
+ # ==============
79
+ # parzen damping
80
+ # ==============
81
+
82
+ def parzen_damping(K):
83
+ """
84
+ Compute Parzen damping coefficients for orders k = 0, 1, ..., K-1.
85
+ """
86
+
87
+ k = numpy.arange(K)
88
+ return 1 - numpy.abs((k - K/2) / (K/2))**3
freealg/_jacobi.py ADDED
@@ -0,0 +1,188 @@
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.special import eval_jacobi, roots_jacobi
16
+ from scipy.special import gammaln, beta as Beta
17
+
18
+ __all__ = ['jacobi_proj', 'jacobi_approx', 'jacobi_stieltjes']
19
+
20
+
21
+ # ==============
22
+ # jacobi sq norm
23
+ # ==============
24
+
25
+ def jacobi_sq_norm(k, alpha, beta):
26
+ """
27
+ Norm of P_k
28
+ Special-case k = 0 to avoid gamma(0) issues when alpha + beta + 1 = 0.
29
+ """
30
+
31
+ if k == 0:
32
+ return 2.0**(alpha + beta + 1) * Beta(alpha + 1, beta + 1)
33
+
34
+ # Use logs instead to avoid overflow in gamma function.
35
+ lg_num = (alpha + beta + 1) * numpy.log(2.0) \
36
+ + gammaln(k + alpha + 1) \
37
+ + gammaln(k + beta + 1)
38
+
39
+ lg_den = numpy.log(2*k + alpha + beta + 1) \
40
+ + gammaln(k + 1) \
41
+ + gammaln(k + alpha + beta + 1)
42
+
43
+ return numpy.exp(lg_num - lg_den)
44
+
45
+
46
+ # ===========
47
+ # jacobi pro
48
+ # ===========
49
+
50
+ def jacobi_proj(eig, support, K=10, alpha=0.0, beta=0.0, reg=0.0):
51
+ """
52
+ """
53
+
54
+ lam_m, lam_p = support
55
+
56
+ # Convert to [-1, 1] interval
57
+ x = (2.0 * eig - (lam_p + lam_m)) / (lam_p - lam_m)
58
+
59
+ psi = numpy.empty(K + 1)
60
+
61
+ # Empirical moments and coefficients
62
+ for k in range(K + 1):
63
+ moment = numpy.mean(eval_jacobi(k, alpha, beta, x))
64
+ N_k = jacobi_sq_norm(k, alpha, beta) # normalization
65
+
66
+ if k == 0:
67
+ # Do not penalize at k=0, as this keeps unit mass.
68
+ # k=0 has unit mass, while k>0 has zero mass by orthogonality.
69
+ penalty = 0
70
+ else:
71
+ penalty = reg * (k / (K + 1))**2
72
+
73
+ # Add regularization on the diagonal
74
+ psi[k] = moment / (N_k + penalty)
75
+
76
+ return psi
77
+
78
+
79
+ # =============
80
+ # jacobi approx
81
+ # =============
82
+
83
+ def jacobi_approx(x, psi, support, alpha=0.0, beta=0.0):
84
+ """
85
+ Reconstruct Jacobi approximation.
86
+
87
+ Parameters
88
+ ----------
89
+
90
+ psi : array_like, shape (K+1, )
91
+ Jacobi expansion coefficients.
92
+
93
+ x : array_like
94
+ Points (in original eigenvalue scale) to evaluate at.
95
+
96
+ support : tuple (lam_m, lam_p)
97
+
98
+ alpha : float
99
+ Jacobi parameter.
100
+
101
+ beta : float
102
+ Jacobi parameter.
103
+
104
+ Returns
105
+ -------
106
+
107
+ rho : ndarray
108
+ """
109
+
110
+ lam_m, lam_p = support
111
+ t = (2 * x - (lam_p + lam_m)) / (lam_p - lam_m)
112
+ w = (1 - t)**alpha * (1 + t)**beta
113
+ P = numpy.vstack([eval_jacobi(k, alpha, beta, t) for k in range(len(psi))])
114
+
115
+ rho_t = w * (psi @ P) # density in t–variable
116
+ rho_x = rho_t * (2.0 / (lam_p - lam_m)) # back to x–variable
117
+
118
+ return rho_x
119
+
120
+
121
+ # ================
122
+ # jacobi stieltjes
123
+ # ================
124
+
125
+ def jacobi_stieltjes(z, psi, support, alpha=0.0, beta=0.0, n_base=40):
126
+ """
127
+ Compute m(z) = sum_k psi_k * m_k(z) where
128
+
129
+ m_k(z) = \\int w^{(alpha, beta)}(t) P_k^{(alpha, beta)}(t) / (u(z)-t) dt
130
+
131
+ Each m_k is evaluated *separately* with a Gauss–Jacobi rule sized
132
+ for that k. This follows the user's request: 1 quadrature rule per P_k.
133
+
134
+ Parameters
135
+ ----------
136
+
137
+ z : complex or ndarray
138
+
139
+ psi : (K+1,) array_like
140
+
141
+ support : (lambda_minus, lambda_plus)
142
+
143
+ alpha, beta : float
144
+
145
+ n_base : int
146
+ Minimum quadrature size. For degree-k polynomial we use
147
+ n_quad = max(n_base, k+1).
148
+
149
+ Returns
150
+ -------
151
+
152
+ m1 : ndarray (same shape as z)
153
+
154
+ m12 : ndarray (same shape as z)
155
+ """
156
+
157
+ z = numpy.asarray(z, dtype=numpy.complex128)
158
+ lam_minus, lam_plus = support
159
+ span = lam_plus - lam_minus
160
+ centre = 0.5 * (lam_plus + lam_minus)
161
+ u_z = (2.0 / span) * (z - centre) # map z -> u
162
+
163
+ m_total = numpy.zeros_like(z, dtype=numpy.complex128)
164
+
165
+ for k, psi_k in enumerate(psi):
166
+ # Select quadrature size tailored to this P_k
167
+ n_quad = max(n_base, k + 1)
168
+ t_nodes, w_nodes = roots_jacobi(n_quad, alpha, beta) # (n_quad,)
169
+
170
+ # Evaluate P_k at the quadrature nodes
171
+ P_k_nodes = eval_jacobi(k, alpha, beta, t_nodes) # (n_quad,)
172
+
173
+ # Integrand values at nodes: w_nodes already include the weight
174
+ integrand = w_nodes * P_k_nodes # (n_quad,)
175
+
176
+ # Broadcast over z: shape (n_quad, ...) / ...
177
+ # diff = u_z[None, ...] - t_nodes[:, None] # (n_quad, ...)
178
+ diff = u_z[None, ...] - t_nodes[:, None, None] # (n_quad, Ny, Nx)
179
+ # m_k = (integrand[:, None] / diff).sum(axis=0) # shape like z
180
+ m_k = (integrand[:, None, None] / diff).sum(axis=0)
181
+
182
+ # Accumulate with factor 2/span
183
+ m_total += psi_k * (2.0 / span) * m_k
184
+
185
+ # We use a negative sign convention
186
+ m_total = -m_total
187
+
188
+ return m_total
freealg/_pade.py ADDED
@@ -0,0 +1,139 @@
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 itertools import product
16
+ from scipy.optimize import least_squares, differential_evolution
17
+
18
+ __all__ = ['fit_pade', 'eval_pade']
19
+
20
+
21
+ # ========
22
+ # fit pade
23
+ # ========
24
+
25
+ def fit_pade(x, f, lam_m, lam_p, p, q, delta=1e-8, B=numpy.inf, S=numpy.inf,
26
+ B_default=10.0, S_factor=2.0, maxiter_de=200):
27
+ """
28
+ Fit a [p/q] rational P/Q of the form:
29
+ P(x) = s * prod_{i=0..p-1}(x - a_i)
30
+ Q(x) = prod_{j=0..q-1}(x - b_j)
31
+
32
+ Constraints:
33
+ a_i ∈ [lam_m, lam_p]
34
+ b_j ∈ (-infty, lam_m - delta] cup [lam_p + delta, infty)
35
+
36
+ Approach:
37
+ - Brute‐force all 2^q left/right assignments for denominator roots
38
+ - Global search with differential_evolution, fallback to zeros if needed
39
+ - Local refinement with least_squares
40
+
41
+ Returns a dict with keys:
42
+ 's' : optimal scale factor
43
+ 'a' : array of p numerator roots (in [lam_m, lam_p])
44
+ 'b' : array of q denominator roots (outside the interval)
45
+ 'resid' : final residual norm
46
+ 'signs' : tuple indicating left/right pattern for each b_j
47
+ """
48
+
49
+ # Determine finite bounds for DE
50
+ if not numpy.isfinite(B):
51
+ B_eff = B_default
52
+ else:
53
+ B_eff = B
54
+ if not numpy.isfinite(S):
55
+ # scale bound: S_factor * max|f| * interval width + safety
56
+ S_eff = S_factor * numpy.max(numpy.abs(f)) * (lam_p - lam_m) + 1.0
57
+ if S_eff <= 0:
58
+ S_eff = 1.0
59
+ else:
60
+ S_eff = S
61
+
62
+ def map_roots(signs, b):
63
+ """Map unconstrained b_j -> real root outside the interval."""
64
+ out = numpy.empty_like(b)
65
+ for j, (s_val, bj) in enumerate(zip(signs, b)):
66
+ if s_val > 0:
67
+ out[j] = lam_p + delta + numpy.exp(bj)
68
+ else:
69
+ out[j] = lam_m - delta - numpy.exp(bj)
70
+ return out
71
+
72
+ best = {'resid': numpy.inf}
73
+
74
+ # Enumerate all left/right sign patterns
75
+ for signs in product([-1, 1], repeat=q):
76
+ # Residual vector for current pattern
77
+ def resid_vec(z):
78
+ s_val = z[0]
79
+ a = z[1:1+p]
80
+ b = z[1+p:]
81
+ P = s_val * numpy.prod(x[:, None] - a[None, :], axis=1)
82
+ roots_Q = map_roots(signs, b)
83
+ Q = numpy.prod(x[:, None] - roots_Q[None, :], axis=1)
84
+ return P - f * Q
85
+
86
+ def obj(z):
87
+ r = resid_vec(z)
88
+ return r.dot(r)
89
+
90
+ # Build bounds for DE
91
+ bounds = []
92
+ bounds.append((-S_eff, S_eff)) # s
93
+ bounds += [(lam_m, lam_p)] * p # a_i
94
+ bounds += [(-B_eff, B_eff)] * q # b_j
95
+
96
+ # 1) Global search
97
+ try:
98
+ de = differential_evolution(obj, bounds,
99
+ maxiter=maxiter_de,
100
+ polish=False)
101
+ z0 = de.x
102
+ except ValueError:
103
+ # fallback: start at zeros
104
+ z0 = numpy.zeros(1 + p + q)
105
+
106
+ # 2) Local refinement
107
+ ls = least_squares(resid_vec, z0, xtol=1e-12, ftol=1e-12)
108
+
109
+ rnorm = numpy.linalg.norm(resid_vec(ls.x))
110
+ if rnorm < best['resid']:
111
+ best.update(resid=rnorm, signs=signs, x=ls.x.copy())
112
+
113
+ # Unpack best solution
114
+ z_best = best['x']
115
+ s_opt = z_best[0]
116
+ a_opt = z_best[1:1+p]
117
+ b_opt = map_roots(best['signs'], z_best[1+p:])
118
+
119
+ return {
120
+ 's': s_opt,
121
+ 'a': a_opt,
122
+ 'b': b_opt,
123
+ 'resid': best['resid'],
124
+ 'signs': best['signs'],
125
+ }
126
+
127
+
128
+ # =========
129
+ # eval pade
130
+ # =========
131
+
132
+ def eval_pade(z, s, a, b):
133
+ """
134
+ """
135
+
136
+ Pz = s * numpy.prod([z - aj for aj in a], axis=0)
137
+ Qz = numpy.prod([z - bj for bj in b], axis=0)
138
+
139
+ return Pz / Qz