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 +13 -0
- freealg/__version__.py +1 -0
- freealg/_chebyshev.py +201 -0
- freealg/_damp.py +88 -0
- freealg/_jacobi.py +188 -0
- freealg/_pade.py +139 -0
- freealg/_plot_util.py +499 -0
- freealg/_util.py +92 -0
- freealg/distributions/__init__.py +16 -0
- freealg/distributions/marchenko_pastur.py +512 -0
- freealg/freeform.py +648 -0
- freealg-0.0.1.dist-info/METADATA +145 -0
- freealg-0.0.1.dist-info/RECORD +16 -0
- freealg-0.0.1.dist-info/WHEEL +5 -0
- freealg-0.0.1.dist-info/licenses/LICENSE.txt +24 -0
- freealg-0.0.1.dist-info/top_level.txt +1 -0
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
|