Coverage for tests/test_chebyMap.py: 8%
244 statements
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-13 02:50 -0700
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-13 02:50 -0700
1import unittest
3import numpy as np
4from numpy.polynomial.chebyshev import chebval, chebval2d
5import numpy.testing as npt
7import astshim as ast
8from astshim.test import MappingTestCase
11def normalize(inArray, lbnd, ubnd):
12 """Return the value of x normalized to [-1, 1]
14 This is a linear scaling with no bounds checking,
15 so if an input value is less than lbnd or greater than ubnd,
16 the returned value will be less than -1 or greater than 1
18 Parameters
19 ----------
20 inArray : `numpy.array` of float
21 Value(s) to normalize; a list of nAxes x nPoints values
22 (the form used by ast.Mapping.applyForward)
23 lbnd : sequence of `float`
24 Lower bounds (one element per axis)
25 ubnd : sequence of `float`
26 Upper bounds (one element per axis)
28 Returns
29 -------
30 `numpy.array` of float
31 Each value is scaled such to -1 if x = lbnd, 1 if x = ubnd
32 """
33 # normalize x in the range [-1, 1]
34 lbnd = np.array(lbnd)
35 ubnd = np.array(ubnd)
36 delta = ubnd - lbnd
37 return (-1 + ((inArray.T - lbnd) * 2.0 / delta)).T
40class ReferenceCheby(object):
42 def __init__(self, referenceCheby, lbnd, ubnd):
43 """Construct a reference Chebyshev polynomial
45 Parameters
46 ----------
47 referenceCheby : callable
48 A function that takes a normalized point (as a list of floats)
49 that has been normalized to the range [-1, 1]
50 and returns the expected results from ChebyPoly.applyForward
51 or applyInverse for the corresponding un-normalized point
52 lbnd : list of float
53 Lower bounds of inputs (for normalization)
54 ubnd : list of float
55 Upper bounds of inputs (for normalization)
56 """
57 self.referenceCheby = referenceCheby
58 self.lbnd = lbnd
59 self.ubnd = ubnd
61 def transform(self, inArray):
62 """Transform data using the reference function
64 Parameters
65 ----------
66 inArray : `numpy.array`
67 Input array of points in the form used by ChebyMap.applyForward
68 or applyInverse.
70 Returns
71 -------
72 outArray : `numpy.array`
73 inArray transformed by referenceCheby (after normalizing inArray)
74 """
75 inNormalized = normalize(inArray, self.lbnd, self.ubnd)
76 outdata = [self.referenceCheby(inPoint) for inPoint in inNormalized.T]
77 arr = np.array(outdata)
78 if len(arr.shape) > 2:
79 # trim unwanted extra dimension (occurs when nin=1)
80 arr.shape = arr.shape[0:2]
81 return arr.T
84class TestChebyMap(MappingTestCase):
86 def setUp(self):
87 self.normErr = "Invalid {0} normalization: min={1}, max={2}, min/max norm=({3}, {4}) != (-1, 1)"
88 # We need a slightly larger than the full floating point tolerance for
89 # many of these tests.
90 self.atol = 5e-14
92 def test_chebyMapUnidirectional_2_2(self):
93 """Test one-directional ChebyMap with 2 inputs and 2 outputs
95 This is a long test because it is a bit of a nuisance setting up
96 the reference transform, so once I have it, I use it for three
97 different ChebyMaps (forward-only, forward with no inverse,
98 and inverse with no forward).
99 """
100 nin = 2
101 nout = 2
102 lbnd_f = [-2.0, -2.5]
103 ubnd_f = [1.5, 2.5]
104 # Coefficients for the following polynomial:
105 # y1 = 1.2 T2(x1') T0(x2') - 0.5 T1(x1') T1(x2')
106 # y2 = 1.0 T0(x1') T1(x2')
107 coeff_f = np.array([
108 [1.2, 1, 2, 0],
109 [-0.5, 1, 1, 1],
110 [1.0, 2, 0, 1],
111 ])
112 self.assertEqual(nin, coeff_f.shape[1] - 2)
114 def referenceFunc(point):
115 """Reference implementation; point must be in range [-1, 1]
116 """
117 c1 = np.zeros((3, 3))
118 c1[2, 0] = 1.2
119 c1[1, 1] = -0.5
120 c2 = np.zeros((3, 3))
121 c2[0, 1] = 1.0
122 x1, x2 = point
123 return (
124 chebval2d(x1, x2, c1),
125 chebval2d(x1, x2, c2),
126 )
128 # TODO: DM-38580
129 # This two-step way of creating a zero-size array gives
130 # an array with non-zero strides which makes pybind11/
131 # ndarray happy.
132 null_coeff = np.array([], dtype=float)
133 null_coeff.shape = (0, 4)
134 self.assertEqual(nin, null_coeff.shape[1] - 2)
136 # arbitary input points that cover the full domain
137 indata = np.array([
138 [-2.0, -0.5, 0.5, 1.5],
139 [-2.5, 1.5, -0.5, 2.5],
140 ])
142 refCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f)
144 # forward-only constructor
145 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
146 self.assertIsInstance(chebyMap1, ast.Object)
147 self.assertIsInstance(chebyMap1, ast.Mapping)
148 self.assertIsInstance(chebyMap1, ast.ChebyMap)
149 self.assertEqual(chebyMap1.nIn, nin)
150 self.assertEqual(chebyMap1.nOut, nout)
151 self.assertTrue(chebyMap1.hasForward)
152 self.assertFalse(chebyMap1.hasInverse)
153 self.checkBasicSimplify(chebyMap1)
154 self.checkCopy(chebyMap1)
155 self.checkMappingPersistence(chebyMap1, indata)
156 domain1 = chebyMap1.getDomain(forward=True)
157 npt.assert_allclose(domain1.lbnd, lbnd_f)
158 npt.assert_allclose(domain1.ubnd, ubnd_f)
160 outdata = chebyMap1.applyForward(indata)
162 with self.assertRaises(RuntimeError):
163 chebyMap1.applyInverse(indata)
165 pred_outdata = refCheby.transform(indata)
166 npt.assert_allclose(outdata, pred_outdata)
168 # bidirectional constructor, forward only specified
169 chebyMap2 = ast.ChebyMap(coeff_f, null_coeff, lbnd_f, ubnd_f, [], [])
170 self.assertIsInstance(chebyMap2, ast.Object)
171 self.assertIsInstance(chebyMap2, ast.Mapping)
172 self.assertIsInstance(chebyMap2, ast.ChebyMap)
173 self.assertEqual(chebyMap2.nIn, nin)
174 self.assertEqual(chebyMap2.nOut, nout)
175 self.assertTrue(chebyMap2.hasForward)
176 self.assertFalse(chebyMap2.hasInverse)
177 self.checkBasicSimplify(chebyMap2)
178 self.checkCopy(chebyMap2)
179 self.checkMappingPersistence(chebyMap1, indata)
180 domain2 = chebyMap2.getDomain(forward=True)
181 npt.assert_allclose(domain2.lbnd, lbnd_f)
182 npt.assert_allclose(domain2.ubnd, ubnd_f)
184 outdata2 = chebyMap2.applyForward(indata)
185 npt.assert_allclose(outdata2, outdata)
187 with self.assertRaises(RuntimeError):
188 chebyMap2.applyInverse(indata)
190 # bidirectional constructor, inverse only specified
191 chebyMap3 = ast.ChebyMap(null_coeff, coeff_f, [], [], lbnd_f, ubnd_f)
192 self.assertIsInstance(chebyMap3, ast.Object)
193 self.assertIsInstance(chebyMap3, ast.Mapping)
194 self.assertIsInstance(chebyMap3, ast.ChebyMap)
195 self.assertEqual(chebyMap3.nIn, nin)
196 self.assertEqual(chebyMap3.nOut, nout)
197 self.assertFalse(chebyMap3.hasForward)
198 self.assertTrue(chebyMap3.hasInverse)
199 domain3 = chebyMap3.getDomain(forward=False)
200 npt.assert_allclose(domain3.lbnd, lbnd_f)
201 npt.assert_allclose(domain3.ubnd, ubnd_f)
203 outdata3 = chebyMap3.applyInverse(indata)
204 npt.assert_allclose(outdata3, outdata)
206 with self.assertRaises(RuntimeError):
207 chebyMap3.applyForward(indata)
209 def test_ChebyMapBidirectional(self):
210 """Test a ChebyMap with separate forward and inverse mappings
212 For simplicity, they are not the inverse of each other.
213 """
214 nin = 2
215 nout = 1
216 lbnd_f = [-2.0, -2.5]
217 ubnd_f = [1.5, -0.5]
219 # cover the domain
220 indata_f = np.array([
221 [-2.0, -1.5, 0.1, 1.5],
222 [-1.0, -2.5, -0.5, -0.5],
223 ])
225 lbnd_i = [-3.0]
226 ubnd_i = [-1.0]
228 # cover the domain
229 indata_i = np.array([
230 [-3.0, -1.1, -1.5, -2.3, -1.0],
231 ])
232 # Coefficients for the following polynomial:
233 # y1 = -1.1 T2(x1') T0(x2') + 1.3 T3(x1') T1(x2')
234 coeff_f = np.array([
235 [-1.1, 1, 2, 0],
236 [1.3, 1, 3, 1],
237 ])
238 self.assertEqual(nin, coeff_f.shape[1] - 2)
240 def referenceFunc_f(point):
241 """Reference forward implementation; point must be in range [-1, 1]
242 """
243 c1 = np.zeros((4, 4))
244 c1[2, 0] = -1.1
245 c1[3, 1] = 1.3
246 x1, x2 = point
247 return (
248 chebval2d(x1, x2, c1),
249 )
251 # Coefficients for the following polynomial:
252 # y1 = 1.6 T3(x1')
253 # y2 = -3.6 T1(x1')
254 coeff_i = np.array([
255 [1.6, 1, 3],
256 [-3.6, 2, 1],
257 ])
258 self.assertEqual(nout, coeff_i.shape[1] - 2)
260 def referenceFunc_i(point):
261 """Reference inverse implementation; point must be in range [-1, 1]
262 """
263 c1 = np.array([0, 0, 0, 1.6], dtype=float)
264 c2 = np.array([0, -3.6], dtype=float)
265 x1 = point
266 return (
267 chebval(x1, c1),
268 chebval(x1, c2),
269 )
271 refCheby_f = ReferenceCheby(referenceFunc_f, lbnd_f, ubnd_f)
272 refCheby_i = ReferenceCheby(referenceFunc_i, lbnd_i, ubnd_i)
274 chebyMap = ast.ChebyMap(coeff_f, coeff_i, lbnd_f, ubnd_f, lbnd_i, ubnd_i)
275 self.assertEqual(chebyMap.nIn, 2)
276 self.assertEqual(chebyMap.nOut, 1)
278 self.checkBasicSimplify(chebyMap)
279 self.checkCopy(chebyMap)
280 self.checkMappingPersistence(chebyMap, indata_f)
282 outdata_f = chebyMap.applyForward(indata_f)
283 des_outdata_f = refCheby_f.transform(indata_f)
285 npt.assert_allclose(outdata_f, des_outdata_f)
287 outdata_i = chebyMap.applyInverse(indata_i)
288 des_outdata_i = refCheby_i.transform(indata_i)
290 npt.assert_allclose(outdata_i, des_outdata_i)
292 def test_ChebyMapPolyTran(self):
293 nin = 2
294 nout = 2
295 lbnd_f = [-2.0, -2.5]
296 ubnd_f = [1.5, 2.5]
298 # arbitrary points that cover the input range
299 indata = np.array([
300 [-2.0, -1.0, 0.1, 1.5, 1.0],
301 [0.0, -2.5, -0.2, 2.5, 2.5],
302 ])
304 # Coefficients for the following gently varying polynomial:
305 # y1 = -2.0 T0(x1') T0(x2') + 0.11 T1(x1') T0(x2')
306 # - 0.2 T0(x1') T1(x2') + 0.001 T2(x1') T1(x2')
307 # y2 = 5.1 T0(x1') T0(x2') - 0.55 T1(x1') T0(x2')
308 # + 0.13 T0(x1') T1(x2') - 0.002 T1(x1') T2(x2')
309 coeff_f = np.array([
310 [-2.0, 1, 0, 0],
311 [0.11, 1, 1, 0],
312 [-0.2, 1, 0, 1],
313 [0.001, 1, 2, 1],
314 [5.1, 2, 0, 0],
315 [-0.55, 2, 1, 0],
316 [0.13, 2, 0, 1],
317 [-0.002, 2, 1, 2]
318 ])
319 self.assertEqual(nin, coeff_f.shape[1] - 2)
321 def referenceFunc(point):
322 """Reference implementation; point must be in range [-1, 1]
323 """
324 c1 = np.zeros((3, 3))
325 c1[0, 0] = -2
326 c1[1, 0] = 0.11
327 c1[0, 1] = -0.2
328 c1[2, 1] = 0.001
329 c2 = np.zeros((3, 3))
330 c2[0, 0] = 5.1
331 c2[1, 0] = -0.55
332 c2[0, 1] = 0.13
333 c2[1, 2] = -0.002
334 x1, x2 = point
335 return (
336 chebval2d(x1, x2, c1),
337 chebval2d(x1, x2, c2),
338 )
340 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
341 self.checkBasicSimplify(chebyMap1)
342 self.assertTrue(chebyMap1.hasForward)
343 self.assertFalse(chebyMap1.hasInverse)
345 outdata = chebyMap1.applyForward(indata)
347 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f)
348 des_outdata = referenceCheby.transform(indata)
350 npt.assert_allclose(outdata, des_outdata)
352 # fit an inverse transform
353 chebyMap2 = chebyMap1.polyTran(forward=False, acc=0.0001, maxacc=0.001, maxorder=6,
354 lbnd=lbnd_f, ubnd=ubnd_f)
355 self.assertTrue(chebyMap2.hasForward)
356 self.assertTrue(chebyMap2.hasInverse)
357 # forward should be identical to the original
358 npt.assert_equal(chebyMap2.applyForward(indata), outdata)
359 roundTripIn2 = chebyMap2.applyInverse(outdata)
360 npt.assert_allclose(roundTripIn2, indata, atol=0.0002)
362 # fit an inverse transform with default bounds (which are the same
363 # bounds used for fitting chebyMap2, so the results should be the same)
364 chebyMap3 = chebyMap1.polyTran(forward=False, acc=0.0001, maxacc=0.001, maxorder=6)
365 self.assertTrue(chebyMap2.hasForward)
366 self.assertTrue(chebyMap2.hasInverse)
367 # forward should be identical to the original
368 npt.assert_equal(chebyMap3.applyForward(indata), outdata)
369 # inverse should be basically the same
370 roundTripIn3 = chebyMap3.applyInverse(outdata)
371 npt.assert_allclose(roundTripIn3, roundTripIn2)
373 def test_ChebyMapChebyMapUnivertible(self):
374 """Test polyTran on a ChebyMap without a single-valued inverse
375 """
376 nin = 2
377 nout = 2
378 lbnd_f = [-2.0, -2.5]
379 ubnd_f = [1.5, 2.5]
381 # arbitrary points that cover the input range
382 indata = np.array([
383 [-2.0, -1.0, 0.1, 1.5, 1.0],
384 [0.0, -2.5, -0.2, 2.5, 2.5],
385 ])
387 # Coefficients for the following not-gently-varying polynomial:
388 # y1 = 2.0 T2(x1') T0(x2') - 2.0 T0(x1') T2(x2')
389 # y2 = 1.0 T3(x1') T0(x2') - 2.0 T0(x1') T3(x2')
390 coeff_f = np.array([
391 [2.0, 1, 2, 0],
392 [-2.0, 1, 0, 2],
393 [1.0, 2, 3, 0],
394 [-2.0, 2, 0, 3],
395 ])
396 self.assertEqual(nin, coeff_f.shape[1] - 2)
398 def referenceFunc(point):
399 """Reference implementation; point must be in range [-1, 1]
400 """
401 c1 = np.zeros((3, 3))
402 c1[2, 0] = 2.0
403 c1[0, 2] = -2.0
404 c2 = np.zeros((4, 4))
405 c2[3, 0] = 1.0
406 c2[0, 3] = -2.0
407 x1, x2 = point
408 return (
409 chebval2d(x1, x2, c1),
410 chebval2d(x1, x2, c2),
411 )
413 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
414 self.checkBasicSimplify(chebyMap1)
415 self.assertTrue(chebyMap1.hasForward)
416 self.assertFalse(chebyMap1.hasInverse)
418 outdata = chebyMap1.applyForward(indata)
420 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f)
421 des_outdata = referenceCheby.transform(indata)
423 npt.assert_allclose(outdata, des_outdata)
425 with self.assertRaises(RuntimeError):
426 chebyMap1.polyTran(forward=False, acc=0.0001, maxacc=0.001, maxorder=6,
427 lbnd=lbnd_f, ubnd=ubnd_f)
429 def test_chebyGetDomain(self):
430 """Test ChebyMap.getDomain's ability to estimate values
432 This occurs when there is only one map and you want the inverse
433 """
434 nout = 2
435 lbnd_f = [-2.0, -2.5]
436 ubnd_f = [1.5, 2.5]
438 # Coefficients for the following not-gently-varying polynomial:
439 # y1 = 2.0 T2(x1') T0(x2') - 2.0 T0(x1') T2(x2')
440 # y2 = 1.0 T3(x1') T0(x2') - 2.0 T0(x1') T3(x2')
441 coeff_f = np.array([
442 [2.0, 1, 2, 0],
443 [-2.0, 1, 0, 2],
444 [1.0, 2, 3, 0],
445 [-2.0, 2, 0, 3],
446 ])
448 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
450 # compute indata as a grid of points that cover the input range
451 x1Edge = np.linspace(lbnd_f[0], ubnd_f[0], 1000)
452 x2Edge = np.linspace(lbnd_f[1], ubnd_f[1], 1000)
453 x1Grid, x2Grid = np.meshgrid(x1Edge, x2Edge)
454 indata = np.array([x1Grid.ravel(), x2Grid.ravel()])
456 outdata = chebyMap1.applyForward(indata)
457 pred_lbnd = outdata.min(1)
458 pred_ubnd = outdata.max(1)
460 domain = chebyMap1.getDomain(forward=False)
461 npt.assert_allclose(domain.lbnd, pred_lbnd, atol=0.0001)
462 npt.assert_allclose(domain.ubnd, pred_ubnd, atol=0.0001)
464 def test_normalize(self):
465 """Test the local utility function `normalize`
466 """
467 lbnd = [-2.0, -2.5]
468 ubnd = [1.5, 2.5]
470 # points that cover the full domain
471 points = np.array([
472 [-2.0, -0.5, 0.5, 1.5],
473 [-2.5, 1.5, 0.5, 2.5]
474 ])
476 normPoints = normalize(points, lbnd, ubnd)
477 for normAxis in normPoints:
478 self.assertAlmostEqual(normAxis.min(), -1)
479 self.assertAlmostEqual(normAxis.max(), 1)
481 def test_ChebyMapDM10496(self):
482 """Test for a segfault when simplifying a SeriesMap
484 We saw an intermittent segfault when simplifying a SeriesMap
485 consisting of the inverse of PolyMap with 2 inputs and one output
486 followed by its inverse (which should simplify to a UnitMap
487 with one input and one output). David Berry fixed this bug in AST
488 2017-05-10.
490 I tried this test on an older version of astshim and found that it
491 triggering a segfault nearly every time.
492 """
493 coeff_f = np.array([
494 [-1.1, 1, 2, 0],
495 [1.3, 1, 3, 1],
496 ])
497 coeff_i = np.array([
498 [1.6, 1, 3],
499 [-3.6, 2, 1],
500 ])
501 lbnd_f = [-2.0, -2.5]
502 ubnd_f = [1.5, -0.5]
503 lbnd_i = [-3.0]
504 ubnd_i = [-1.0]
506 # execute many times to increase the odds of a segfault
507 for i in range(1000):
508 amap = ast.ChebyMap(coeff_f, coeff_i, lbnd_f, ubnd_f, lbnd_i, ubnd_i)
509 amapinv = amap.inverted()
510 cmp2 = amapinv.then(amap)
511 result = cmp2.simplified()
512 self.assertIsInstance(result, ast.UnitMap)
515if __name__ == "__main__": 515 ↛ 516line 515 didn't jump to line 516, because the condition on line 515 was never true
516 unittest.main()