ppapp 1.0.0__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.
- ppapp/__init__.py +58 -0
- ppapp/__main__.py +6 -0
- ppapp/cheb_coeffs.py +231 -0
- ppapp/demo_functions/__init__.py +20 -0
- ppapp/demo_functions/erfcx.py +47 -0
- ppapp/demo_functions/imwofx.py +47 -0
- ppapp/demo_functions/polynomial.py +51 -0
- ppapp/demo_functions/voigt_hwhm.py +139 -0
- ppapp/docs/userManual.pdf +0 -0
- ppapp/ppapp.py +704 -0
- ppapp/target_algorithm.py +90 -0
- ppapp-1.0.0.dist-info/METADATA +171 -0
- ppapp-1.0.0.dist-info/RECORD +21 -0
- ppapp-1.0.0.dist-info/WHEEL +5 -0
- ppapp-1.0.0.dist-info/entry_points.txt +2 -0
- ppapp-1.0.0.dist-info/licenses/LICENSE +674 -0
- ppapp-1.0.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_app.py +374 -0
- userManual/userManual.pdf +0 -0
- userManual/userManual.tex +368 -0
ppapp/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ppapp - Piecewise Polynomial Approximation
|
|
3
|
+
|
|
4
|
+
Code generator for piecewise Chebyshev approximation of mathematical functions.
|
|
5
|
+
|
|
6
|
+
Reference: Joachim Wuttke and Alexander Kleinsorge,
|
|
7
|
+
"Algorithm 1XXX: Code generation for piecewise Chebyshev approximation"
|
|
8
|
+
|
|
9
|
+
License: GNU General Public License, version 3 or higher
|
|
10
|
+
Copyright: Forschungszentrum Jülich GmbH 2025
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ppapp.ppapp import (
|
|
14
|
+
Subdomain,
|
|
15
|
+
analyse_row,
|
|
16
|
+
compute_subdomains,
|
|
17
|
+
error_bound,
|
|
18
|
+
hexfloat,
|
|
19
|
+
n_output_coeffs,
|
|
20
|
+
)
|
|
21
|
+
from ppapp.cheb_coeffs import (
|
|
22
|
+
cheb_cn,
|
|
23
|
+
cheb_pm,
|
|
24
|
+
check_coeffs,
|
|
25
|
+
ref_f,
|
|
26
|
+
set_reference_function,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_docs_path() -> str:
|
|
33
|
+
"""Return path to the documentation directory."""
|
|
34
|
+
from importlib.resources import files
|
|
35
|
+
return str(files("ppapp") / "docs")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_user_manual_path() -> str:
|
|
39
|
+
"""Return path to the user manual PDF."""
|
|
40
|
+
from importlib.resources import files
|
|
41
|
+
return str(files("ppapp") / "docs" / "userManual.pdf")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"cheb_cn",
|
|
46
|
+
"cheb_pm",
|
|
47
|
+
"ref_f",
|
|
48
|
+
"check_coeffs",
|
|
49
|
+
"set_reference_function",
|
|
50
|
+
"Subdomain",
|
|
51
|
+
"analyse_row",
|
|
52
|
+
"error_bound",
|
|
53
|
+
"compute_subdomains",
|
|
54
|
+
"hexfloat",
|
|
55
|
+
"n_output_coeffs",
|
|
56
|
+
"get_docs_path",
|
|
57
|
+
"get_user_manual_path",
|
|
58
|
+
]
|
ppapp/__main__.py
ADDED
ppapp/cheb_coeffs.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code generating code for piecewise Chebyshev approximation
|
|
3
|
+
|
|
4
|
+
Reference: Joachim Wuttke and Alexander Kleinsorge,
|
|
5
|
+
Algorithm 1XXX: Code generation for piecewise Chebyshev approximation
|
|
6
|
+
|
|
7
|
+
File: ppapp/cheb_coeffs.py
|
|
8
|
+
|
|
9
|
+
License: GNU General Public License, version 3 or higher (see LICENSE)
|
|
10
|
+
Copyright: Forschungszentrum Jülich GmbH 2025
|
|
11
|
+
Author: Joachim Wuttke <j.wuttke@fz-juelich.de>
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from math import isfinite
|
|
15
|
+
from typing import Callable, Optional
|
|
16
|
+
|
|
17
|
+
from flint import arb, ctx
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def str_arb(A: arb) -> str:
|
|
21
|
+
"""Convert arbitrary precision number to string representation."""
|
|
22
|
+
a: float = float(A)
|
|
23
|
+
return f"{a:10.5e}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sizeT(N: int) -> int:
|
|
27
|
+
"""Returns total number of coefficients T_nm with n<=N.
|
|
28
|
+
For N = -2,-1,0,1,..., result is 0,0,1,2,4,6,9,12,16,20,... (OEIS A002620)."""
|
|
29
|
+
assert N >= -2
|
|
30
|
+
return ((N + 2) // 2) * ((N + 1) // 2 + 1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def precompute_Tnm(N: int) -> list[int]:
|
|
34
|
+
"""Returns flat list of nonzero coefficients T_nm with n<=N.
|
|
35
|
+
For N = 0,1,..., result is 1,1,-1,2,-3,4,1,-8,8,5,-20,16,-1,18,-48,32,... (OEIS A008310)."""
|
|
36
|
+
assert N >= 0
|
|
37
|
+
if N == 0:
|
|
38
|
+
return [1]
|
|
39
|
+
if N == 1:
|
|
40
|
+
return [1, 1]
|
|
41
|
+
# The first two coefficients:
|
|
42
|
+
# R.append(1)
|
|
43
|
+
# if N >= 1: R.append(1)
|
|
44
|
+
R = [1, 1]
|
|
45
|
+
# Remaining coefficients by recursion:
|
|
46
|
+
for n in range(2, N + 1):
|
|
47
|
+
if n % 2 == 0:
|
|
48
|
+
R.append(-R[sizeT(n - 3)])
|
|
49
|
+
for j in range((n + 1) & 1, n // 2):
|
|
50
|
+
R.append(2 * R[sizeT(n - 2) + j - ((n + 1) & 1)] - R[sizeT(n - 3) + j])
|
|
51
|
+
R.append(2 * R[sizeT(n - 2) + (n - 1) // 2])
|
|
52
|
+
assert len(R) == sizeT(N)
|
|
53
|
+
assert R[-1] == (1 << (0 if N == 0 else N - 1)) # T_nn is 2^{n-1}; will fail if there is overflow
|
|
54
|
+
return R
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cheb_cn_arb(a: float, b: float, N: int, prec: int) -> list[arb]:
|
|
58
|
+
"""Returns the Chebyshev coefficients c_n as vector of arbitrary-precision numbers."""
|
|
59
|
+
assert my_arb_f is not None, "Reference function not set; call set_reference_function first"
|
|
60
|
+
|
|
61
|
+
# Set global precision
|
|
62
|
+
ctx.prec = prec
|
|
63
|
+
|
|
64
|
+
# Compute the c_n
|
|
65
|
+
M = arb((a + b) * 0.5) # midpoint
|
|
66
|
+
H = arb((b - a) * 0.5) # halfrange
|
|
67
|
+
|
|
68
|
+
assert N > 0 # otherwise division by 0; no obvious way to salvage the formalism
|
|
69
|
+
N_inv: float = 1.0 / N
|
|
70
|
+
|
|
71
|
+
CC = []
|
|
72
|
+
for n in range(N + 1):
|
|
73
|
+
sn = 1 if n == 0 else 2
|
|
74
|
+
C = arb(0)
|
|
75
|
+
for k in range(N + 1): # increment c_n = sum_{k=0}^N sk f(xk) Tn((xk-a)/(b-a))
|
|
76
|
+
sk = 1 if (k == 0 or k == N) else 2
|
|
77
|
+
E = arb(k) * arb.pi() * N_inv # E = k*pi/N
|
|
78
|
+
E = E.cos() # E = xk = cos(k*pi/N) = Chebyshev extremal point
|
|
79
|
+
T = E.chebyshev_t(n) # T = T_n(ek)
|
|
80
|
+
E = E * H
|
|
81
|
+
E = E + M # E = midpoint + halfrange*xk
|
|
82
|
+
F = my_arb_f(E, prec) # F = f(xk), here we evaluate the reference function
|
|
83
|
+
F = F * T # F = f(xk) * T_n(ek)
|
|
84
|
+
C = C + sk * F # increment C by sigma_k * f(xk) * T_n(ek)
|
|
85
|
+
|
|
86
|
+
E = arb(sn * 0.5)
|
|
87
|
+
E = E * N_inv # E = sigma_n/(2N)
|
|
88
|
+
C = C * E
|
|
89
|
+
CC.append(C)
|
|
90
|
+
|
|
91
|
+
return CC
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Module-level cache for precomputed T_nm coefficients
|
|
95
|
+
_cached_Tnm: list[int] = []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cheb_pm_prec(a: float, b: float, N: int, prec: int) -> list[float]:
|
|
99
|
+
"""Returns monomial coefficents p_m for m=0,..,N, computed with precision prec."""
|
|
100
|
+
global _cached_Tnm
|
|
101
|
+
|
|
102
|
+
# Recompute T_nm if needed for larger N
|
|
103
|
+
if len(_cached_Tnm) < sizeT(N):
|
|
104
|
+
_cached_Tnm = precompute_Tnm(N)
|
|
105
|
+
|
|
106
|
+
Tnm = _cached_Tnm
|
|
107
|
+
|
|
108
|
+
CC = cheb_cn_arb(a, b, N, prec)
|
|
109
|
+
|
|
110
|
+
# Compute the p_m
|
|
111
|
+
# ctx.prec is already set by cheb_cn_arb
|
|
112
|
+
result: list[float] = []
|
|
113
|
+
p0: Optional[float] = None # Store first coefficient for comparison
|
|
114
|
+
eps = 2.0 ** -53 # Machine epsilon for double precision
|
|
115
|
+
for m in range(N + 1): # compute p_m
|
|
116
|
+
P = arb(0)
|
|
117
|
+
for n in range(m, N + 1, 2): # increment p_m = sum_{n=0}^m c_n T_{nm}
|
|
118
|
+
t = Tnm[sizeT(n - 1) + (m - n % 2) // 2]
|
|
119
|
+
P = P + t * CC[n] # increment p_m by t*C
|
|
120
|
+
|
|
121
|
+
p: float = float(P.mid())
|
|
122
|
+
if not isfinite(p):
|
|
123
|
+
raise RuntimeError("Invalid coefficient p_m, not a number or infinite")
|
|
124
|
+
dp = float(P.rad())
|
|
125
|
+
if m == 0:
|
|
126
|
+
if p == 0: # p_0 must be nonzero
|
|
127
|
+
raise RuntimeError("Unexpected coefficient value 0 for p_0")
|
|
128
|
+
p0 = abs(p)
|
|
129
|
+
# Accept coefficient if either:
|
|
130
|
+
# 1. Relative precision is good (dp/|p| <= 1e-18), or
|
|
131
|
+
# 2. Coefficient is negligible (|p| <= |p_0| * eps, i.e., below machine precision)
|
|
132
|
+
assert p0 is not None # p0 is set on first iteration (m == 0)
|
|
133
|
+
if abs(dp / p) > 1e-18 if p != 0 else True:
|
|
134
|
+
if abs(p) > p0 * eps: # Significant coefficient with poor precision
|
|
135
|
+
result = []
|
|
136
|
+
break
|
|
137
|
+
result.append(p)
|
|
138
|
+
# import sys
|
|
139
|
+
# print(f"p_{m}={p}", file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def ref_f(x: float) -> float:
|
|
145
|
+
"""Returns f(x), computed to relative precision 1e-18."""
|
|
146
|
+
assert my_arb_f is not None, "Reference function not set; call set_reference_function first"
|
|
147
|
+
|
|
148
|
+
for prec in range(77, 170, 9):
|
|
149
|
+
ctx.prec = prec
|
|
150
|
+
X = arb(x)
|
|
151
|
+
F = my_arb_f(X, prec)
|
|
152
|
+
y = float(F.mid())
|
|
153
|
+
dy = float(F.rad())
|
|
154
|
+
if y == 0:
|
|
155
|
+
raise RuntimeError("Unexpected function value 0")
|
|
156
|
+
if abs(dy / y) < 1e-18:
|
|
157
|
+
return y
|
|
158
|
+
|
|
159
|
+
raise RuntimeError("Reference function did not achieve accuracy 1e-18")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def check_coeffs(a: float, b: float, P: list[float], tin: float, relerr: float) -> None:
|
|
163
|
+
"""Checks whether economized coefficients reproduce reference function within a few epsilon."""
|
|
164
|
+
from sys import stderr
|
|
165
|
+
x: float = (b + a) * 0.5 + (b - a) * 0.5 * tin
|
|
166
|
+
t: float = (x - (a + b) * 0.5) / ((b - a) * 0.5) # convert back to suppress irrelevant conversion error from t->x
|
|
167
|
+
fnew = P[-1]
|
|
168
|
+
for c in reversed(P[:-1]):
|
|
169
|
+
fnew = fnew * t + c
|
|
170
|
+
|
|
171
|
+
fref = ref_f(x)
|
|
172
|
+
if abs((fnew - fref) / fref) > relerr * (2.0 ** -53): # epsilon = 2^-53
|
|
173
|
+
print(f"t={t} x={x} => relerr={abs((fnew - fref) / fref)}:", file=stderr)
|
|
174
|
+
f3 = 0.0
|
|
175
|
+
|
|
176
|
+
for m in range(len(P)):
|
|
177
|
+
tm = t ** m
|
|
178
|
+
f3 += P[m] * tm
|
|
179
|
+
print(f"m={m:2d} t^m={tm:8.2g} P[m]={P[m]:8.2g} term={tm * P[m]:8.2g} sum={f3:22.16e}", file=stderr)
|
|
180
|
+
|
|
181
|
+
print(f" hor={fnew:22.16e}", file=stderr)
|
|
182
|
+
print(f" ref={fref:22.16e}", file=stderr)
|
|
183
|
+
raise RuntimeError("Approximation not accurate enough")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def cheb_cn(a: float, b: float, N: int) -> list[float]:
|
|
187
|
+
"""Returns polynomial coefficents c_n for m=0,..,N,
|
|
188
|
+
such that the reference function f is approximated by sum_{n=0}^N c_n T_n((x-a)/(b-a)).
|
|
189
|
+
Full double-precision accuracy is ensured by running the arbitrary-precision computation
|
|
190
|
+
in function cheb_cn_arb with different precisions."""
|
|
191
|
+
# Compute with increasing precision until results agree.
|
|
192
|
+
for prec in range(72, 400, 32):
|
|
193
|
+
CC = cheb_cn_arb(a, b, N, prec)
|
|
194
|
+
result: list[float] = []
|
|
195
|
+
for C in CC:
|
|
196
|
+
c = float(C.mid())
|
|
197
|
+
if not isfinite(c):
|
|
198
|
+
raise RuntimeError("Invalid coefficient c_n, not a number or infinite")
|
|
199
|
+
dc = float(C.rad())
|
|
200
|
+
if abs(dc / c) > 1e-18:
|
|
201
|
+
result = []
|
|
202
|
+
break
|
|
203
|
+
result.append(c)
|
|
204
|
+
if result:
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
raise RuntimeError("Loop in precision did not converge")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cheb_pm(a: float, b: float, N: int) -> list[float]:
|
|
211
|
+
"""Returns polynomial coefficents p_m for m=0,..,N,
|
|
212
|
+
such that the reference function f is approximated by sum_{m=0}^N p_m ((x-a)/(b-a))^m.
|
|
213
|
+
Full double-precision accuracy is ensured by running the arbitrary-precision computation
|
|
214
|
+
in function cheb_coeffs_prec with different precisions."""
|
|
215
|
+
# Compute with increasing precision until results agree.
|
|
216
|
+
for prec in range(72, 800, 32):
|
|
217
|
+
result = cheb_pm_prec(a, b, N, prec)
|
|
218
|
+
if result:
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
raise RuntimeError("Loop in precision did not converge")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# The reference function my_arb_f will be set dynamically by the calling code
|
|
225
|
+
my_arb_f: Optional[Callable[[arb, int], arb]] = None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def set_reference_function(arb_f_func: Callable[[arb, int], arb]) -> None:
|
|
229
|
+
"""Set the reference function to use for coefficient computation."""
|
|
230
|
+
global my_arb_f
|
|
231
|
+
my_arb_f = arb_f_func
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Demo functions for ppapp.
|
|
3
|
+
|
|
4
|
+
These modules demonstrate how to define target functions for the
|
|
5
|
+
piecewise Chebyshev approximation generator. Each module defines:
|
|
6
|
+
|
|
7
|
+
- my_arb_f(X: arb, prec: int) -> arb: The function to approximate
|
|
8
|
+
- my_domain: Tuple[float, float]: The domain [a, b)
|
|
9
|
+
- my_testcases: List of (x, f_expected, tolerance) tuples
|
|
10
|
+
|
|
11
|
+
Available demo functions:
|
|
12
|
+
- imwofx: Im w(x) = exp(-x^2) * erfi(x)
|
|
13
|
+
- erfcx: erfcx(x) = exp(x^2) * erfc(x)
|
|
14
|
+
- polynomial: f(x) = x^3 - x^2 + x - 1
|
|
15
|
+
- voigt_hwhm: Voigt half-width at half-maximum
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from ppapp.demo_functions import erfcx, imwofx, polynomial, voigt_hwhm
|
|
19
|
+
|
|
20
|
+
__all__ = ["imwofx", "erfcx", "polynomial", "voigt_hwhm"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Code generating code for piecewise Chebyshev approximation
|
|
5
|
+
|
|
6
|
+
Reference: Joachim Wuttke and Alexander Kleinsorge,
|
|
7
|
+
Algorithm 1XXX: Code generation for piecewise Chebyshev approximation
|
|
8
|
+
|
|
9
|
+
File: ppapp/demo_functions/erfcx.py
|
|
10
|
+
|
|
11
|
+
Arbitrary-precision implementation of function erfcx(x).
|
|
12
|
+
|
|
13
|
+
License: GNU General Public License, version 3 or higher (see src/LICENSE)
|
|
14
|
+
Copyright: Forschungszentrum Jülich GmbH 2025
|
|
15
|
+
Author: Joachim Wuttke <j.wuttke@fz-juelich.de>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from flint import arb
|
|
20
|
+
|
|
21
|
+
# The total intermediate domain [a,b) to be covered by piecewise polynomial approximation:
|
|
22
|
+
my_domain: tuple[float, float] = (0.125, 12.0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def my_arb_f(X: arb, prec: int) -> arb:
|
|
26
|
+
"""
|
|
27
|
+
Evaluates the overflow-compensated complementary error function
|
|
28
|
+
f(x) = erfcx(x) = exp(x^2) * erfc(x),
|
|
29
|
+
using 'prec' binary digits.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
X: Input value as arbitrary precision number
|
|
33
|
+
prec: Precision in binary digits
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
F = f(X) with given precision
|
|
37
|
+
"""
|
|
38
|
+
return X.erfc() * (X ** 2).exp()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# A few test cases, just to secure against gross errors.
|
|
42
|
+
# Each entry is (x, f_expected(x), tolerance)
|
|
43
|
+
my_testcases: list[tuple[float, float, float]] = [
|
|
44
|
+
(0.2, 0.8090195, 1e-5),
|
|
45
|
+
(1.0, 0.4275836, 1e-5),
|
|
46
|
+
(12.0, 0.046854221, 1e-5),
|
|
47
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Code generating code for piecewise Chebyshev approximation
|
|
5
|
+
|
|
6
|
+
Reference: Joachim Wuttke and Alexander Kleinsorge,
|
|
7
|
+
Algorithm 1XXX: Code generation for piecewise Chebyshev approximation
|
|
8
|
+
|
|
9
|
+
File: ppapp/demo_functions/imwofx.py
|
|
10
|
+
|
|
11
|
+
Arbitrary-precision implementation of function Im w(x).
|
|
12
|
+
|
|
13
|
+
License: GNU General Public License, version 3 or higher (see src/LICENSE)
|
|
14
|
+
Copyright: Forschungszentrum Jülich GmbH 2025
|
|
15
|
+
Author: Joachim Wuttke <j.wuttke@fz-juelich.de>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from flint import arb
|
|
20
|
+
|
|
21
|
+
# The total intermediate domain [a,b) to be covered by piecewise polynomial approximation:
|
|
22
|
+
my_domain: tuple[float, float] = (0.5, 12.0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def my_arb_f(X: arb, prec: int) -> arb:
|
|
26
|
+
"""
|
|
27
|
+
Evaluates the overflow-compensated imaginary error function
|
|
28
|
+
f(x) = exp(-x^2) * erfi(x) = Im w(x),
|
|
29
|
+
using 'prec' binary digits.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
X: Input value as arbitrary precision number
|
|
33
|
+
prec: Precision in binary digits
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
F = f(X) with given precision
|
|
37
|
+
"""
|
|
38
|
+
return X.erfi() * (-(X ** 2)).exp()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# A few test cases, just to secure against gross errors.
|
|
42
|
+
# Each entry is (x, f_expected(x), tolerance)
|
|
43
|
+
my_testcases: list[tuple[float, float, float]] = [
|
|
44
|
+
(0.5, 0.478925, 1e-5),
|
|
45
|
+
(1.0, 0.607158, 1e-5),
|
|
46
|
+
(12.0, 0.0471808, 1e-5),
|
|
47
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Code generating code for piecewise Chebyshev approximation
|
|
5
|
+
|
|
6
|
+
Reference: Joachim Wuttke and Alexander Kleinsorge,
|
|
7
|
+
Algorithm 1XXX: Code generation for piecewise Chebyshev approximation
|
|
8
|
+
|
|
9
|
+
File: ppapp/demo_functions/polynomial.py
|
|
10
|
+
|
|
11
|
+
Arbitrary-precision implementation of polynomial f(x) = x^3 - x^2 + x - 1.
|
|
12
|
+
|
|
13
|
+
License: GNU General Public License, version 3 or higher (see src/LICENSE)
|
|
14
|
+
Copyright: Forschungszentrum Jülich GmbH 2025
|
|
15
|
+
Author: Joachim Wuttke <j.wuttke@fz-juelich.de>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from flint import arb
|
|
20
|
+
|
|
21
|
+
# The total intermediate domain [a,b) to be covered by piecewise polynomial approximation:
|
|
22
|
+
# Avoid x=1 where f(x)=0 (would cause division by zero in relative error checks)
|
|
23
|
+
my_domain: tuple[float, float] = (1.5, 4.0)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def my_arb_f(X: arb, prec: int) -> arb:
|
|
27
|
+
"""
|
|
28
|
+
Evaluates the polynomial
|
|
29
|
+
f(x) = x^3 - x^2 + x - 1 = (x-1)(x^2+1),
|
|
30
|
+
using 'prec' binary digits.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
X: Input value as arbitrary precision number
|
|
34
|
+
prec: Precision in binary digits
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
F = f(X) with given precision
|
|
38
|
+
"""
|
|
39
|
+
# Compute x^3 - x^2 + x - 1
|
|
40
|
+
X2 = X ** 2
|
|
41
|
+
X3 = X2 * X
|
|
42
|
+
return X3 - X2 + X - 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# A few test cases, just to secure against gross errors.
|
|
46
|
+
# Each entry is (x, f_expected(x), tolerance)
|
|
47
|
+
my_testcases: list[tuple[float, float, float]] = [
|
|
48
|
+
(1.5, 1.625, 1e-10), # 3.375 - 2.25 + 1.5 - 1 = 1.625
|
|
49
|
+
(2.0, 5.0, 1e-10), # 8 - 4 + 2 - 1 = 5
|
|
50
|
+
(3.0, 20.0, 1e-10), # 27 - 9 + 3 - 1 = 20
|
|
51
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Code generating code for piecewise Chebyshev approximation
|
|
5
|
+
|
|
6
|
+
Reference: Joachim Wuttke and Alexander Kleinsorge,
|
|
7
|
+
Algorithm 1XXX: Code generation for piecewise Chebyshev approximation
|
|
8
|
+
|
|
9
|
+
File: ppapp/demo_functions/voigt_hwhm.py
|
|
10
|
+
|
|
11
|
+
Arbitrary-precision implementation of the half width at half maximum of the Voigt function.
|
|
12
|
+
|
|
13
|
+
License: GNU General Public License, version 3 or higher (see src/LICENSE)
|
|
14
|
+
Copyright: Forschungszentrum Jülich GmbH 2025
|
|
15
|
+
Authors: Eva Mukherjee, Joachim Wuttke <j.wuttke@fz-juelich.de>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from flint import acb, arb, ctx
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def acb_faddeeva_w(z: acb, prec: int) -> acb:
|
|
24
|
+
"""
|
|
25
|
+
Computes the Faddeeva function w(z).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
z: Complex input as arbitrary precision complex number
|
|
29
|
+
prec: Precision in binary digits
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
w(z) = exp(-z^2) * erfc(-i*z)
|
|
33
|
+
"""
|
|
34
|
+
ctx.prec = prec
|
|
35
|
+
|
|
36
|
+
minus_iz = acb(z.imag, -z.real)
|
|
37
|
+
return (-(z * z)).exp() * minus_iz.erfc()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def my_arb_voigt(q: arb, sigma: arb, gamma: arb, prec: int) -> arb:
|
|
41
|
+
"""
|
|
42
|
+
Computes the Voigt function V(q; sigma, gamma).
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
q: Position parameter
|
|
46
|
+
sigma: Gaussian width parameter
|
|
47
|
+
gamma: Lorentzian width parameter
|
|
48
|
+
prec: Precision in binary digits
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
V(q; sigma, gamma)
|
|
52
|
+
"""
|
|
53
|
+
ctx.prec = prec
|
|
54
|
+
|
|
55
|
+
# Calculating the complex argument z = q / (sqrt(2) * sigma) + i * gamma / (sqrt(2) * sigma)
|
|
56
|
+
T2 = arb(0.5).sqrt() / sigma
|
|
57
|
+
z_complex_arg = acb(q * T2, gamma * T2) # (q+i*gamma) / (sqrt(2)*sigma)
|
|
58
|
+
|
|
59
|
+
# Faddeeva function
|
|
60
|
+
w_z_result = acb_faddeeva_w(z_complex_arg, prec)
|
|
61
|
+
T1 = w_z_result.real
|
|
62
|
+
|
|
63
|
+
# Calculating 1 / (sqrt(2*pi)*sigma)
|
|
64
|
+
T2 = T2 / arb.pi().sqrt()
|
|
65
|
+
|
|
66
|
+
# Computing the Voigt Function
|
|
67
|
+
return T2 * T1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def my_arb_f(X: arb, prec: int) -> arb:
|
|
71
|
+
"""
|
|
72
|
+
Computes the half width at half maximum (HWHM) of the Voigt function
|
|
73
|
+
as a function of the ratio sigma/gamma.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
X: Input value representing sigma/gamma ratio
|
|
77
|
+
prec: Precision in binary digits
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
HWHM for the given sigma/gamma ratio
|
|
81
|
+
"""
|
|
82
|
+
ctx.prec = prec
|
|
83
|
+
|
|
84
|
+
# The input 'X' represents the ratio sigma/gamma.
|
|
85
|
+
gamma = arb(1.0)
|
|
86
|
+
sigma = X * gamma # sigma = (sigma/gamma) * gamma = X * 1
|
|
87
|
+
|
|
88
|
+
# Calculating V(0; sigma, gamma)
|
|
89
|
+
V_zero = my_arb_voigt(arb(0.0), sigma, gamma, prec)
|
|
90
|
+
|
|
91
|
+
# Calculating the target value: 0.5 * V(0; sigma, gamma)
|
|
92
|
+
target_V = V_zero / 2
|
|
93
|
+
|
|
94
|
+
# Bisection Method
|
|
95
|
+
# Finding the root of the function f(q) = V(q; sigma, gamma) - target value
|
|
96
|
+
a = arb(0.0)
|
|
97
|
+
b = arb(1.0)
|
|
98
|
+
|
|
99
|
+
# Finding b such that V(b) < target_V
|
|
100
|
+
for _ in range(10):
|
|
101
|
+
val_c = my_arb_voigt(b, sigma, gamma, prec)
|
|
102
|
+
if val_c < target_V:
|
|
103
|
+
break
|
|
104
|
+
b *= 2
|
|
105
|
+
|
|
106
|
+
# Bisection loop
|
|
107
|
+
max_iter = 2 * prec
|
|
108
|
+
tol = arb(2.0) ** (-prec)
|
|
109
|
+
|
|
110
|
+
for _ in range(max_iter):
|
|
111
|
+
diff = b - a
|
|
112
|
+
if diff <= tol:
|
|
113
|
+
break
|
|
114
|
+
c = (a + b) / 2
|
|
115
|
+
val_c = my_arb_voigt(c, sigma, gamma, prec)
|
|
116
|
+
if val_c > target_V:
|
|
117
|
+
a = c # V(c) too high => mid is too small
|
|
118
|
+
else:
|
|
119
|
+
b = c # V(c) too low => mid is too large
|
|
120
|
+
|
|
121
|
+
# Check convergence
|
|
122
|
+
diff = b - a
|
|
123
|
+
if diff > tol:
|
|
124
|
+
print(f"Warning: bisection may not have converged for X={float(X)}", file=sys.stderr)
|
|
125
|
+
|
|
126
|
+
# The result is the HWHM which is midpoint of [a, b]
|
|
127
|
+
return (a + b) / 2
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# The total domain [a,b) to be covered by piecewise polynomial approximation.
|
|
131
|
+
my_domain: tuple[float, float] = (0.5, 12.0)
|
|
132
|
+
|
|
133
|
+
# Test cases.
|
|
134
|
+
# Each entry is (x, f_expected(x), tolerance)
|
|
135
|
+
my_testcases: list[tuple[float, float, float]] = [
|
|
136
|
+
(1.0, 1.80057, 1e-5),
|
|
137
|
+
(2.0, 2.93434, 1e-5),
|
|
138
|
+
(0.5, 1.28521, 1e-5),
|
|
139
|
+
]
|
|
Binary file
|