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/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())