Coverage for tests/test_polyMap.py: 9%
207 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
2import unittest
4import numpy as np
5import numpy.testing as npt
7import astshim as ast
8from astshim.test import MappingTestCase
11class TestPolyMap(MappingTestCase):
13 def test_PolyMapIterativeInverse(self):
14 """Test a unidirectional polymap with its default iterative inverse
15 """
16 coeff_f = np.array([
17 [1.2, 1, 2, 0],
18 [-0.5, 1, 1, 1],
19 [1.0, 2, 0, 1],
20 ])
21 pm = ast.PolyMap(coeff_f, 2, "IterInverse=1")
22 self.assertIsInstance(pm, ast.Object)
23 self.assertIsInstance(pm, ast.Mapping)
24 self.assertIsInstance(pm, ast.PolyMap)
25 self.assertEqual(pm.nIn, 2)
26 self.assertEqual(pm.nOut, 2)
27 self.assertTrue(pm.iterInverse)
28 self.assertEqual(pm.nIterInverse, 4)
29 self.assertAlmostEqual(pm.tolInverse, 1.0E-6)
30 self.assertTrue(pm.hasForward)
31 self.assertTrue(pm.hasInverse)
33 self.checkBasicSimplify(pm)
34 self.checkCopy(pm)
36 indata = np.array([
37 [1.0, 2.0, 3.0],
38 [0.0, 1.0, 2.0],
39 ])
40 outdata = pm.applyForward(indata)
41 xin, yin = indata
42 pred_xout = (1.2 * xin * xin) - (0.5 * yin * xin)
43 pred_yout = yin
44 xout, yout = outdata
45 npt.assert_allclose(xout, pred_xout)
46 npt.assert_allclose(yout, pred_yout)
48 indata_roundtrip = pm.applyInverse(outdata)
49 npt.assert_allclose(indata, indata_roundtrip, atol=1.0e-4)
51 self.checkMappingPersistence(pm, indata)
53 def test_polyMapAttributes(self):
54 coeff_f = np.array([
55 [1.2, 1, 2, 0],
56 [-0.5, 1, 1, 1],
57 [1.0, 2, 0, 1],
58 ])
59 pm = ast.PolyMap(coeff_f, 2, "IterInverse=1, NIterInverse=6, TolInverse=1.2e-7")
60 self.assertIsInstance(pm, ast.Object)
61 self.assertIsInstance(pm, ast.Mapping)
62 self.assertIsInstance(pm, ast.PolyMap)
63 self.assertEqual(pm.nIn, 2)
64 self.assertEqual(pm.nOut, 2)
65 self.assertTrue(pm.iterInverse)
66 self.assertEqual(pm.nIterInverse, 6)
67 self.assertAlmostEqual(pm.tolInverse, 1.2E-7)
68 self.assertTrue(pm.hasForward)
69 self.assertTrue(pm.hasInverse)
71 indata = np.array([
72 [1.0, 2.0, 3.0],
73 [0.0, 1.0, 2.0],
74 ])
75 outdata = pm.applyForward(indata)
76 xin, yin = indata
77 pred_xout = (1.2 * xin * xin) - (0.5 * yin * xin)
78 pred_yout = yin
79 xout, yout = outdata
80 npt.assert_allclose(xout, pred_xout)
81 npt.assert_allclose(yout, pred_yout)
83 indata_roundtrip = pm.applyInverse(outdata)
84 npt.assert_allclose(indata, indata_roundtrip, atol=1.0e-6)
86 self.checkMappingPersistence(pm, indata)
88 def test_polyMapNoInverse(self):
89 """Test a unidirectional polymap with no numeric inverse
90 """
91 coeff_f = np.array([
92 [1.2, 1, 2, 0],
93 [-0.5, 1, 1, 1],
94 [1.0, 2, 0, 1],
95 ])
96 pm = ast.PolyMap(coeff_f, 2)
97 self.assertIsInstance(pm, ast.PolyMap)
98 self.assertEqual(pm.nIn, 2)
99 self.assertEqual(pm.nOut, 2)
100 self.assertTrue(pm.hasForward)
101 self.assertFalse(pm.hasInverse)
102 self.assertFalse(pm.iterInverse)
104 indata = np.array([
105 [1.0, 2.0, 3.0],
106 [0.0, 1.0, 2.0],
107 ])
108 outdata = pm.applyForward(indata)
109 with self.assertRaises(RuntimeError):
110 pm.applyInverse(indata)
112 pminv = pm.inverted()
113 self.assertFalse(pminv.hasForward)
114 self.assertTrue(pminv.hasInverse)
115 self.assertTrue(pminv.isInverted)
116 self.assertFalse(pm.iterInverse)
118 outdata2 = pminv.applyInverse(indata)
119 # outdata and outdata2 should be identical because inverting
120 # swaps the behavior of applyForward and applyInverse
121 npt.assert_equal(outdata, outdata2)
122 with self.assertRaises(RuntimeError):
123 pminv.applyForward(indata)
125 self.checkMappingPersistence(pm, indata)
127 def test_PolyMapBidirectional(self):
128 coeff_f = np.array([
129 [1., 1, 1, 0],
130 [1., 1, 0, 1],
131 [1., 2, 1, 0],
132 [-1., 2, 0, 1]
133 ])
134 coeff_i = np.array([
135 [0.5, 1, 1, 0],
136 [0.5, 1, 0, 1],
137 [0.5, 2, 1, 0],
138 [-0.5, 2, 0, 1],
139 ])
140 pm = ast.PolyMap(coeff_f, coeff_i)
141 self.assertEqual(pm.nIn, 2)
142 self.assertEqual(pm.nOut, 2)
144 self.checkBasicSimplify(pm)
145 self.checkCopy(pm)
147 indata = np.array([
148 [1.0, 2.0, 3.0],
149 [0.0, 1.0, 2.0],
150 ])
152 self.checkRoundTrip(pm, indata)
153 self.checkMappingPersistence(pm, indata)
155 def test_PolyMapEmptyForwardCoeffs(self):
156 """Test constructing a PolyMap with empty forward coefficients
157 """
158 # TODO: DM-38580
159 # This two-step way of creating a zero-size array gives
160 # an array with non-zero strides which makes pybind11/
161 # ndarray happy.
162 coeff_f = np.array([], dtype=float)
163 coeff_f.shape = (0, 4)
164 coeff_i = np.array([
165 [0.5, 1, 1, 0],
166 [0.5, 1, 0, 1],
167 [0.5, 2, 1, 0],
168 [-0.5, 2, 0, 1],
169 ])
170 pm = ast.PolyMap(coeff_f, coeff_i)
171 self.assertEqual(pm.nIn, 2)
172 self.assertEqual(pm.nOut, 2)
174 self.checkBasicSimplify(pm)
175 self.checkCopy(pm)
177 self.assertFalse(pm.hasForward)
178 self.assertTrue(pm.hasInverse)
179 self.assertFalse(pm.iterInverse)
181 def test_PolyMapEmptyInverseCoeffs(self):
182 """Test constructing a PolyMap with empty inverse coefficients
183 """
184 coeff_f = np.array([
185 [1., 1, 1, 0],
186 [1., 1, 0, 1],
187 [1., 2, 1, 0],
188 [-1., 2, 0, 1]
189 ])
190 # TODO: DM-38580
191 # This two-step way of creating a zero-size array gives
192 # an array with non-zero strides which makes pybind11/
193 # ndarray happy.
194 coeff_i = np.array([], dtype=float)
195 coeff_i.shape = (0, 4)
196 pm = ast.PolyMap(coeff_f, coeff_i)
197 self.assertEqual(pm.nIn, 2)
198 self.assertEqual(pm.nOut, 2)
200 self.checkBasicSimplify(pm)
201 self.checkCopy(pm)
203 self.assertTrue(pm.hasForward)
204 self.assertFalse(pm.hasInverse)
205 self.assertFalse(pm.iterInverse)
207 indata = np.array([
208 [1.0, 2.0, 3.0],
209 [0.0, 1.0, 2.0],
210 ])
211 self.checkMappingPersistence(pm, indata)
213 def test_PolyMapNoTransform(self):
214 """Test constructing a PolyMap with neither forward nor inverse
215 coefficients
216 """
217 # TODO: DM-38580
218 # This two-step way of creating a zero-size array gives
219 # an array with non-zero strides which makes pybind11/
220 # ndarray happy.
221 coeff_f = np.array([], dtype=float)
222 coeff_f.shape = (0, 4)
223 coeff_i = np.array([], dtype=float)
224 coeff_i.shape = (0, 3)
226 with self.assertRaises(ValueError):
227 ast.PolyMap(coeff_f, coeff_i)
229 with self.assertRaises(ValueError):
230 ast.PolyMap(coeff_f, 3)
232 def test_PolyMapPolyTranTrivial(self):
233 coeff_f = np.array([
234 [1., 1, 1, 0],
235 [1., 1, 0, 1],
236 [1., 2, 1, 0],
237 [-1., 2, 0, 1]
238 ])
239 coeff_i = np.array([
240 [0.5, 1, 1, 0],
241 [0.5, 1, 0, 1],
242 [0.5, 2, 1, 0],
243 [-0.5, 2, 0, 1],
244 ])
245 pm = ast.PolyMap(coeff_f, coeff_i)
247 indata = np.array([
248 [1.0, 2.0, 3.0],
249 [0.0, 1.0, 2.0],
250 ])
252 outdata = pm.applyForward(indata)
254 # create a PolyMap with an identical forward transform and a fit
255 # inverse.
256 forward = False
257 pm2 = pm.polyTran(forward, 1.0E-10, 1.0E-10, 4, [-1.0, -1.0], [1.0, 1.0])
258 outdata2 = pm2.applyForward(indata)
259 npt.assert_equal(outdata, outdata2)
260 indata2 = pm2.applyInverse(outdata)
261 npt.assert_allclose(indata, indata2, atol=1.0e-10)
263 self.checkMappingPersistence(pm, indata)
264 self.checkMappingPersistence(pm2, indata)
266 def test_PolyMapPolyTranNontrivial(self):
267 """Test PolyMap.polyTran on a non-trivial case
268 """
269 # Approximate "field angle to focal plane" transformation coefficients
270 # for LSST thus the domain of the forward direction is
271 # 1.75 degrees = 0.0305 radians.
272 # The camera has 10 um pixels = 0.01 mm
273 # The desired accuracy of the inverse transformation is
274 # 0.001 pixels = 1e-5 mm = 9.69e-10 radians.
275 plateScaleRad = 9.69627362219072e-05 # radians per mm
276 radialCoeff = np.array([0.0, 1.0, 0.0, 0.925]) / plateScaleRad
277 polyCoeffs = []
278 for i, coeff in enumerate(radialCoeff):
279 polyCoeffs.append((coeff, 1, i))
280 polyCoeffs = np.array(polyCoeffs)
281 fieldAngleToFocalPlane = ast.PolyMap(polyCoeffs, 1)
283 atolRad = 1.0e-9
284 fieldAngleToFocalPlane2 = fieldAngleToFocalPlane.polyTran(forward=False, acc=atolRad, maxacc=atolRad,
285 maxorder=10, lbnd=[0], ubnd=[0.0305])
286 fieldAngle = np.linspace(0, 0.0305, 100)
287 focalPlane = fieldAngleToFocalPlane.applyForward(fieldAngle)
288 fieldAngleRoundTrip = fieldAngleToFocalPlane2.applyInverse(focalPlane)
289 npt.assert_allclose(fieldAngle, fieldAngleRoundTrip, atol=atolRad)
291 # Verify that polyTran cannot fit the inverse when maxorder is
292 # too small.
293 with self.assertRaises(RuntimeError):
294 fieldAngleToFocalPlane.polyTran(forward=False, acc=atolRad, maxacc=atolRad,
295 maxorder=3, lbnd=[0], ubnd=[0.0305])
297 def test_PolyMapIterInverseDominates(self):
298 """Test that IterInverse dominates inverse coefficients
299 for applyInverse.
300 """
301 coeff_f = np.array([
302 [1., 1, 1],
303 ])
304 # these coefficients don't match coeff_f, in that the inverse mapping
305 # does not undo the forward mapping (as proven below)
306 coeff_i = np.array([
307 [25., 1, 2],
308 ])
309 polyMap = ast.PolyMap(coeff_f, coeff_i, "IterInverse=1")
311 indata = np.array([-0.5, 0.5, 1.1, 1.8])
312 outdata = polyMap.applyForward(indata)
313 indata_roundtrip = polyMap.applyInverse(outdata)
314 npt.assert_allclose(indata, indata_roundtrip)
316 # Prove that without the iterative inverse the PolyMap does not invert
317 # correctly.
318 polyMap2 = ast.PolyMap(coeff_f, coeff_i)
319 indata_roundtrip2 = polyMap2.applyInverse(outdata)
320 self.assertFalse(np.allclose(indata, indata_roundtrip2))
322 def test_PolyMapPolyTranIterInverse(self):
323 """Test PolyTran on a PolyMap that has an iterative inverse
325 The result should use the fit inverse, not the iterative inverse
326 """
327 coeff_f = np.array([
328 [1., 1, 1],
329 ])
330 for polyMap in (
331 ast.PolyMap(coeff_f, 1, "IterInverse=1"),
332 ast.PolyMap(coeff_f, coeff_f, "IterInverse=1"),
333 ):
334 # make sure IterInverse is True and set
335 self.assertTrue(polyMap.iterInverse)
336 self.assertTrue(polyMap.test("IterInverse"))
338 # fit inverse; this should clear iterInverse
339 polyMapFitInv = polyMap.polyTran(False, 1.0E-10, 1.0E-10, 4, [-1.0], [1.0])
340 self.assertFalse(polyMapFitInv.iterInverse)
341 self.assertFalse(polyMapFitInv.test("IterInverse"))
343 # Fit forward direction of inverted mapping; this should also
344 # clear IterInverse.
345 polyMapInvFitFwd = polyMap.inverted().polyTran(True, 1.0E-10, 1.0E-10, 4, [-1.0], [1.0])
346 self.assertFalse(polyMapInvFitFwd.iterInverse)
347 self.assertFalse(polyMapFitInv.test("IterInverse"))
349 # cannot fit forward because inverse is iterative
350 with self.assertRaises(ValueError):
351 polyMap.polyTran(True, 1.0E-10, 1.0E-10, 4, [-1.0], [1.0])
353 # Cannot fit inverse of inverted mapping because forward is
354 # iterative.
355 with self.assertRaises(ValueError):
356 polyMap.inverted().polyTran(False, 1.0E-10, 1.0E-10, 4, [-1.0], [1.0])
358 def test_PolyMapPolyMapUnivertible(self):
359 """Test polyTran on a PolyMap without a single-valued inverse
361 The equation is y = x^2 - x^3, whose inverse has 3 values
362 between roughly -0.66 and 2.0
363 """
364 coeff_f = np.array([
365 [2.0, 1, 2],
366 [-1.0, 1, 3],
367 ])
368 pm = ast.PolyMap(coeff_f, 1, "IterInverse=1")
370 self.checkBasicSimplify(pm)
371 self.checkCopy(pm)
373 indata = np.array([-0.5, 0.5, 1.1, 1.8])
374 pred_outdata = (2.0*indata.T**2 - indata.T**3).T
375 outdata = pm.applyForward(indata)
376 npt.assert_allclose(outdata, pred_outdata)
378 # the iterative inverse should give valid values
379 indata_iterative = pm.applyInverse(outdata)
380 outdata_roundtrip = pm.applyForward(indata_iterative)
381 npt.assert_allclose(outdata, outdata_roundtrip)
383 self.checkMappingPersistence(pm, indata)
385 with self.assertRaises(RuntimeError):
386 # includes the range where the inverse has multiple values,
387 # so no inverse is possible
388 pm.polyTran(False, 1e-3, 1e-3, 10, [-1.0], [2.5])
390 def test_PolyMapDM10496(self):
391 """Test for a segfault when simplifying a SeriesMap
393 We saw an intermittent segfault when simplifying a SeriesMap
394 consisting of the inverse of PolyMap with 2 inputs and one output
395 followed by its inverse (which should simplify to a UnitMap
396 with one input and one output). David Berry fixed this bug in AST
397 2017-05-10.
399 I tried this test on an older version of astshim and found that it
400 triggering a segfault nearly every time.
401 """
402 coeff_f = np.array([
403 [-1.1, 1, 2, 0],
404 [1.3, 1, 3, 1],
405 ])
406 coeff_i = np.array([
407 [1.6, 1, 3],
408 [-3.6, 2, 1],
409 ])
411 # execute many times to increase the odds of a segfault
412 for i in range(1000):
413 amap = ast.PolyMap(coeff_f, coeff_i)
414 amapinv = amap.inverted()
415 cmp2 = amapinv.then(amap)
416 result = cmp2.simplified()
417 self.assertIsInstance(result, ast.UnitMap)
420if __name__ == "__main__": 420 ↛ 421line 420 didn't jump to line 421, because the condition on line 420 was never true
421 unittest.main()