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 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
@@ -0,0 +1,6 @@
1
+ """Entry point for running ppapp as a module: python -m ppapp"""
2
+
3
+ from ppapp.ppapp import main
4
+
5
+ if __name__ == "__main__":
6
+ exit(main())
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