Coverage for tests/test_transforms.py: 18%

391 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-17 02:14 -0700

1# 

2# LSST Data Management System 

3# Copyright 2016-2017 LSST/AURA. 

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 

23import os 

24import unittest 

25 

26import numpy as np 

27 

28import lsst.utils.tests 

29import lsst.pex.exceptions 

30import lsst.geom 

31import lsst.afw.geom 

32import lsst.afw.image 

33import lsst.afw.math 

34from lsst.afw.fits import readMetadata 

35from lsst.afw.geom.testUtils import makeSipIwcToPixel, makeSipPixelToIwc 

36from lsst.meas.astrom import ( 

37 PolynomialTransform, 

38 ScaledPolynomialTransform, 

39 SipForwardTransform, 

40 SipReverseTransform, 

41 ScaledPolynomialTransformFitter, 

42 transformWcsPixels, 

43 rotateWcsPixelsBy90 

44) 

45from lsst.afw.geom.wcsUtils import getSipMatrixFromMetadata, getCdMatrixFromMetadata 

46 

47 

48def makeRandomCoefficientMatrix(n): 

49 matrix = np.random.randn(n, n) 

50 for i in range(1, n): 

51 matrix[i, (n-i):] = 0 

52 return matrix 

53 

54 

55def makeRandomAffineTransform(): 

56 return lsst.geom.AffineTransform( 

57 lsst.geom.LinearTransform(np.random.randn(2, 2)), 

58 lsst.geom.Extent2D(*np.random.randn(2)) 

59 ) 

60 

61 

62def makeRandomPolynomialTransform(order, sip=False): 

63 xc = makeRandomCoefficientMatrix(order + 1) 

64 yc = makeRandomCoefficientMatrix(order + 1) 

65 if sip: 

66 xc[0, 0] = 0 

67 yc[0, 0] = 0 

68 xc[0, 1] = 0 

69 yc[0, 1] = 0 

70 xc[1, 0] = 0 

71 yc[1, 0] = 0 

72 return PolynomialTransform(xc, yc) 

73 

74 

75def makeRandomScaledPolynomialTransform(order): 

76 return ScaledPolynomialTransform( 

77 makeRandomPolynomialTransform(order), 

78 makeRandomAffineTransform(), 

79 makeRandomAffineTransform() 

80 ) 

81 

82 

83def makeRandomSipForwardTransform(order): 

84 return SipForwardTransform( 

85 lsst.geom.Point2D(*np.random.randn(2)), 

86 lsst.geom.LinearTransform(np.random.randn(2, 2)), 

87 makeRandomPolynomialTransform(order, sip=True) 

88 ) 

89 

90 

91def makeRandomSipReverseTransform(order): 

92 origin = lsst.geom.Point2D(*np.random.randn(2)) 

93 cd = lsst.geom.LinearTransform(np.random.randn(2, 2)) 

94 poly = makeRandomPolynomialTransform(order, sip=False) 

95 return SipReverseTransform(origin, cd, poly) 

96 

97 

98class TransformTestMixin: 

99 

100 def makeRandom(self): 

101 """Create an instance of the transform being tested with random testing. 

102 """ 

103 raise NotImplementedError() 

104 

105 def assertTransformsAlmostEqual(self, a, b, minval=0.0, maxval=1.0, atol=0, rtol=1E-8): 

106 rangeval = maxval - minval 

107 aArr = [] 

108 bArr = [] 

109 for i in range(10): 

110 xy = rangeval * np.random.rand(2) - minval 

111 point = lsst.geom.Point2D(*xy) 

112 aArr.append(list(a(point))) 

113 bArr.append(list(b(point))) 

114 self.assertFloatsAlmostEqual(np.array(aArr), np.array(bArr), atol=atol, rtol=rtol) 

115 

116 def testLinearize(self): 

117 """Test that the AffineTransform returned by linearize() is equivalent 

118 to the transform at the expansion point, and matches finite differences. 

119 """ 

120 transform = self.makeRandom() 

121 point = lsst.geom.Point2D(*np.random.randn(2)) 

122 affine = transform.linearize(point) 

123 self.assertFloatsAlmostEqual(np.array(transform(point)), np.array(affine(point)), rtol=1E-14) 

124 delta = 1E-4 

125 deltaX = lsst.geom.Extent2D(delta, 0.0) 

126 deltaY = lsst.geom.Extent2D(0.0, delta) 

127 dtdx = (transform(point + deltaX) - transform(point - deltaX)) / (2*delta) 

128 dtdy = (transform(point + deltaY) - transform(point - deltaY)) / (2*delta) 

129 self.assertFloatsAlmostEqual(affine[affine.XX], dtdx.getX(), rtol=1E-6) 

130 self.assertFloatsAlmostEqual(affine[affine.YX], dtdx.getY(), rtol=1E-6) 

131 self.assertFloatsAlmostEqual(affine[affine.XY], dtdy.getX(), rtol=1E-6) 

132 self.assertFloatsAlmostEqual(affine[affine.YY], dtdy.getY(), rtol=1E-6) 

133 

134 

135class PolynomialTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin): 

136 

137 def setUp(self): 

138 np.random.seed(50) 

139 

140 def makeRandom(self): 

141 return makeRandomPolynomialTransform(4) 

142 

143 def testArrayConstructor(self): 

144 """Test that construction with coefficient arrays yields an object with 

145 copies of those arrays, and that all dimensions must be the same. 

146 """ 

147 order = 3 

148 xc = makeRandomCoefficientMatrix(order + 1) 

149 yc = makeRandomCoefficientMatrix(order + 1) 

150 p = PolynomialTransform(xc, yc) 

151 self.assertEqual(p.getOrder(), order) 

152 self.assertFloatsAlmostEqual(p.getXCoeffs(), xc, atol=0, rtol=0) 

153 self.assertFloatsAlmostEqual(p.getYCoeffs(), yc, atol=0, rtol=0) 

154 # Test that the coefficients are not a view. 

155 old = xc[0, 0] 

156 xc[0, 0] += 100.0 

157 self.assertEqual(p.getXCoeffs()[0, 0], old) 

158 # Test that rectangular coefficient arrays are not allowed. 

159 self.assertRaises( 

160 lsst.pex.exceptions.LengthError, 

161 PolynomialTransform, 

162 np.zeros((5, 4), dtype=float), 

163 np.zeros((5, 4), dtype=float) 

164 ) 

165 # Test that x and y coefficient arrays must have the same shape. 

166 self.assertRaises( 

167 lsst.pex.exceptions.LengthError, 

168 PolynomialTransform, 

169 np.zeros((5, 5), dtype=float), 

170 np.zeros((4, 4), dtype=float) 

171 ) 

172 

173 def testConvertScaledPolynomial(self): 

174 """Test that we can convert a ScaledPolynomialTransform to a PolynomialTransform. 

175 """ 

176 scaled = makeRandomScaledPolynomialTransform(4) 

177 converted = PolynomialTransform.convert(scaled) 

178 self.assertTransformsAlmostEqual(scaled, converted) 

179 

180 def testConvertSipForward(self): 

181 """Test that we can convert a SipForwardTransform to a PolynomialTransform. 

182 """ 

183 sipForward = makeRandomSipForwardTransform(4) 

184 converted = PolynomialTransform.convert(sipForward) 

185 self.assertTransformsAlmostEqual(sipForward, converted) 

186 

187 def testConvertSipReverse(self): 

188 """Test that we can convert a SipForwardTransform to a PolynomialTransform. 

189 """ 

190 sipReverse = makeRandomSipReverseTransform(4) 

191 converted = PolynomialTransform.convert(sipReverse) 

192 self.assertTransformsAlmostEqual(sipReverse, converted) 

193 

194 def testCompose(self): 

195 """Test that AffineTransforms and PolynomialTransforms can be composed 

196 into an equivalent PolynomialTransform. 

197 """ 

198 poly = makeRandomPolynomialTransform(4) 

199 affine = lsst.geom.AffineTransform( 

200 lsst.geom.LinearTransform(np.random.randn(2, 2)), 

201 lsst.geom.Extent2D(*np.random.randn(2)) 

202 ) 

203 composed1 = lsst.meas.astrom.compose(poly, affine) 

204 composed2 = lsst.meas.astrom.compose(affine, poly) 

205 self.assertTransformsAlmostEqual(composed1, lambda p: poly(affine(p))) 

206 self.assertTransformsAlmostEqual(composed2, lambda p: affine(poly(p))) 

207 # Test that composition with an identity transform is a no-op 

208 composed3 = lsst.meas.astrom.compose(poly, lsst.geom.AffineTransform()) 

209 composed4 = lsst.meas.astrom.compose(lsst.geom.AffineTransform(), poly) 

210 self.assertFloatsAlmostEqual(composed3.getXCoeffs(), poly.getXCoeffs()) 

211 self.assertFloatsAlmostEqual(composed3.getYCoeffs(), poly.getYCoeffs()) 

212 self.assertFloatsAlmostEqual(composed4.getXCoeffs(), poly.getXCoeffs()) 

213 self.assertFloatsAlmostEqual(composed4.getYCoeffs(), poly.getYCoeffs()) 

214 

215 

216class ScaledPolynomialTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin): 

217 

218 def setUp(self): 

219 np.random.seed(50) 

220 

221 def makeRandom(self): 

222 return makeRandomScaledPolynomialTransform(4) 

223 

224 def testConstruction(self): 

225 poly = makeRandomPolynomialTransform(4) 

226 inputScaling = makeRandomAffineTransform() 

227 outputScalingInverse = makeRandomAffineTransform() 

228 scaled = ScaledPolynomialTransform(poly, inputScaling, outputScalingInverse) 

229 self.assertTransformsAlmostEqual( 

230 scaled, 

231 lambda p: outputScalingInverse(poly(inputScaling(p))) 

232 ) 

233 

234 def testConvertPolynomial(self): 

235 """Test that we can convert a PolynomialTransform to a ScaledPolynomialTransform. 

236 """ 

237 poly = makeRandomPolynomialTransform(4) 

238 converted = ScaledPolynomialTransform.convert(poly) 

239 self.assertTransformsAlmostEqual(poly, converted) 

240 

241 def testConvertSipForward(self): 

242 """Test that we can convert a SipForwardTransform to a ScaledPolynomialTransform. 

243 """ 

244 sipForward = makeRandomSipForwardTransform(4) 

245 converted = ScaledPolynomialTransform.convert(sipForward) 

246 self.assertTransformsAlmostEqual(sipForward, converted) 

247 

248 def testConvertSipReverse(self): 

249 """Test that we can convert a SipReverseTransform to a ScaledPolynomialTransform. 

250 """ 

251 sipReverse = makeRandomSipReverseTransform(4) 

252 converted = ScaledPolynomialTransform.convert(sipReverse) 

253 self.assertTransformsAlmostEqual(sipReverse, converted) 

254 

255 

256class SipForwardTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin): 

257 

258 def setUp(self): 

259 np.random.seed(50) 

260 

261 def makeRandom(self): 

262 return makeRandomSipForwardTransform(4) 

263 

264 def testConstruction(self): 

265 poly = makeRandomPolynomialTransform(4, sip=True) 

266 cd = lsst.geom.LinearTransform(np.random.randn(2, 2)) 

267 crpix = lsst.geom.Point2D(*np.random.randn(2)) 

268 sip = SipForwardTransform(crpix, cd, poly) 

269 self.assertTransformsAlmostEqual( 

270 sip, 

271 lambda p: cd((p - crpix) + poly(lsst.geom.Point2D(p - crpix))) 

272 ) 

273 

274 def testConvertPolynomial(self): 

275 poly = makeRandomPolynomialTransform(4) 

276 cd = lsst.geom.LinearTransform(np.random.randn(2, 2)) 

277 crpix = lsst.geom.Point2D(*np.random.randn(2)) 

278 sip = lsst.meas.astrom.SipForwardTransform.convert(poly, crpix, cd) 

279 self.assertTransformsAlmostEqual(sip, poly) 

280 

281 def testConvertScaledPolynomialManual(self): 

282 scaled = makeRandomScaledPolynomialTransform(4) 

283 cd = lsst.geom.LinearTransform(np.random.randn(2, 2)) 

284 crpix = lsst.geom.Point2D(*np.random.randn(2)) 

285 sip = lsst.meas.astrom.SipForwardTransform.convert(scaled, crpix, cd) 

286 self.assertTransformsAlmostEqual(sip, scaled) 

287 

288 def testConvertScaledPolynomialAutomatic(self): 

289 scaled = makeRandomScaledPolynomialTransform(4) 

290 sip = lsst.meas.astrom.SipForwardTransform.convert(scaled) 

291 self.assertTransformsAlmostEqual(sip, scaled) 

292 

293 def testTransformPixels(self): 

294 sip = makeRandomSipForwardTransform(4) 

295 affine = makeRandomAffineTransform() 

296 self.assertTransformsAlmostEqual( 

297 sip.transformPixels(affine), 

298 lambda p: sip(affine.inverted()(p)) 

299 ) 

300 

301 def testMakeWcs(self): 

302 """Test SipForwardTransform, SipReverseTransform and makeWcs 

303 """ 

304 filename = os.path.join(os.path.dirname(__file__), 

305 'imgCharSources-v85501867-R01-S00.sipheader') 

306 sipMetadata = readMetadata(filename) 

307 # We're building an ICRS-based TAN-SIP using coefficients read from metadata 

308 # so ignore the RADESYS in metadata (which is missing anyway, falling back to FK5) 

309 sipMetadata.set("RADESYS", "ICRS") 

310 crpix = lsst.geom.Point2D( 

311 sipMetadata.getScalar("CRPIX1") - 1, 

312 sipMetadata.getScalar("CRPIX2") - 1, 

313 ) 

314 crval = lsst.geom.SpherePoint( 

315 sipMetadata.getScalar("CRVAL1"), 

316 sipMetadata.getScalar("CRVAL2"), lsst.geom.degrees, 

317 ) 

318 cdLinearTransform = lsst.geom.LinearTransform(getCdMatrixFromMetadata(sipMetadata)) 

319 aArr = getSipMatrixFromMetadata(sipMetadata, "A") 

320 bArr = getSipMatrixFromMetadata(sipMetadata, "B") 

321 apArr = getSipMatrixFromMetadata(sipMetadata, "AP") 

322 bpArr = getSipMatrixFromMetadata(sipMetadata, "BP") 

323 abPoly = PolynomialTransform(aArr, bArr) 

324 abRevPoly = PolynomialTransform(apArr, bpArr) 

325 fwd = SipForwardTransform(crpix, cdLinearTransform, abPoly) 

326 rev = SipReverseTransform(crpix, cdLinearTransform, abRevPoly) 

327 wcsFromMakeWcs = lsst.meas.astrom.makeWcs(fwd, rev, crval) 

328 wcsFromMetadata = lsst.afw.geom.makeSkyWcs(sipMetadata, strip=False) 

329 

330 # Check SipForwardTransform against a local implementation 

331 localPixelToIwc = makeSipPixelToIwc(sipMetadata) 

332 self.assertTransformsAlmostEqual(fwd, localPixelToIwc.applyForward, maxval=2000) 

333 

334 # Compare SipReverseTransform against a local implementation 

335 # Use the forward direction first to get sensible inputs 

336 localIwcToPixel = makeSipIwcToPixel(sipMetadata) 

337 

338 def fwdThenRev(p): 

339 return rev(fwd(p)) 

340 

341 def fwdThenLocalRev(p): 

342 return localIwcToPixel.applyForward(fwd(p)) 

343 

344 self.assertTransformsAlmostEqual(fwdThenRev, fwdThenLocalRev, maxval=2000) 

345 

346 # Check that SipReverseTransform is the inverse of SipForwardTransform; 

347 # this is not perfect because the coefficients don't define a perfect inverse 

348 def nullTransform(p): 

349 return p 

350 

351 self.assertTransformsAlmostEqual(fwdThenRev, nullTransform, maxval=2000, atol=1e-3) 

352 

353 # Check SipForwardTransform against the one contained in wcsFromMakeWcs 

354 # (Don't bother with the other direction because the WCS transform is iterative, 

355 # so it doesn't tell us anything useful about SipReverseTransform 

356 pixelToIwc = lsst.afw.geom.getPixelToIntermediateWorldCoords(wcsFromMetadata) 

357 self.assertTransformsAlmostEqual(fwd, pixelToIwc.applyForward, maxval=2000) 

358 

359 # Check a WCS constructed from SipForwardTransform, SipReverseTransform 

360 # against one constructed directly from the metadata 

361 bbox = lsst.geom.Box2D(lsst.geom.Point2D(0, 0), lsst.geom.Extent2D(2000, 2000)) 

362 self.assertWcsAlmostEqualOverBBox(wcsFromMakeWcs, wcsFromMetadata, bbox) 

363 

364 def testTransformWcsPixels(self): 

365 filename = os.path.join(os.path.dirname(__file__), 

366 'imgCharSources-v85501867-R01-S00.sipheader') 

367 wcs1 = lsst.afw.geom.makeSkyWcs(readMetadata(filename)) 

368 s = makeRandomAffineTransform() 

369 wcs2 = transformWcsPixels(wcs1, s) 

370 crvalDeg = wcs1.getSkyOrigin().getPosition(lsst.geom.degrees) 

371 

372 def t1a(p): 

373 raDeg, decDeg = crvalDeg + lsst.geom.Extent2D(p) 

374 sky = lsst.geom.SpherePoint(raDeg, decDeg, lsst.geom.degrees) 

375 return s(wcs1.skyToPixel(sky)) 

376 

377 def t2a(p): 

378 raDeg, decDeg = crvalDeg + lsst.geom.Extent2D(p) 

379 sky = lsst.geom.SpherePoint(raDeg, decDeg, lsst.geom.degrees) 

380 return wcs2.skyToPixel(sky) 

381 

382 self.assertTransformsAlmostEqual(t1a, t2a) 

383 

384 def t1b(p): 

385 sky = wcs1.pixelToSky(s.inverted()(p)) 

386 return sky.getPosition(lsst.geom.degrees) 

387 

388 def t2b(p): 

389 sky = wcs2.pixelToSky(p) 

390 return sky.getPosition(lsst.geom.degrees) 

391 

392 self.assertTransformsAlmostEqual(t1b, t2b) 

393 

394 def testRotateWcsPixelsBy90(self): 

395 filename = os.path.join(os.path.dirname(__file__), 

396 'imgCharSources-v85501867-R01-S00.sipheader') 

397 wcs0 = lsst.afw.geom.makeSkyWcs(readMetadata(filename)) 

398 w, h = 11, 12 

399 image0 = lsst.afw.image.ImageD(w, h) 

400 x, y = np.meshgrid(np.arange(w), np.arange(h)) 

401 # Make a slowly-varying image of an asymmetric function 

402 image0.getArray()[:, :] = (x/w)**2 + 0.5*(x/w)*(y/h) - 3.0*(y/h)**2 

403 dimensions = image0.getBBox().getDimensions() 

404 

405 image1 = lsst.afw.math.rotateImageBy90(image0, 1) 

406 wcs1 = rotateWcsPixelsBy90(wcs0, 1, dimensions) 

407 image2 = lsst.afw.math.rotateImageBy90(image0, 2) 

408 wcs2 = rotateWcsPixelsBy90(wcs0, 2, dimensions) 

409 image3 = lsst.afw.math.rotateImageBy90(image0, 3) 

410 wcs3 = rotateWcsPixelsBy90(wcs0, 3, dimensions) 

411 

412 bbox = image0.getBBox() 

413 image0r = lsst.afw.image.ImageD(bbox) 

414 image1r = lsst.afw.image.ImageD(bbox) 

415 image2r = lsst.afw.image.ImageD(bbox) 

416 image3r = lsst.afw.image.ImageD(bbox) 

417 

418 ctrl = lsst.afw.math.WarpingControl("nearest") 

419 lsst.afw.math.warpImage(image0r, wcs0, image0, wcs0, ctrl) 

420 lsst.afw.math.warpImage(image1r, wcs0, image1, wcs1, ctrl) 

421 lsst.afw.math.warpImage(image2r, wcs0, image2, wcs2, ctrl) 

422 lsst.afw.math.warpImage(image3r, wcs0, image3, wcs3, ctrl) 

423 

424 # warpImage doesn't seem to handle the first row and column, 

425 # even with nearest-neighbor interpolation, so we have to 

426 # ignore pixels it didn't know how to populate. 

427 def compareFinite(ref, target): 

428 finitPixels = np.isfinite(target.getArray()) 

429 self.assertGreater(finitPixels.sum(), 0.7*target.getArray().size) 

430 self.assertFloatsAlmostEqual( 

431 ref.getArray()[finitPixels], 

432 target.getArray()[finitPixels], 

433 rtol=1E-6 

434 ) 

435 

436 compareFinite(image0, image0r) 

437 compareFinite(image0, image1r) 

438 compareFinite(image0, image2r) 

439 compareFinite(image0, image3r) 

440 

441 

442class SipReverseTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin): 

443 

444 def setUp(self): 

445 np.random.seed(50) 

446 

447 def makeRandom(self): 

448 return makeRandomSipReverseTransform(4) 

449 

450 def testConstruction(self): 

451 poly = makeRandomPolynomialTransform(4) 

452 cd = lsst.geom.LinearTransform(np.random.randn(2, 2)) 

453 crpix = lsst.geom.Point2D(*np.random.randn(2)) 

454 sip = SipReverseTransform(crpix, cd, poly) 

455 offset = lsst.geom.Extent2D(crpix) 

456 cdInverse = cd.inverted() 

457 self.assertTransformsAlmostEqual( 

458 sip, 

459 lambda p: offset + lsst.geom.Extent2D(cdInverse(p)) + poly(cdInverse(p)) 

460 ) 

461 

462 def testConvertPolynomial(self): 

463 poly = makeRandomPolynomialTransform(4) 

464 cd = lsst.geom.LinearTransform(np.random.randn(2, 2)) 

465 crpix = lsst.geom.Point2D(*np.random.randn(2)) 

466 sip = lsst.meas.astrom.SipReverseTransform.convert(poly, crpix, cd) 

467 self.assertTransformsAlmostEqual(sip, poly) 

468 

469 def testConvertScaledPolynomialManual(self): 

470 scaled = makeRandomScaledPolynomialTransform(4) 

471 cd = lsst.geom.LinearTransform(np.random.randn(2, 2)) 

472 crpix = lsst.geom.Point2D(*np.random.randn(2)) 

473 sip = lsst.meas.astrom.SipReverseTransform.convert(scaled, crpix, cd) 

474 self.assertTransformsAlmostEqual(sip, scaled) 

475 

476 def testConvertScaledPolynomialAutomatic(self): 

477 scaled = makeRandomScaledPolynomialTransform(4) 

478 sip = lsst.meas.astrom.SipReverseTransform.convert(scaled) 

479 self.assertTransformsAlmostEqual(sip, scaled) 

480 

481 def testTransformPixels(self): 

482 sip = makeRandomSipReverseTransform(4) 

483 affine = makeRandomAffineTransform() 

484 self.assertTransformsAlmostEqual( 

485 sip.transformPixels(affine), 

486 lambda p: affine(sip(p)) 

487 ) 

488 

489 

490class ScaledPolynomialTransformFitterTestCase(lsst.utils.tests.TestCase): 

491 

492 def setUp(self): 

493 np.random.seed(50) 

494 

495 def testFromMatches(self): 

496 # Setup artifical matches that correspond to a known (random) PolynomialTransform. 

497 order = 3 

498 truePoly = makeRandomPolynomialTransform(order) 

499 crval = lsst.geom.SpherePoint(35.0, 10.0, lsst.geom.degrees) 

500 crpix = lsst.geom.Point2D(50, 50) 

501 cd = lsst.geom.LinearTransform.makeScaling((0.2*lsst.geom.arcseconds).asDegrees()).getMatrix() 

502 initialWcs = lsst.afw.geom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cd) 

503 bbox = lsst.geom.Box2D( 

504 crval.getPosition(lsst.geom.arcseconds) - lsst.geom.Extent2D(20, 20), 

505 crval.getPosition(lsst.geom.arcseconds) + lsst.geom.Extent2D(20, 20), 

506 ) 

507 srcSchema = lsst.afw.table.SourceTable.makeMinimalSchema() 

508 srcPosKey = lsst.afw.table.Point2DKey.addFields(srcSchema, "pos", "source position", "pix") 

509 srcErrKey = lsst.afw.table.CovarianceMatrix2fKey.addFields(srcSchema, "pos", 

510 ["x", "y"], ["pix", "pix"]) 

511 srcSchema.getAliasMap().set("slot_Centroid", "pos") 

512 nPoints = 10 

513 trueSrc = lsst.afw.table.SourceCatalog(srcSchema) 

514 trueSrc.reserve(nPoints) 

515 measSrc = lsst.afw.table.SourceCatalog(srcSchema) 

516 measSrc.reserve(nPoints) 

517 ref = lsst.afw.table.SimpleCatalog(lsst.afw.table.SimpleTable.makeMinimalSchema()) 

518 ref.reserve(nPoints) 

519 refCoordKey = ref.getCoordKey() 

520 errScaling = 1E-14 

521 matches = [] 

522 initialIwcToSky = lsst.afw.geom.getIntermediateWorldCoordsToSky(initialWcs) 

523 for i in range(nPoints): 

524 refRec = ref.addNew() 

525 raDeg, decDeg = ( 

526 np.random.uniform(low=bbox.getMinX(), high=bbox.getMaxX()), 

527 np.random.uniform(low=bbox.getMinY(), high=bbox.getMaxY()), 

528 ) 

529 skyCoord = lsst.geom.SpherePoint(raDeg, decDeg, lsst.geom.arcseconds) 

530 refRec.set(refCoordKey, skyCoord) 

531 trueRec = trueSrc.addNew() 

532 truePos = truePoly(initialIwcToSky.applyInverse(skyCoord)) 

533 trueRec.set(srcPosKey, truePos) 

534 measRec = measSrc.addNew() 

535 covSqrt = np.random.randn(3, 2) 

536 cov = (errScaling*(np.dot(covSqrt.transpose(), covSqrt) 

537 + np.diag([1.0, 1.0]))).astype(np.float32) 

538 # We don't actually perturb positions according to noise level, as 

539 # this makes it much harder to test that the result agrees with 

540 # what we put in. 

541 measPos = truePos 

542 measRec.set(srcPosKey, measPos) 

543 measRec.set(srcErrKey, cov) 

544 match = lsst.afw.table.ReferenceMatch(refRec, measRec, (measPos - truePos).computeNorm()) 

545 matches.append(match) 

546 # Construct a fitter, and verify that the internal catalog it constructs is what we expect. 

547 fitter = ScaledPolynomialTransformFitter.fromMatches(order, matches, initialWcs, 0.0) 

548 expected = lsst.meas.astrom.compose( 

549 fitter.getOutputScaling(), 

550 lsst.meas.astrom.compose(truePoly, fitter.getInputScaling().inverted()) 

551 ) 

552 data = fitter.getData() 

553 dataOutKey = lsst.afw.table.Point2DKey(data.schema["src"]) 

554 dataInKey = lsst.afw.table.Point2DKey(data.schema["intermediate"]) 

555 dataErrKey = lsst.afw.table.CovarianceMatrix2fKey(data.schema["src"], ["x", "y"]) 

556 scaledInBBox = lsst.geom.Box2D() 

557 scaledOutBBox = lsst.geom.Box2D() 

558 vandermonde = np.zeros((nPoints, (order + 1)*(order + 2)//2), dtype=float) 

559 for i, (match, dataRec, trueRec) in enumerate(zip(matches, data, trueSrc)): 

560 self.assertEqual(match.second.getX(), dataRec.get("src_x")) 

561 self.assertEqual(match.second.getY(), dataRec.get("src_y")) 

562 self.assertEqual(match.first.getId(), dataRec.get("ref_id")) 

563 self.assertEqual(match.second.getId(), dataRec.get("src_id")) 

564 refPos = initialIwcToSky.applyInverse(match.first.getCoord()) 

565 self.assertEqual(refPos.getX(), dataRec.get("intermediate_x")) 

566 self.assertEqual(refPos.getY(), dataRec.get("intermediate_y")) 

567 self.assertFloatsAlmostEqual(match.second.get(srcErrKey), dataRec.get(dataErrKey), rtol=1E-7) 

568 scaledIn = fitter.getInputScaling()(dataRec.get(dataInKey)) 

569 scaledOut = fitter.getOutputScaling()(dataRec.get(dataOutKey)) 

570 scaledInBBox.include(scaledIn) 

571 scaledOutBBox.include(scaledOut) 

572 self.assertFloatsAlmostEqual(np.array(expected(scaledIn)), np.array(scaledOut), rtol=1E-7) 

573 j = 0 

574 for n in range(order + 1): 

575 for p in range(n + 1): 

576 q = n - p 

577 vandermonde[i, j] = scaledIn.getX()**p * scaledIn.getY()**q 

578 j += 1 

579 # Verify that scaling transforms move inputs and outputs into [-1, 1] 

580 self.assertFloatsAlmostEqual(scaledInBBox.getMinX(), -1.0, rtol=1E-12) 

581 self.assertFloatsAlmostEqual(scaledInBBox.getMinY(), -1.0, rtol=1E-12) 

582 self.assertFloatsAlmostEqual(scaledInBBox.getMaxX(), 1.0, rtol=1E-12) 

583 self.assertFloatsAlmostEqual(scaledInBBox.getMaxY(), 1.0, rtol=1E-12) 

584 self.assertFloatsAlmostEqual(scaledOutBBox.getMinX(), -1.0, rtol=1E-12) 

585 self.assertFloatsAlmostEqual(scaledOutBBox.getMinY(), -1.0, rtol=1E-12) 

586 self.assertFloatsAlmostEqual(scaledOutBBox.getMaxX(), 1.0, rtol=1E-12) 

587 self.assertFloatsAlmostEqual(scaledOutBBox.getMaxY(), 1.0, rtol=1E-12) 

588 # Run the fitter, and check that we get out approximately what we put in. 

589 fitter.fit(order) 

590 fitter.updateModel() 

591 # Check the transformed input points. 

592 self.assertFloatsAlmostEqual(data.get("model_x"), trueSrc.getX(), rtol=1E-15) 

593 self.assertFloatsAlmostEqual(data.get("model_y"), trueSrc.getY(), rtol=1E-15) 

594 # Check the actual transform's coefficients (after composing in the scaling, which is 

595 # a lot of the reason we lose a lot of precision here). 

596 fittedPoly = lsst.meas.astrom.PolynomialTransform.convert(fitter.getTransform()) 

597 self.assertFloatsAlmostEqual(fittedPoly.getXCoeffs(), truePoly.getXCoeffs(), rtol=1E-5, atol=1E-5) 

598 self.assertFloatsAlmostEqual(fittedPoly.getYCoeffs(), truePoly.getYCoeffs(), rtol=1E-5, atol=1E-5) 

599 

600 def testFromGrid(self): 

601 outOrder = 8 

602 inOrder = 2 

603 toInvert = makeRandomScaledPolynomialTransform(inOrder) 

604 bbox = lsst.geom.Box2D(lsst.geom.Point2D(432, -671), lsst.geom.Point2D(527, -463)) 

605 fitter = ScaledPolynomialTransformFitter.fromGrid(outOrder, bbox, 50, 50, toInvert) 

606 fitter.fit(outOrder) 

607 fitter.updateModel() 

608 data = fitter.getData() 

609 result = fitter.getTransform() 

610 inputKey = lsst.afw.table.Point2DKey(data.schema["input"]) 

611 outputKey = lsst.afw.table.Point2DKey(data.schema["output"]) 

612 for record in data: 

613 self.assertFloatsAlmostEqual(np.array(record.get(inputKey)), 

614 np.array(toInvert(record.get(outputKey)))) 

615 self.assertFloatsAlmostEqual(np.array(result(record.get(inputKey))), 

616 np.array(record.get(outputKey)), 

617 rtol=1E-2) # even at much higher order, inverse can't be perfect. 

618 

619 

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

621 pass 

622 

623 

624def setup_module(module): 

625 lsst.utils.tests.init() 

626 

627 

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

629 lsst.utils.tests.init() 

630 unittest.main()