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.
tests/test_app.py ADDED
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Unit tests for the piecewise Chebyshev approximation generator.
5
+
6
+ Reference: Joachim Wuttke and Alexander Kleinsorge,
7
+ Algorithm 1XXX: Code generation for piecewise Chebyshev approximation
8
+
9
+ File: tests/test_app.py
10
+
11
+ License: GNU General Public License, version 3 or higher (see LICENSE)
12
+ Copyright: Forschungszentrum Jülich GmbH 2025
13
+ """
14
+
15
+ import unittest
16
+
17
+ # Import modules to test
18
+ from ppapp.ppapp import (
19
+ Subdomain,
20
+ analyse_row,
21
+ compute_subdomains,
22
+ error_bound,
23
+ hexfloat,
24
+ n_output_coeffs,
25
+ )
26
+ from ppapp.cheb_coeffs import (
27
+ cheb_cn,
28
+ cheb_pm,
29
+ precompute_Tnm,
30
+ ref_f,
31
+ set_reference_function,
32
+ sizeT,
33
+ )
34
+
35
+
36
+ class TestSizeT(unittest.TestCase):
37
+ """Tests for the sizeT function that computes OEIS A002620."""
38
+
39
+ def test_known_values(self):
40
+ """Test against known OEIS A002620 values."""
41
+ # N = -2,-1,0,1,2,3,4,5,6,7,...
42
+ # result = 0,0,1,2,4,6,9,12,16,20,...
43
+ expected = [(0, 1), (1, 2), (2, 4), (3, 6), (4, 9), (5, 12), (6, 16), (7, 20)]
44
+ for n, exp in expected:
45
+ with self.subTest(n=n):
46
+ self.assertEqual(sizeT(n), exp)
47
+
48
+
49
+ class TestPrecomputeTnm(unittest.TestCase):
50
+ """Tests for the Chebyshev polynomial coefficient computation."""
51
+
52
+ def test_first_coefficients(self):
53
+ """Test first few T_nm coefficients against OEIS A008310."""
54
+ # Expected: 1,1,-1,2,-3,4,1,-8,8,5,-20,16,...
55
+ tnm = precompute_Tnm(6)
56
+ expected_start = [1, 1, -1, 2, -3, 4, 1, -8, 8, 5, -20, 16]
57
+ self.assertEqual(tnm[:len(expected_start)], expected_start)
58
+
59
+ def test_size(self):
60
+ """Test that result has correct size."""
61
+ for N in range(0, 10):
62
+ tnm = precompute_Tnm(N)
63
+ self.assertEqual(len(tnm), sizeT(N))
64
+
65
+ def test_last_coefficient(self):
66
+ """Test that T_nn = 2^{n-1} for n >= 1."""
67
+ for N in range(1, 12):
68
+ tnm = precompute_Tnm(N)
69
+ expected_last = 1 << (N - 1) # 2^{N-1}
70
+ self.assertEqual(tnm[-1], expected_last)
71
+
72
+
73
+ class TestAnalyseRow(unittest.TestCase):
74
+ """Tests for the power law analysis function."""
75
+
76
+ def test_simple_power_law(self):
77
+ """Test with a simple geometric sequence."""
78
+ # y[n] = 2 * 0.5^n = [2, 1, 0.5, 0.25, ...]
79
+ coeffs = [2.0 * (0.5 ** n) for n in range(10)]
80
+ s, r = analyse_row(coeffs)
81
+ # Should get s=2, r=0.5
82
+ self.assertAlmostEqual(s, 2.0, places=10)
83
+ self.assertAlmostEqual(r, 0.5, places=10)
84
+
85
+ def test_returns_bounding_power_law(self):
86
+ """Test that returned power law bounds all coefficients."""
87
+ coeffs = [1.0, 0.5, 0.3, 0.1, 0.08, 0.03, 0.01]
88
+ s, r = analyse_row(coeffs)
89
+ # Check that s*r^n >= |coeffs[n]| for all n
90
+ for n in range(len(coeffs)):
91
+ bound = s * (r ** n)
92
+ self.assertGreaterEqual(bound * 1.01, abs(coeffs[n])) # 1% tolerance
93
+
94
+
95
+ class TestErrorBound(unittest.TestCase):
96
+ """Tests for the error bound computation."""
97
+
98
+ def test_positive_error_bound(self):
99
+ """Test that error bound is positive for valid input."""
100
+ d = Subdomain(0, 0, 0.5, 0.515625)
101
+ d.coeffs = [0.5, 0.01, 0.0001, 1e-6, 1e-8, 1e-10, 1e-12]
102
+ d.s = 1.0
103
+ d.r = 0.01
104
+ N = 6
105
+ err = error_bound(d, N)
106
+ self.assertGreater(err, 0)
107
+
108
+
109
+ class TestComputeSubdomains(unittest.TestCase):
110
+ """Tests for the subdomain computation."""
111
+
112
+ def test_subdomain_count(self):
113
+ """Test that correct number of subdomains is generated."""
114
+ # For domain [0.5, 12) with M=5, expect 144 subdomains
115
+ j0, l0, D = compute_subdomains(0.5, 12.0, 5)
116
+ self.assertEqual(len(D), 144)
117
+
118
+ def test_subdomain_coverage(self):
119
+ """Test that subdomains cover the entire domain."""
120
+ a, b = 0.5, 12.0
121
+ j0, l0, D = compute_subdomains(a, b, 5)
122
+
123
+ # First subdomain should start at a
124
+ self.assertLessEqual(D[0].a, a)
125
+ # Last subdomain should extend past b
126
+ self.assertGreaterEqual(D[-1].b, b)
127
+
128
+ # Check contiguity
129
+ for i in range(len(D) - 1):
130
+ # End of subdomain i should match or exceed start of i+1
131
+ self.assertLessEqual(D[i].b, D[i + 1].b)
132
+
133
+ def test_subdomain_boundaries(self):
134
+ """Test first subdomain boundaries for M=5."""
135
+ j0, l0, D = compute_subdomains(0.5, 12.0, 5)
136
+ self.assertAlmostEqual(D[0].a, 0.5)
137
+ self.assertAlmostEqual(D[0].b, 0.515625)
138
+
139
+
140
+ class TestHexfloat(unittest.TestCase):
141
+ """Tests for hexadecimal floating point formatting."""
142
+
143
+ def test_positive_number(self):
144
+ """Test formatting of positive number."""
145
+ result = hexfloat(1.5)
146
+ self.assertEqual(result, "0x1.8000000000000p+0")
147
+
148
+ def test_negative_number(self):
149
+ """Test formatting of negative number."""
150
+ result = hexfloat(-1.5)
151
+ self.assertEqual(result, "-0x1.8000000000000p+0")
152
+
153
+ def test_zero(self):
154
+ """Test formatting of zero."""
155
+ result = hexfloat(0.0)
156
+ self.assertEqual(result, "0x0.0p+0")
157
+
158
+
159
+ class TestNOutputCoeffs(unittest.TestCase):
160
+ """Tests for the alignment padding computation."""
161
+
162
+ def test_alignment_values(self):
163
+ """Test alignment computation for various N values."""
164
+ # From the paper/code:
165
+ # N+1 mod 8 == 0,1,2,4: no padding
166
+ # N+1 mod 8 == 3: pad to 4
167
+ # Otherwise: pad to 8
168
+ test_cases = [
169
+ (5, 8), # N+1=6, 6%8=6, pad to 8
170
+ (6, 8), # N+1=7, 7%8=7, pad to 8
171
+ (7, 8), # N+1=8, 8%8=0, no padding
172
+ (8, 9), # N+1=9, 9%8=1, no padding
173
+ (9, 10), # N+1=10, 10%8=2, no padding
174
+ (10, 12), # N+1=11, 11%8=3, pad to 12
175
+ (11, 12), # N+1=12, 12%8=4, no padding
176
+ ]
177
+ for N, expected in test_cases:
178
+ with self.subTest(N=N):
179
+ self.assertEqual(n_output_coeffs(N), expected)
180
+
181
+
182
+ class TestFunctionModules(unittest.TestCase):
183
+ """Tests for the function modules (f_imwofx, f_erfcx)."""
184
+
185
+ def test_imwofx_basic(self):
186
+ """Test Im w(x) function for basic values."""
187
+ from ppapp.demo_functions.imwofx import my_arb_f, my_domain, my_testcases
188
+ set_reference_function(my_arb_f)
189
+
190
+ # Test domain
191
+ self.assertEqual(my_domain, (0.5, 12.0))
192
+
193
+ # Test against expected values
194
+ for x, f_expected, tol in my_testcases:
195
+ f_computed = ref_f(x)
196
+ rel_error = abs(f_computed - f_expected) / f_expected
197
+ self.assertLess(rel_error, tol)
198
+
199
+ def test_erfcx_basic(self):
200
+ """Test erfcx function for basic values."""
201
+ from ppapp.demo_functions.erfcx import my_arb_f, my_domain, my_testcases
202
+ set_reference_function(my_arb_f)
203
+
204
+ # Test domain
205
+ self.assertEqual(my_domain, (0.125, 12.0))
206
+
207
+ # Test against expected values
208
+ for x, f_expected, tol in my_testcases:
209
+ f_computed = ref_f(x)
210
+ rel_error = abs(f_computed - f_expected) / f_expected
211
+ self.assertLess(rel_error, tol)
212
+
213
+ def test_polynomial_basic(self):
214
+ """Test polynomial f(x) = x^3 - x^2 + x - 1 for basic values."""
215
+ from ppapp.demo_functions.polynomial import my_arb_f, my_domain, my_testcases
216
+ set_reference_function(my_arb_f)
217
+
218
+ # Test domain
219
+ self.assertEqual(my_domain, (1.5, 4.0))
220
+
221
+ # Test against expected values
222
+ for x, f_expected, tol in my_testcases:
223
+ f_computed = ref_f(x)
224
+ rel_error = abs(f_computed - f_expected) / abs(f_expected)
225
+ self.assertLess(rel_error, tol)
226
+
227
+
228
+ class TestChebCoeffs(unittest.TestCase):
229
+ """Tests for Chebyshev coefficient computation."""
230
+
231
+ def setUp(self):
232
+ """Set up reference function."""
233
+ from ppapp.demo_functions.imwofx import my_arb_f
234
+ set_reference_function(my_arb_f)
235
+
236
+ def test_cheb_cn_returns_correct_count(self):
237
+ """Test that cheb_cn returns N+1 coefficients."""
238
+ for N in [5, 7, 10]:
239
+ coeffs = cheb_cn(0.5, 0.515625, N)
240
+ self.assertEqual(len(coeffs), N + 1)
241
+
242
+ def test_cheb_pm_returns_correct_count(self):
243
+ """Test that cheb_pm returns N+1 coefficients."""
244
+ for N in [5, 7, 10]:
245
+ coeffs = cheb_pm(0.5, 0.515625, N)
246
+ self.assertEqual(len(coeffs), N + 1)
247
+
248
+ def test_first_coeff_approximates_function_value(self):
249
+ """Test that p_0 is close to the function value at the midpoint."""
250
+ a, b = 0.5, 0.515625
251
+ coeffs = cheb_pm(a, b, 10)
252
+ mid = (a + b) / 2
253
+ f_mid = ref_f(mid)
254
+ # p_0 should be close to f(midpoint)
255
+ self.assertAlmostEqual(coeffs[0], f_mid, places=2)
256
+
257
+
258
+ class TestPolynomialApproximation(unittest.TestCase):
259
+ """Tests using f_polynomial to verify exact polynomial approximation."""
260
+
261
+ def setUp(self):
262
+ """Set up reference function to polynomial."""
263
+ from ppapp.demo_functions.polynomial import my_arb_f
264
+ set_reference_function(my_arb_f)
265
+
266
+ def test_higher_coeffs_negligible(self):
267
+ """For degree-3 polynomial, coefficients p_4 onwards should be negligible."""
268
+ # Use a subdomain within f_polynomial's domain
269
+ a, b = 2.0, 2.0625
270
+ N = 10
271
+ coeffs = cheb_pm(a, b, N)
272
+
273
+ # First 4 coefficients (p_0 to p_3) should be significant
274
+ for i in range(4):
275
+ self.assertGreater(abs(coeffs[i]), 1e-10,
276
+ f"Coefficient p_{i} should be significant")
277
+
278
+ # Coefficients p_4 onwards should be negligible (numerical noise)
279
+ p0 = abs(coeffs[0])
280
+ for i in range(4, N + 1):
281
+ self.assertLess(abs(coeffs[i]), p0 * 1e-10,
282
+ f"Coefficient p_{i} should be negligible")
283
+
284
+ def test_approximation_exact(self):
285
+ """Chebyshev approximation of degree-3 polynomial should be exact."""
286
+ a, b = 2.0, 2.0625
287
+ N = 10
288
+ coeffs = cheb_pm(a, b, N)
289
+
290
+ # Test at several points in the subdomain
291
+ test_points = [a, (a + b) / 2, b, a + 0.25 * (b - a), a + 0.75 * (b - a)]
292
+ for x in test_points:
293
+ # Evaluate polynomial using Horner's method
294
+ t = (x - (a + b) / 2) / ((b - a) / 2)
295
+ f_approx = coeffs[-1]
296
+ for m in range(len(coeffs) - 2, -1, -1):
297
+ f_approx *= t
298
+ f_approx += coeffs[m]
299
+
300
+ f_exact = ref_f(x)
301
+ rel_error = abs(f_approx - f_exact) / abs(f_exact)
302
+ # Should be exact within machine precision
303
+ self.assertLess(rel_error, 1e-14,
304
+ f"Approximation at x={x} should be exact")
305
+
306
+ def test_chebyshev_coeffs_decay(self):
307
+ """Chebyshev coefficients c_n for degree-3 polynomial should be zero for n>3."""
308
+ a, b = 2.0, 2.0625
309
+ N = 10
310
+ coeffs = cheb_cn(a, b, N)
311
+
312
+ # c_0 to c_3 should be significant
313
+ for i in range(4):
314
+ self.assertGreater(abs(coeffs[i]), 1e-10,
315
+ f"Chebyshev coefficient c_{i} should be significant")
316
+
317
+ # c_4 onwards should be essentially zero (numerical noise)
318
+ c0 = abs(coeffs[0])
319
+ for i in range(4, N + 1):
320
+ self.assertLess(abs(coeffs[i]), c0 * 1e-10,
321
+ f"Chebyshev coefficient c_{i} should be negligible")
322
+
323
+ def test_polynomial_coefficients_correct(self):
324
+ """Verify the actual polynomial coefficients match x^3 - x^2 + x - 1."""
325
+ # For f(x) = x^3 - x^2 + x - 1 on a small interval, we can verify
326
+ # the monomial coefficients p_m relate correctly to the original polynomial.
327
+ # With transformation t = (x - mid) / halfrange, we get transformed coefficients.
328
+ a, b = 2.0, 2.0625
329
+ mid = (a + b) / 2
330
+ N = 5
331
+ coeffs = cheb_pm(a, b, N)
332
+
333
+ # Verify by evaluating at x = mid (t = 0), should give p_0
334
+ f_mid = ref_f(mid)
335
+ self.assertAlmostEqual(coeffs[0], f_mid, places=12)
336
+
337
+ # Verify by evaluating at x = b (t = 1), should give sum of all coeffs
338
+ f_b = ref_f(b)
339
+ f_approx = sum(coeffs[:4]) # Only first 4 matter
340
+ self.assertAlmostEqual(f_approx, f_b, places=12)
341
+
342
+
343
+ class TestIntegration(unittest.TestCase):
344
+ """Integration tests combining multiple components."""
345
+
346
+ def setUp(self):
347
+ """Set up reference function."""
348
+ from ppapp.demo_functions.imwofx import my_arb_f
349
+ set_reference_function(my_arb_f)
350
+
351
+ def test_full_pipeline(self):
352
+ """Test the full pipeline: compute subdomains, coefficients, and error bound."""
353
+ a, b = 0.5, 12.0
354
+ M = 5
355
+ N = 10
356
+
357
+ j0, l0, D = compute_subdomains(a, b, M)
358
+
359
+ # Process first few subdomains
360
+ max_err = 0.0
361
+ for d in D[:5]:
362
+ d.coeffs = cheb_pm(d.a, d.b, N)
363
+ s, r = analyse_row(d.coeffs)
364
+ d.s = s
365
+ d.r = r
366
+ err = error_bound(d, N)
367
+ max_err = max(max_err, err)
368
+
369
+ # Error should be reasonable (less than 10 epsilon for M=5, N=10)
370
+ self.assertLess(max_err, 10.0)
371
+
372
+
373
+ if __name__ == "__main__":
374
+ unittest.main()
Binary file