Coverage for tests/test_chebyMap.py: 8%

244 statements  

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

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

135 

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

141 

142 refCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f) 

143 

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) 

159 

160 outdata = chebyMap1.applyForward(indata) 

161 

162 with self.assertRaises(RuntimeError): 

163 chebyMap1.applyInverse(indata) 

164 

165 pred_outdata = refCheby.transform(indata) 

166 npt.assert_allclose(outdata, pred_outdata) 

167 

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) 

183 

184 outdata2 = chebyMap2.applyForward(indata) 

185 npt.assert_allclose(outdata2, outdata) 

186 

187 with self.assertRaises(RuntimeError): 

188 chebyMap2.applyInverse(indata) 

189 

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) 

202 

203 outdata3 = chebyMap3.applyInverse(indata) 

204 npt.assert_allclose(outdata3, outdata) 

205 

206 with self.assertRaises(RuntimeError): 

207 chebyMap3.applyForward(indata) 

208 

209 def test_ChebyMapBidirectional(self): 

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

211 

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] 

218 

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

224 

225 lbnd_i = [-3.0] 

226 ubnd_i = [-1.0] 

227 

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) 

239 

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 ) 

250 

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) 

259 

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 ) 

270 

271 refCheby_f = ReferenceCheby(referenceFunc_f, lbnd_f, ubnd_f) 

272 refCheby_i = ReferenceCheby(referenceFunc_i, lbnd_i, ubnd_i) 

273 

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) 

277 

278 self.checkBasicSimplify(chebyMap) 

279 self.checkCopy(chebyMap) 

280 self.checkMappingPersistence(chebyMap, indata_f) 

281 

282 outdata_f = chebyMap.applyForward(indata_f) 

283 des_outdata_f = refCheby_f.transform(indata_f) 

284 

285 npt.assert_allclose(outdata_f, des_outdata_f) 

286 

287 outdata_i = chebyMap.applyInverse(indata_i) 

288 des_outdata_i = refCheby_i.transform(indata_i) 

289 

290 npt.assert_allclose(outdata_i, des_outdata_i) 

291 

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] 

297 

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

303 

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) 

320 

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 ) 

339 

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) 

344 

345 outdata = chebyMap1.applyForward(indata) 

346 

347 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f) 

348 des_outdata = referenceCheby.transform(indata) 

349 

350 npt.assert_allclose(outdata, des_outdata) 

351 

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) 

361 

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) 

372 

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] 

380 

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

386 

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) 

397 

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 ) 

412 

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) 

417 

418 outdata = chebyMap1.applyForward(indata) 

419 

420 referenceCheby = ReferenceCheby(referenceFunc, lbnd_f, ubnd_f) 

421 des_outdata = referenceCheby.transform(indata) 

422 

423 npt.assert_allclose(outdata, des_outdata) 

424 

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) 

428 

429 def test_chebyGetDomain(self): 

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

431 

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] 

437 

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

447 

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

449 

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

455 

456 outdata = chebyMap1.applyForward(indata) 

457 pred_lbnd = outdata.min(1) 

458 pred_ubnd = outdata.max(1) 

459 

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) 

463 

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] 

469 

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

475 

476 normPoints = normalize(points, lbnd, ubnd) 

477 for normAxis in normPoints: 

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

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

480 

481 def test_ChebyMapDM10496(self): 

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

483 

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. 

489 

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] 

505 

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) 

513 

514 

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

516 unittest.main()