Coverage for tests/test_photometryModel.py: 26%

189 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-04 11:22 +0000

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 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100) 

51 self.ccdImageList = struct.ccdImageList 

52 self.camera = struct.camera 

53 self.catalogs = struct.catalogs 

54 self.fluxFieldName = struct.fluxFieldName 

55 

56 self.stars = [] 

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

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

59 lsst.afw.cameraGeom.FOCAL_PLANE) 

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

61 

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

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

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

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

66 self.fittedStar.y, 

67 self.fittedStar.flux + 50, 

68 self.fittedStar.fluxErr * 0.01) 

69 

70 self.firstIndex = 0 # for assignIndices 

71 

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

73 self.useMagnitude = False 

74 

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

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

77 photoCalib = self.model.toPhotoCalib(ccdImage) 

78 if self.useMagnitude: 

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

80 else: 

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

82 

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

84 for i, star in enumerate(stars): 

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

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

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

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

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

90 

91 def test_toPhotoCalib(self): 

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

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

94 

95 def test_freezeErrorTransform(self): 

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

97 by offsetParams(). 

98 """ 

99 ccdImage = self.ccdImageList[0] 

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

101 

102 self.model.offsetParams(self.delta) 

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

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

105 self.model.freezeErrorTransform() 

106 self.model.offsetParams(self.delta) 

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

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

109 

110 self.assertFloatsNotEqual(t1, t2) 

111 self.assertFloatsEqual(t1Err, t2Err) 

112 

113 

114class FluxTestBase: 

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

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

117 """ 

118 def test_offsetFittedStar(self): 

119 value = self.fittedStar.flux 

120 

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

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

123 

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

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

126 

127 def test_computeRefResidual(self): 

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

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

130 

131 

132class MagnitudeTestBase: 

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

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

135 """ 

136 def test_offsetFittedStar(self): 

137 value = self.fittedStar.mag 

138 

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

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

141 

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

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

144 

145 def test_computeRefResidual(self): 

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

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

148 

149 

150class SimplePhotometryModelTestBase(PhotometryModelTestBase): 

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

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

153 """ 

154 def test_getNpar(self): 

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

156 self.assertEqual(result, 1) 

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

158 self.assertEqual(result, 1) 

159 

160 def testGetTotalParameters(self): 

161 result = self.model.getTotalParameters() 

162 self.assertEqual(result, 2) 

163 

164 

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

166 def setUp(self): 

167 super().setUp() 

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

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

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

171 

172 

173class SimpleMagnitudeModelTestCase(SimplePhotometryModelTestBase, 

174 MagnitudeTestBase, 

175 lsst.utils.tests.TestCase): 

176 def setUp(self): 

177 super().setUp() 

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

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

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

181 self.useMagnitude = True 

182 

183 

184class ConstrainedPhotometryModelTestCase(PhotometryModelTestBase): 

185 def setUp(self): 

186 super().setUp() 

187 self.visitOrder = 3 

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

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

190 # for the second ccdImage. 

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

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

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

194 self.delta[:10] = 0.0 

195 self.delta[0] = -5.0 

196 

197 def _initModel2(self, Model): 

198 """ 

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

200 

201 Parameters 

202 ---------- 

203 Model : `PhotometryModel`-type 

204 The PhotometryModel-derived class to construct. 

205 """ 

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

207 # in `test_assignIndices()`. 

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

209 # so there will be 2 visits total here. 

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

211 fakeDetectorId=12, 

212 photoCalibMean1=1e-2, 

213 photoCalibMean2=1.2e-2) 

214 self.ccdImageList2 = struct1.ccdImageList 

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

216 fakeDetectorId=13, 

217 photoCalibMean1=2.0e-2, 

218 photoCalibMean2=2.2e-2) 

219 self.ccdImageList2.extend(struct2.ccdImageList) 

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

221 focalPlaneBBox = camera.getFpBBox() 

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

223 

224 def test_getNpar(self): 

225 """ 

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

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

228 """ 

229 expect = getNParametersPolynomial(self.visitOrder) 

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

231 self.assertEqual(result, expect) 

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

233 self.assertEqual(result, expect) 

234 

235 def testGetTotalParameters(self): 

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

237 expect = getNParametersPolynomial(self.visitOrder) * 2 

238 result = self.model.getTotalParameters() 

239 self.assertEqual(result, expect) 

240 

241 def test_assignIndices(self): 

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

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

244 indices. 

245 """ 

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

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

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

249 self.assertEqual(index, expect) 

250 

251 # one polynomial per visit 

252 expect = 2 * getNParametersPolynomial(self.visitOrder) 

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

254 self.assertEqual(index, expect) 

255 

256 # one fitted chip 

257 expect = 1 

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

259 self.assertEqual(index, expect) 

260 

261 def _testConstructor(self, expectVisit, expectChips): 

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

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

264 """ 

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

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

267 

268 # check the visitMappings 

269 for ccdImage in self.ccdImageList2: 

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

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

272 

273 # check the chipMappings 

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

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

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

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

278 

279 def test_photoCalibMean(self): 

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

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

282 image[:] = 1 

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

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

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

286 

287 

288class ConstrainedFluxModelTestCase(ConstrainedPhotometryModelTestCase, 

289 FluxTestBase, 

290 lsst.utils.tests.TestCase): 

291 def setUp(self): 

292 super().setUp() 

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

294 self.focalPlaneBBox, 

295 self.visitOrder) 

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

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

298 self.model.offsetParams(self.delta) 

299 

300 self._initModel2(lsst.jointcal.ConstrainedFluxModel) 

301 

302 def testConstructor(self): 

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

304 expectVisit[0] = 1 

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

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

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

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

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

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

311 self._testConstructor(expectVisit, expectChips) 

312 

313 def test_checkPositiveOnBBox(self): 

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

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

316 

317 # make the model go negative 

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

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

320 

321 def test_validate(self): 

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

323 """ 

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

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

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

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

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

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

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

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

332 

333 

334class ConstrainedMagnitudeModelTestCase(ConstrainedPhotometryModelTestCase, 

335 MagnitudeTestBase, 

336 lsst.utils.tests.TestCase): 

337 def setUp(self): 

338 super().setUp() 

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

340 self.focalPlaneBBox, 

341 self.visitOrder) 

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

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

344 self.model.offsetParams(self.delta) 

345 

346 self._initModel2(lsst.jointcal.ConstrainedMagnitudeModel) 

347 

348 self.useMagnitude = True 

349 

350 def testConstructor(self): 

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

352 

353 def fluxToMag(flux): 

354 # Yay astropy! 

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

356 

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

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

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

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

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

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

363 self._testConstructor(expectVisit, expectChips) 

364 

365 

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

367 pass 

368 

369 

370def setup_module(module): 

371 lsst.utils.tests.init() 

372 

373 

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

375 lsst.utils.tests.init() 

376 unittest.main()