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/ppapp.py
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
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/ppapp.py
|
|
10
|
+
|
|
11
|
+
Main program that prints polynomial coefficients in different formats.
|
|
12
|
+
These coefficients pertain to a piecewise Chebyshev approximation of a function f.
|
|
13
|
+
A high-precision reference implementation of function f must be provided in reference_f.py.
|
|
14
|
+
|
|
15
|
+
Usage of the generated file is demonstrated in directory src/dem.
|
|
16
|
+
|
|
17
|
+
License: GNU General Public License, version 3 or higher (see src/LICENSE)
|
|
18
|
+
Copyright: Forschungszentrum Jülich GmbH 2025
|
|
19
|
+
Author: Joachim Wuttke <j.wuttke@fz-juelich.de>
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from math import frexp, isfinite, log
|
|
25
|
+
from sys import argv, stderr
|
|
26
|
+
from time import ctime, localtime, strftime, time
|
|
27
|
+
|
|
28
|
+
# Import high-precision Chebyshev coefficient computation
|
|
29
|
+
try:
|
|
30
|
+
from ppapp.cheb_coeffs import cheb_cn, cheb_pm, check_coeffs, ref_f, set_reference_function
|
|
31
|
+
except ModuleNotFoundError:
|
|
32
|
+
print("Error: Cannot import ppapp.cheb_coeffs.", file=sys.stderr)
|
|
33
|
+
print("", file=sys.stderr)
|
|
34
|
+
print("Run from the directory containing the 'ppapp' package:", file=sys.stderr)
|
|
35
|
+
print(" cd /path/to/py/R", file=sys.stderr)
|
|
36
|
+
print(" python -m ppapp <mode> <args>", file=sys.stderr)
|
|
37
|
+
print("", file=sys.stderr)
|
|
38
|
+
print("Or install via pip and run from anywhere:", file=sys.stderr)
|
|
39
|
+
print(" ppapp <mode> <args>", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Subdomain class definition
|
|
44
|
+
class Subdomain:
|
|
45
|
+
"""
|
|
46
|
+
Represents a subdomain for piecewise approximation.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, j=0, i=0, a=0.0, b=0.0):
|
|
50
|
+
self.j = j # octave index
|
|
51
|
+
self.i = i # subdomain index within octave
|
|
52
|
+
self.a = a # lower bound
|
|
53
|
+
self.b = b # upper bound
|
|
54
|
+
self.coeffs = [] # polynomial coefficients
|
|
55
|
+
self.s = 0.0 # power law parameter
|
|
56
|
+
self.r = 0.0 # power law parameter
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Power law analysis
|
|
60
|
+
def analyse_row(coeffs: list[float]) -> tuple[float, float]:
|
|
61
|
+
"""
|
|
62
|
+
Returns parameters s,r of power law h(n)=s*r^n that approximates |coeffs[n]|
|
|
63
|
+
while |coeffs[n]| <= h(n).
|
|
64
|
+
|
|
65
|
+
Solution is by brute force, considering all power laws that touch a pair of data points.
|
|
66
|
+
|
|
67
|
+
This matches the C++ implementation in power_law.cpp.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
coeffs: List of polynomial coefficients
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Tuple (s, r) where s*r^n bounds the coefficient magnitudes
|
|
74
|
+
"""
|
|
75
|
+
assert len(coeffs) >= 2
|
|
76
|
+
|
|
77
|
+
# Take absolute values
|
|
78
|
+
y = [abs(v) for v in coeffs]
|
|
79
|
+
N = len(y) - 1
|
|
80
|
+
|
|
81
|
+
# Power-law model: h(n) = s * r^n
|
|
82
|
+
def make_model(y_vals, i, j):
|
|
83
|
+
"""Create power law model that passes through y[i] and y[j]."""
|
|
84
|
+
assert 0 <= i < j < len(y_vals)
|
|
85
|
+
assert y_vals[i] > 0
|
|
86
|
+
r = (y_vals[j] / y_vals[i]) ** (1.0 / (j - i))
|
|
87
|
+
s = y_vals[i] * (r ** -i)
|
|
88
|
+
return s, r
|
|
89
|
+
|
|
90
|
+
def model_f(s, r, i):
|
|
91
|
+
"""Evaluate model at index i."""
|
|
92
|
+
return s * (r ** i)
|
|
93
|
+
|
|
94
|
+
def least_squares(s, r, y_vals):
|
|
95
|
+
"""Compute sum of squared log deviations."""
|
|
96
|
+
result = 0.0
|
|
97
|
+
for k in range(len(y_vals)):
|
|
98
|
+
assert y_vals[k] > 0
|
|
99
|
+
result += log(y_vals[k] / model_f(s, r, k)) ** 2
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
# Initial solution: endpoints
|
|
103
|
+
solution_s, solution_r = make_model(y, 0, N)
|
|
104
|
+
squares = float('inf')
|
|
105
|
+
|
|
106
|
+
for i in range(N - 1):
|
|
107
|
+
for j in range(i + 1, N):
|
|
108
|
+
s, r = make_model(y, i, j)
|
|
109
|
+
|
|
110
|
+
# Check that this model bounds all data points
|
|
111
|
+
valid = True
|
|
112
|
+
for k in range(N):
|
|
113
|
+
if y[k] > model_f(s, r, k) * (1 + 1e-13):
|
|
114
|
+
valid = False
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
if not valid:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
sq = least_squares(s, r, y)
|
|
121
|
+
if sq < squares:
|
|
122
|
+
squares = sq
|
|
123
|
+
solution_s, solution_r = s, r
|
|
124
|
+
|
|
125
|
+
if not isfinite(squares):
|
|
126
|
+
for i in range(N - 1):
|
|
127
|
+
print(f"y[{i}] = {coeffs[i]}")
|
|
128
|
+
raise RuntimeError("fit failed: squares=infty")
|
|
129
|
+
|
|
130
|
+
return solution_s, solution_r
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Error estimation
|
|
134
|
+
def error_bound(d: Subdomain, N: int) -> float:
|
|
135
|
+
"""
|
|
136
|
+
Returns maximum relative error, computed from coefficients and power-law parameters provided by d.
|
|
137
|
+
Implements Eq (46) of Wuttke and Kleinsorge, "Code generation for piecewise Chebyshev approximation".
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
d: Subdomain with computed coefficients, s, and r values
|
|
141
|
+
N: Polynomial degree
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Error bound in units of machine epsilon
|
|
145
|
+
"""
|
|
146
|
+
assert len(d.coeffs) > N
|
|
147
|
+
|
|
148
|
+
p0 = d.coeffs[0]
|
|
149
|
+
denom = p0 - d.s * (d.r ** (N + 1)) / (1 - d.r)
|
|
150
|
+
|
|
151
|
+
re = 2 * p0
|
|
152
|
+
for n in range(1, N + 1):
|
|
153
|
+
denom -= abs(d.coeffs[n])
|
|
154
|
+
re += (3 + n) * abs(d.coeffs[n])
|
|
155
|
+
|
|
156
|
+
eps = 2.0 ** -53
|
|
157
|
+
te = d.s * (d.r ** (N + 1)) / (1 - d.r) / eps
|
|
158
|
+
|
|
159
|
+
assert denom > 0, f"denom={denom} must be positive"
|
|
160
|
+
return (re + te) / denom
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Subdomain computation
|
|
164
|
+
def compute_subdomains(a: float, b: float, M: int) -> tuple[int, int, list[Subdomain]]:
|
|
165
|
+
"""
|
|
166
|
+
Divide the interval [a, b] into 2^M subdomains per octave.
|
|
167
|
+
|
|
168
|
+
This matches the C++ implementation in subdomain.cpp.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
a: Lower bound of domain
|
|
172
|
+
b: Upper bound of domain
|
|
173
|
+
M: Number of subdivisions per octave (2^M subdomains per octave)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Tuple (j0, l0, D) where:
|
|
177
|
+
- j0 is the exponent for frexp(a) (first octave starts at 2^(j0-1))
|
|
178
|
+
- l0 is the starting subdomain index in first octave
|
|
179
|
+
- D is the list of Subdomain objects
|
|
180
|
+
"""
|
|
181
|
+
assert a < b, "Lower bound must be less than upper bound"
|
|
182
|
+
assert M >= 0, "M must be non-negative"
|
|
183
|
+
|
|
184
|
+
ni = 1 << M # 2^M subdomains per octave
|
|
185
|
+
|
|
186
|
+
# Use frexp to get exponents (like C++ implementation)
|
|
187
|
+
# frexp returns (mantissa, exponent) where a = mantissa * 2^exponent
|
|
188
|
+
# and mantissa is in [0.5, 1.0)
|
|
189
|
+
_, ea = frexp(a)
|
|
190
|
+
_, eb = frexp(b)
|
|
191
|
+
|
|
192
|
+
D = []
|
|
193
|
+
result_l0 = -1
|
|
194
|
+
|
|
195
|
+
for j in range(ea - 1, eb):
|
|
196
|
+
for i in range(ni):
|
|
197
|
+
asu = (ni + i) * (2.0 ** (j - M))
|
|
198
|
+
bsu = asu + (2.0 ** (j - M))
|
|
199
|
+
if bsu > a and asu < b:
|
|
200
|
+
# Store j relative to ea (like C++ does: j - ea + 1)
|
|
201
|
+
d = Subdomain(j - ea + 1, i, asu, bsu)
|
|
202
|
+
D.append(d)
|
|
203
|
+
if result_l0 == -1:
|
|
204
|
+
result_l0 = i
|
|
205
|
+
|
|
206
|
+
return ea, result_l0, D
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# Output functions
|
|
210
|
+
def hexfloat(x: float) -> str:
|
|
211
|
+
"""
|
|
212
|
+
Returns hexadecimal floating-point representation of x.
|
|
213
|
+
Result looks like -0x1.8....p-27.
|
|
214
|
+
This is in decimal notation -1.5.... * 2^(-27).
|
|
215
|
+
Note that binary exponent (following 'p') is rendered as a decimal number.
|
|
216
|
+
"""
|
|
217
|
+
if x >= 0:
|
|
218
|
+
return x.hex()
|
|
219
|
+
else:
|
|
220
|
+
# Python's hex() already handles negative numbers, but we want consistent format
|
|
221
|
+
return "-" + (-x).hex()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def n_output_coeffs(N: int) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Returns number of polynomial coefficients per subdomain that are written to the C tables.
|
|
227
|
+
For given polynomial degree N, this is normally N+1.
|
|
228
|
+
However, for alignment reasons as described in Sect 2.3 of the reference paper,
|
|
229
|
+
for certain N we fill up with zeroes.
|
|
230
|
+
"""
|
|
231
|
+
assert N >= 0
|
|
232
|
+
remainder = (N + 1) % 8
|
|
233
|
+
if remainder in (0, 1, 2, 4):
|
|
234
|
+
return N + 1
|
|
235
|
+
elif remainder == 3:
|
|
236
|
+
return N + 2 # fill up to 4
|
|
237
|
+
else:
|
|
238
|
+
return ((N + 1) // 8 + 1) * 8 # fill up to 8
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def print_table(start_time: float, output_c: bool, a: float, b: float, M: int, N: int,
|
|
242
|
+
j0: int, l0: int, D: list[Subdomain]) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Print plain table of coefficients.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
start_time: Time when program started
|
|
248
|
+
output_c: If True, print Chebyshev coefficients c_n, else power coefficients p_m
|
|
249
|
+
a: Lower domain bound
|
|
250
|
+
b: Upper domain bound
|
|
251
|
+
M: Subdivision parameter
|
|
252
|
+
N: Polynomial degree
|
|
253
|
+
j0: Starting octave index
|
|
254
|
+
l0: Number of octaves
|
|
255
|
+
D: List of subdomains
|
|
256
|
+
"""
|
|
257
|
+
# Print header comment
|
|
258
|
+
coeff_type = "c_n" if output_c else "p_m"
|
|
259
|
+
print(f"# Table of {'Chebyshev' if output_c else 'economized'} coefficients {coeff_type}")
|
|
260
|
+
print(f"# Generated: {ctime(start_time)}")
|
|
261
|
+
print(f"# Domain: [{a}, {b})")
|
|
262
|
+
print(f"# M={M}, N={N}, j0={j0}, l0={l0}")
|
|
263
|
+
print(f"# Number of subdomains: {len(D)}")
|
|
264
|
+
print("#")
|
|
265
|
+
print("# Format: j i a b coeff[0] coeff[1] ... coeff[N]")
|
|
266
|
+
print("#")
|
|
267
|
+
|
|
268
|
+
for d in D:
|
|
269
|
+
print(f"{d.j} {d.i} {d.a:.16e} {d.b:.16e}", end="")
|
|
270
|
+
for coeff in d.coeffs:
|
|
271
|
+
print(f" {coeff:.16e}", end="")
|
|
272
|
+
print()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def print_source(start_time: float, a: float, b: float, M: int, N: int,
|
|
276
|
+
j0: int, l0: int, D: list[Subdomain]) -> None:
|
|
277
|
+
"""
|
|
278
|
+
Print C source code defining economized coefficients.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
start_time: Time when program started
|
|
282
|
+
a: Lower domain bound
|
|
283
|
+
b: Upper domain bound
|
|
284
|
+
M: Subdivision parameter
|
|
285
|
+
N: Polynomial degree
|
|
286
|
+
j0: Starting octave index
|
|
287
|
+
l0: Number of octaves
|
|
288
|
+
D: List of subdomains
|
|
289
|
+
"""
|
|
290
|
+
# Derived parameters
|
|
291
|
+
Nout = n_output_coeffs(N)
|
|
292
|
+
assert Nout > N
|
|
293
|
+
nTables = (Nout - 1) // 8 + 1
|
|
294
|
+
assert nTables >= 1
|
|
295
|
+
nr = len(D)
|
|
296
|
+
|
|
297
|
+
# Write top lines to file
|
|
298
|
+
print("//--- Begin of auto-generated code; do not edit")
|
|
299
|
+
print("//")
|
|
300
|
+
print(f"// Generated on {strftime('%Y-%m-%d, %H:%M:%S', localtime(start_time))}")
|
|
301
|
+
print("// by the piecewise polynomial approximation generator (https://jugit.fz-juelich.de/mlz/ppapp)")
|
|
302
|
+
print("// Reference: Wuttke and Kleinsorge,")
|
|
303
|
+
print('// "Code generation for piecewise Chebyshev approximation."')
|
|
304
|
+
print("//")
|
|
305
|
+
print("// clang-format off")
|
|
306
|
+
# Format a and b to avoid unnecessary .0 suffix
|
|
307
|
+
a_str = str(int(a)) if a == int(a) else str(a)
|
|
308
|
+
b_str = str(int(b)) if b == int(b) else str(b)
|
|
309
|
+
print(f"static const double ppapp_a = {a_str}; // begin of domain")
|
|
310
|
+
print(f"static const double ppapp_b = {b_str}; // end of domain")
|
|
311
|
+
print(f"static const int ppapp_M = {M}; // 2^M subdomains per octave")
|
|
312
|
+
print(f"static const int ppapp_N = {N}; // polynomial degree")
|
|
313
|
+
print(f"static const int ppapp_nr = {nr}; // total number of subdomains")
|
|
314
|
+
print(f"static const int ppapp_j0 = {j0}; // first octave starts at 2^(j0−1)")
|
|
315
|
+
print(f"static const int ppapp_l0 = {l0}; // index of a in first octave")
|
|
316
|
+
print(f"static const int ppapp_Nout = {Nout}; // stored coeffs per subdomain")
|
|
317
|
+
print(f"static const int ppapp_nTables = {nTables};")
|
|
318
|
+
|
|
319
|
+
# Write coefficient tables to file
|
|
320
|
+
for k in range(nTables):
|
|
321
|
+
print()
|
|
322
|
+
print(f"alignas(64) static const double ppapp_Coeffs{k}[{nr} * {min(8, Nout - k*8)}] = {{")
|
|
323
|
+
for d in D:
|
|
324
|
+
print(" ", end="")
|
|
325
|
+
for n in range(k * 8, min(Nout, (k + 1) * 8)):
|
|
326
|
+
if N - n < 0:
|
|
327
|
+
print("0", end="")
|
|
328
|
+
else:
|
|
329
|
+
print(hexfloat(d.coeffs[N - n]), end="")
|
|
330
|
+
print(", ", end="")
|
|
331
|
+
print(f"// subdomain {d.j}:{d.i} ({d.a}..{d.b})")
|
|
332
|
+
print("};")
|
|
333
|
+
|
|
334
|
+
# Write bottom lines to file
|
|
335
|
+
print("// clang-format on")
|
|
336
|
+
print("//--- End of auto-generated code")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def print_testcases(start_time: float, a: float, b: float, M: int, Nxo: int,
|
|
340
|
+
D: list[Subdomain], relerr: float) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Print C source code defining test cases.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
start_time: Time when program started
|
|
346
|
+
a: Lower domain bound
|
|
347
|
+
b: Upper domain bound
|
|
348
|
+
M: Subdivision parameter
|
|
349
|
+
Nxo: Number of extra octaves
|
|
350
|
+
D: List of subdomains (extended domain)
|
|
351
|
+
relerr: Maximum relative error
|
|
352
|
+
"""
|
|
353
|
+
print("/*")
|
|
354
|
+
print(" * Test cases for Chebyshev approximation")
|
|
355
|
+
print(f" * Generated: {ctime(start_time)}")
|
|
356
|
+
print(f" * Domain: [{a}, {b})")
|
|
357
|
+
print(f" * M={M}, Nxo={Nxo}, relerr={relerr}")
|
|
358
|
+
print(f" * Extended domain includes {Nxo} extra octaves on each side")
|
|
359
|
+
print(" */")
|
|
360
|
+
print()
|
|
361
|
+
print("typedef struct {")
|
|
362
|
+
print(" double x;")
|
|
363
|
+
print(" double f_ref;")
|
|
364
|
+
print("} test_case_t;")
|
|
365
|
+
print()
|
|
366
|
+
|
|
367
|
+
# Generate test points
|
|
368
|
+
test_points = []
|
|
369
|
+
for d in D:
|
|
370
|
+
# Test at subdomain boundaries and midpoint
|
|
371
|
+
for frac in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
|
372
|
+
x = d.a + frac * (d.b - d.a)
|
|
373
|
+
try:
|
|
374
|
+
f_val = ref_f(x)
|
|
375
|
+
test_points.append((x, f_val))
|
|
376
|
+
except Exception:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
print("static const test_case_t test_cases[] = {")
|
|
380
|
+
for i, (x, f_val) in enumerate(test_points):
|
|
381
|
+
comma = "," if i < len(test_points) - 1 else ""
|
|
382
|
+
print(f" {{{x:.16e}, {f_val:.16e}}}{comma}")
|
|
383
|
+
print("};")
|
|
384
|
+
print()
|
|
385
|
+
print(f"#define NUM_TEST_CASES {len(test_points)}")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# Helper function for parallel processing
|
|
389
|
+
def compute_subdomain_coeffs_for_nmin(args):
|
|
390
|
+
"""Helper function for parallel computation of N_min."""
|
|
391
|
+
k, d_data, M, N, relerr, output_c = args
|
|
392
|
+
d = Subdomain(d_data['j'], d_data['i'], d_data['a'], d_data['b'])
|
|
393
|
+
|
|
394
|
+
for n in range(2, N + 1):
|
|
395
|
+
try:
|
|
396
|
+
d.coeffs = cheb_cn(d.a, d.b, n) if output_c else cheb_pm(d.a, d.b, n)
|
|
397
|
+
assert len(d.coeffs) == n + 1
|
|
398
|
+
|
|
399
|
+
d.s, d.r = analyse_row(d.coeffs)
|
|
400
|
+
r = error_bound(d, n)
|
|
401
|
+
|
|
402
|
+
if r <= relerr:
|
|
403
|
+
return (k, n)
|
|
404
|
+
except Exception as ex:
|
|
405
|
+
print(f"# M = {M}")
|
|
406
|
+
print(f"# n = {n}")
|
|
407
|
+
print(f"# domain = [{d.a}, {d.b})")
|
|
408
|
+
print(f"# {ex}")
|
|
409
|
+
|
|
410
|
+
return (k, 0)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def compute_subdomain_coeffs(args):
|
|
414
|
+
"""Helper function for parallel computation of coefficients."""
|
|
415
|
+
k, d_data, N, output_c = args
|
|
416
|
+
d = Subdomain(d_data['j'], d_data['i'], d_data['a'], d_data['b'])
|
|
417
|
+
|
|
418
|
+
d.coeffs = cheb_cn(d.a, d.b, N) if output_c else cheb_pm(d.a, d.b, N)
|
|
419
|
+
assert len(d.coeffs) == N + 1
|
|
420
|
+
|
|
421
|
+
d.s, d.r = analyse_row(d.coeffs)
|
|
422
|
+
err = error_bound(d, N)
|
|
423
|
+
|
|
424
|
+
return (k, {'j': d.j, 'i': d.i, 'a': d.a, 'b': d.b, 'coeffs': d.coeffs, 's': d.s, 'r': d.r}, err)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# Function module loading
|
|
428
|
+
def load_function_module(module_path):
|
|
429
|
+
"""
|
|
430
|
+
Dynamically load a function definition file.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
module_path: File path (e.g., 'f_imwofx.py' or 'path/to/f_custom.py')
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
The loaded module object
|
|
437
|
+
"""
|
|
438
|
+
import importlib.util
|
|
439
|
+
|
|
440
|
+
if not module_path.endswith('.py'):
|
|
441
|
+
module_path = module_path + '.py'
|
|
442
|
+
|
|
443
|
+
if not os.path.isabs(module_path):
|
|
444
|
+
module_path = os.path.abspath(module_path)
|
|
445
|
+
|
|
446
|
+
if not os.path.exists(module_path):
|
|
447
|
+
print(f"Error: Function definition file not found: {module_path}", file=stderr)
|
|
448
|
+
exit(1)
|
|
449
|
+
|
|
450
|
+
module_name = os.path.splitext(os.path.basename(module_path))[0]
|
|
451
|
+
|
|
452
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
453
|
+
if spec is None or spec.loader is None:
|
|
454
|
+
print(f"Error: Cannot load module from: {module_path}", file=stderr)
|
|
455
|
+
exit(1)
|
|
456
|
+
|
|
457
|
+
module = importlib.util.module_from_spec(spec)
|
|
458
|
+
spec.loader.exec_module(module)
|
|
459
|
+
|
|
460
|
+
# Validate that required attributes exist
|
|
461
|
+
if not hasattr(module, 'my_arb_f'):
|
|
462
|
+
print(f"Error: Module {module_spec} does not define 'my_arb_f'", file=stderr)
|
|
463
|
+
exit(1)
|
|
464
|
+
if not hasattr(module, 'my_domain'):
|
|
465
|
+
print(f"Error: Module {module_spec} does not define 'my_domain'", file=stderr)
|
|
466
|
+
exit(1)
|
|
467
|
+
if not hasattr(module, 'my_testcases'):
|
|
468
|
+
print(f"Error: Module {module_spec} does not define 'my_testcases'", file=stderr)
|
|
469
|
+
exit(1)
|
|
470
|
+
|
|
471
|
+
return module
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Help function
|
|
475
|
+
def help_and_exit():
|
|
476
|
+
"""Prints help and terminates program."""
|
|
477
|
+
print("Usage:\n")
|
|
478
|
+
print("ppapp i <f_module> - run initial tests from my_testcases")
|
|
479
|
+
print("ppapp v <f_module> <x> - print function value f(x)")
|
|
480
|
+
print("ppapp n <f_module> <M> <Nmax> <E> - print N_min(M',E), up to given Nmax")
|
|
481
|
+
print("ppapp e <f_module> <M> <N> - print maximum relative error, in units of epsilon")
|
|
482
|
+
print("ppapp c <f_module> <M> <N> [<E>] - print plain table of Chebyshev coefficients c_n")
|
|
483
|
+
print("ppapp p <f_module> <M> <N> [<E>] - print plain table of economized coefficients p_m")
|
|
484
|
+
print("ppapp s <f_module> <M> <N> [<E>] - print C source defining economized coefficients p_m")
|
|
485
|
+
print("ppapp t <f_module> <M> <Nxo> <E> - print C source defining test cases")
|
|
486
|
+
print("\nwhere\n")
|
|
487
|
+
print("<f_module> - path to function definition file (e.g., 'mydir/f_imwofx.py')")
|
|
488
|
+
print("<M> - integer M >= 0 specifies 2^M subdomains per octave")
|
|
489
|
+
print("<N> - integer N >= 1 is the polynomial degree")
|
|
490
|
+
print("<E> - double E > 0 is the maximum relative error, in units of epsilon=2^-53")
|
|
491
|
+
print("<Nxo> - number of extra (non-Chebyshev) octaves on each side of the Chebyshev range")
|
|
492
|
+
exit(1)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# Main function
|
|
496
|
+
def main():
|
|
497
|
+
"""
|
|
498
|
+
Computes and prints polynomial coefficients that approximate function ref_f.
|
|
499
|
+
"""
|
|
500
|
+
start_time = time() # needed for comment line in output
|
|
501
|
+
|
|
502
|
+
# Process command-line arguments
|
|
503
|
+
if len(argv) < 2:
|
|
504
|
+
print("No mode given\n", file=stderr)
|
|
505
|
+
help_and_exit()
|
|
506
|
+
|
|
507
|
+
mode = argv[1][0]
|
|
508
|
+
|
|
509
|
+
# Handle help mode first
|
|
510
|
+
if mode == 'h':
|
|
511
|
+
help_and_exit()
|
|
512
|
+
|
|
513
|
+
if len(argv) < 3:
|
|
514
|
+
print("No function module given\n", file=stderr)
|
|
515
|
+
help_and_exit()
|
|
516
|
+
|
|
517
|
+
output_c = False
|
|
518
|
+
allow_E = True
|
|
519
|
+
request_E = False
|
|
520
|
+
|
|
521
|
+
# Determine mode characteristics
|
|
522
|
+
if mode == 'c':
|
|
523
|
+
output_c = True
|
|
524
|
+
elif mode == 'e':
|
|
525
|
+
allow_E = False
|
|
526
|
+
elif mode in ['n', 't']:
|
|
527
|
+
request_E = True
|
|
528
|
+
elif mode in ['p', 's']:
|
|
529
|
+
pass
|
|
530
|
+
elif mode == 'i':
|
|
531
|
+
# Special case: mode 'i' only requires f_module
|
|
532
|
+
if len(argv) != 3:
|
|
533
|
+
print("Wrong number of arguments\n", file=stderr)
|
|
534
|
+
help_and_exit()
|
|
535
|
+
|
|
536
|
+
# Load function module
|
|
537
|
+
f_module = load_function_module(argv[2])
|
|
538
|
+
set_reference_function(f_module.my_arb_f)
|
|
539
|
+
|
|
540
|
+
# Run tests from my_testcases
|
|
541
|
+
failed = 0
|
|
542
|
+
for x, f_expected, tol in f_module.my_testcases:
|
|
543
|
+
f_val = ref_f(x)
|
|
544
|
+
rel_err = abs((f_val - f_expected) / f_expected)
|
|
545
|
+
if rel_err > tol:
|
|
546
|
+
print(f"FAIL: x={x}: expected {f_expected}, got {f_val}, rel_err={rel_err}, tol={tol}")
|
|
547
|
+
failed += 1
|
|
548
|
+
else:
|
|
549
|
+
print(f"PASS: x={x}: f={f_val}, rel_err={rel_err:.2e} <= {tol}")
|
|
550
|
+
|
|
551
|
+
if failed:
|
|
552
|
+
print(f"\n{failed} test(s) failed")
|
|
553
|
+
return 1
|
|
554
|
+
print(f"\nAll {len(f_module.my_testcases)} tests passed")
|
|
555
|
+
return 0
|
|
556
|
+
elif mode == 'v':
|
|
557
|
+
# Special case: mode 'v' has different argument structure
|
|
558
|
+
if len(argv) != 4:
|
|
559
|
+
print("Wrong number of arguments\n", file=stderr)
|
|
560
|
+
help_and_exit()
|
|
561
|
+
|
|
562
|
+
# Load function module
|
|
563
|
+
f_module = load_function_module(argv[2])
|
|
564
|
+
set_reference_function(f_module.my_arb_f)
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
x = float(argv[3])
|
|
568
|
+
except ValueError:
|
|
569
|
+
print(f"Invalid argument x: {argv[3]}\n", file=stderr)
|
|
570
|
+
help_and_exit()
|
|
571
|
+
|
|
572
|
+
print(f"{x:.16e} {ref_f(x):.16e}")
|
|
573
|
+
return 0
|
|
574
|
+
else:
|
|
575
|
+
print("Invalid mode\n", file=stderr)
|
|
576
|
+
help_and_exit()
|
|
577
|
+
|
|
578
|
+
# Load function module (for all modes except 'v' which already loaded it)
|
|
579
|
+
f_module = load_function_module(argv[2])
|
|
580
|
+
set_reference_function(f_module.my_arb_f)
|
|
581
|
+
|
|
582
|
+
min_args = 5 + (1 if request_E else 0)
|
|
583
|
+
max_args = 5 + (1 if allow_E else 0)
|
|
584
|
+
|
|
585
|
+
if len(argv) < min_args or len(argv) > max_args:
|
|
586
|
+
print("Wrong number of arguments\n", file=stderr)
|
|
587
|
+
help_and_exit()
|
|
588
|
+
|
|
589
|
+
# Parse M
|
|
590
|
+
try:
|
|
591
|
+
M = int(argv[3])
|
|
592
|
+
if M < 0:
|
|
593
|
+
raise ValueError
|
|
594
|
+
except ValueError:
|
|
595
|
+
print(f"Invalid M={argv[3]}\n", file=stderr)
|
|
596
|
+
help_and_exit()
|
|
597
|
+
|
|
598
|
+
# Parse N
|
|
599
|
+
try:
|
|
600
|
+
N = int(argv[4])
|
|
601
|
+
if N < 1:
|
|
602
|
+
raise ValueError
|
|
603
|
+
except ValueError:
|
|
604
|
+
print(f"Invalid N={argv[4]}\n", file=stderr)
|
|
605
|
+
help_and_exit()
|
|
606
|
+
|
|
607
|
+
# Parse relerr if present
|
|
608
|
+
relerr = 0.0
|
|
609
|
+
if len(argv) == 6:
|
|
610
|
+
try:
|
|
611
|
+
relerr = float(argv[5])
|
|
612
|
+
if not isfinite(relerr) or relerr <= 0:
|
|
613
|
+
raise ValueError
|
|
614
|
+
except ValueError:
|
|
615
|
+
print(f"Invalid rel_err: {argv[5]}\n", file=stderr)
|
|
616
|
+
help_and_exit()
|
|
617
|
+
|
|
618
|
+
# Take domain limits from loaded module and check
|
|
619
|
+
a, b = f_module.my_domain
|
|
620
|
+
assert a > 0, "Lower domain bound must be positive"
|
|
621
|
+
assert b > a, "Upper domain bound must be greater than lower bound"
|
|
622
|
+
|
|
623
|
+
j0 = 0
|
|
624
|
+
l0 = 0
|
|
625
|
+
D = []
|
|
626
|
+
|
|
627
|
+
# Determine N_min for M
|
|
628
|
+
if mode == 'n':
|
|
629
|
+
assert relerr > 0.0
|
|
630
|
+
j0, l0, D = compute_subdomains(a, b, M)
|
|
631
|
+
Nmin = [0] * len(D)
|
|
632
|
+
|
|
633
|
+
# Parallel computation using multiprocessing
|
|
634
|
+
# Note: Using sequential computation for simplicity and compatibility
|
|
635
|
+
# OpenMP-style parallelization can be achieved with multiprocessing.Pool
|
|
636
|
+
for k, d in enumerate(D):
|
|
637
|
+
# print(f"j,i={d.j},{d.i} a,b={d.a},{d.b}:")
|
|
638
|
+
for n in range(2, N + 1):
|
|
639
|
+
d.coeffs = cheb_pm(d.a, d.b, n)
|
|
640
|
+
assert len(d.coeffs) == n + 1
|
|
641
|
+
try:
|
|
642
|
+
d.s, d.r = analyse_row(d.coeffs)
|
|
643
|
+
r = error_bound(d, n)
|
|
644
|
+
# print(f"# n={n} r={r}")
|
|
645
|
+
if r <= relerr:
|
|
646
|
+
Nmin[k] = n
|
|
647
|
+
break
|
|
648
|
+
except Exception as ex:
|
|
649
|
+
print(f"# M = {M}")
|
|
650
|
+
print(f"# n = {n}")
|
|
651
|
+
print(f"# domain = [{a}, {b})")
|
|
652
|
+
print(f"# {ex}")
|
|
653
|
+
|
|
654
|
+
print("# Per subdomain: j i a b Nmin:")
|
|
655
|
+
for k, d in enumerate(D):
|
|
656
|
+
print(f"{d.j} {d.i} {d.a} {d.b} {Nmin[k]}")
|
|
657
|
+
|
|
658
|
+
print(f"# Total N_min: {max(Nmin)}")
|
|
659
|
+
return 0
|
|
660
|
+
|
|
661
|
+
# Compute polynomial coefficients
|
|
662
|
+
if mode == 't':
|
|
663
|
+
j0, l0, D = compute_subdomains(a / (2 ** N), b * (2 ** N), M)
|
|
664
|
+
print_testcases(start_time, a, b, M, N, D, relerr)
|
|
665
|
+
return 0
|
|
666
|
+
|
|
667
|
+
j0, l0, D = compute_subdomains(a, b, M)
|
|
668
|
+
Err = [0.0] * len(D)
|
|
669
|
+
|
|
670
|
+
# Parallel computation (sequential for compatibility)
|
|
671
|
+
for k, d in enumerate(D):
|
|
672
|
+
d.coeffs = cheb_cn(d.a, d.b, N) if output_c else cheb_pm(d.a, d.b, N)
|
|
673
|
+
assert len(d.coeffs) == N + 1
|
|
674
|
+
d.s, d.r = analyse_row(d.coeffs)
|
|
675
|
+
Err[k] = error_bound(d, N)
|
|
676
|
+
|
|
677
|
+
# Calculate maximum error
|
|
678
|
+
err = max(Err)
|
|
679
|
+
|
|
680
|
+
if mode == 'e':
|
|
681
|
+
print(f"Maximum error bound: {err} epsilon")
|
|
682
|
+
return 0
|
|
683
|
+
elif relerr and err > relerr:
|
|
684
|
+
print(f"Maximum error bound: {err} larger than given bound", file=stderr)
|
|
685
|
+
return 1
|
|
686
|
+
|
|
687
|
+
# Print results
|
|
688
|
+
if mode == 's':
|
|
689
|
+
assert not output_c
|
|
690
|
+
if relerr:
|
|
691
|
+
for d in D:
|
|
692
|
+
for t in [-1., -.99897969, -.7924, -.5, 0., 1e-8, .25, .9987654321, 1.]:
|
|
693
|
+
check_coeffs(d.a, d.b, d.coeffs, t, relerr)
|
|
694
|
+
print_source(start_time, a, b, M, N, j0, l0, D)
|
|
695
|
+
elif mode in ('c', 'p'):
|
|
696
|
+
print_table(start_time, output_c, a, b, M, N, j0, l0, D)
|
|
697
|
+
else:
|
|
698
|
+
raise AssertionError("Invalid mode")
|
|
699
|
+
|
|
700
|
+
return 0
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
if __name__ == "__main__":
|
|
704
|
+
exit(main())
|