Coverage for tests/test_chebyMap.py : 8%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 null_coeff = np.zeros(shape=(0, 4))
129 self.assertEqual(nin, null_coeff.shape[1] - 2)
131 # arbitary input points that cover the full domain
132 indata = np.array([
133 [-2.0, -0.5, 0.5, 1.5],
134 [-2.5, 1.5, -0.5, 2.5],
135 ])
137 refCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f)
139 # forward-only constructor
140 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
141 self.assertIsInstance(chebyMap1, ast.Object)
142 self.assertIsInstance(chebyMap1, ast.Mapping)
143 self.assertIsInstance(chebyMap1, ast.ChebyMap)
144 self.assertEqual(chebyMap1.nIn, nin)
145 self.assertEqual(chebyMap1.nOut, nout)
146 self.assertTrue(chebyMap1.hasForward)
147 self.assertFalse(chebyMap1.hasInverse)
148 self.checkBasicSimplify(chebyMap1)
149 self.checkCopy(chebyMap1)
150 self.checkMappingPersistence(chebyMap1, indata)
151 domain1 = chebyMap1.getDomain(forward=True)
152 npt.assert_allclose(domain1.lbnd, lbnd_f)
153 npt.assert_allclose(domain1.ubnd, ubnd_f)
155 outdata = chebyMap1.applyForward(indata)
157 with self.assertRaises(RuntimeError):
158 chebyMap1.applyInverse(indata)
160 pred_outdata = refCheby.transform(indata)
161 npt.assert_allclose(outdata, pred_outdata)
163 # bidirectional constructor, forward only specified
164 chebyMap2 = ast.ChebyMap(coeff_f, null_coeff, lbnd_f, ubnd_f, [], [])
165 self.assertIsInstance(chebyMap2, ast.Object)
166 self.assertIsInstance(chebyMap2, ast.Mapping)
167 self.assertIsInstance(chebyMap2, ast.ChebyMap)
168 self.assertEqual(chebyMap2.nIn, nin)
169 self.assertEqual(chebyMap2.nOut, nout)
170 self.assertTrue(chebyMap2.hasForward)
171 self.assertFalse(chebyMap2.hasInverse)
172 self.checkBasicSimplify(chebyMap2)
173 self.checkCopy(chebyMap2)
174 self.checkMappingPersistence(chebyMap1, indata)
175 domain2 = chebyMap2.getDomain(forward=True)
176 npt.assert_allclose(domain2.lbnd, lbnd_f)
177 npt.assert_allclose(domain2.ubnd, ubnd_f)
179 outdata2 = chebyMap2.applyForward(indata)
180 npt.assert_allclose(outdata2, outdata)
182 with self.assertRaises(RuntimeError):
183 chebyMap2.applyInverse(indata)
185 # bidirectional constructor, inverse only specified
186 chebyMap3 = ast.ChebyMap(null_coeff, coeff_f, [], [], lbnd_f, ubnd_f)
187 self.assertIsInstance(chebyMap3, ast.Object)
188 self.assertIsInstance(chebyMap3, ast.Mapping)
189 self.assertIsInstance(chebyMap3, ast.ChebyMap)
190 self.assertEqual(chebyMap3.nIn, nin)
191 self.assertEqual(chebyMap3.nOut, nout)
192 self.assertFalse(chebyMap3.hasForward)
193 self.assertTrue(chebyMap3.hasInverse)
194 domain3 = chebyMap3.getDomain(forward=False)
195 npt.assert_allclose(domain3.lbnd, lbnd_f)
196 npt.assert_allclose(domain3.ubnd, ubnd_f)
198 outdata3 = chebyMap3.applyInverse(indata)
199 npt.assert_allclose(outdata3, outdata)
201 with self.assertRaises(RuntimeError):
202 chebyMap3.applyForward(indata)
204 def test_ChebyMapBidirectional(self):
205 """Test a ChebyMap with separate forward and inverse mappings
207 For simplicity, they are not the inverse of each other.
208 """
209 nin = 2
210 nout = 1
211 lbnd_f = [-2.0, -2.5]
212 ubnd_f = [1.5, -0.5]
214 # cover the domain
215 indata_f = np.array([
216 [-2.0, -1.5, 0.1, 1.5],
217 [-1.0, -2.5, -0.5, -0.5],
218 ])
220 lbnd_i = [-3.0]
221 ubnd_i = [-1.0]
223 # cover the domain
224 indata_i = np.array([
225 [-3.0, -1.1, -1.5, -2.3, -1.0],
226 ])
227 # Coefficients for the following polynomial:
228 # y1 = -1.1 T2(x1') T0(x2') + 1.3 T3(x1') T1(x2')
229 coeff_f = np.array([
230 [-1.1, 1, 2, 0],
231 [1.3, 1, 3, 1],
232 ])
233 self.assertEqual(nin, coeff_f.shape[1] - 2)
235 def referenceFunc_f(point):
236 """Reference forward implementation; point must be in range [-1, 1]
237 """
238 c1 = np.zeros((4, 4))
239 c1[2, 0] = -1.1
240 c1[3, 1] = 1.3
241 x1, x2 = point
242 return (
243 chebval2d(x1, x2, c1),
244 )
246 # Coefficients for the following polynomial:
247 # y1 = 1.6 T3(x1')
248 # y2 = -3.6 T1(x1')
249 coeff_i = np.array([
250 [1.6, 1, 3],
251 [-3.6, 2, 1],
252 ])
253 self.assertEqual(nout, coeff_i.shape[1] - 2)
255 def referenceFunc_i(point):
256 """Reference inverse implementation; point must be in range [-1, 1]
257 """
258 c1 = np.array([0, 0, 0, 1.6], dtype=float)
259 c2 = np.array([0, -3.6], dtype=float)
260 x1 = point
261 return (
262 chebval(x1, c1),
263 chebval(x1, c2),
264 )
266 refCheby_f = ReferenceCheby(referenceFunc_f, lbnd_f, ubnd_f)
267 refCheby_i = ReferenceCheby(referenceFunc_i, lbnd_i, ubnd_i)
269 chebyMap = ast.ChebyMap(coeff_f, coeff_i, lbnd_f, ubnd_f, lbnd_i, ubnd_i)
270 self.assertEqual(chebyMap.nIn, 2)
271 self.assertEqual(chebyMap.nOut, 1)
273 self.checkBasicSimplify(chebyMap)
274 self.checkCopy(chebyMap)
275 self.checkMappingPersistence(chebyMap, indata_f)
277 outdata_f = chebyMap.applyForward(indata_f)
278 des_outdata_f = refCheby_f.transform(indata_f)
280 npt.assert_allclose(outdata_f, des_outdata_f)
282 outdata_i = chebyMap.applyInverse(indata_i)
283 des_outdata_i = refCheby_i.transform(indata_i)
285 npt.assert_allclose(outdata_i, des_outdata_i)
287 def test_ChebyMapPolyTran(self):
288 nin = 2
289 nout = 2
290 lbnd_f = [-2.0, -2.5]
291 ubnd_f = [1.5, 2.5]
293 # arbitrary points that cover the input range
294 indata = np.array([
295 [-2.0, -1.0, 0.1, 1.5, 1.0],
296 [0.0, -2.5, -0.2, 2.5, 2.5],
297 ])
299 # Coefficients for the following gently varying polynomial:
300 # y1 = -2.0 T0(x1') T0(x2') + 0.11 T1(x1') T0(x2')
301 # - 0.2 T0(x1') T1(x2') + 0.001 T2(x1') T1(x2')
302 # y2 = 5.1 T0(x1') T0(x2') - 0.55 T1(x1') T0(x2')
303 # + 0.13 T0(x1') T1(x2') - 0.002 T1(x1') T2(x2')
304 coeff_f = np.array([
305 [-2.0, 1, 0, 0],
306 [0.11, 1, 1, 0],
307 [-0.2, 1, 0, 1],
308 [0.001, 1, 2, 1],
309 [5.1, 2, 0, 0],
310 [-0.55, 2, 1, 0],
311 [0.13, 2, 0, 1],
312 [-0.002, 2, 1, 2]
313 ])
314 self.assertEqual(nin, coeff_f.shape[1] - 2)
316 def referenceFunc(point):
317 """Reference implementation; point must be in range [-1, 1]
318 """
319 c1 = np.zeros((3, 3))
320 c1[0, 0] = -2
321 c1[1, 0] = 0.11
322 c1[0, 1] = -0.2
323 c1[2, 1] = 0.001
324 c2 = np.zeros((3, 3))
325 c2[0, 0] = 5.1
326 c2[1, 0] = -0.55
327 c2[0, 1] = 0.13
328 c2[1, 2] = -0.002
329 x1, x2 = point
330 return (
331 chebval2d(x1, x2, c1),
332 chebval2d(x1, x2, c2),
333 )
335 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
336 self.checkBasicSimplify(chebyMap1)
337 self.assertTrue(chebyMap1.hasForward)
338 self.assertFalse(chebyMap1.hasInverse)
340 outdata = chebyMap1.applyForward(indata)
342 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f)
343 des_outdata = referenceCheby.transform(indata)
345 npt.assert_allclose(outdata, des_outdata)
347 # fit an inverse transform
348 chebyMap2 = chebyMap1.polyTran(forward=False, acc=0.0001, maxacc=0.001, maxorder=6,
349 lbnd=lbnd_f, ubnd=ubnd_f)
350 self.assertTrue(chebyMap2.hasForward)
351 self.assertTrue(chebyMap2.hasInverse)
352 # forward should be identical to the original
353 npt.assert_equal(chebyMap2.applyForward(indata), outdata)
354 roundTripIn2 = chebyMap2.applyInverse(outdata)
355 npt.assert_allclose(roundTripIn2, indata, atol=0.0002)
357 # fit an inverse transform with default bounds (which are the same
358 # bounds used for fitting chebyMap2, so the results should be the same)
359 chebyMap3 = chebyMap1.polyTran(forward=False, acc=0.0001, maxacc=0.001, maxorder=6)
360 self.assertTrue(chebyMap2.hasForward)
361 self.assertTrue(chebyMap2.hasInverse)
362 # forward should be identical to the original
363 npt.assert_equal(chebyMap3.applyForward(indata), outdata)
364 # inverse should be basically the same
365 roundTripIn3 = chebyMap3.applyInverse(outdata)
366 npt.assert_allclose(roundTripIn3, roundTripIn2)
368 def test_ChebyMapChebyMapUnivertible(self):
369 """Test polyTran on a ChebyMap without a single-valued inverse
370 """
371 nin = 2
372 nout = 2
373 lbnd_f = [-2.0, -2.5]
374 ubnd_f = [1.5, 2.5]
376 # arbitrary points that cover the input range
377 indata = np.array([
378 [-2.0, -1.0, 0.1, 1.5, 1.0],
379 [0.0, -2.5, -0.2, 2.5, 2.5],
380 ])
382 # Coefficients for the following not-gently-varying polynomial:
383 # y1 = 2.0 T2(x1') T0(x2') - 2.0 T0(x1') T2(x2')
384 # y2 = 1.0 T3(x1') T0(x2') - 2.0 T0(x1') T3(x2')
385 coeff_f = np.array([
386 [2.0, 1, 2, 0],
387 [-2.0, 1, 0, 2],
388 [1.0, 2, 3, 0],
389 [-2.0, 2, 0, 3],
390 ])
391 self.assertEqual(nin, coeff_f.shape[1] - 2)
393 def referenceFunc(point):
394 """Reference implementation; point must be in range [-1, 1]
395 """
396 c1 = np.zeros((3, 3))
397 c1[2, 0] = 2.0
398 c1[0, 2] = -2.0
399 c2 = np.zeros((4, 4))
400 c2[3, 0] = 1.0
401 c2[0, 3] = -2.0
402 x1, x2 = point
403 return (
404 chebval2d(x1, x2, c1),
405 chebval2d(x1, x2, c2),
406 )
408 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
409 self.checkBasicSimplify(chebyMap1)
410 self.assertTrue(chebyMap1.hasForward)
411 self.assertFalse(chebyMap1.hasInverse)
413 outdata = chebyMap1.applyForward(indata)
415 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f)
416 des_outdata = referenceCheby.transform(indata)
418 npt.assert_allclose(outdata, des_outdata)
420 with self.assertRaises(RuntimeError):
421 chebyMap1.polyTran(forward=False, acc=0.0001, maxacc=0.001, maxorder=6,
422 lbnd=lbnd_f, ubnd=ubnd_f)
424 def test_chebyGetDomain(self):
425 """Test ChebyMap.getDomain's ability to estimate values
427 This occurs when there is only one map and you want the inverse
428 """
429 nout = 2
430 lbnd_f = [-2.0, -2.5]
431 ubnd_f = [1.5, 2.5]
433 # Coefficients for the following not-gently-varying polynomial:
434 # y1 = 2.0 T2(x1') T0(x2') - 2.0 T0(x1') T2(x2')
435 # y2 = 1.0 T3(x1') T0(x2') - 2.0 T0(x1') T3(x2')
436 coeff_f = np.array([
437 [2.0, 1, 2, 0],
438 [-2.0, 1, 0, 2],
439 [1.0, 2, 3, 0],
440 [-2.0, 2, 0, 3],
441 ])
443 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f)
445 # compute indata as a grid of points that cover the input range
446 x1Edge = np.linspace(lbnd_f[0], ubnd_f[0], 1000)
447 x2Edge = np.linspace(lbnd_f[1], ubnd_f[1], 1000)
448 x1Grid, x2Grid = np.meshgrid(x1Edge, x2Edge)
449 indata = np.array([x1Grid.ravel(), x2Grid.ravel()])
451 outdata = chebyMap1.applyForward(indata)
452 pred_lbnd = outdata.min(1)
453 pred_ubnd = outdata.max(1)
455 domain = chebyMap1.getDomain(forward=False)
456 npt.assert_allclose(domain.lbnd, pred_lbnd, atol=0.0001)
457 npt.assert_allclose(domain.ubnd, pred_ubnd, atol=0.0001)
459 def test_normalize(self):
460 """Test the local utility function `normalize`
461 """
462 lbnd = [-2.0, -2.5]
463 ubnd = [1.5, 2.5]
465 # points that cover the full domain
466 points = np.array([
467 [-2.0, -0.5, 0.5, 1.5],
468 [-2.5, 1.5, 0.5, 2.5]
469 ])
471 normPoints = normalize(points, lbnd, ubnd)
472 for normAxis in normPoints:
473 self.assertAlmostEqual(normAxis.min(), -1)
474 self.assertAlmostEqual(normAxis.max(), 1)
476 def test_ChebyMapDM10496(self):
477 """Test for a segfault when simplifying a SeriesMap
479 We saw an intermittent segfault when simplifying a SeriesMap
480 consisting of the inverse of PolyMap with 2 inputs and one output
481 followed by its inverse (which should simplify to a UnitMap
482 with one input and one output). David Berry fixed this bug in AST
483 2017-05-10.
485 I tried this test on an older version of astshim and found that it
486 triggering a segfault nearly every time.
487 """
488 coeff_f = np.array([
489 [-1.1, 1, 2, 0],
490 [1.3, 1, 3, 1],
491 ])
492 coeff_i = np.array([
493 [1.6, 1, 3],
494 [-3.6, 2, 1],
495 ])
496 lbnd_f = [-2.0, -2.5]
497 ubnd_f = [1.5, -0.5]
498 lbnd_i = [-3.0]
499 ubnd_i = [-1.0]
501 # execute many times to increase the odds of a segfault
502 for i in range(1000):
503 amap = ast.ChebyMap(coeff_f, coeff_i, lbnd_f, ubnd_f, lbnd_i, ubnd_i)
504 amapinv = amap.inverted()
505 cmp2 = amapinv.then(amap)
506 result = cmp2.simplified()
507 self.assertIsInstance(result, ast.UnitMap)
510if __name__ == "__main__": 510 ↛ 511line 510 didn't jump to line 511, because the condition on line 510 was never true
511 unittest.main()