Coverage for tests/test_fitAffineWcsTask.py: 20%

137 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-29 03:29 -0700

1# This file is part of meas_astrom. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import math 

23import unittest 

24 

25import numpy as np 

26 

27import lsst.pipe.base 

28import lsst.utils.tests 

29import lsst.geom 

30import lsst.afw.geom as afwGeom 

31import lsst.afw.table as afwTable 

32from lsst.meas.algorithms import convertReferenceCatalog 

33from lsst.meas.base import SingleFrameMeasurementTask 

34from lsst.meas.astrom import FitAffineWcsTask, TransformedSkyWcsMaker 

35 

36 

37class BaseTestCase: 

38 

39 """A test case for FitAffineWcsTask. 

40 

41 Use involves setting one class attribute: 

42 * MatchClass: match class, e.g. ReferenceMatch or SourceMatch 

43 """ 

44 MatchClass = None 

45 

46 def setUp(self): 

47 crval = lsst.geom.SpherePoint(44, 45, lsst.geom.degrees) 

48 crpix = lsst.geom.Point2D(15000, 4000) 

49 

50 scale = 1 * lsst.geom.arcseconds 

51 cdMatrix = afwGeom.makeCdMatrix(scale=scale, flipX=True) 

52 self.tanWcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix) 

53 self.loadData() 

54 

55 def loadData(self, rangePix=3000, numPoints=25): 

56 """Load catalogs and make the match list 

57 

58 This is a separate function so data can be reloaded if fitting more than once 

59 (each time a WCS is fit it may update the source catalog, reference catalog and match list) 

60 """ 

61 refSchema = convertReferenceCatalog._makeSchema(filterNameList=["r"], addIsPhotometric=True, 

62 addCentroid=True) 

63 self.refCat = afwTable.SimpleCatalog(refSchema) 

64 srcSchema = afwTable.SourceTable.makeMinimalSchema() 

65 SingleFrameMeasurementTask(schema=srcSchema) 

66 afwTable.CoordKey.addErrorFields(srcSchema) 

67 self.srcCoordKey = afwTable.CoordKey(srcSchema["coord"]) 

68 self.srcCentroidKey = afwTable.Point2DKey(srcSchema["slot_Centroid"]) 

69 self.srcCentroidKey_xErr = srcSchema["slot_Centroid_xErr"].asKey() 

70 self.srcCentroidKey_yErr = srcSchema["slot_Centroid_yErr"].asKey() 

71 self.sourceCat = afwTable.SourceCatalog(srcSchema) 

72 

73 self.matches = [] 

74 

75 for i in np.linspace(0., rangePix, numPoints): 

76 for j in np.linspace(0., rangePix, numPoints): 

77 src = self.sourceCat.addNew() 

78 refObj = self.refCat.addNew() 

79 

80 src.set(self.srcCentroidKey, lsst.geom.Point2D(i, j)) 

81 src.set(self.srcCentroidKey_xErr, 0.1) 

82 src.set(self.srcCentroidKey_yErr, 0.1) 

83 

84 c = self.tanWcs.pixelToSky(i, j) 

85 refObj.setCoord(c) 

86 

87 self.matches.append(self.MatchClass(refObj, src, 0.0)) 

88 

89 def tearDown(self): 

90 del self.refCat 

91 del self.sourceCat 

92 del self.matches 

93 del self.tanWcs 

94 

95 def checkResults(self, fitRes, catsUpdated): 

96 """Check results. 

97 """ 

98 self.assertLess(fitRes.scatterOnSky.asArcseconds(), 0.001) 

99 affineWcs = fitRes.wcs 

100 maxAngSep = 0*lsst.geom.radians 

101 maxPixSep = 0 

102 refCoordKey = afwTable.CoordKey(self.refCat.schema["coord"]) 

103 if catsUpdated: 

104 refCentroidKey = afwTable.Point2DKey(self.refCat.schema["centroid"]) 

105 maxDistErr = 0*lsst.geom.radians 

106 for refObj, src, distRad in self.matches: 

107 srcPixPos = src.get(self.srcCentroidKey) 

108 refCoord = refObj.get(refCoordKey) 

109 if catsUpdated: 

110 refPixPos = refObj.get(refCentroidKey) 

111 srcCoord = src.get(self.srcCoordKey) 

112 else: 

113 refPixPos = affineWcs.skyToPixel(refCoord) 

114 srcCoord = affineWcs.pixelToSky(srcPixPos) 

115 

116 angSep = refCoord.separation(srcCoord) 

117 dist = distRad*lsst.geom.radians 

118 distErr = abs(dist - angSep) 

119 maxDistErr = max(maxDistErr, distErr) 

120 maxAngSep = max(maxAngSep, angSep) 

121 

122 pixSep = math.hypot(*(srcPixPos - refPixPos)) 

123 maxPixSep = max(maxPixSep, pixSep) 

124 

125 print("max angular separation = %0.4f arcsec" % (maxAngSep.asArcseconds(),)) 

126 print("max pixel separation = %0.3f" % (maxPixSep,)) 

127 self.assertLess(maxAngSep.asArcseconds(), 0.001) 

128 self.assertLess(maxPixSep, 0.005) 

129 if catsUpdated: 

130 allowedDistErr = 1e-7 

131 else: 

132 allowedDistErr = 0.001 

133 self.assertLess(maxDistErr.asArcseconds(), allowedDistErr, 

134 "Computed distance in match list is off by %s arcsec" % (maxDistErr.asArcseconds(),)) 

135 

136 def doTest(self, name, func): 

137 """Apply func(x, y) to each source in self.sourceCat, then fit and 

138 check the resulting WCS. 

139 """ 

140 bbox = lsst.geom.Box2I() 

141 for refObj, src, d in self.matches: 

142 origPos = src.get(self.srcCentroidKey) 

143 x, y = func(*origPos) 

144 distortedPos = lsst.geom.Point2D(x, y) 

145 src.set(self.srcCentroidKey, distortedPos) 

146 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(distortedPos))) 

147 

148 fitter = FitAffineWcsTask() 

149 fitRes = fitter.fitWcs( 

150 matches=self.matches, 

151 initWcs=self.tanWcs, 

152 bbox=bbox, 

153 refCat=self.refCat, 

154 sourceCat=self.sourceCat, 

155 ) 

156 

157 self.checkResults(fitRes, catsUpdated=True) 

158 

159 def doTestAffine(self, name, offset, matrix): 

160 """Apply func(x, y) to each source in self.sourceCat, then fit and 

161 check the resulting WCS. 

162 """ 

163 wcsMaker = TransformedSkyWcsMaker(self.tanWcs) 

164 

165 newWcs = wcsMaker.makeWcs(offset, matrix) 

166 bbox = lsst.geom.Box2I() 

167 for refObj, src, d in self.matches: 

168 origPos = src.get(self.srcCentroidKey) 

169 newCoord = newWcs.pixelToSky(origPos) 

170 src.setCoord(newCoord) 

171 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(origPos))) 

172 

173 fitter = FitAffineWcsTask() 

174 fitRes = fitter.fitWcs( 

175 matches=self.matches, 

176 initWcs=newWcs, 

177 bbox=bbox, 

178 refCat=self.refCat, 

179 sourceCat=self.sourceCat, 

180 ) 

181 

182 self.checkResults(fitRes, catsUpdated=True) 

183 

184 def doTestAffineReverse(self, name, offset, matrix): 

185 """Apply func(x, y) to each source in self.sourceCat, then fit and 

186 check the resulting WCS. 

187 """ 

188 wcsMaker = TransformedSkyWcsMaker(self.tanWcs) 

189 

190 newWcs = wcsMaker.makeWcs(offset, matrix) 

191 bbox = lsst.geom.Box2I() 

192 for refObj, src, d in self.matches: 

193 origCoord = src.get(self.srcCoordKey) 

194 newCentroid = newWcs.skyToPixel(origCoord) 

195 src.set(self.srcCentroidKey, newCentroid) 

196 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(newCentroid))) 

197 

198 fitter = FitAffineWcsTask() 

199 fitRes = fitter.fitWcs( 

200 matches=self.matches, 

201 initWcs=self.tanWcs, 

202 bbox=bbox, 

203 refCat=self.refCat, 

204 sourceCat=self.sourceCat, 

205 ) 

206 

207 self.checkResults(fitRes, catsUpdated=True) 

208 

209 

210class SideLoadTestCases: 

211 

212 """Base class implementations of testing methods. 

213 

214 Explicitly does not inherit from unittest.TestCase""" 

215 

216 def testTrivial(self): 

217 """Add no distortion""" 

218 self.doTest("testTrivial", lambda x, y: (x, y)) 

219 

220 def testOffset(self): 

221 """Add an offset""" 

222 self.doTest("testOffset", lambda x, y: (x + 5, y + 7)) 

223 

224 def testSkyOffset(self): 

225 """Add an on sky offset""" 

226 self.doTestAffine("testSkyOffset", 

227 [77, -200], 

228 np.array([[1.0, 0.0], [0.0, 1.0]])) 

229 self.doTestAffineReverse("testSkyOffsetRev", 

230 [77, -200], 

231 np.array([[1.0, 0.0], [0.0, 1.0]])) 

232 

233 def testAffine(self): 

234 """Add an Affine (shear + scale + rot) distortion""" 

235 self.doTestAffine("testAffine", 

236 [0, 0], 

237 np.array([[0.4, 0.1], [-0.21, 2.0]])) 

238 self.doTestAffineReverse("testAffineRev", 

239 [0, 0], 

240 np.array([[0.4, 0.1], [-0.21, 2.0]])) 

241 

242 def testAffineAndOffset(self): 

243 """Add a transform and offset""" 

244 self.doTestAffine("testAffineAndOffset", 

245 [30, 100], 

246 np.array([[0.5, 0.01], [-0.2, 0.3]])) 

247 self.doTestAffineReverse("testAffineAndOffsetRev", 

248 [30, 100], 

249 np.array([[0.5, 0.01], [-0.2, 0.3]])) 

250 

251 

252# The test classes inherit from two base classes and differ in the match 

253# class being used. 

254 

255class FitAffineWcsTaskTestCaseReferenceMatch(BaseTestCase, 

256 SideLoadTestCases, 

257 lsst.utils.tests.TestCase): 

258 MatchClass = afwTable.ReferenceMatch 

259 

260 

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

262 pass 

263 

264 

265def setup_module(module): 

266 lsst.utils.tests.init() 

267 

268 

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

270 lsst.utils.tests.init() 

271 unittest.main()