Coverage for tests/test_photometryModel.py: 33%

190 statements  

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

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 numpy as np 

23 

24import unittest 

25import lsst.utils.tests 

26import lsst.jointcal.testUtils 

27 

28from astropy import units 

29 

30import lsst.afw.cameraGeom 

31import lsst.afw.table 

32import lsst.afw.image 

33import lsst.afw.image.utils 

34import lsst.jointcal.ccdImage 

35import lsst.jointcal.photometryModels 

36import lsst.jointcal.star 

37import lsst.obs.base 

38 

39 

40def getNParametersPolynomial(order): 

41 """Number of parameters in a photometry polynomial model is (d+1)(d+2)/2.""" 

42 return (order + 1)*(order + 2)/2 

43 

44 

45class PhotometryModelTestBase: 

46 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause 

47 unittest to use the test_* methods in this class. 

48 """ 

49 def setUp(self): 

50 # Ensure that the filter list is reset for each test so that we avoid 

51 # confusion or contamination each time we create a cfht camera below. 

52 lsst.obs.base.FilterDefinitionCollection.reset() 

53 

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

55 self.ccdImageList = struct.ccdImageList 

56 self.camera = struct.camera 

57 self.catalogs = struct.catalogs 

58 self.fluxFieldName = struct.fluxFieldName 

59 

60 self.stars = [] 

61 for catalog, ccdImage in zip(self.catalogs, self.ccdImageList): 

62 pixToFocal = ccdImage.getDetector().getTransform(lsst.afw.cameraGeom.PIXELS, 

63 lsst.afw.cameraGeom.FOCAL_PLANE) 

64 self.stars.append(lsst.jointcal.testUtils.getMeasuredStarsFromCatalog(catalog, pixToFocal)) 

65 

66 self.fittedStar = lsst.jointcal.star.FittedStar(self.stars[0][0]) 

67 # Make a refStar at this fittedStar position, but with different 

68 # flux and fluxErr, so that it does interesting things when subtracted. 

69 self.refStar = lsst.jointcal.star.RefStar(self.fittedStar.x, 

70 self.fittedStar.y, 

71 self.fittedStar.flux + 50, 

72 self.fittedStar.fluxErr * 0.01) 

73 

74 self.firstIndex = 0 # for assignIndices 

75 

76 # Set to True in the subclass constructor to do the PhotoCalib calculations in magnitudes. 

77 self.useMagnitude = False 

78 

79 def _toPhotoCalib(self, ccdImage, catalog, stars): 

80 """Test converting this object to a PhotoCalib.""" 

81 photoCalib = self.model.toPhotoCalib(ccdImage) 

82 if self.useMagnitude: 

83 result = photoCalib.instFluxToMagnitude(catalog, self.fluxFieldName) 

84 else: 

85 result = photoCalib.instFluxToNanojansky(catalog, self.fluxFieldName) 

86 

87 expects = np.empty(len(stars)) 

88 for i, star in enumerate(stars): 

89 expects[i] = self.model.transform(ccdImage, star) 

90 self.assertFloatsAlmostEqual(result[:, 0], expects, rtol=2e-13) 

91 # NOTE: don't compare transformed errors, as they will be different: 

92 # photoCalib incorporates the model error, while jointcal computes the 

93 # full covariance matrix, from which the model error should be derived. 

94 

95 def test_toPhotoCalib(self): 

96 self._toPhotoCalib(self.ccdImageList[0], self.catalogs[0], self.stars[0]) 

97 self._toPhotoCalib(self.ccdImageList[1], self.catalogs[1], self.stars[1]) 

98 

99 def test_freezeErrorTransform(self): 

100 """After calling freezeErrorTransform(), the error transform is unchanged 

101 by offsetParams(). 

102 """ 

103 ccdImage = self.ccdImageList[0] 

104 star0 = self.stars[0][0] 

105 

106 self.model.offsetParams(self.delta) 

107 t1 = self.model.transform(ccdImage, star0) 

108 t1Err = self.model.transformError(ccdImage, star0) 

109 self.model.freezeErrorTransform() 

110 self.model.offsetParams(self.delta) 

111 t2 = self.model.transform(ccdImage, star0) 

112 t2Err = self.model.transformError(ccdImage, star0) 

113 

114 self.assertFloatsNotEqual(t1, t2) 

115 self.assertFloatsEqual(t1Err, t2Err) 

116 

117 

118class FluxTestBase: 

119 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause 

120 unittest to use the test_* methods in this class. 

121 """ 

122 def test_offsetFittedStar(self): 

123 value = self.fittedStar.flux 

124 

125 self.model.offsetFittedStar(self.fittedStar, 0) 

126 self.assertEqual(self.fittedStar.flux, value) 

127 

128 self.model.offsetFittedStar(self.fittedStar, 1) 

129 self.assertEqual(self.fittedStar.flux, value-1) 

130 

131 def test_computeRefResidual(self): 

132 result = self.model.computeRefResidual(self.fittedStar, self.refStar) 

133 self.assertEqual(result, self.fittedStar.flux - self.refStar.flux) 

134 

135 

136class MagnitudeTestBase: 

137 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause 

138 unittest to use the test_* methods in this class. 

139 """ 

140 def test_offsetFittedStar(self): 

141 value = self.fittedStar.mag 

142 

143 self.model.offsetFittedStar(self.fittedStar, 0) 

144 self.assertEqual(self.fittedStar.mag, value) 

145 

146 self.model.offsetFittedStar(self.fittedStar, 1) 

147 self.assertEqual(self.fittedStar.mag, value-1) 

148 

149 def test_computeRefResidual(self): 

150 result = self.model.computeRefResidual(self.fittedStar, self.refStar) 

151 self.assertEqual(result, self.fittedStar.mag - self.refStar.mag) 

152 

153 

154class SimplePhotometryModelTestBase(PhotometryModelTestBase): 

155 """Have the sublass also derive from ``lsst.utils.tests.TestCase`` to cause 

156 unittest to use the test_* methods in this class. 

157 """ 

158 def test_getNpar(self): 

159 result = self.model.getNpar(self.ccdImageList[0]) 

160 self.assertEqual(result, 1) 

161 result = self.model.getNpar(self.ccdImageList[1]) 

162 self.assertEqual(result, 1) 

163 

164 def testGetTotalParameters(self): 

165 result = self.model.getTotalParameters() 

166 self.assertEqual(result, 2) 

167 

168 

169class SimpleFluxModelTestCase(SimplePhotometryModelTestBase, FluxTestBase, lsst.utils.tests.TestCase): 

170 def setUp(self): 

171 super().setUp() 

172 self.model = lsst.jointcal.photometryModels.SimpleFluxModel(self.ccdImageList) 

173 self.model.assignIndices("", self.firstIndex) # have to call this once to let offsetParams work. 

174 self.delta = np.arange(len(self.ccdImageList), dtype=float)*-0.2 + 1 

175 

176 

177class SimpleMagnitudeModelTestCase(SimplePhotometryModelTestBase, 

178 MagnitudeTestBase, 

179 lsst.utils.tests.TestCase): 

180 def setUp(self): 

181 super().setUp() 

182 self.model = lsst.jointcal.photometryModels.SimpleMagnitudeModel(self.ccdImageList) 

183 self.model.assignIndices("", self.firstIndex) # have to call this once to let offsetParams work. 

184 self.delta = np.arange(len(self.ccdImageList), dtype=float)*-0.2 + 1 

185 self.useMagnitude = True 

186 

187 

188class ConstrainedPhotometryModelTestCase(PhotometryModelTestBase): 

189 def setUp(self): 

190 super().setUp() 

191 self.visitOrder = 3 

192 self.focalPlaneBBox = self.camera.getFpBBox() 

193 # Amount to shift the parameters to get more than just a constant field 

194 # for the second ccdImage. 

195 # Reverse the range so that the low order terms are the largest. 

196 self.delta = (np.arange(20, dtype=float)*-0.2 + 1)[::-1] 

197 # but keep the first ccdImage constant, to help distinguish test failures. 

198 self.delta[:10] = 0.0 

199 self.delta[0] = -5.0 

200 

201 def _initModel2(self, Model): 

202 """ 

203 Initialize self.model2 with 2 fake sensor catalogs. Call after setUp(). 

204 

205 Parameters 

206 ---------- 

207 Model : `PhotometryModel`-type 

208 The PhotometryModel-derived class to construct. 

209 """ 

210 # We need at least two sensors to distinguish "Model" from "ModelVisit" 

211 # in `test_assignIndices()`. 

212 # createTwoFakeCcdImages() always uses the same two visitIds, 

213 # so there will be 2 visits total here. 

214 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100, seed=100, 

215 fakeDetectorId=12, 

216 photoCalibMean1=1e-2, 

217 photoCalibMean2=1.2e-2) 

218 self.ccdImageList2 = struct1.ccdImageList 

219 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100, seed=101, 

220 fakeDetectorId=13, 

221 photoCalibMean1=2.0e-2, 

222 photoCalibMean2=2.2e-2) 

223 self.ccdImageList2.extend(struct2.ccdImageList) 

224 camera = struct1.camera # the camera is the same in both structs 

225 focalPlaneBBox = camera.getFpBBox() 

226 self.model2 = Model(self.ccdImageList2, focalPlaneBBox, self.visitOrder) 

227 

228 def test_getNpar(self): 

229 """ 

230 Order 3 => (3+1)*(3+2))/2 = 10 parameters, 

231 and the chip map is fixed (only one ccd), so does not contribute. 

232 """ 

233 expect = getNParametersPolynomial(self.visitOrder) 

234 result = self.model.getNpar(self.ccdImageList[0]) 

235 self.assertEqual(result, expect) 

236 result = self.model.getNpar(self.ccdImageList[1]) 

237 self.assertEqual(result, expect) 

238 

239 def testGetTotalParameters(self): 

240 """Two visits, one (fixed) ccd.""" 

241 expect = getNParametersPolynomial(self.visitOrder) * 2 

242 result = self.model.getTotalParameters() 

243 self.assertEqual(result, expect) 

244 

245 def test_assignIndices(self): 

246 """Test that the correct number of indices were assigned. 

247 Does not check that the internal mappings are assigned the correct 

248 indices. 

249 """ 

250 # one polynomial per visit, plus one fitted scale for the second chip. 

251 expect = 2 * getNParametersPolynomial(self.visitOrder) + 1 

252 index = self.model2.assignIndices("Model", self.firstIndex) 

253 self.assertEqual(index, expect) 

254 

255 # one polynomial per visit 

256 expect = 2 * getNParametersPolynomial(self.visitOrder) 

257 index = self.model2.assignIndices("ModelVisit", self.firstIndex) 

258 self.assertEqual(index, expect) 

259 

260 # one fitted chip 

261 expect = 1 

262 index = self.model2.assignIndices("ModelChip", self.firstIndex) 

263 self.assertEqual(index, expect) 

264 

265 def _testConstructor(self, expectVisit, expectChips): 

266 """Post-construction, the ChipTransforms should be the PhotoCalib mean of 

267 the first visit's ccds, and the VisitTransforms should be the identity. 

268 """ 

269 # Identify to the model that we're fitting both components. 

270 self.model2.assignIndices("Model", self.firstIndex) 

271 

272 # check the visitMappings 

273 for ccdImage in self.ccdImageList2: 

274 result = self.model2.getMapping(ccdImage).getVisitMapping().getTransform().getParameters() 

275 self.assertFloatsEqual(result, expectVisit, msg=ccdImage.getName()) 

276 

277 # check the chipMappings 

278 for ccdImage, expect in zip(self.ccdImageList2, expectChips): 

279 result = self.model2.getMapping(ccdImage).getChipMapping().getTransform().getParameters() 

280 # almost equal because log() may have been involved in the math 

281 self.assertFloatsAlmostEqual(result, expect, msg=ccdImage.getName()) 

282 

283 def test_photoCalibMean(self): 

284 """The mean of the photoCalib should match the mean over a calibrated image.""" 

285 image = lsst.afw.image.MaskedImageF(self.ccdImageList[0].getDetector().getBBox()) 

286 image[:] = 1 

287 photoCalib = self.model.toPhotoCalib(self.ccdImageList[0]) 

288 expect = photoCalib.calibrateImage(image).image.array.mean() 

289 self.assertFloatsAlmostEqual(expect, photoCalib.getCalibrationMean(), rtol=2e-5) 

290 

291 

292class ConstrainedFluxModelTestCase(ConstrainedPhotometryModelTestCase, 

293 FluxTestBase, 

294 lsst.utils.tests.TestCase): 

295 def setUp(self): 

296 super().setUp() 

297 self.model = lsst.jointcal.ConstrainedFluxModel(self.ccdImageList, 

298 self.focalPlaneBBox, 

299 self.visitOrder) 

300 # have to call this once to let offsetParams work. 

301 self.model.assignIndices("Model", self.firstIndex) 

302 self.model.offsetParams(self.delta) 

303 

304 self._initModel2(lsst.jointcal.ConstrainedFluxModel) 

305 

306 def testConstructor(self): 

307 expectVisit = np.zeros(int(getNParametersPolynomial(self.visitOrder))) 

308 expectVisit[0] = 1 

309 # chipMappings are fixed per-chip, and thus are 

310 # shared between the first pair and second pair of fake ccdImages 

311 expectChips = [self.ccdImageList2[0].getPhotoCalib().getCalibrationMean(), 

312 self.ccdImageList2[0].getPhotoCalib().getCalibrationMean(), 

313 self.ccdImageList2[2].getPhotoCalib().getCalibrationMean(), 

314 self.ccdImageList2[2].getPhotoCalib().getCalibrationMean()] 

315 self._testConstructor(expectVisit, expectChips) 

316 

317 def test_checkPositiveOnBBox(self): 

318 self.assertTrue(self.model.checkPositiveOnBBox(self.ccdImageList[0])) 

319 self.assertTrue(self.model.checkPositiveOnBBox(self.ccdImageList[1])) 

320 

321 # make the model go negative 

322 self.model.offsetParams(-5*self.delta) 

323 self.assertFalse(self.model.checkPositiveOnBBox(self.ccdImageList[0])) 

324 

325 def test_validate(self): 

326 """Test that invalid models fail validate(), and that valid ones pass. 

327 """ 

328 # We need at least 0 degrees of freedom (data - parameters) for the model to be valid. 

329 # NOTE: model has 20 parameters (2 visits, 10 params each) 

330 self.assertTrue(self.model.validate(self.ccdImageList, 0)) 

331 self.assertFalse(self.model.validate(self.ccdImageList, -1)) 

332 # Models that are negative on the bounding box are invalid 

333 self.model.offsetParams(-5*self.delta) 

334 # ensure ndof is high enough that it will not cause a failure 

335 self.assertFalse(self.model.validate(self.ccdImageList, 100)) 

336 

337 

338class ConstrainedMagnitudeModelTestCase(ConstrainedPhotometryModelTestCase, 

339 MagnitudeTestBase, 

340 lsst.utils.tests.TestCase): 

341 def setUp(self): 

342 super().setUp() 

343 self.model = lsst.jointcal.ConstrainedMagnitudeModel(self.ccdImageList, 

344 self.focalPlaneBBox, 

345 self.visitOrder) 

346 # have to call this once to let offsetParams work. 

347 self.model.assignIndices("Model", self.firstIndex) 

348 self.model.offsetParams(self.delta) 

349 

350 self._initModel2(lsst.jointcal.ConstrainedMagnitudeModel) 

351 

352 self.useMagnitude = True 

353 

354 def testConstructor(self): 

355 expectVisit = np.zeros(int(getNParametersPolynomial(self.visitOrder))) 

356 

357 def fluxToMag(flux): 

358 # Yay astropy! 

359 return (flux * units.nanojansky).to(units.ABmag).value 

360 

361 # chipMappings are fixed per-chip, and thus are 

362 # shared between the first pair and second pair of fake ccdImages 

363 expectChips = [fluxToMag(self.ccdImageList2[0].getPhotoCalib().getCalibrationMean()), 

364 fluxToMag(self.ccdImageList2[0].getPhotoCalib().getCalibrationMean()), 

365 fluxToMag(self.ccdImageList2[2].getPhotoCalib().getCalibrationMean()), 

366 fluxToMag(self.ccdImageList2[2].getPhotoCalib().getCalibrationMean())] 

367 self._testConstructor(expectVisit, expectChips) 

368 

369 

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

371 pass 

372 

373 

374def setup_module(module): 

375 lsst.utils.tests.init() 

376 

377 

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

379 lsst.utils.tests.init() 

380 unittest.main()