Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of jointcal. 

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 unittest 

23from unittest import mock 

24 

25import numpy as np 

26 

27import lsst.log 

28import lsst.utils 

29 

30import lsst.afw.table 

31import lsst.daf.persistence 

32from lsst.daf.base import DateTime 

33import lsst.geom 

34from lsst.meas.algorithms import getRefFluxField, LoadIndexedReferenceObjectsTask, DatasetConfig 

35import lsst.pipe.base 

36import lsst.jointcal 

37from lsst.jointcal import MinimizeResult 

38import lsst.jointcal.chi2 

39import lsst.jointcal.testUtils 

40 

41 

42# for MemoryTestCase 

43def setup_module(module): 

44 lsst.utils.tests.init() 

45 

46 

47def make_fake_refcat(center, flux, filterName): 

48 """Make a fake reference catalog.""" 

49 schema = LoadIndexedReferenceObjectsTask.makeMinimalSchema([filterName]) 

50 catalog = lsst.afw.table.SimpleCatalog(schema) 

51 record = catalog.addNew() 

52 record.setCoord(center) 

53 record[filterName + '_flux'] = flux 

54 record[filterName + '_fluxErr'] = flux*0.1 

55 return catalog 

56 

57 

58class JointcalTestBase: 

59 def setUp(self): 

60 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100) 

61 self.ccdImageList = struct.ccdImageList 

62 # so that countStars() returns nonzero results 

63 for ccdImage in self.ccdImageList: 

64 ccdImage.resetCatalogForFit() 

65 

66 self.goodChi2 = lsst.jointcal.chi2.Chi2Statistic() 

67 # chi2/ndof == 2.0 should be non-bad 

68 self.goodChi2.chi2 = 200.0 

69 self.goodChi2.ndof = 100 

70 

71 self.badChi2 = lsst.jointcal.chi2.Chi2Statistic() 

72 self.badChi2.chi2 = 600.0 

73 self.badChi2.ndof = 100 

74 

75 self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic() 

76 self.nanChi2.chi2 = np.nan 

77 self.nanChi2.ndof = 100 

78 

79 self.maxSteps = 20 

80 self.name = "testing" 

81 self.dataName = "fake" 

82 self.whatToFit = "" # unneeded, since we're mocking the fitter 

83 

84 # Mock a Butler so the refObjLoaders have something to call `get()` on. 

85 self.butler = unittest.mock.Mock(spec=lsst.daf.persistence.Butler) 

86 self.butler.get.return_value.indexer = DatasetConfig().indexer 

87 

88 # Mock the association manager and give it access to the ccd list above. 

89 self.associations = mock.Mock(spec=lsst.jointcal.Associations) 

90 self.associations.getCcdImageList.return_value = self.ccdImageList 

91 

92 # a default config to be modified by individual tests 

93 self.config = lsst.jointcal.jointcal.JointcalConfig() 

94 

95 

96class TestJointcalIterateFit(JointcalTestBase, lsst.utils.tests.TestCase): 

97 def setUp(self): 

98 super().setUp() 

99 # Mock the fitter and model, so we can force particular 

100 # return values/exceptions. Default to "good" return values. 

101 self.fitter = mock.Mock(spec=lsst.jointcal.PhotometryFit) 

102 self.fitter.computeChi2.return_value = self.goodChi2 

103 self.fitter.minimize.return_value = MinimizeResult.Converged 

104 self.model = mock.Mock(spec=lsst.jointcal.SimpleFluxModel) 

105 

106 self.jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler) 

107 

108 def test_iterateFit_success(self): 

109 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

110 self.maxSteps, self.name, self.whatToFit) 

111 self.assertEqual(chi2, self.goodChi2) 

112 # Once for the for loop, the second time for the rank update. 

113 self.assertEqual(self.fitter.minimize.call_count, 2) 

114 

115 def test_iterateFit_writeChi2Outer(self): 

116 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

117 self.maxSteps, self.name, self.whatToFit, 

118 dataName=self.dataName) 

119 self.assertEqual(chi2, self.goodChi2) 

120 # Once for the for loop, the second time for the rank update. 

121 self.assertEqual(self.fitter.minimize.call_count, 2) 

122 # Default config should not call saveChi2Contributions 

123 self.fitter.saveChi2Contributions.assert_not_called() 

124 

125 def test_iterateFit_failed(self): 

126 self.fitter.minimize.return_value = MinimizeResult.Failed 

127 

128 with self.assertRaises(RuntimeError): 

129 self.jointcal._iterate_fit(self.associations, self.fitter, 

130 self.maxSteps, self.name, self.whatToFit) 

131 self.assertEqual(self.fitter.minimize.call_count, 1) 

132 

133 def test_iterateFit_badFinalChi2(self): 

134 log = mock.Mock(spec=lsst.log.Log) 

135 self.jointcal.log = log 

136 self.fitter.computeChi2.return_value = self.badChi2 

137 

138 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

139 self.maxSteps, self.name, self.whatToFit) 

140 self.assertEqual(chi2, self.badChi2) 

141 log.info.assert_called_with("%s %s", "Fit completed", self.badChi2) 

142 log.error.assert_called_with("Potentially bad fit: High chi-squared/ndof.") 

143 

144 def test_iterateFit_exceedMaxSteps(self): 

145 log = mock.Mock(spec=lsst.log.Log) 

146 self.jointcal.log = log 

147 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased 

148 maxSteps = 3 

149 

150 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter, 

151 maxSteps, self.name, self.whatToFit) 

152 self.assertEqual(chi2, self.goodChi2) 

153 self.assertEqual(self.fitter.minimize.call_count, maxSteps) 

154 log.error.assert_called_with("testing failed to converge after %s steps" % maxSteps) 

155 

156 def test_invalid_model(self): 

157 self.model.validate.return_value = False 

158 with(self.assertRaises(ValueError)): 

159 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model) 

160 

161 def test_nonfinite_chi2(self): 

162 self.fitter.computeChi2.return_value = self.nanChi2 

163 with(self.assertRaises(FloatingPointError)): 

164 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model) 

165 

166 def test_writeChi2(self): 

167 filename = "somefile" 

168 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, 

169 writeChi2Name=filename) 

170 # logChi2AndValidate prepends `config.debugOutputPath` to the filename 

171 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}") 

172 

173 

174class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase): 

175 

176 def _make_fake_refcat(self): 

177 """Make a fake reference catalog and the bits necessary to use it.""" 

178 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees) 

179 flux = 10 

180 radius = 1 * lsst.geom.degrees 

181 filterName = 'fake' 

182 

183 fakeRefCat = make_fake_refcat(center, flux, filterName) 

184 fluxField = getRefFluxField(fakeRefCat.schema, filterName) 

185 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField) 

186 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask) 

187 refObjLoader.loadSkyCircle.return_value = returnStruct 

188 

189 return refObjLoader, center, radius, filterName, fakeRefCat 

190 

191 def test_load_reference_catalog(self): 

192 refObjLoader, center, radius, filterName, fakeRefCat = self._make_fake_refcat() 

193 

194 config = lsst.jointcal.jointcal.JointcalConfig() 

195 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors 

196 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler) 

197 

198 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

199 jointcal.astrometryReferenceSelector, 

200 center, 

201 radius, 

202 filterName) 

203 # operator== isn't implemented for Catalogs, so we have to check like 

204 # this, in case the records are copied during load. 

205 self.assertEqual(len(refCat), len(fakeRefCat)) 

206 for r1, r2 in zip(refCat, fakeRefCat): 

207 self.assertEqual(r1, r2) 

208 

209 def test_load_reference_catalog_subselect(self): 

210 """Test that we can select out the one source in the fake refcat 

211 with a ridiculous S/N cut. 

212 """ 

213 refObjLoader, center, radius, filterName, fakeRefCat = self._make_fake_refcat() 

214 

215 config = lsst.jointcal.jointcal.JointcalConfig() 

216 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors 

217 config.astrometryReferenceSelector.doSignalToNoise = True 

218 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10 

219 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux" 

220 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr" 

221 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler) 

222 

223 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

224 jointcal.astrometryReferenceSelector, 

225 center, 

226 radius, 

227 filterName) 

228 self.assertEqual(len(refCat), 0) 

229 

230 

231class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase): 

232 def test_fit_photometry_writeChi2(self): 

233 """Test that we are calling saveChi2 with appropriate file prefixes.""" 

234 self.config.photometryModel = "constrainedFlux" 

235 self.config.writeChi2FilesOuterLoop = True 

236 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler) 

237 jointcal.focalPlaneBBox = lsst.geom.Box2D() 

238 

239 # Mock the fitter, so we can pretend it found a good fit 

240 with mock.patch("lsst.jointcal.PhotometryFit", autospect=True) as fitPatch: 

241 fitPatch.return_value.computeChi2.return_value = self.goodChi2 

242 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged 

243 

244 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions 

245 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2", 

246 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"] 

247 expected = [mock.call(x+"-fake{type}") for x in expected] 

248 jointcal._fit_photometry(self.associations, dataName=self.dataName) 

249 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected) 

250 

251 def test_fit_astrometry_writeChi2(self): 

252 """Test that we are calling saveChi2 with appropriate file prefixes.""" 

253 self.config.astrometryModel = "constrained" 

254 self.config.writeChi2FilesOuterLoop = True 

255 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler) 

256 jointcal.focalPlaneBBox = lsst.geom.Box2D() 

257 

258 # Mock the fitter, so we can pretend it found a good fit 

259 fitPatch = mock.patch("lsst.jointcal.AstrometryFit") 

260 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages 

261 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler") 

262 with fitPatch as fit, projectorPatch as projector: 

263 fit.return_value.computeChi2.return_value = self.goodChi2 

264 fit.return_value.minimize.return_value = MinimizeResult.Converged 

265 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy 

266 projector.return_value = lsst.jointcal.IdentityProjectionHandler() 

267 

268 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions 

269 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2", 

270 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"] 

271 expected = [mock.call(x+"-fake{type}") for x in expected] 

272 jointcal._fit_astrometry(self.associations, dataName=self.dataName) 

273 fit.return_value.saveChi2Contributions.assert_has_calls(expected) 

274 

275 

276class TestComputeBoundingCircle(lsst.utils.tests.TestCase): 

277 """Tests of Associations.computeBoundingCircle()""" 

278 def _checkPointsInCircle(self, points, center, radius): 

279 """Check that all points are within the (center, radius) circle. 

280 

281 The test is whether the max(points - center) separation is equal to 

282 (or slightly less than) radius. 

283 """ 

284 maxSeparation = 0*lsst.geom.degrees 

285 for point in points: 

286 maxSeparation = max(maxSeparation, center.separation(point)) 

287 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds) 

288 self.assertLess(maxSeparation, radius) 

289 

290 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox): 

291 """Fill an Associations object and test that it computes the correct 

292 bounding circle for the input data. 

293 

294 Parameters 

295 ---------- 

296 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage` 

297 The CcdImages to add to the Associations object. 

298 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs` 

299 The WCS of each of the above images. 

300 bbox : `lsst.geom.Box2D` 

301 The ccd bounding box of both images. 

302 """ 

303 lsst.log.setLevel('jointcal', lsst.log.DEBUG) 

304 associations = lsst.jointcal.Associations() 

305 associations.addCcdImage(ccdImage1) 

306 associations.addCcdImage(ccdImage2) 

307 associations.computeCommonTangentPoint() 

308 

309 circle = associations.computeBoundingCircle() 

310 center = lsst.geom.SpherePoint(circle.getCenter()) 

311 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians) 

312 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x))) 

313 for x in bbox.getCorners()] 

314 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x))) 

315 for x in bbox.getCorners()]) 

316 self._checkPointsInCircle(points, center, radius) 

317 

318 def testPoints(self): 

319 """Test for points in an "easy" area, far from RA=0 or the poles.""" 

320 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages() 

321 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1], 

322 struct.skyWcs[0], struct.skyWcs[1], struct.bbox) 

323 

324 def testPointsRA0(self): 

325 """Test for CcdImages crossing RA=0; this demonstrates a fix for 

326 the bug described in DM-19802. 

327 """ 

328 # Use the same pixel origins as the cfht_minimal data, but put the sky origin at RA=0 

329 crpix = lsst.geom.Point2D(931.517869, 2438.572109) 

330 cd = np.array([[5.19513851e-05, -2.81124812e-07], 

331 [-3.25186974e-07, -5.19112119e-05]]) 

332 crval1 = lsst.geom.SpherePoint(0.01, -0.01, lsst.geom.degrees) 

333 crval2 = lsst.geom.SpherePoint(-0.01, 0.01, lsst.geom.degrees) 

334 wcs1 = lsst.afw.geom.makeSkyWcs(crpix, crval1, cd) 

335 wcs2 = lsst.afw.geom.makeSkyWcs(crpix, crval2, cd) 

336 

337 # Put the visit boresights at the WCS origin, for consistency 

338 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512, 

339 date=DateTime(65321.1), 

340 boresightRaDec=wcs1.getSkyOrigin()) 

341 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144, 

342 date=DateTime(65322.1), 

343 boresightRaDec=wcs1.getSkyOrigin()) 

344 

345 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2], 

346 fakeVisitInfos=[visitInfo1, visitInfo2]) 

347 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1], 

348 struct.skyWcs[0], struct.skyWcs[1], struct.bbox) 

349 

350 

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

352 pass 

353 

354 

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

356 lsst.utils.tests.init() 

357 unittest.main()