Coverage for tests/test_polyMap.py: 9%

207 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-21 03:01 -0700

1 

2import unittest 

3 

4import numpy as np 

5import numpy.testing as npt 

6 

7import astshim as ast 

8from astshim.test import MappingTestCase 

9 

10 

11class TestPolyMap(MappingTestCase): 

12 

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) 

32 

33 self.checkBasicSimplify(pm) 

34 self.checkCopy(pm) 

35 

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) 

47 

48 indata_roundtrip = pm.applyInverse(outdata) 

49 npt.assert_allclose(indata, indata_roundtrip, atol=1.0e-4) 

50 

51 self.checkMappingPersistence(pm, indata) 

52 

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) 

70 

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) 

82 

83 indata_roundtrip = pm.applyInverse(outdata) 

84 npt.assert_allclose(indata, indata_roundtrip, atol=1.0e-6) 

85 

86 self.checkMappingPersistence(pm, indata) 

87 

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) 

103 

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) 

111 

112 pminv = pm.inverted() 

113 self.assertFalse(pminv.hasForward) 

114 self.assertTrue(pminv.hasInverse) 

115 self.assertTrue(pminv.isInverted) 

116 self.assertFalse(pm.iterInverse) 

117 

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) 

124 

125 self.checkMappingPersistence(pm, indata) 

126 

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) 

143 

144 self.checkBasicSimplify(pm) 

145 self.checkCopy(pm) 

146 

147 indata = np.array([ 

148 [1.0, 2.0, 3.0], 

149 [0.0, 1.0, 2.0], 

150 ]) 

151 

152 self.checkRoundTrip(pm, indata) 

153 self.checkMappingPersistence(pm, indata) 

154 

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) 

173 

174 self.checkBasicSimplify(pm) 

175 self.checkCopy(pm) 

176 

177 self.assertFalse(pm.hasForward) 

178 self.assertTrue(pm.hasInverse) 

179 self.assertFalse(pm.iterInverse) 

180 

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) 

199 

200 self.checkBasicSimplify(pm) 

201 self.checkCopy(pm) 

202 

203 self.assertTrue(pm.hasForward) 

204 self.assertFalse(pm.hasInverse) 

205 self.assertFalse(pm.iterInverse) 

206 

207 indata = np.array([ 

208 [1.0, 2.0, 3.0], 

209 [0.0, 1.0, 2.0], 

210 ]) 

211 self.checkMappingPersistence(pm, indata) 

212 

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) 

225 

226 with self.assertRaises(ValueError): 

227 ast.PolyMap(coeff_f, coeff_i) 

228 

229 with self.assertRaises(ValueError): 

230 ast.PolyMap(coeff_f, 3) 

231 

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) 

246 

247 indata = np.array([ 

248 [1.0, 2.0, 3.0], 

249 [0.0, 1.0, 2.0], 

250 ]) 

251 

252 outdata = pm.applyForward(indata) 

253 

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) 

262 

263 self.checkMappingPersistence(pm, indata) 

264 self.checkMappingPersistence(pm2, indata) 

265 

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) 

282 

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) 

290 

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]) 

296 

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") 

310 

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) 

315 

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

321 

322 def test_PolyMapPolyTranIterInverse(self): 

323 """Test PolyTran on a PolyMap that has an iterative inverse 

324 

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")) 

337 

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")) 

342 

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")) 

348 

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]) 

352 

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]) 

357 

358 def test_PolyMapPolyMapUnivertible(self): 

359 """Test polyTran on a PolyMap without a single-valued inverse 

360 

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") 

369 

370 self.checkBasicSimplify(pm) 

371 self.checkCopy(pm) 

372 

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) 

377 

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) 

382 

383 self.checkMappingPersistence(pm, indata) 

384 

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]) 

389 

390 def test_PolyMapDM10496(self): 

391 """Test for a segfault when simplifying a SeriesMap 

392 

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. 

398 

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 ]) 

410 

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) 

418 

419 

420if __name__ == "__main__": 420 ↛ 421line 420 didn't jump to line 421, because the condition on line 420 was never true

421 unittest.main()