Coverage for tests/test_chebyMap.py: 8%

243 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-12 01:17 -0800

1import unittest 

2 

3import numpy as np 

4from numpy.polynomial.chebyshev import chebval, chebval2d 

5import numpy.testing as npt 

6 

7import astshim as ast 

8from astshim.test import MappingTestCase 

9 

10 

11def normalize(inArray, lbnd, ubnd): 

12 """Return the value of x normalized to [-1, 1] 

13 

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 

17 

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) 

27 

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 

38 

39 

40class ReferenceCheby(object): 

41 

42 def __init__(self, referenceCheby, lbnd, ubnd): 

43 """Construct a reference Chebyshev polynomial 

44 

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 

60 

61 def transform(self, inArray): 

62 """Transform data using the reference function 

63 

64 Parameters 

65 ---------- 

66 inArray : `numpy.array` 

67 Input array of points in the form used by ChebyMap.applyForward 

68 or applyInverse. 

69 

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 

82 

83 

84class TestChebyMap(MappingTestCase): 

85 

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 

91 

92 def test_chebyMapUnidirectional_2_2(self): 

93 """Test one-directional ChebyMap with 2 inputs and 2 outputs 

94 

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) 

113 

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 ) 

127 

128 null_coeff = np.zeros(shape=(0, 4)) 

129 self.assertEqual(nin, null_coeff.shape[1] - 2) 

130 

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

136 

137 refCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f) 

138 

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) 

154 

155 outdata = chebyMap1.applyForward(indata) 

156 

157 with self.assertRaises(RuntimeError): 

158 chebyMap1.applyInverse(indata) 

159 

160 pred_outdata = refCheby.transform(indata) 

161 npt.assert_allclose(outdata, pred_outdata) 

162 

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) 

178 

179 outdata2 = chebyMap2.applyForward(indata) 

180 npt.assert_allclose(outdata2, outdata) 

181 

182 with self.assertRaises(RuntimeError): 

183 chebyMap2.applyInverse(indata) 

184 

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) 

197 

198 outdata3 = chebyMap3.applyInverse(indata) 

199 npt.assert_allclose(outdata3, outdata) 

200 

201 with self.assertRaises(RuntimeError): 

202 chebyMap3.applyForward(indata) 

203 

204 def test_ChebyMapBidirectional(self): 

205 """Test a ChebyMap with separate forward and inverse mappings 

206 

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] 

213 

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

219 

220 lbnd_i = [-3.0] 

221 ubnd_i = [-1.0] 

222 

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) 

234 

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 ) 

245 

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) 

254 

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 ) 

265 

266 refCheby_f = ReferenceCheby(referenceFunc_f, lbnd_f, ubnd_f) 

267 refCheby_i = ReferenceCheby(referenceFunc_i, lbnd_i, ubnd_i) 

268 

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) 

272 

273 self.checkBasicSimplify(chebyMap) 

274 self.checkCopy(chebyMap) 

275 self.checkMappingPersistence(chebyMap, indata_f) 

276 

277 outdata_f = chebyMap.applyForward(indata_f) 

278 des_outdata_f = refCheby_f.transform(indata_f) 

279 

280 npt.assert_allclose(outdata_f, des_outdata_f) 

281 

282 outdata_i = chebyMap.applyInverse(indata_i) 

283 des_outdata_i = refCheby_i.transform(indata_i) 

284 

285 npt.assert_allclose(outdata_i, des_outdata_i) 

286 

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] 

292 

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

298 

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) 

315 

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 ) 

334 

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) 

339 

340 outdata = chebyMap1.applyForward(indata) 

341 

342 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f) 

343 des_outdata = referenceCheby.transform(indata) 

344 

345 npt.assert_allclose(outdata, des_outdata) 

346 

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) 

356 

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) 

367 

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] 

375 

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

381 

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) 

392 

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 ) 

407 

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) 

412 

413 outdata = chebyMap1.applyForward(indata) 

414 

415 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f) 

416 des_outdata = referenceCheby.transform(indata) 

417 

418 npt.assert_allclose(outdata, des_outdata) 

419 

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) 

423 

424 def test_chebyGetDomain(self): 

425 """Test ChebyMap.getDomain's ability to estimate values 

426 

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] 

432 

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

442 

443 chebyMap1 = ast.ChebyMap(coeff_f, nout, lbnd_f, ubnd_f) 

444 

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

450 

451 outdata = chebyMap1.applyForward(indata) 

452 pred_lbnd = outdata.min(1) 

453 pred_ubnd = outdata.max(1) 

454 

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) 

458 

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] 

464 

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

470 

471 normPoints = normalize(points, lbnd, ubnd) 

472 for normAxis in normPoints: 

473 self.assertAlmostEqual(normAxis.min(), -1) 

474 self.assertAlmostEqual(normAxis.max(), 1) 

475 

476 def test_ChebyMapDM10496(self): 

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

478 

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. 

484 

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] 

500 

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) 

508 

509 

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

511 unittest.main()