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 

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.afw.image.utils.resetFilters() 

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, fakeCcdId=12, 

215 photoCalibMean1=1e-2, 

216 photoCalibMean2=1.2e-2) 

217 self.ccdImageList2 = struct1.ccdImageList 

218 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100, seed=101, fakeCcdId=13, 

219 photoCalibMean1=2.0e-2, 

220 photoCalibMean2=2.2e-2) 

221 self.ccdImageList2.extend(struct2.ccdImageList) 

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

223 focalPlaneBBox = camera.getFpBBox() 

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

225 

226 def test_getNpar(self): 

227 """ 

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

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

230 """ 

231 expect = getNParametersPolynomial(self.visitOrder) 

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

233 self.assertEqual(result, expect) 

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

235 self.assertEqual(result, expect) 

236 

237 def testGetTotalParameters(self): 

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

239 expect = getNParametersPolynomial(self.visitOrder) * 2 

240 result = self.model.getTotalParameters() 

241 self.assertEqual(result, expect) 

242 

243 def test_assignIndices(self): 

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

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

246 indices. 

247 """ 

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

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

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

251 self.assertEqual(index, expect) 

252 

253 # one polynomial per visit 

254 expect = 2 * getNParametersPolynomial(self.visitOrder) 

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

256 self.assertEqual(index, expect) 

257 

258 # one fitted chip 

259 expect = 1 

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

261 self.assertEqual(index, expect) 

262 

263 def _testConstructor(self, expectVisit, expectChips): 

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

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

266 """ 

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

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

269 

270 # check the visitMappings 

271 for ccdImage in self.ccdImageList2: 

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

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

274 

275 # check the chipMappings 

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

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

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

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

280 

281 def test_photoCalibMean(self): 

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

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

284 image[:] = 1 

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

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

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

288 

289 

290class ConstrainedFluxModelTestCase(ConstrainedPhotometryModelTestCase, 

291 FluxTestBase, 

292 lsst.utils.tests.TestCase): 

293 def setUp(self): 

294 super().setUp() 

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

296 self.focalPlaneBBox, 

297 self.visitOrder) 

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

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

300 self.model.offsetParams(self.delta) 

301 

302 self._initModel2(lsst.jointcal.ConstrainedFluxModel) 

303 

304 def testConstructor(self): 

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

306 expectVisit[0] = 1 

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

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

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

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

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

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

313 self._testConstructor(expectVisit, expectChips) 

314 

315 def test_checkPositiveOnBBox(self): 

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

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

318 

319 # make the model go negative 

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

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

322 

323 def test_validate(self): 

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

325 """ 

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

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

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

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

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

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

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

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

334 

335 

336class ConstrainedMagnitudeModelTestCase(ConstrainedPhotometryModelTestCase, 

337 MagnitudeTestBase, 

338 lsst.utils.tests.TestCase): 

339 def setUp(self): 

340 super().setUp() 

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

342 self.focalPlaneBBox, 

343 self.visitOrder) 

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

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

346 self.model.offsetParams(self.delta) 

347 

348 self._initModel2(lsst.jointcal.ConstrainedMagnitudeModel) 

349 

350 self.useMagnitude = True 

351 

352 def testConstructor(self): 

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

354 

355 def fluxToMag(flux): 

356 # Yay astropy! 

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

358 

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

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

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

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

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

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

365 self._testConstructor(expectVisit, expectChips) 

366 

367 

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

369 pass 

370 

371 

372def setup_module(module): 

373 lsst.utils.tests.init() 

374 

375 

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

377 lsst.utils.tests.init() 

378 unittest.main()