Coverage for tests/test_processCcd.py: 16%

Shortcuts 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

159 statements  

1# 

2# LSST Data Management System 

3# Copyright 2008-2016 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22"""Test ProcessCcdTask and its immediate subtasks. 

23 

24Run the task on one obs_test image and perform various checks on the results 

25""" 

26import os 

27import shutil 

28import tempfile 

29import unittest 

30 

31import numpy as np 

32 

33import lsst.utils 

34import lsst.geom as geom 

35import lsst.utils.tests 

36from lsst.ip.isr import IsrTask # we assume obs_test uses base IsrTask here; may change in future. 

37from lsst.pipe.tasks.characterizeImage import CharacterizeImageTask 

38from lsst.pipe.tasks.calibrate import CalibrateTask 

39from lsst.pipe.tasks.processCcd import ProcessCcdTask 

40 

41obsTestDir = lsst.utils.getPackageDir('obs_test') 

42InputDir = os.path.join(obsTestDir, "data", "input") 

43 

44OutputName = None # specify a name (as a string) to save the output repository 

45 

46 

47def getObsTestConfig(TaskClass): 

48 """Helper function to get a command-line task config customized by obs_test. 

49 

50 This duplicates the config override code in pipe_base's ArgumentParser, but 

51 essentially in order to test it. 

52 """ 

53 config = TaskClass.ConfigClass() 

54 filename = os.path.join(obsTestDir, "config", TaskClass._DefaultName + ".py") 

55 if os.path.exists(filename): 

56 config.load(filename) 

57 return config 

58 

59 

60class ProcessCcdTestCase(lsst.utils.tests.TestCase): 

61 

62 def testProcessCcd(self): 

63 """test ProcessCcdTask via parseAndRun (simulating the command line) 

64 

65 This is intended as a sanity check of the task, not a detailed test of its sub-tasks. As such 

66 comparisons are intentionally loose, so as to allow evolution of the sub-tasks over time 

67 without breaking this test. 

68 """ 

69 outPath = tempfile.mkdtemp() if OutputName is None else "{}-ProcessCcd".format(OutputName) 

70 try: 

71 dataId = dict(visit=1) 

72 dataIdStrList = ["%s=%s" % (key, val) for key, val in dataId.items()] 

73 fullResult = ProcessCcdTask.parseAndRun( 

74 args=[InputDir, "--output", outPath, "--clobber-config", "--doraise", 

75 "-c", "charImage.doWriteExposure=True", "--id"] + dataIdStrList, 

76 doReturnResults=True 

77 ) 

78 butler = fullResult.parsedCmd.butler 

79 self.assertEqual(len(fullResult.resultList), 1) 

80 runResult = fullResult.resultList[0] 

81 fullDataId = runResult.dataRef.dataId 

82 self.assertEqual(len(fullDataId), 2) 

83 self.assertEqual(fullDataId["visit"], dataId["visit"]) 

84 self.assertEqual(fullDataId["filter"], "g") 

85 result = runResult.result 

86 

87 icExpBackground = butler.get("icExpBackground", dataId, immediate=True) 

88 bg0Arr = icExpBackground.getImage().getArray() 

89 bgMean = bg0Arr.mean(dtype=np.float64) 

90 bgStdDev = bg0Arr.std(dtype=np.float64) 

91 

92 icSrc = butler.get("icSrc", dataId) 

93 src = butler.get("src", dataId) 

94 

95 # the following makes pyflakes linter happy and the code more robust 

96 oldImMean = None 

97 oldImStdDev = None 

98 oldVarMean = None 

99 oldVarStdDev = None 

100 oldPsfIxx = None 

101 oldPsfIyy = None 

102 oldPsfIxy = None 

103 

104 for i, exposure in enumerate((butler.get("calexp", dataId), result.exposure)): 

105 self.assertEqual(exposure.getBBox(), 

106 geom.Box2I(geom.Point2I(0, 0), geom.Extent2I(1018, 2000))) 

107 maskedImage = exposure.getMaskedImage() 

108 maskArr = maskedImage.getMask().getArray() 

109 numGoodPix = np.sum(maskArr == 0) 

110 

111 imageArr = maskedImage.getImage().getArray() 

112 imMean = imageArr.mean(dtype=np.float64) 

113 imStdDev = imageArr.std(dtype=np.float64) 

114 varArr = maskedImage.getVariance().getArray() 

115 varMean = varArr.mean(dtype=np.float64) 

116 varStdDev = varArr.std(dtype=np.float64) 

117 

118 summary = exposure.getInfo().getSummaryStats() 

119 

120 psf = exposure.getPsf() 

121 psfAvgPos = psf.getAveragePosition() 

122 psfShape = psf.computeShape(psfAvgPos) 

123 psfIxx = psfShape.getIxx() 

124 psfIyy = psfShape.getIyy() 

125 psfIxy = psfShape.getIxy() 

126 

127 if i == 0: 

128 print("\nMeasured results:") 

129 print("background mean = %r, stdDev = %r" % (bgMean, bgStdDev)) 

130 print("len(icSrc) :", len(icSrc)) 

131 print("len(src) :", len(src)) 

132 

133 print("numGoodPix =", numGoodPix) 

134 print("image mean = %r, stdDev = %r" % (imMean, imStdDev)) 

135 print("variance mean = %r, stdDev = %r" % (varMean, varStdDev)) 

136 print("psf Ixx = %r, Iyy = %r, Ixy = %r" % (psfIxx, psfIyy, psfIxy)) 

137 

138 self.assertEqual(len(icSrc), 28) 

139 self.assertEqual(len(src), 184 + 100) # 184 real sources plus 100 sky sources 

140 

141 # NOTE: These values are purely empirical, and need to be 

142 # updated to reflect major algorithmic changes. 

143 # If this test fails after an algorithmic change due to 

144 # small numeric changes here, check on slack at 

145 # #dm-science-pipelines as to whether the changes are 

146 # reasonable, and then replace the failing values by 

147 # running the test to determine the updated values. 

148 expectedPlaces = 7 # Tolerance for numerical comparisons 

149 for name, var, val in [ 

150 ("bgMean", bgMean, 191.48618251407825), 

151 ("bgStdDev", bgStdDev, 0.23993854036946), 

152 ("numGoodPix", numGoodPix, 1966533), 

153 ("imMean", imMean, 1.12428293713628), 

154 ("imStdDev", imStdDev, 85.81297524976699), 

155 ("varMean", varMean, 131.24003624152013), 

156 ("varStdDev", varStdDev, 55.98012493452948), 

157 ("psfIxx", psfIxx, 2.86673377350734), 

158 ("psfIyy", psfIyy, 2.22794091001822), 

159 ("psfIxy", psfIxy, 0.15356726688314), 

160 ("summary_psfSigma", summary.psfSigma, 1.58741282647433), 

161 ("summary_psfIxx", summary.psfIxx, 2.87668148939724), 

162 ("summary_psfIyy", summary.psfIyy, 2.21573211240281), 

163 ("summary_psfIxy", summary.psfIxy, 0.15544436672073), 

164 ("summary_psfArea", summary.psfArea, 38.06710084772205), 

165 ("summary_ra", summary.ra, 78.85551507080474), 

166 ("summary_decl", summary.decl, -9.80025868759230), 

167 ("summary_zenithDistance", float('%.6f' % (summary.zenithDistance)), 42.361001), 

168 ("summary_zeroPoint", summary.zeroPoint, 30.94022814763921), 

169 ("summary_skyBg", summary.skyBg, 191.37631678208709), 

170 ("summary_skyNoise", summary.skyNoise, 12.51227414082489), 

171 ("summary_meanVar", summary.meanVar, 130.61335199119068), 

172 ]: 

173 # Uncomment following line to get replacement code when values need updating. 

174 # Watch out for zenithDistance though! 

175 # print(f'("{name}", {name.replace("_", ".")}, {var:.14f}),') 

176 self.assertAlmostEqual(var, val, places=expectedPlaces, msg=name) 

177 

178 else: 

179 self.assertEqual(imMean, oldImMean) 

180 self.assertEqual(imStdDev, oldImStdDev) 

181 self.assertEqual(varMean, oldVarMean) 

182 self.assertEqual(varStdDev, oldVarStdDev) 

183 self.assertEqual(psfIxx, oldPsfIxx) 

184 self.assertEqual(psfIyy, oldPsfIyy) 

185 self.assertEqual(psfIxy, oldPsfIxy) 

186 

187 oldImMean = imMean 

188 oldImStdDev = imStdDev 

189 oldVarMean = varMean 

190 oldVarStdDev = varStdDev 

191 oldPsfIxx = psfIxx 

192 oldPsfIyy = psfIyy 

193 oldPsfIxy = psfIxy 

194 

195 finally: 

196 if OutputName is None: 

197 shutil.rmtree(outPath) 

198 else: 

199 print("testProcessCcd.py's output data saved to %r" % (OutputName,)) 

200 

201 def assertCatalogsEqual(self, catalog1, catalog2, skipCols=()): 

202 """Compare two Catalogs for equality. 

203 

204 This should only be used in contexts where it's unlikely that the catalogs will be subtly different; 

205 instead of comparing all values we simply do a spot check of a few cells. 

206 

207 This does not require that either catalog be contiguous (which is why we can't use column access). 

208 """ 

209 self.assertEqual(catalog1.schema, catalog2.schema) 

210 self.assertEqual(len(catalog1), len(catalog2)) 

211 d = catalog1.schema.extract("*") 

212 

213 def fixNaN(x): 

214 if x != x: 

215 return "NaN" 

216 

217 for record1, record2 in zip(catalog1, catalog2): 

218 for name, item in d.items(): 

219 if name not in skipCols: 

220 self.assertEqual( 

221 fixNaN(record1.get(item.key)), fixNaN(record2.get(item.key)), 

222 "{} != {} in field {}".format(record1.get(item.key), record2.get(item.key), name) 

223 ) 

224 

225 def assertBackgroundListsEqual(self, bkg1, bkg2): 

226 """Compare two BackgroundLists for equality. 

227 

228 This should only be used in contexts where it's unlikely that the catalogs will be subtly different; 

229 instead of comparing all values we simply do a spot check of a few cells. 

230 

231 This does not require that either catalog be contiguous (which is why we can't use column access). 

232 """ 

233 im1 = bkg1.getImage() 

234 im2 = bkg2.getImage() 

235 self.assertEqual(im1.getBBox(), im2.getBBox()) 

236 self.assertImagesEqual(im1, im2) 

237 

238 def testComponents(self): 

239 """Test that we can run the first-level subtasks of ProcessCcdTasks. 

240 

241 This tests that we can run these subtasks from the command-line independently (they're all 

242 CmdLineTasks) as well as directly from Python (without giving them access to a Butler). 

243 

244 Aside from verifying that no exceptions are raised, we simply tests that most persisted results are 

245 present and equivalent to both in-memory results. 

246 """ 

247 outPath = tempfile.mkdtemp() if OutputName is None else "{}-Components".format(OutputName) 

248 # We'll use an input butler to get data for the tasks we call from Python, but we won't ever give it 

249 # to those tasks. 

250 inputButler = lsst.daf.persistence.Butler(InputDir) 

251 # Construct task instances we can use directly from Python 

252 isrTask = IsrTask( 

253 config=getObsTestConfig(IsrTask), 

254 name="isr2" 

255 ) 

256 # If we ever enable astrometry and photocal in obs_test, we'll need to pass a refObjLoader to these 

257 # tasks. To maintain the spirit of these tests, we'd ideally have a LoadReferenceObjectsTask class 

258 # that doesn't require a Butler. If we don't, we should construct a butler-based on outside these 

259 # task constructors and pass the LoadReferenceObjectsTask instance to the task constructors. 

260 charImageTask = CharacterizeImageTask( 

261 config=getObsTestConfig(CharacterizeImageTask), 

262 name="charImage2" 

263 ) 

264 calibrateTask = CalibrateTask( 

265 config=getObsTestConfig(CalibrateTask), 

266 name="calibrate2", 

267 icSourceSchema=charImageTask.schema 

268 ) 

269 try: 

270 dataId = dict(visit=1) 

271 dataIdStrList = ["%s=%s" % (key, val) for key, val in dataId.items()] 

272 

273 isrResult1 = IsrTask.parseAndRun( 

274 args=[InputDir, "--output", outPath, "--clobber-config", "--doraise", "--id"] + dataIdStrList, 

275 doReturnResults=True, 

276 ) 

277 # We'll just use the butler to get the original image and calibration frames; it's not clear 

278 # extending the test coverage to include that is worth it. 

279 dataRef = inputButler.dataRef("raw", dataId=dataId) 

280 rawExposure = dataRef.get("raw", immediate=True) 

281 camera = dataRef.get("camera") 

282 isrData = isrTask.readIsrData(dataRef, rawExposure) 

283 exposureIdInfo = inputButler.get("expIdInfo", dataId=dataId) 

284 isrResult2 = isrTask.run( 

285 rawExposure, 

286 bias=isrData.bias, 

287 linearizer=isrData.linearizer, 

288 flat=isrData.flat, 

289 defects=isrData.defects, 

290 fringes=isrData.fringes, 

291 bfKernel=isrData.bfKernel, 

292 camera=camera, 

293 ) 

294 self.assertMaskedImagesEqual( 

295 isrResult1.parsedCmd.butler.get("postISRCCD", dataId, immediate=True).getMaskedImage(), 

296 isrResult1.resultList[0].result.exposure.getMaskedImage() 

297 ) 

298 self.assertMaskedImagesEqual( 

299 isrResult2.exposure.getMaskedImage(), 

300 isrResult1.resultList[0].result.exposure.getMaskedImage() 

301 ) 

302 

303 icResult1 = CharacterizeImageTask.parseAndRun( 

304 args=[InputDir, "--output", outPath, "--clobber-config", "--doraise", "--id"] + dataIdStrList, 

305 doReturnResults=True, 

306 ) 

307 icResult2 = charImageTask.run(isrResult2.exposure, exposureIdInfo=exposureIdInfo) 

308 self.assertMaskedImagesEqual( 

309 icResult1.parsedCmd.butler.get("icExp", dataId, immediate=True).getMaskedImage(), 

310 icResult1.resultList[0].result.exposure.getMaskedImage() 

311 ) 

312 self.assertMaskedImagesEqual( 

313 icResult2.exposure.getMaskedImage(), 

314 icResult1.resultList[0].result.exposure.getMaskedImage() 

315 ) 

316 self.assertCatalogsEqual( 

317 icResult1.parsedCmd.butler.get("icSrc", dataId, immediate=True), 

318 icResult1.resultList[0].result.sourceCat 

319 ) 

320 self.assertCatalogsEqual( 

321 icResult2.sourceCat, 

322 icResult1.resultList[0].result.sourceCat, 

323 ) 

324 self.assertBackgroundListsEqual( 

325 icResult1.parsedCmd.butler.get("icExpBackground", dataId, immediate=True), 

326 icResult1.resultList[0].result.background 

327 ) 

328 self.assertBackgroundListsEqual( 

329 icResult2.background, 

330 icResult1.resultList[0].result.background 

331 ) 

332 

333 calResult1 = CalibrateTask.parseAndRun( 

334 args=[InputDir, "--output", outPath, "--clobber-config", "--doraise", "--id"] + dataIdStrList, 

335 doReturnResults=True, 

336 ) 

337 calResult2 = calibrateTask.run( 

338 icResult2.exposure, 

339 background=icResult2.background, 

340 icSourceCat=icResult2.sourceCat, 

341 exposureIdInfo=exposureIdInfo, 

342 ) 

343 self.assertMaskedImagesEqual( 

344 calResult1.parsedCmd.butler.get("calexp", dataId, immediate=True).getMaskedImage(), 

345 calResult1.resultList[0].result.exposure.getMaskedImage() 

346 ) 

347 self.assertMaskedImagesEqual( 

348 calResult2.exposure.getMaskedImage(), 

349 calResult1.resultList[0].result.exposure.getMaskedImage() 

350 ) 

351 self.assertCatalogsEqual( 

352 calResult1.parsedCmd.butler.get("src", dataId, immediate=True), 

353 calResult1.resultList[0].result.sourceCat 

354 ) 

355 self.assertCatalogsEqual( 

356 calResult2.sourceCat, 

357 calResult1.resultList[0].result.sourceCat, 

358 skipCols=("id", "parent") 

359 ) 

360 self.assertBackgroundListsEqual( 

361 calResult1.parsedCmd.butler.get("calexpBackground", dataId, immediate=True), 

362 calResult1.resultList[0].result.background 

363 ) 

364 self.assertBackgroundListsEqual( 

365 calResult2.background, 

366 calResult1.resultList[0].result.background 

367 ) 

368 

369 finally: 

370 if OutputName is None: 

371 shutil.rmtree(outPath) 

372 else: 

373 print("testProcessCcd.py's output data saved to %r" % (OutputName,)) 

374 

375 

376def setup_module(module): 

377 lsst.utils.tests.init() 

378 

379 

380class MemoryTestCase(lsst.utils.tests.MemoryTestCase): 

381 pass 

382 

383 

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

385 lsst.utils.tests.init() 

386 unittest.main()