Coverage for tests/test_transformFactory.py: 16%

227 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-14 02:44 -0700

1# 

2# LSST Data Management System 

3# Copyright 2017 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23"""Tests for custom Transforms and their factories 

24""" 

25 

26import math 

27import unittest 

28 

29import numpy as np 

30from numpy.testing import assert_allclose 

31 

32from astshim.test import makeForwardPolyMap 

33 

34import lsst.geom 

35import lsst.afw.geom as afwGeom 

36from lsst.afw.geom.testUtils import TransformTestBaseClass 

37import lsst.pex.exceptions as pexExcept 

38import lsst.utils.tests 

39 

40 

41class TransformFactoryTestSuite(TransformTestBaseClass): 

42 

43 def setUp(self): 

44 TransformTestBaseClass.setUp(self) 

45 self.endpointPrefixes = tuple( 

46 x for x in self.endpointPrefixes if x != "SpherePoint") 

47 

48 def point2DList(self): 

49 for x in (-1.1, 0, 2.2): 

50 for y in (3.1, 0, 2.1): 

51 yield lsst.geom.Point2D(x, y) 

52 

53 def testLinearize(self): 

54 for transform, invertible in ( 

55 (afwGeom.TransformPoint2ToPoint2(makeForwardPolyMap(2, 2)), False), 

56 (afwGeom.makeIdentityTransform(), True), 

57 (afwGeom.makeTransform(lsst.geom.AffineTransform(np.array([[3.0, -2.0], [2.0, -1.0]]))), True), 

58 (afwGeom.makeRadialTransform([0.0, 8.0e-05, 0.0, -4.5e-12]), True), 

59 ): 

60 self.checkLinearize(transform, invertible) 

61 

62 def checkLinearize(self, transform, invertible): 

63 """Test whether a specific transform is correctly linearized. 

64 

65 Parameters 

66 ---------- 

67 transform: `lsst.afw.geom.Transform` 

68 the transform whose linearization will be tested. Should not be 

69 strongly curved within ~1 unit of the origin, or the test may rule 

70 the approximation isn't good enough. 

71 invertible: `bool` 

72 whether `transform` is invertible. The test will verify that the 

73 linearized form is invertible iff `transform` is. If `transform` 

74 is invertible, the test will also verify that the inverse of the 

75 linearization approximates the inverse of `transform`. 

76 """ 

77 fromEndpoint = transform.fromEndpoint 

78 toEndpoint = transform.toEndpoint 

79 nIn = fromEndpoint.nAxes 

80 nOut = toEndpoint.nAxes 

81 msg = "TransformClass={}, nIn={}, nOut={}".format(type(transform).__name__, nIn, nOut) 

82 

83 rawLinPoint = self.makeRawPointData(nIn) 

84 linPoint = fromEndpoint.pointFromData(rawLinPoint) 

85 affine = afwGeom.linearizeTransform(transform, linPoint) 

86 self.assertIsInstance(affine, lsst.geom.AffineTransform) 

87 

88 # Does affine match exact transform at linPoint? 

89 outPoint = transform.applyForward(linPoint) 

90 outPointLinearized = affine(linPoint) 

91 assert_allclose(toEndpoint.dataFromPoint(outPoint), 

92 toEndpoint.dataFromPoint(outPointLinearized), 

93 err_msg=msg) 

94 jacobian = transform.getJacobian(linPoint) 

95 jacobianLinearized = affine.getLinear().getMatrix() 

96 assert_allclose(jacobian, jacobianLinearized) 

97 

98 # Is affine a local approximation around linPoint? 

99 for deltaFrom in ( 

100 np.zeros(nIn), 

101 np.full(nIn, 0.1), 

102 np.array([0.1, -0.15, 0.20, -0.05, 0.0, -0.1][0:nIn]) 

103 ): 

104 tweakedInPoint = fromEndpoint.pointFromData( 

105 rawLinPoint + deltaFrom) 

106 tweakedOutPoint = transform.applyForward(tweakedInPoint) 

107 tweakedOutPointLinearized = affine(tweakedInPoint) 

108 assert_allclose( 

109 toEndpoint.dataFromPoint(tweakedOutPoint), 

110 toEndpoint.dataFromPoint(tweakedOutPointLinearized), 

111 atol=1e-3, 

112 err_msg=msg) 

113 

114 # Is affine invertible? 

115 # AST lets all-zero MatrixMaps be invertible though inverse 

116 # ill-defined; exclude this case 

117 if invertible: 

118 rng = np.random.RandomState(42) 

119 nDelta = 100 

120 inverse = affine.inverted() 

121 deltaFrom = rng.normal(0.0, 10.0, (nIn, nDelta)) 

122 for i in range(nDelta): 

123 pointMsg = "{}, point={}".format(msg, tweakedInPoint) 

124 tweakedInPoint = fromEndpoint.pointFromData( 

125 rawLinPoint + deltaFrom[:, i]) 

126 tweakedOutPoint = affine(tweakedInPoint) 

127 

128 roundTrip = inverse(tweakedOutPoint) 

129 assert_allclose( 

130 roundTrip, tweakedInPoint, 

131 err_msg=pointMsg) 

132 assert_allclose( 

133 inverse.getLinear().getMatrix(), 

134 np.linalg.inv(jacobian), 

135 err_msg=pointMsg) 

136 else: 

137 # TODO: replace with correct type after fixing DM-11248 

138 with self.assertRaises(Exception): 

139 affine.inverted() 

140 

141 # Can't test exceptions without reliable way to make invalid transform 

142 

143 def checkGenericTransform(self, tFactory, tConfig, transformCheck=None, 

144 **kwargs): 

145 """Check Transform by building it from a factory. 

146 """ 

147 self.checkConfig(tFactory, tConfig, transformCheck, **kwargs) 

148 

149 with lsst.utils.tests.getTempFilePath(".py") as filePath: 

150 tConfig.save(filePath) 

151 loadConfig = tConfig.__class__() 

152 loadConfig.load(filePath) 

153 self.checkConfig(tFactory, loadConfig, transformCheck, **kwargs) 

154 

155 def checkConfig(self, tFactory, tConfig, transformCheck, **kwargs): 

156 """Check Transform built from a particular config 

157 """ 

158 transform = tFactory(tConfig) 

159 self.checkRoundTrip(transform, **kwargs) 

160 if transformCheck is not None: 

161 transformCheck(transform) 

162 

163 def checkRoundTrip(self, transform, **kwargs): 

164 """Check round trip of transform 

165 """ 

166 for fromPoint in self.point2DList(): 

167 toPoint = transform.applyForward(fromPoint) 

168 roundTripPoint = transform.applyInverse(toPoint) 

169 # Don't let NaNs pass the test! 

170 assert_allclose(roundTripPoint, fromPoint, atol=1e-14, **kwargs) 

171 

172 def testIdentity(self): 

173 """Test identity transform. 

174 """ 

175 identFactory = afwGeom.transformRegistry["identity"] 

176 identConfig = identFactory.ConfigClass() 

177 self.checkGenericTransform(identFactory, identConfig, 

178 self.checkIdentity) 

179 

180 def checkIdentity(self, transform): 

181 for fromPoint in self.point2DList(): 

182 toPoint = transform.applyForward(fromPoint) 

183 self.assertPairsAlmostEqual(fromPoint, toPoint) 

184 

185 def testDefaultAffine(self): 

186 """Test affine = affine Transform with default coeffs (identity transform) 

187 """ 

188 affineFactory = afwGeom.transformRegistry["affine"] 

189 affineConfig = affineFactory.ConfigClass() 

190 self.checkGenericTransform(affineFactory, affineConfig, 

191 self.checkIdentity) 

192 

193 def testTranslateAffine(self): 

194 """Test affine = affine Transform with just translation coefficients 

195 """ 

196 affineFactory = afwGeom.transformRegistry["affine"] 

197 affineConfig = affineFactory.ConfigClass() 

198 affineConfig.translation = (1.2, -3.4) 

199 

200 def check(transform): 

201 self.checkTranslateAffine( 

202 transform, 

203 lsst.geom.Extent2D(*affineConfig.translation)) 

204 self.checkGenericTransform(affineFactory, affineConfig, check) 

205 

206 def checkTranslateAffine(self, transform, offset): 

207 for fromPoint in self.point2DList(): 

208 toPoint = transform.applyForward(fromPoint) 

209 predToPoint = fromPoint + offset 

210 self.assertPairsAlmostEqual(toPoint, predToPoint) 

211 

212 def testLinearAffine(self): 

213 """Test affine = affine Transform with just linear coefficients 

214 """ 

215 affineFactory = afwGeom.transformRegistry["affine"] 

216 affineConfig = affineFactory.ConfigClass() 

217 rotAng = 0.25 # radians 

218 xScale = 1.2 

219 yScale = 0.8 

220 affineConfig.linear = ( 

221 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale, 

222 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale, 

223 ) 

224 

225 def check(transform): 

226 self.checkLinearAffine(transform, affineConfig.linear) 

227 self.checkGenericTransform(affineFactory, affineConfig, check) 

228 

229 def checkLinearAffine(self, transform, matrix): 

230 for fromPoint in self.point2DList(): 

231 toPoint = transform.applyForward(fromPoint) 

232 predToPoint = lsst.geom.Point2D( 

233 matrix[0] * fromPoint[0] 

234 + matrix[1] * fromPoint[1], 

235 matrix[2] * fromPoint[0] 

236 + matrix[3] * fromPoint[1], 

237 ) 

238 self.assertPairsAlmostEqual(toPoint, predToPoint) 

239 

240 def testFullAffine(self): 

241 """Test affine = affine Transform with arbitrary coefficients 

242 """ 

243 affineFactory = afwGeom.transformRegistry["affine"] 

244 affineConfig = affineFactory.ConfigClass() 

245 affineConfig.translation = (-2.1, 3.4) 

246 rotAng = 0.832 # radians 

247 xScale = 3.7 

248 yScale = 45.3 

249 affineConfig.linear = ( 

250 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale, 

251 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale, 

252 ) 

253 

254 def check(transform): 

255 self.checkFullAffine( 

256 transform, 

257 lsst.geom.Extent2D(*affineConfig.translation), 

258 affineConfig.linear) 

259 self.checkGenericTransform(affineFactory, affineConfig, check) 

260 

261 def checkFullAffine(self, transform, offset, matrix): 

262 for fromPoint in self.point2DList(): 

263 toPoint = transform.applyForward(fromPoint) 

264 predToPoint = lsst.geom.Point2D( 

265 matrix[0] * fromPoint[0] 

266 + matrix[1] * fromPoint[1], 

267 matrix[2] * fromPoint[0] 

268 + matrix[3] * fromPoint[1], 

269 ) 

270 predToPoint = predToPoint + offset 

271 self.assertPairsAlmostEqual(toPoint, predToPoint) 

272 

273 def testRadial(self): 

274 """Test radial = radial Transform 

275 """ 

276 radialFactory = afwGeom.transformRegistry["radial"] 

277 radialConfig = radialFactory.ConfigClass() 

278 radialConfig.coeffs = (0.0, 8.5165e-05, 0.0, -4.5014e-12) 

279 

280 def check(transform): 

281 self.checkRadial(transform, radialConfig.coeffs) 

282 self.checkGenericTransform(radialFactory, radialConfig, check) 

283 

284 invertibleCoeffs = (0.0, 1.0, 0.05) 

285 inverseCoeffs = (0.0, 1.0, -0.05, 0.005, -0.000625, 0.0000875, 

286 -1.3125e-5, 2.0625e-6, -3.3515625e-7, 5.5859375e-8, 

287 -9.49609375e-9, 1.640234375e-9, -2.870410156e-10) 

288 transform = afwGeom.makeRadialTransform(invertibleCoeffs, 

289 inverseCoeffs) 

290 self.checkRadialInvertible(transform, invertibleCoeffs) 

291 

292 def checkRadial(self, transform, coeffs): 

293 if len(coeffs) < 4: 

294 coeffs = tuple(coeffs + (0.0,) * (4 - len(coeffs))) 

295 for fromPoint in self.point2DList(): 

296 fromRadius = math.hypot(fromPoint[0], fromPoint[1]) 

297 fromAngle = math.atan2(fromPoint[1], fromPoint[0]) 

298 predToRadius = fromRadius * \ 

299 (coeffs[3] * fromRadius**2 + coeffs[2] * fromRadius + coeffs[1]) 

300 if predToRadius > 0: 

301 predToPoint = lsst.geom.Point2D( 

302 predToRadius * math.cos(fromAngle), 

303 predToRadius * math.sin(fromAngle)) 

304 else: 

305 predToPoint = lsst.geom.Point2D() 

306 toPoint = transform.applyForward(fromPoint) 

307 # Don't let NaNs pass the test! 

308 assert_allclose(toPoint, predToPoint, atol=1e-14) 

309 

310 def checkRadialInvertible(self, transform, coeffs): 

311 self.checkRadial(transform, coeffs) 

312 self.checkRoundTrip(transform, rtol=0.01) 

313 

314 def testBadRadial(self): 

315 """Test radial with invalid coefficients 

316 """ 

317 for badCoeffs in ( 

318 (0.0,), # len(coeffs) must be > 1 

319 (0.1, 1.0), # coeffs[0] must be zero 

320 (0.0, 0.0), # coeffs[1] must be nonzero 

321 (0.0, 0.0, 0.1), # coeffs[1] must be nonzero 

322 ): 

323 with self.assertRaises(pexExcept.InvalidParameterError): 

324 afwGeom.makeRadialTransform(badCoeffs) 

325 

326 radialFactory = afwGeom.transformRegistry["radial"] 

327 radialConfig = radialFactory.ConfigClass() 

328 radialConfig.coeffs = badCoeffs 

329 with self.assertRaises(Exception): 

330 radialConfig.validate() 

331 

332 def testInverted(self): 

333 """Test radial = radial Transform 

334 """ 

335 affineFactory = afwGeom.transformRegistry["affine"] 

336 wrapper = afwGeom.OneTransformConfig() 

337 wrapper.transform.retarget(affineFactory) 

338 affineConfig = wrapper.transform 

339 affineConfig.translation = (-2.1, 3.4) 

340 rotAng = 0.832 # radians 

341 xScale = 3.7 

342 yScale = 45.3 

343 affineConfig.linear = ( 

344 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale, 

345 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale, 

346 ) 

347 

348 inverseFactory = afwGeom.transformRegistry["inverted"] 

349 inverseConfig = inverseFactory.ConfigClass() 

350 inverseConfig.transform = affineConfig 

351 

352 def check(transform): 

353 self.checkInverted(transform, affineConfig.apply()) 

354 self.checkGenericTransform(inverseFactory, inverseConfig, check) 

355 

356 def checkInverted(self, transform, original): 

357 for fromPoint in self.point2DList(): 

358 toPoint = transform.applyForward(fromPoint) 

359 predToPoint = original.applyInverse(fromPoint) 

360 self.assertPairsAlmostEqual(toPoint, predToPoint) 

361 roundTrip = transform.applyInverse(toPoint) 

362 predRoundTrip = original.applyForward(toPoint) 

363 self.assertPairsAlmostEqual(roundTrip, predRoundTrip) 

364 

365 def testMulti(self): 

366 """Test multi transform 

367 """ 

368 affineFactory = afwGeom.transformRegistry["affine"] 

369 wrapper0 = afwGeom.OneTransformConfig() 

370 wrapper0.transform.retarget(affineFactory) 

371 affineConfig0 = wrapper0.transform 

372 affineConfig0.translation = (-2.1, 3.4) 

373 rotAng = 0.832 # radians 

374 xScale = 3.7 

375 yScale = 45.3 

376 affineConfig0.linear = ( 

377 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale, 

378 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale, 

379 ) 

380 

381 wrapper1 = afwGeom.OneTransformConfig() 

382 wrapper1.transform.retarget(affineFactory) 

383 affineConfig1 = wrapper1.transform 

384 affineConfig1.translation = (26.5, -35.1) 

385 rotAng = -0.25 # radians 

386 xScale = 1.45 

387 yScale = 0.9 

388 affineConfig1.linear = ( 

389 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale, 

390 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale, 

391 ) 

392 

393 multiFactory = afwGeom.transformRegistry["multi"] 

394 multiConfig = multiFactory.ConfigClass() 

395 multiConfig.transformDict = { 

396 0: wrapper0, 

397 1: wrapper1, 

398 } 

399 

400 def check(transform): 

401 self.checkMulti(transform, 

402 [c.apply() for c in 

403 [affineConfig0, affineConfig1]]) 

404 self.checkGenericTransform(multiFactory, multiConfig, check) 

405 

406 def checkMulti(self, multiTransform, transformList): 

407 for fromPoint in self.point2DList(): 

408 toPoint = multiTransform.applyForward(fromPoint) 

409 predToPoint = fromPoint 

410 for transform in transformList: 

411 predToPoint = transform.applyForward(predToPoint) 

412 self.assertPairsAlmostEqual(toPoint, predToPoint) 

413 

414 

415class MemoryTester(lsst.utils.tests.MemoryTestCase): 

416 pass 

417 

418 

419def setup_module(module): 

420 lsst.utils.tests.init() 

421 

422 

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

424 lsst.utils.tests.init() 

425 unittest.main()