Coverage for tests/test_photometryModel.py: 26%

187 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-11 03:14 -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 

35import lsst.obs.base 

36 

37 

38def getNParametersPolynomial(order): 

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

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

41 

42 

43class PhotometryModelTestBase: 

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

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

46 """ 

47 def setUp(self): 

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

49 self.ccdImageList = struct.ccdImageList 

50 self.camera = struct.camera 

51 self.catalogs = struct.catalogs 

52 self.fluxFieldName = struct.fluxFieldName 

53 

54 self.stars = [] 

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

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

57 lsst.afw.cameraGeom.FOCAL_PLANE) 

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

59 

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

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

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

63 self.refStar = lsst.jointcal.RefStar(self.fittedStar.x, 

64 self.fittedStar.y, 

65 self.fittedStar.flux + 50, 

66 self.fittedStar.fluxErr * 0.01) 

67 

68 self.firstIndex = 0 # for assignIndices 

69 

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

71 self.useMagnitude = False 

72 

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

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

75 photoCalib = self.model.toPhotoCalib(ccdImage) 

76 if self.useMagnitude: 

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

78 else: 

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

80 

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

82 for i, star in enumerate(stars): 

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

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

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

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

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

88 

89 def test_toPhotoCalib(self): 

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

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

92 

93 def test_freezeErrorTransform(self): 

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

95 by offsetParams(). 

96 """ 

97 ccdImage = self.ccdImageList[0] 

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

99 

100 self.model.offsetParams(self.delta) 

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

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

103 self.model.freezeErrorTransform() 

104 self.model.offsetParams(self.delta) 

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

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

107 

108 self.assertFloatsNotEqual(t1, t2) 

109 self.assertFloatsEqual(t1Err, t2Err) 

110 

111 

112class FluxTestBase: 

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

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

115 """ 

116 def test_offsetFittedStar(self): 

117 value = self.fittedStar.flux 

118 

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

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

121 

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

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

124 

125 def test_computeRefResidual(self): 

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

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

128 

129 

130class MagnitudeTestBase: 

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

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

133 """ 

134 def test_offsetFittedStar(self): 

135 value = self.fittedStar.mag 

136 

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

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

139 

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

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

142 

143 def test_computeRefResidual(self): 

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

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

146 

147 

148class SimplePhotometryModelTestBase(PhotometryModelTestBase): 

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

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

151 """ 

152 def test_getNpar(self): 

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

154 self.assertEqual(result, 1) 

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

156 self.assertEqual(result, 1) 

157 

158 def testGetTotalParameters(self): 

159 result = self.model.getTotalParameters() 

160 self.assertEqual(result, 2) 

161 

162 

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

164 def setUp(self): 

165 super().setUp() 

166 self.model = lsst.jointcal.SimpleFluxModel(self.ccdImageList) 

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

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

169 

170 

171class SimpleMagnitudeModelTestCase(SimplePhotometryModelTestBase, 

172 MagnitudeTestBase, 

173 lsst.utils.tests.TestCase): 

174 def setUp(self): 

175 super().setUp() 

176 self.model = lsst.jointcal.SimpleMagnitudeModel(self.ccdImageList) 

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

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

179 self.useMagnitude = True 

180 

181 

182class ConstrainedPhotometryModelTestCase(PhotometryModelTestBase): 

183 def setUp(self): 

184 super().setUp() 

185 self.visitOrder = 3 

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

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

188 # for the second ccdImage. 

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

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

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

192 self.delta[:10] = 0.0 

193 self.delta[0] = -5.0 

194 

195 def _initModel2(self, Model): 

196 """ 

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

198 

199 Parameters 

200 ---------- 

201 Model : `PhotometryModel`-type 

202 The PhotometryModel-derived class to construct. 

203 """ 

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

205 # in `test_assignIndices()`. 

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

207 # so there will be 2 visits total here. 

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

209 fakeDetectorId=12, 

210 photoCalibMean1=1e-2, 

211 photoCalibMean2=1.2e-2) 

212 self.ccdImageList2 = struct1.ccdImageList 

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

214 fakeDetectorId=13, 

215 photoCalibMean1=2.0e-2, 

216 photoCalibMean2=2.2e-2) 

217 self.ccdImageList2.extend(struct2.ccdImageList) 

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

219 focalPlaneBBox = camera.getFpBBox() 

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

221 

222 def test_getNpar(self): 

223 """ 

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

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

226 """ 

227 expect = getNParametersPolynomial(self.visitOrder) 

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

229 self.assertEqual(result, expect) 

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

231 self.assertEqual(result, expect) 

232 

233 def testGetTotalParameters(self): 

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

235 expect = getNParametersPolynomial(self.visitOrder) * 2 

236 result = self.model.getTotalParameters() 

237 self.assertEqual(result, expect) 

238 

239 def test_assignIndices(self): 

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

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

242 indices. 

243 """ 

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

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

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

247 self.assertEqual(index, expect) 

248 

249 # one polynomial per visit 

250 expect = 2 * getNParametersPolynomial(self.visitOrder) 

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

252 self.assertEqual(index, expect) 

253 

254 # one fitted chip 

255 expect = 1 

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

257 self.assertEqual(index, expect) 

258 

259 def _testConstructor(self, expectVisit, expectChips): 

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

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

262 """ 

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

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

265 

266 # check the visitMappings 

267 for ccdImage in self.ccdImageList2: 

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

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

270 

271 # check the chipMappings 

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

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

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

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

276 

277 def test_photoCalibMean(self): 

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

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

280 image[:] = 1 

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

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

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

284 

285 

286class ConstrainedFluxModelTestCase(ConstrainedPhotometryModelTestCase, 

287 FluxTestBase, 

288 lsst.utils.tests.TestCase): 

289 def setUp(self): 

290 super().setUp() 

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

292 self.focalPlaneBBox, 

293 self.visitOrder) 

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

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

296 self.model.offsetParams(self.delta) 

297 

298 self._initModel2(lsst.jointcal.ConstrainedFluxModel) 

299 

300 def testConstructor(self): 

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

302 expectVisit[0] = 1 

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

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

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

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

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

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

309 self._testConstructor(expectVisit, expectChips) 

310 

311 def test_checkPositiveOnBBox(self): 

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

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

314 

315 # make the model go negative 

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

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

318 

319 def test_validate(self): 

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

321 """ 

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

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

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

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

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

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

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

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

330 

331 

332class ConstrainedMagnitudeModelTestCase(ConstrainedPhotometryModelTestCase, 

333 MagnitudeTestBase, 

334 lsst.utils.tests.TestCase): 

335 def setUp(self): 

336 super().setUp() 

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

338 self.focalPlaneBBox, 

339 self.visitOrder) 

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

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

342 self.model.offsetParams(self.delta) 

343 

344 self._initModel2(lsst.jointcal.ConstrainedMagnitudeModel) 

345 

346 self.useMagnitude = True 

347 

348 def testConstructor(self): 

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

350 

351 def fluxToMag(flux): 

352 # Yay astropy! 

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

354 

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

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

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

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

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

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

361 self._testConstructor(expectVisit, expectChips) 

362 

363 

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

365 pass 

366 

367 

368def setup_module(module): 

369 lsst.utils.tests.init() 

370 

371 

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

373 lsst.utils.tests.init() 

374 unittest.main()