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 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.daf.persistence 

35import lsst.jointcal.ccdImage 

36import lsst.jointcal.photometryModels 

37import lsst.jointcal.star 

38import lsst.obs.base 

39 

40 

41def getNParametersPolynomial(order): 

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

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

44 

45 

46class PhotometryModelTestBase: 

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

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

49 """ 

50 def setUp(self): 

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

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

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

54 

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

56 self.ccdImageList = struct.ccdImageList 

57 self.camera = struct.camera 

58 self.catalogs = struct.catalogs 

59 self.fluxFieldName = struct.fluxFieldName 

60 

61 self.stars = [] 

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

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

64 lsst.afw.cameraGeom.FOCAL_PLANE) 

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

66 

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

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

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

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

71 self.fittedStar.y, 

72 self.fittedStar.flux + 50, 

73 self.fittedStar.fluxErr * 0.01) 

74 

75 self.firstIndex = 0 # for assignIndices 

76 

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

78 self.useMagnitude = False 

79 

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

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

82 photoCalib = self.model.toPhotoCalib(ccdImage) 

83 if self.useMagnitude: 

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

85 else: 

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

87 

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

89 for i, star in enumerate(stars): 

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

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

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

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

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

95 

96 def test_toPhotoCalib(self): 

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

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

99 

100 def test_freezeErrorTransform(self): 

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

102 by offsetParams(). 

103 """ 

104 ccdImage = self.ccdImageList[0] 

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

106 

107 self.model.offsetParams(self.delta) 

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

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

110 self.model.freezeErrorTransform() 

111 self.model.offsetParams(self.delta) 

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

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

114 

115 self.assertFloatsNotEqual(t1, t2) 

116 self.assertFloatsEqual(t1Err, t2Err) 

117 

118 

119class FluxTestBase: 

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

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

122 """ 

123 def test_offsetFittedStar(self): 

124 value = self.fittedStar.flux 

125 

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

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

128 

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

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

131 

132 def test_computeRefResidual(self): 

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

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

135 

136 

137class MagnitudeTestBase: 

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

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

140 """ 

141 def test_offsetFittedStar(self): 

142 value = self.fittedStar.mag 

143 

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

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

146 

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

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

149 

150 def test_computeRefResidual(self): 

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

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

153 

154 

155class SimplePhotometryModelTestBase(PhotometryModelTestBase): 

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

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

158 """ 

159 def test_getNpar(self): 

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

161 self.assertEqual(result, 1) 

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

163 self.assertEqual(result, 1) 

164 

165 def testGetTotalParameters(self): 

166 result = self.model.getTotalParameters() 

167 self.assertEqual(result, 2) 

168 

169 

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

171 def setUp(self): 

172 super().setUp() 

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

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

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

176 

177 

178class SimpleMagnitudeModelTestCase(SimplePhotometryModelTestBase, 

179 MagnitudeTestBase, 

180 lsst.utils.tests.TestCase): 

181 def setUp(self): 

182 super().setUp() 

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

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

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

186 self.useMagnitude = True 

187 

188 

189class ConstrainedPhotometryModelTestCase(PhotometryModelTestBase): 

190 def setUp(self): 

191 super().setUp() 

192 self.visitOrder = 3 

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

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

195 # for the second ccdImage. 

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

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

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

199 self.delta[:10] = 0.0 

200 self.delta[0] = -5.0 

201 

202 def _initModel2(self, Model): 

203 """ 

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

205 

206 Parameters 

207 ---------- 

208 Model : `PhotometryModel`-type 

209 The PhotometryModel-derived class to construct. 

210 """ 

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

212 # in `test_assignIndices()`. 

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

214 # so there will be 2 visits total here. 

215 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100, seed=100, fakeCcdId=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, fakeCcdId=13, 

220 photoCalibMean1=2.0e-2, 

221 photoCalibMean2=2.2e-2) 

222 self.ccdImageList2.extend(struct2.ccdImageList) 

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

224 focalPlaneBBox = camera.getFpBBox() 

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

226 

227 def test_getNpar(self): 

228 """ 

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

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

231 """ 

232 expect = getNParametersPolynomial(self.visitOrder) 

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

234 self.assertEqual(result, expect) 

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

236 self.assertEqual(result, expect) 

237 

238 def testGetTotalParameters(self): 

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

240 expect = getNParametersPolynomial(self.visitOrder) * 2 

241 result = self.model.getTotalParameters() 

242 self.assertEqual(result, expect) 

243 

244 def test_assignIndices(self): 

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

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

247 indices. 

248 """ 

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

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

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

252 self.assertEqual(index, expect) 

253 

254 # one polynomial per visit 

255 expect = 2 * getNParametersPolynomial(self.visitOrder) 

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

257 self.assertEqual(index, expect) 

258 

259 # one fitted chip 

260 expect = 1 

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

262 self.assertEqual(index, expect) 

263 

264 def _testConstructor(self, expectVisit, expectChips): 

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

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

267 """ 

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

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

270 

271 # check the visitMappings 

272 for ccdImage in self.ccdImageList2: 

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

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

275 

276 # check the chipMappings 

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

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

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

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

281 

282 def test_photoCalibMean(self): 

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

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

285 image[:] = 1 

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

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

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

289 

290 

291class ConstrainedFluxModelTestCase(ConstrainedPhotometryModelTestCase, 

292 FluxTestBase, 

293 lsst.utils.tests.TestCase): 

294 def setUp(self): 

295 super().setUp() 

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

297 self.focalPlaneBBox, 

298 self.visitOrder) 

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

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

301 self.model.offsetParams(self.delta) 

302 

303 self._initModel2(lsst.jointcal.ConstrainedFluxModel) 

304 

305 def testConstructor(self): 

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

307 expectVisit[0] = 1 

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

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

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

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

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

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

314 self._testConstructor(expectVisit, expectChips) 

315 

316 def test_checkPositiveOnBBox(self): 

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

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

319 

320 # make the model go negative 

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

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

323 

324 def test_validate(self): 

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

326 """ 

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

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

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

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

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

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

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

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

335 

336 

337class ConstrainedMagnitudeModelTestCase(ConstrainedPhotometryModelTestCase, 

338 MagnitudeTestBase, 

339 lsst.utils.tests.TestCase): 

340 def setUp(self): 

341 super().setUp() 

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

343 self.focalPlaneBBox, 

344 self.visitOrder) 

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

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

347 self.model.offsetParams(self.delta) 

348 

349 self._initModel2(lsst.jointcal.ConstrainedMagnitudeModel) 

350 

351 self.useMagnitude = True 

352 

353 def testConstructor(self): 

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

355 

356 def fluxToMag(flux): 

357 # Yay astropy! 

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

359 

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

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

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

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

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

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

366 self._testConstructor(expectVisit, expectChips) 

367 

368 

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

370 pass 

371 

372 

373def setup_module(module): 

374 lsst.utils.tests.init() 

375 

376 

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

378 lsst.utils.tests.init() 

379 unittest.main()