Coverage for tests/test_detectAndMeasure.py: 6%

440 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-18 12:57 +0000

1# This file is part of ip_diffim. 

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 

23import unittest 

24 

25import lsst.geom 

26from lsst.ip.diffim import detectAndMeasure, subtractImages 

27from lsst.ip.diffim.utils import makeTestImage 

28from lsst.pipe.base import InvalidQuantumError 

29import lsst.utils.tests 

30 

31 

32class DetectAndMeasureTestBase(lsst.utils.tests.TestCase): 

33 

34 def _check_diaSource(self, refSources, diaSource, refIds=None, 

35 matchDistance=1., scale=1., usePsfFlux=True, 

36 rtol=0.02, atol=None): 

37 """Match a diaSource with a source in a reference catalog 

38 and compare properties. 

39 

40 Parameters 

41 ---------- 

42 refSources : `lsst.afw.table.SourceCatalog` 

43 The reference catalog. 

44 diaSource : `lsst.afw.table.SourceRecord` 

45 The new diaSource to match to the reference catalog. 

46 refIds : `list` of `int`, optional 

47 Source IDs of previously associated diaSources. 

48 matchDistance : `float`, optional 

49 Maximum distance allowed between the detected and reference source 

50 locations, in pixels. 

51 scale : `float`, optional 

52 Optional factor to scale the flux by before performing the test. 

53 usePsfFlux : `bool`, optional 

54 If set, test the PsfInstFlux field, otherwise use ApInstFlux. 

55 rtol : `float`, optional 

56 Relative tolerance of the flux value test. 

57 atol : `float`, optional 

58 Absolute tolerance of the flux value test. 

59 """ 

60 distance = np.sqrt((diaSource.getX() - refSources.getX())**2 

61 + (diaSource.getY() - refSources.getY())**2) 

62 self.assertLess(min(distance), matchDistance) 

63 src = refSources[np.argmin(distance)] 

64 if refIds is not None: 

65 # Check that the same source was not previously associated 

66 self.assertNotIn(src.getId(), refIds) 

67 refIds.append(src.getId()) 

68 if atol is None: 

69 atol = rtol*src.getPsfInstFlux() if usePsfFlux else rtol*src.getApInstFlux() 

70 if usePsfFlux: 

71 self.assertFloatsAlmostEqual(src.getPsfInstFlux()*scale, diaSource.getPsfInstFlux(), 

72 rtol=rtol, atol=atol) 

73 else: 

74 self.assertFloatsAlmostEqual(src.getApInstFlux()*scale, diaSource.getApInstFlux(), 

75 rtol=rtol, atol=atol) 

76 

77 def _check_values(self, values, minValue=None, maxValue=None): 

78 """Verify that an array has finite values, and optionally that they are 

79 within specified minimum and maximum bounds. 

80 

81 Parameters 

82 ---------- 

83 values : `numpy.ndarray` 

84 Array of values to check. 

85 minValue : `float`, optional 

86 Minimum allowable value. 

87 maxValue : `float`, optional 

88 Maximum allowable value. 

89 """ 

90 self.assertTrue(np.all(np.isfinite(values))) 

91 if minValue is not None: 

92 self.assertTrue(np.all(values >= minValue)) 

93 if maxValue is not None: 

94 self.assertTrue(np.all(values <= maxValue)) 

95 

96 def _setup_detection(self, doSkySources=False, nSkySources=5, **kwargs): 

97 """Setup and configure the detection and measurement PipelineTask. 

98 

99 Parameters 

100 ---------- 

101 doSkySources : `bool`, optional 

102 Generate sky sources. 

103 nSkySources : `int`, optional 

104 The number of sky sources to add in isolated background regions. 

105 **kwargs 

106 Any additional config parameters to set. 

107 

108 Returns 

109 ------- 

110 `lsst.pipe.base.PipelineTask` 

111 The configured Task to use for detection and measurement. 

112 """ 

113 config = self.detectionTask.ConfigClass() 

114 config.doSkySources = doSkySources 

115 if doSkySources: 

116 config.skySources.nSources = nSkySources 

117 config.update(**kwargs) 

118 return self.detectionTask(config=config) 

119 

120 

121class DetectAndMeasureTest(DetectAndMeasureTestBase): 

122 detectionTask = detectAndMeasure.DetectAndMeasureTask 

123 

124 def test_detection_xy0(self): 

125 """Basic functionality test with non-zero x0 and y0. 

126 """ 

127 # Set up the simulated images 

128 noiseLevel = 1. 

129 staticSeed = 1 

130 fluxLevel = 500 

131 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "x0": 12345, "y0": 67890} 

132 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

133 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

134 difference = science.clone() 

135 

136 # Configure the detection Task 

137 detectionTask = self._setup_detection() 

138 

139 # Run detection and check the results 

140 output = detectionTask.run(science, matchedTemplate, difference) 

141 subtractedMeasuredExposure = output.subtractedMeasuredExposure 

142 

143 self.assertImagesEqual(subtractedMeasuredExposure.image, difference.image) 

144 

145 def test_measurements_finite(self): 

146 """Measured fluxes and centroids should always be finite. 

147 """ 

148 columnNames = ["coord_ra", "coord_dec", "ip_diffim_forced_PsfFlux_instFlux"] 

149 

150 # Set up the simulated images 

151 noiseLevel = 1. 

152 staticSeed = 1 

153 transientSeed = 6 

154 xSize = 256 

155 ySize = 256 

156 kwargs = {"psfSize": 2.4, "x0": 0, "y0": 0, 

157 "xSize": xSize, "ySize": ySize} 

158 science, sources = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel, noiseSeed=6, 

159 nSrc=1, **kwargs) 

160 matchedTemplate, _ = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel/4, noiseSeed=7, 

161 nSrc=1, **kwargs) 

162 rng = np.random.RandomState(3) 

163 xLoc = np.arange(-5, xSize+5, 10) 

164 rng.shuffle(xLoc) 

165 yLoc = np.arange(-5, ySize+5, 10) 

166 rng.shuffle(yLoc) 

167 transients, transientSources = makeTestImage(seed=transientSeed, 

168 nSrc=len(xLoc), fluxLevel=1000., 

169 noiseLevel=noiseLevel, noiseSeed=8, 

170 xLoc=xLoc, yLoc=yLoc, 

171 **kwargs) 

172 difference = science.clone() 

173 difference.maskedImage -= matchedTemplate.maskedImage 

174 difference.maskedImage += transients.maskedImage 

175 

176 # Configure the detection Task 

177 detectionTask = self._setup_detection(doForcedMeasurement=True) 

178 

179 # Run detection and check the results 

180 output = detectionTask.run(science, matchedTemplate, difference) 

181 

182 for column in columnNames: 

183 self._check_values(output.diaSources[column]) 

184 self._check_values(output.diaSources.getX(), minValue=0, maxValue=xSize) 

185 self._check_values(output.diaSources.getY(), minValue=0, maxValue=ySize) 

186 self._check_values(output.diaSources.getPsfInstFlux()) 

187 

188 def test_raise_config_schema_mismatch(self): 

189 """Check that sources with specified flags are removed from the catalog. 

190 """ 

191 # Configure the detection Task, and and set a config that is not in the schema 

192 with self.assertRaises(InvalidQuantumError): 

193 self._setup_detection(badSourceFlags=["Bogus_flag_42"]) 

194 

195 def test_remove_unphysical(self): 

196 """Check that sources with specified flags are removed from the catalog. 

197 """ 

198 # Set up the simulated images 

199 noiseLevel = 1. 

200 staticSeed = 1 

201 xSize = 256 

202 ySize = 256 

203 kwargs = {"psfSize": 2.4, "xSize": xSize, "ySize": ySize} 

204 science, sources = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel, noiseSeed=6, 

205 nSrc=1, **kwargs) 

206 matchedTemplate, _ = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel/4, noiseSeed=7, 

207 nSrc=1, **kwargs) 

208 difference = science.clone() 

209 bbox = difference.getBBox() 

210 difference.maskedImage -= matchedTemplate.maskedImage 

211 

212 # Configure the detection Task, and do not remove unphysical sources 

213 detectionTask = self._setup_detection(doForcedMeasurement=False, doSkySources=True, nSkySources=20, 

214 badSourceFlags=[]) 

215 

216 # Run detection and check the results 

217 diaSources = detectionTask.run(science, matchedTemplate, difference).diaSources 

218 badDiaSrcNoRemove = ~bbox.contains(diaSources.getX(), diaSources.getY()) 

219 nBadNoRemove = np.count_nonzero(badDiaSrcNoRemove) 

220 # Verify that unphysical sources exist 

221 self.assertGreater(nBadNoRemove, 0) 

222 

223 # Configure the detection Task, and remove unphysical sources 

224 detectionTask = self._setup_detection(doForcedMeasurement=False, doSkySources=True, nSkySources=20, 

225 badSourceFlags=["base_PixelFlags_flag_offimage", ]) 

226 

227 # Run detection and check the results 

228 diaSources = detectionTask.run(science, matchedTemplate, difference).diaSources 

229 badDiaSrcDoRemove = ~bbox.contains(diaSources.getX(), diaSources.getY()) 

230 nBadDoRemove = np.count_nonzero(badDiaSrcDoRemove) 

231 # Verify that all sources are physical 

232 self.assertEqual(nBadDoRemove, 0) 

233 # Set a few centroids outside the image bounding box 

234 nSetBad = 5 

235 for src in diaSources[0: nSetBad]: 

236 src["slot_Centroid_x"] += xSize 

237 src["slot_Centroid_y"] += ySize 

238 src["base_PixelFlags_flag_offimage"] = True 

239 # Verify that these sources are outside the image 

240 badDiaSrc = ~bbox.contains(diaSources.getX(), diaSources.getY()) 

241 nBad = np.count_nonzero(badDiaSrc) 

242 self.assertEqual(nBad, nSetBad) 

243 diaSourcesNoBad = detectionTask._removeBadSources(diaSources) 

244 badDiaSrcNoBad = ~bbox.contains(diaSourcesNoBad.getX(), diaSourcesNoBad.getY()) 

245 

246 # Verify that no sources outside the image bounding box remain 

247 self.assertEqual(np.count_nonzero(badDiaSrcNoBad), 0) 

248 self.assertEqual(len(diaSourcesNoBad), len(diaSources) - nSetBad) 

249 

250 def test_detect_transients(self): 

251 """Run detection on a difference image containing transients. 

252 """ 

253 # Set up the simulated images 

254 noiseLevel = 1. 

255 staticSeed = 1 

256 transientSeed = 6 

257 fluxLevel = 500 

258 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel} 

259 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

260 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

261 

262 # Configure the detection Task 

263 detectionTask = self._setup_detection(doMerge=False) 

264 kwargs["seed"] = transientSeed 

265 kwargs["nSrc"] = 10 

266 kwargs["fluxLevel"] = 1000 

267 

268 # Run detection and check the results 

269 def _detection_wrapper(positive=True): 

270 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs) 

271 difference = science.clone() 

272 difference.maskedImage -= matchedTemplate.maskedImage 

273 if positive: 

274 difference.maskedImage += transients.maskedImage 

275 else: 

276 difference.maskedImage -= transients.maskedImage 

277 output = detectionTask.run(science, matchedTemplate, difference) 

278 refIds = [] 

279 scale = 1. if positive else -1. 

280 for diaSource in output.diaSources: 

281 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=scale) 

282 _detection_wrapper(positive=True) 

283 _detection_wrapper(positive=False) 

284 

285 def test_detect_dipoles(self): 

286 """Run detection on a difference image containing dipoles. 

287 """ 

288 # Set up the simulated images 

289 noiseLevel = 1. 

290 staticSeed = 1 

291 fluxLevel = 1000 

292 fluxRange = 1.5 

293 nSources = 10 

294 offset = 1 

295 xSize = 300 

296 ySize = 300 

297 kernelSize = 32 

298 # Avoid placing sources near the edge for this test, so that we can 

299 # easily check that the correct number of sources are detected. 

300 templateBorderSize = kernelSize//2 

301 dipoleFlag = "ip_diffim_DipoleFit_flag_classification" 

302 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "fluxRange": fluxRange, 

303 "nSrc": nSources, "templateBorderSize": templateBorderSize, "kernelSize": kernelSize, 

304 "xSize": xSize, "ySize": ySize} 

305 dipoleFlag = "ip_diffim_DipoleFit_flag_classification" 

306 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

307 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

308 difference = science.clone() 

309 matchedTemplate.image.array[...] = np.roll(matchedTemplate.image.array[...], offset, axis=0) 

310 matchedTemplate.variance.array[...] = np.roll(matchedTemplate.variance.array[...], offset, axis=0) 

311 matchedTemplate.mask.array[...] = np.roll(matchedTemplate.mask.array[...], offset, axis=0) 

312 difference.maskedImage -= matchedTemplate.maskedImage[science.getBBox()] 

313 

314 # Configure the detection Task 

315 detectionTask = self._setup_detection(doMerge=False) 

316 

317 # Run detection and check the results 

318 output = detectionTask.run(science, matchedTemplate, difference) 

319 self.assertIn(dipoleFlag, output.diaSources.schema.getNames()) 

320 nSourcesDet = len(sources) 

321 self.assertEqual(len(output.diaSources), 2*nSourcesDet) 

322 refIds = [] 

323 # The diaSource check should fail if we don't merge positive and negative footprints 

324 for diaSource in output.diaSources: 

325 with self.assertRaises(AssertionError): 

326 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0, 

327 atol=np.sqrt(fluxRange*fluxLevel)) 

328 

329 detectionTask2 = self._setup_detection(doMerge=True) 

330 output2 = detectionTask2.run(science, matchedTemplate, difference) 

331 self.assertEqual(len(output2.diaSources), nSourcesDet) 

332 refIds = [] 

333 for diaSource in output2.diaSources: 

334 if diaSource[dipoleFlag]: 

335 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0, 

336 rtol=0.05, atol=None, usePsfFlux=False) 

337 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_orientation"], -90., atol=2.) 

338 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_separation"], offset, rtol=0.1) 

339 else: 

340 raise ValueError("DiaSource with ID %s is not a dipole!", diaSource.getId()) 

341 

342 def test_sky_sources(self): 

343 """Add sky sources and check that they are sufficiently far from other 

344 sources and have negligible flux. 

345 """ 

346 # Set up the simulated images 

347 noiseLevel = 1. 

348 staticSeed = 1 

349 transientSeed = 6 

350 transientFluxLevel = 1000. 

351 transientFluxRange = 1.5 

352 fluxLevel = 500 

353 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel} 

354 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

355 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

356 transients, transientSources = makeTestImage(seed=transientSeed, psfSize=2.4, 

357 nSrc=10, fluxLevel=transientFluxLevel, 

358 fluxRange=transientFluxRange, 

359 noiseLevel=noiseLevel, noiseSeed=8) 

360 difference = science.clone() 

361 difference.maskedImage -= matchedTemplate.maskedImage 

362 difference.maskedImage += transients.maskedImage 

363 kernelWidth = np.max(science.psf.getKernel().getDimensions())//2 

364 

365 # Configure the detection Task 

366 detectionTask = self._setup_detection(doSkySources=True) 

367 

368 # Run detection and check the results 

369 output = detectionTask.run(science, matchedTemplate, difference) 

370 skySources = output.diaSources[output.diaSources["sky_source"]] 

371 self.assertEqual(len(skySources), detectionTask.config.skySources.nSources) 

372 for skySource in skySources: 

373 # The sky sources should not be close to any other source 

374 with self.assertRaises(AssertionError): 

375 self._check_diaSource(transientSources, skySource, matchDistance=kernelWidth) 

376 with self.assertRaises(AssertionError): 

377 self._check_diaSource(sources, skySource, matchDistance=kernelWidth) 

378 # The sky sources should have low flux levels. 

379 self._check_diaSource(transientSources, skySource, matchDistance=1000, scale=0., 

380 atol=np.sqrt(transientFluxRange*transientFluxLevel)) 

381 

382 def test_edge_detections(self): 

383 """Sources with certain bad mask planes set should not be detected. 

384 """ 

385 # Set up the simulated images 

386 noiseLevel = 1. 

387 staticSeed = 1 

388 transientSeed = 6 

389 fluxLevel = 500 

390 radius = 2 

391 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel} 

392 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

393 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

394 

395 _checkMask = subtractImages.AlardLuptonSubtractTask._checkMask 

396 # Configure the detection Task 

397 detectionTask = self._setup_detection() 

398 excludeMaskPlanes = detectionTask.config.detection.excludeMaskPlanes 

399 nBad = len(excludeMaskPlanes) 

400 self.assertGreater(nBad, 0) 

401 kwargs["seed"] = transientSeed 

402 kwargs["nSrc"] = nBad 

403 kwargs["fluxLevel"] = 1000 

404 

405 # Run detection and check the results 

406 def _detection_wrapper(setFlags=True): 

407 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs) 

408 difference = science.clone() 

409 difference.maskedImage -= matchedTemplate.maskedImage 

410 difference.maskedImage += transients.maskedImage 

411 if setFlags: 

412 for src, badMask in zip(transientSources, excludeMaskPlanes): 

413 srcX = int(src.getX()) 

414 srcY = int(src.getY()) 

415 srcBbox = lsst.geom.Box2I(lsst.geom.Point2I(srcX - radius, srcY - radius), 

416 lsst.geom.Extent2I(2*radius + 1, 2*radius + 1)) 

417 difference[srcBbox].mask.array |= lsst.afw.image.Mask.getPlaneBitMask(badMask) 

418 output = detectionTask.run(science, matchedTemplate, difference) 

419 refIds = [] 

420 goodSrcFlags = _checkMask(difference.mask, transientSources, excludeMaskPlanes) 

421 if setFlags: 

422 self.assertEqual(np.sum(~goodSrcFlags), nBad) 

423 else: 

424 self.assertEqual(np.sum(~goodSrcFlags), 0) 

425 for diaSource, goodSrcFlag in zip(output.diaSources, goodSrcFlags): 

426 if ~goodSrcFlag: 

427 with self.assertRaises(AssertionError): 

428 self._check_diaSource(transientSources, diaSource, refIds=refIds) 

429 else: 

430 self._check_diaSource(transientSources, diaSource, refIds=refIds) 

431 _detection_wrapper(setFlags=False) 

432 _detection_wrapper(setFlags=True) 

433 

434 

435class DetectAndMeasureScoreTest(DetectAndMeasureTestBase): 

436 detectionTask = detectAndMeasure.DetectAndMeasureScoreTask 

437 

438 def test_detection_xy0(self): 

439 """Basic functionality test with non-zero x0 and y0. 

440 """ 

441 # Set up the simulated images 

442 noiseLevel = 1. 

443 staticSeed = 1 

444 fluxLevel = 500 

445 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "x0": 12345, "y0": 67890} 

446 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

447 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

448 difference = science.clone() 

449 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

450 scienceKernel = science.psf.getKernel() 

451 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl) 

452 

453 # Configure the detection Task 

454 detectionTask = self._setup_detection() 

455 

456 # Run detection and check the results 

457 output = detectionTask.run(science, matchedTemplate, difference, score) 

458 subtractedMeasuredExposure = output.subtractedMeasuredExposure 

459 

460 self.assertImagesEqual(subtractedMeasuredExposure.image, difference.image) 

461 

462 def test_measurements_finite(self): 

463 """Measured fluxes and centroids should always be finite. 

464 """ 

465 columnNames = ["coord_ra", "coord_dec", "ip_diffim_forced_PsfFlux_instFlux"] 

466 

467 # Set up the simulated images 

468 noiseLevel = 1. 

469 staticSeed = 1 

470 transientSeed = 6 

471 xSize = 256 

472 ySize = 256 

473 kwargs = {"psfSize": 2.4, "x0": 0, "y0": 0, 

474 "xSize": xSize, "ySize": ySize} 

475 science, sources = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel, noiseSeed=6, 

476 nSrc=1, **kwargs) 

477 matchedTemplate, _ = makeTestImage(seed=staticSeed, noiseLevel=noiseLevel/4, noiseSeed=7, 

478 nSrc=1, **kwargs) 

479 rng = np.random.RandomState(3) 

480 xLoc = np.arange(-5, xSize+5, 10) 

481 rng.shuffle(xLoc) 

482 yLoc = np.arange(-5, ySize+5, 10) 

483 rng.shuffle(yLoc) 

484 transients, transientSources = makeTestImage(seed=transientSeed, 

485 nSrc=len(xLoc), fluxLevel=1000., 

486 noiseLevel=noiseLevel, noiseSeed=8, 

487 xLoc=xLoc, yLoc=yLoc, 

488 **kwargs) 

489 difference = science.clone() 

490 difference.maskedImage -= matchedTemplate.maskedImage 

491 difference.maskedImage += transients.maskedImage 

492 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

493 scienceKernel = science.psf.getKernel() 

494 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl) 

495 

496 # Configure the detection Task 

497 detectionTask = self._setup_detection(doForcedMeasurement=True) 

498 

499 # Run detection and check the results 

500 output = detectionTask.run(science, matchedTemplate, difference, score) 

501 

502 for column in columnNames: 

503 self._check_values(output.diaSources[column]) 

504 self._check_values(output.diaSources.getX(), minValue=0, maxValue=xSize) 

505 self._check_values(output.diaSources.getY(), minValue=0, maxValue=ySize) 

506 self._check_values(output.diaSources.getPsfInstFlux()) 

507 

508 def test_detect_transients(self): 

509 """Run detection on a difference image containing transients. 

510 """ 

511 # Set up the simulated images 

512 noiseLevel = 1. 

513 staticSeed = 1 

514 transientSeed = 6 

515 fluxLevel = 500 

516 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel} 

517 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

518 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

519 scienceKernel = science.psf.getKernel() 

520 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

521 

522 # Configure the detection Task 

523 detectionTask = self._setup_detection(doMerge=False) 

524 kwargs["seed"] = transientSeed 

525 kwargs["nSrc"] = 10 

526 kwargs["fluxLevel"] = 1000 

527 

528 # Run detection and check the results 

529 def _detection_wrapper(positive=True): 

530 """Simulate positive or negative transients and run detection. 

531 

532 Parameters 

533 ---------- 

534 positive : `bool`, optional 

535 If set, use positive transient sources. 

536 """ 

537 

538 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs) 

539 difference = science.clone() 

540 difference.maskedImage -= matchedTemplate.maskedImage 

541 if positive: 

542 difference.maskedImage += transients.maskedImage 

543 else: 

544 difference.maskedImage -= transients.maskedImage 

545 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl) 

546 output = detectionTask.run(science, matchedTemplate, difference, score) 

547 refIds = [] 

548 scale = 1. if positive else -1. 

549 goodSrcFlags = subtractTask._checkMask(score.mask, transientSources, 

550 subtractTask.config.badMaskPlanes) 

551 for diaSource, goodSrcFlag in zip(output.diaSources, goodSrcFlags): 

552 if ~goodSrcFlag: 

553 with self.assertRaises(AssertionError): 

554 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=scale) 

555 else: 

556 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=scale) 

557 _detection_wrapper(positive=True) 

558 _detection_wrapper(positive=False) 

559 

560 def test_detect_dipoles(self): 

561 """Run detection on a difference image containing dipoles. 

562 """ 

563 # Set up the simulated images 

564 noiseLevel = 1. 

565 staticSeed = 1 

566 fluxLevel = 1000 

567 fluxRange = 1.5 

568 nSources = 10 

569 offset = 1 

570 xSize = 300 

571 ySize = 300 

572 kernelSize = 32 

573 # Avoid placing sources near the edge for this test, so that we can 

574 # easily check that the correct number of sources are detected. 

575 templateBorderSize = kernelSize//2 

576 dipoleFlag = "ip_diffim_DipoleFit_flag_classification" 

577 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "fluxRange": fluxRange, 

578 "nSrc": nSources, "templateBorderSize": templateBorderSize, "kernelSize": kernelSize, 

579 "xSize": xSize, "ySize": ySize} 

580 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

581 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

582 difference = science.clone() 

583 # Shift the template by a pixel in order to make dipoles in the difference image. 

584 matchedTemplate.image.array[...] = np.roll(matchedTemplate.image.array[...], offset, axis=0) 

585 matchedTemplate.variance.array[...] = np.roll(matchedTemplate.variance.array[...], offset, axis=0) 

586 matchedTemplate.mask.array[...] = np.roll(matchedTemplate.mask.array[...], offset, axis=0) 

587 difference.maskedImage -= matchedTemplate.maskedImage[science.getBBox()] 

588 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

589 scienceKernel = science.psf.getKernel() 

590 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl) 

591 

592 # Configure the detection Task 

593 detectionTask = self._setup_detection(doMerge=False) 

594 

595 # Run detection and check the results 

596 output = detectionTask.run(science, matchedTemplate, difference, score) 

597 self.assertIn(dipoleFlag, output.diaSources.schema.getNames()) 

598 nSourcesDet = len(sources) 

599 # Since we did not merge the dipoles, each source should result in 

600 # both a positive and a negative diaSource 

601 self.assertEqual(len(output.diaSources), 2*nSourcesDet) 

602 refIds = [] 

603 # The diaSource check should fail if we don't merge positive and negative footprints 

604 for diaSource in output.diaSources: 

605 with self.assertRaises(AssertionError): 

606 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0, 

607 atol=np.sqrt(fluxRange*fluxLevel)) 

608 

609 detectionTask2 = self._setup_detection(doMerge=True) 

610 output2 = detectionTask2.run(science, matchedTemplate, difference, score) 

611 self.assertEqual(len(output2.diaSources), nSourcesDet) 

612 refIds = [] 

613 for diaSource in output2.diaSources: 

614 if diaSource[dipoleFlag]: 

615 self._check_diaSource(sources, diaSource, refIds=refIds, scale=0, 

616 rtol=0.05, atol=None, usePsfFlux=False) 

617 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_orientation"], -90., atol=2.) 

618 self.assertFloatsAlmostEqual(diaSource["ip_diffim_DipoleFit_separation"], offset, rtol=0.1) 

619 else: 

620 raise ValueError("DiaSource with ID %s is not a dipole!", diaSource.getId()) 

621 

622 def test_sky_sources(self): 

623 """Add sky sources and check that they are sufficiently far from other 

624 sources and have negligible flux. 

625 """ 

626 # Set up the simulated images 

627 noiseLevel = 1. 

628 staticSeed = 1 

629 transientSeed = 6 

630 transientFluxLevel = 1000. 

631 transientFluxRange = 1.5 

632 fluxLevel = 500 

633 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel} 

634 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

635 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

636 transients, transientSources = makeTestImage(seed=transientSeed, psfSize=2.4, 

637 nSrc=10, fluxLevel=transientFluxLevel, 

638 fluxRange=transientFluxRange, 

639 noiseLevel=noiseLevel, noiseSeed=8) 

640 difference = science.clone() 

641 difference.maskedImage -= matchedTemplate.maskedImage 

642 difference.maskedImage += transients.maskedImage 

643 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

644 scienceKernel = science.psf.getKernel() 

645 kernelWidth = np.max(scienceKernel.getDimensions())//2 

646 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl) 

647 

648 # Configure the detection Task 

649 detectionTask = self._setup_detection(doSkySources=True) 

650 

651 # Run detection and check the results 

652 output = detectionTask.run(science, matchedTemplate, difference, score) 

653 skySources = output.diaSources[output.diaSources["sky_source"]] 

654 self.assertEqual(len(skySources), detectionTask.config.skySources.nSources) 

655 for skySource in skySources: 

656 # The sky sources should not be close to any other source 

657 with self.assertRaises(AssertionError): 

658 self._check_diaSource(transientSources, skySource, matchDistance=kernelWidth) 

659 with self.assertRaises(AssertionError): 

660 self._check_diaSource(sources, skySource, matchDistance=kernelWidth) 

661 # The sky sources should have low flux levels. 

662 self._check_diaSource(transientSources, skySource, matchDistance=1000, scale=0., 

663 atol=np.sqrt(transientFluxRange*transientFluxLevel)) 

664 

665 def test_edge_detections(self): 

666 """Sources with certain bad mask planes set should not be detected. 

667 """ 

668 # Set up the simulated images 

669 noiseLevel = 1. 

670 staticSeed = 1 

671 transientSeed = 6 

672 fluxLevel = 500 

673 radius = 2 

674 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel} 

675 science, sources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=6, **kwargs) 

676 matchedTemplate, _ = makeTestImage(noiseLevel=noiseLevel/4, noiseSeed=7, **kwargs) 

677 

678 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

679 scienceKernel = science.psf.getKernel() 

680 # Configure the detection Task 

681 detectionTask = self._setup_detection() 

682 excludeMaskPlanes = detectionTask.config.detection.excludeMaskPlanes 

683 nBad = len(excludeMaskPlanes) 

684 self.assertGreater(nBad, 0) 

685 kwargs["seed"] = transientSeed 

686 kwargs["nSrc"] = nBad 

687 kwargs["fluxLevel"] = 1000 

688 

689 # Run detection and check the results 

690 def _detection_wrapper(setFlags=True): 

691 transients, transientSources = makeTestImage(noiseLevel=noiseLevel, noiseSeed=8, **kwargs) 

692 difference = science.clone() 

693 difference.maskedImage -= matchedTemplate.maskedImage 

694 difference.maskedImage += transients.maskedImage 

695 if setFlags: 

696 for src, badMask in zip(transientSources, excludeMaskPlanes): 

697 srcX = int(src.getX()) 

698 srcY = int(src.getY()) 

699 srcBbox = lsst.geom.Box2I(lsst.geom.Point2I(srcX - radius, srcY - radius), 

700 lsst.geom.Extent2I(2*radius + 1, 2*radius + 1)) 

701 difference[srcBbox].mask.array |= lsst.afw.image.Mask.getPlaneBitMask(badMask) 

702 score = subtractTask._convolveExposure(difference, scienceKernel, subtractTask.convolutionControl) 

703 output = detectionTask.run(science, matchedTemplate, difference, score) 

704 refIds = [] 

705 goodSrcFlags = subtractTask._checkMask(difference.mask, transientSources, excludeMaskPlanes) 

706 if setFlags: 

707 self.assertEqual(np.sum(~goodSrcFlags), nBad) 

708 else: 

709 self.assertEqual(np.sum(~goodSrcFlags), 0) 

710 for diaSource, goodSrcFlag in zip(output.diaSources, goodSrcFlags): 

711 if ~goodSrcFlag: 

712 with self.assertRaises(AssertionError): 

713 self._check_diaSource(transientSources, diaSource, refIds=refIds) 

714 else: 

715 self._check_diaSource(transientSources, diaSource, refIds=refIds) 

716 _detection_wrapper(setFlags=False) 

717 _detection_wrapper(setFlags=True) 

718 

719 

720def setup_module(module): 

721 lsst.utils.tests.init() 

722 

723 

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

725 pass 

726 

727 

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

729 lsst.utils.tests.init() 

730 unittest.main()