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 itertools 

23import unittest 

24from unittest import mock 

25 

26import numpy as np 

27 

28import lsst.log 

29import lsst.utils 

30 

31import lsst.afw.table 

32import lsst.daf.persistence 

33from lsst.daf.base import DateTime 

34import lsst.geom 

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

36import lsst.pipe.base 

37import lsst.jointcal 

38from lsst.jointcal import MinimizeResult 

39import lsst.jointcal.chi2 

40import lsst.jointcal.testUtils 

41 

42 

43# for MemoryTestCase 

44def setup_module(module): 

45 lsst.utils.tests.init() 

46 

47 

48def make_fake_refcat(center, flux, filterName): 

49 """Make a fake reference catalog.""" 

50 schema = LoadIndexedReferenceObjectsTask.makeMinimalSchema([filterName], 

51 addProperMotion=True) 

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

53 record = catalog.addNew() 

54 record.setCoord(center) 

55 record[filterName + '_flux'] = flux 

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

57 record['pm_ra'] = lsst.geom.Angle(1) 

58 record['pm_dec'] = lsst.geom.Angle(2) 

59 record['epoch'] = 65432.1 

60 return catalog 

61 

62 

63def make_fake_wcs(): 

64 """Return two simple SkyWcs objects, with slightly different sky positions. 

65 

66 Use the same pixel origins as the cfht_minimal data, but put the sky origin 

67 at RA=0 

68 """ 

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

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

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

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

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

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

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

76 return wcs1, wcs2 

77 

78 

79class JointcalTestBase: 

80 def setUp(self): 

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

82 self.ccdImageList = struct.ccdImageList 

83 # so that countStars() returns nonzero results 

84 for ccdImage in self.ccdImageList: 

85 ccdImage.resetCatalogForFit() 

86 

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

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

89 self.goodChi2.chi2 = 200.0 

90 self.goodChi2.ndof = 100 

91 

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

93 self.badChi2.chi2 = 600.0 

94 self.badChi2.ndof = 100 

95 

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

97 self.nanChi2.chi2 = np.nan 

98 self.nanChi2.ndof = 100 

99 

100 self.maxSteps = 20 

101 self.name = "testing" 

102 self.dataName = "fake" 

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

104 

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

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

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

108 

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

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

111 self.associations.getCcdImageList.return_value = self.ccdImageList 

112 

113 # a default config to be modified by individual tests 

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

115 

116 

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

118 def setUp(self): 

119 super().setUp() 

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

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

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

123 self.fitter.computeChi2.return_value = self.goodChi2 

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

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

126 

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

128 

129 def test_iterateFit_success(self): 

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

131 self.maxSteps, self.name, self.whatToFit) 

132 self.assertEqual(chi2, self.goodChi2) 

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

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

135 

136 def test_iterateFit_writeChi2Outer(self): 

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

138 self.maxSteps, self.name, self.whatToFit, 

139 dataName=self.dataName) 

140 self.assertEqual(chi2, self.goodChi2) 

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

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

143 # Default config should not call saveChi2Contributions 

144 self.fitter.saveChi2Contributions.assert_not_called() 

145 

146 def test_iterateFit_failed(self): 

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

148 

149 with self.assertRaises(RuntimeError): 

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

151 self.maxSteps, self.name, self.whatToFit) 

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

153 

154 def test_iterateFit_badFinalChi2(self): 

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

156 self.jointcal.log = log 

157 self.fitter.computeChi2.return_value = self.badChi2 

158 

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

160 self.maxSteps, self.name, self.whatToFit) 

161 self.assertEqual(chi2, self.badChi2) 

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

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

164 

165 def test_iterateFit_exceedMaxSteps(self): 

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

167 self.jointcal.log = log 

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

169 maxSteps = 3 

170 

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

172 maxSteps, self.name, self.whatToFit) 

173 self.assertEqual(chi2, self.goodChi2) 

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

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

176 

177 def test_invalid_model(self): 

178 self.model.validate.return_value = False 

179 with(self.assertRaises(ValueError)): 

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

181 

182 def test_nonfinite_chi2(self): 

183 self.fitter.computeChi2.return_value = self.nanChi2 

184 with(self.assertRaises(FloatingPointError)): 

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

186 

187 def test_writeChi2(self): 

188 filename = "somefile" 

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

190 writeChi2Name=filename) 

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

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

193 

194 

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

196 

197 def _make_fake_refcat(self): 

198 """Mock a fake reference catalog and the bits necessary to use it.""" 

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

200 flux = 10 

201 radius = 1 * lsst.geom.degrees 

202 filterName = 'fake' 

203 

204 fakeRefCat = make_fake_refcat(center, flux, filterName) 

205 fluxField = getRefFluxField(fakeRefCat.schema, filterName) 

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

207 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask) 

208 refObjLoader.loadSkyCircle.return_value = returnStruct 

209 

210 return refObjLoader, center, radius, filterName, fakeRefCat 

211 

212 def test_load_reference_catalog(self): 

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 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler) 

218 

219 # NOTE: we cannot test application of proper motion here, because we 

220 # mock the refObjLoader, so the real loader is never called. 

221 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

222 jointcal.astrometryReferenceSelector, 

223 center, 

224 radius, 

225 filterName) 

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

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

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

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

230 self.assertEqual(r1, r2) 

231 

232 def test_load_reference_catalog_subselect(self): 

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

234 with a ridiculous S/N cut. 

235 """ 

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

237 

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

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

240 config.astrometryReferenceSelector.doSignalToNoise = True 

241 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10 

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

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

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

245 

246 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader, 

247 jointcal.astrometryReferenceSelector, 

248 center, 

249 radius, 

250 filterName) 

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

252 

253 

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

255 def test_fit_photometry_writeChi2(self): 

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

257 self.config.photometryModel = "constrainedFlux" 

258 self.config.writeChi2FilesOuterLoop = True 

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

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

261 

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

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

264 fitPatch.return_value.computeChi2.return_value = self.goodChi2 

265 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged 

266 

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

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

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

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

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

272 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected) 

273 

274 def test_fit_astrometry_writeChi2(self): 

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

276 self.config.astrometryModel = "constrained" 

277 self.config.writeChi2FilesOuterLoop = True 

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

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

280 

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

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

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

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

285 with fitPatch as fit, projectorPatch as projector: 

286 fit.return_value.computeChi2.return_value = self.goodChi2 

287 fit.return_value.minimize.return_value = MinimizeResult.Converged 

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

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

290 

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

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

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

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

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

296 fit.return_value.saveChi2Contributions.assert_has_calls(expected) 

297 

298 

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

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

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

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

303 

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

305 (or slightly less than) radius. 

306 """ 

307 maxSeparation = 0*lsst.geom.degrees 

308 for point in points: 

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

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

311 self.assertLess(maxSeparation, radius) 

312 

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

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

315 bounding circle for the input data. 

316 

317 Parameters 

318 ---------- 

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

320 The CcdImages to add to the Associations object. 

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

322 The WCS of each of the above images. 

323 bbox : `lsst.geom.Box2D` 

324 The ccd bounding box of both images. 

325 """ 

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

327 associations = lsst.jointcal.Associations() 

328 associations.addCcdImage(ccdImage1) 

329 associations.addCcdImage(ccdImage2) 

330 associations.computeCommonTangentPoint() 

331 

332 circle = associations.computeBoundingCircle() 

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

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

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

336 for x in bbox.getCorners()] 

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

338 for x in bbox.getCorners()]) 

339 self._checkPointsInCircle(points, center, radius) 

340 

341 def testPoints(self): 

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

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

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

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

346 

347 def testPointsRA0(self): 

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

349 the bug described in DM-19802. 

350 """ 

351 wcs1, wcs2 = make_fake_wcs() 

352 

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

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

355 date=DateTime(65321.1), 

356 boresightRaDec=wcs1.getSkyOrigin()) 

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

358 date=DateTime(65322.1), 

359 boresightRaDec=wcs1.getSkyOrigin()) 

360 

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

362 fakeVisitInfos=[visitInfo1, visitInfo2]) 

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

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

365 

366 

367class TestJointcalComputePMDate(JointcalTestBase, lsst.utils.tests.TestCase): 

368 """Tests of jointcal._compute_proper_motion_epoch()""" 

369 def test_compute_proper_motion_epoch(self): 

370 mjds = np.array((65432.1, 66666, 65555, 64322.2)) 

371 

372 wcs1, wcs2 = make_fake_wcs() 

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

374 date=DateTime(mjds[0]), 

375 boresightRaDec=wcs1.getSkyOrigin()) 

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

377 date=DateTime(mjds[1]), 

378 boresightRaDec=wcs2.getSkyOrigin()) 

379 visitInfo3 = lsst.afw.image.VisitInfo(exposureId=30577513, 

380 date=DateTime(mjds[2]), 

381 boresightRaDec=wcs1.getSkyOrigin()) 

382 visitInfo4 = lsst.afw.image.VisitInfo(exposureId=30621145, 

383 date=DateTime(mjds[3]), 

384 boresightRaDec=wcs2.getSkyOrigin()) 

385 

386 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2], 

387 fakeVisitInfos=[visitInfo1, visitInfo2]) 

388 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2], 

389 fakeVisitInfos=[visitInfo3, visitInfo4]) 

390 ccdImageList = list(itertools.chain(struct1.ccdImageList, struct2.ccdImageList)) 

391 associations = lsst.jointcal.Associations() 

392 for ccdImage in ccdImageList: 

393 associations.addCcdImage(ccdImage) 

394 associations.computeCommonTangentPoint() 

395 

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

397 result = jointcal._compute_proper_motion_epoch(ccdImageList) 

398 self.assertEqual(result.mjd, mjds.mean()) 

399 

400 

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

402 pass 

403 

404 

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

406 lsst.utils.tests.init() 

407 unittest.main()