Coverage for tests/test_detectAndMeasure.py: 6%

405 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-25 23:17 +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 

28import lsst.utils.tests 

29 

30 

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

32 

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

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

35 rtol=0.02, atol=None): 

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

37 and compare properties. 

38 

39 Parameters 

40 ---------- 

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

42 The reference catalog. 

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

44 The new diaSource to match to the reference catalog. 

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

46 Source IDs of previously associated diaSources. 

47 matchDistance : `float`, optional 

48 Maximum distance allowed between the detected and reference source 

49 locations, in pixels. 

50 scale : `float`, optional 

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

52 usePsfFlux : `bool`, optional 

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

54 rtol : `float`, optional 

55 Relative tolerance of the flux value test. 

56 atol : `float`, optional 

57 Absolute tolerance of the flux value test. 

58 """ 

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

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

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

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

63 if refIds is not None: 

64 # Check that the same source was not previously associated 

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

66 refIds.append(src.getId()) 

67 if atol is None: 

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

69 if usePsfFlux: 

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

71 rtol=rtol, atol=atol) 

72 else: 

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

74 rtol=rtol, atol=atol) 

75 

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

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

78 within specified minimum and maximum bounds. 

79 

80 Parameters 

81 ---------- 

82 values : `numpy.ndarray` 

83 Array of values to check. 

84 minValue : `float`, optional 

85 Minimum allowable value. 

86 maxValue : `float`, optional 

87 Maximum allowable value. 

88 """ 

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

90 if minValue is not None: 

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

92 if maxValue is not None: 

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

94 

95 def _setup_detection(self, doApCorr=False, doMerge=False, 

96 doSkySources=False, doForcedMeasurement=False): 

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

98 

99 Parameters 

100 ---------- 

101 doApCorr : `bool`, optional 

102 Run subtask to apply aperture corrections. 

103 doMerge : `bool`, optional 

104 Merge positive and negative diaSources. 

105 doSkySources : `bool`, optional 

106 Generate sky sources. 

107 doForcedMeasurement : `bool`, optional 

108 Force photometer diaSource locations on PVI. 

109 

110 Returns 

111 ------- 

112 `lsst.pipe.base.PipelineTask` 

113 The configured Task to use for detection and measurement. 

114 """ 

115 config = self.detectionTask.ConfigClass() 

116 config.doApCorr = doApCorr 

117 config.doMerge = doMerge 

118 config.doSkySources = doSkySources 

119 config.doForcedMeasurement = doForcedMeasurement 

120 if doSkySources: 

121 config.skySources.nSources = 5 

122 return self.detectionTask(config=config) 

123 

124 

125class DetectAndMeasureTest(DetectAndMeasureTestBase): 

126 detectionTask = detectAndMeasure.DetectAndMeasureTask 

127 

128 def test_detection_xy0(self): 

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

130 """ 

131 # Set up the simulated images 

132 noiseLevel = 1. 

133 staticSeed = 1 

134 fluxLevel = 500 

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

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

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

138 difference = science.clone() 

139 

140 # Configure the detection Task 

141 detectionTask = self._setup_detection() 

142 

143 # Run detection and check the results 

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

145 subtractedMeasuredExposure = output.subtractedMeasuredExposure 

146 

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

148 

149 def test_measurements_finite(self): 

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

151 """ 

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

153 

154 # Set up the simulated images 

155 noiseLevel = 1. 

156 staticSeed = 1 

157 transientSeed = 6 

158 xSize = 256 

159 ySize = 256 

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

161 "xSize": xSize, "ySize": ySize} 

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

163 nSrc=1, **kwargs) 

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

165 nSrc=1, **kwargs) 

166 rng = np.random.RandomState(3) 

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

168 rng.shuffle(xLoc) 

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

170 rng.shuffle(yLoc) 

171 transients, transientSources = makeTestImage(seed=transientSeed, 

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

173 noiseLevel=noiseLevel, noiseSeed=8, 

174 xLoc=xLoc, yLoc=yLoc, 

175 **kwargs) 

176 difference = science.clone() 

177 difference.maskedImage -= matchedTemplate.maskedImage 

178 difference.maskedImage += transients.maskedImage 

179 

180 # Configure the detection Task 

181 detectionTask = self._setup_detection(doForcedMeasurement=True) 

182 

183 # Run detection and check the results 

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

185 

186 for column in columnNames: 

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

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

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

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

191 

192 def test_detect_transients(self): 

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

194 """ 

195 # Set up the simulated images 

196 noiseLevel = 1. 

197 staticSeed = 1 

198 transientSeed = 6 

199 fluxLevel = 500 

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

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

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

203 

204 # Configure the detection Task 

205 detectionTask = self._setup_detection() 

206 kwargs["seed"] = transientSeed 

207 kwargs["nSrc"] = 10 

208 kwargs["fluxLevel"] = 1000 

209 

210 # Run detection and check the results 

211 def _detection_wrapper(positive=True): 

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

213 difference = science.clone() 

214 difference.maskedImage -= matchedTemplate.maskedImage 

215 if positive: 

216 difference.maskedImage += transients.maskedImage 

217 else: 

218 difference.maskedImage -= transients.maskedImage 

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

220 refIds = [] 

221 scale = 1. if positive else -1. 

222 for diaSource in output.diaSources: 

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

224 _detection_wrapper(positive=True) 

225 _detection_wrapper(positive=False) 

226 

227 def test_detect_dipoles(self): 

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

229 """ 

230 # Set up the simulated images 

231 noiseLevel = 1. 

232 staticSeed = 1 

233 fluxLevel = 1000 

234 fluxRange = 1.5 

235 nSources = 10 

236 offset = 1 

237 xSize = 300 

238 ySize = 300 

239 kernelSize = 32 

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

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

242 templateBorderSize = kernelSize//2 

243 dipoleFlag = "ip_diffim_DipoleFit_flag_classification" 

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

245 "nSrc": nSources, "templateBorderSize": templateBorderSize, "kernelSize": kernelSize, 

246 "xSize": xSize, "ySize": ySize} 

247 dipoleFlag = "ip_diffim_DipoleFit_flag_classification" 

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

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

250 difference = science.clone() 

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

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

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

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

255 

256 # Configure the detection Task 

257 detectionTask = self._setup_detection() 

258 

259 # Run detection and check the results 

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

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

262 nSourcesDet = len(sources) 

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

264 refIds = [] 

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

266 for diaSource in output.diaSources: 

267 with self.assertRaises(AssertionError): 

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

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

270 

271 detectionTask2 = self._setup_detection(doMerge=True) 

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

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

274 refIds = [] 

275 for diaSource in output2.diaSources: 

276 if diaSource[dipoleFlag]: 

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

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

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

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

281 else: 

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

283 

284 def test_sky_sources(self): 

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

286 sources and have negligible flux. 

287 """ 

288 # Set up the simulated images 

289 noiseLevel = 1. 

290 staticSeed = 1 

291 transientSeed = 6 

292 transientFluxLevel = 1000. 

293 transientFluxRange = 1.5 

294 fluxLevel = 500 

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

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

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

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

299 nSrc=10, fluxLevel=transientFluxLevel, 

300 fluxRange=transientFluxRange, 

301 noiseLevel=noiseLevel, noiseSeed=8) 

302 difference = science.clone() 

303 difference.maskedImage -= matchedTemplate.maskedImage 

304 difference.maskedImage += transients.maskedImage 

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

306 

307 # Configure the detection Task 

308 detectionTask = self._setup_detection(doSkySources=True) 

309 

310 # Run detection and check the results 

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

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

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

314 for skySource in skySources: 

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

316 with self.assertRaises(AssertionError): 

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

318 with self.assertRaises(AssertionError): 

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

320 # The sky sources should have low flux levels. 

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

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

323 

324 def test_edge_detections(self): 

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

326 """ 

327 # Set up the simulated images 

328 noiseLevel = 1. 

329 staticSeed = 1 

330 transientSeed = 6 

331 fluxLevel = 500 

332 radius = 2 

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

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

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

336 

337 _checkMask = subtractImages.AlardLuptonSubtractTask._checkMask 

338 # Configure the detection Task 

339 detectionTask = self._setup_detection() 

340 excludeMaskPlanes = detectionTask.config.detection.excludeMaskPlanes 

341 nBad = len(excludeMaskPlanes) 

342 self.assertGreater(nBad, 0) 

343 kwargs["seed"] = transientSeed 

344 kwargs["nSrc"] = nBad 

345 kwargs["fluxLevel"] = 1000 

346 

347 # Run detection and check the results 

348 def _detection_wrapper(setFlags=True): 

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

350 difference = science.clone() 

351 difference.maskedImage -= matchedTemplate.maskedImage 

352 difference.maskedImage += transients.maskedImage 

353 if setFlags: 

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

355 srcX = int(src.getX()) 

356 srcY = int(src.getY()) 

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

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

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

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

361 refIds = [] 

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

363 if setFlags: 

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

365 else: 

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

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

368 if ~goodSrcFlag: 

369 with self.assertRaises(AssertionError): 

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

371 else: 

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

373 _detection_wrapper(setFlags=False) 

374 _detection_wrapper(setFlags=True) 

375 

376 

377class DetectAndMeasureScoreTest(DetectAndMeasureTestBase): 

378 detectionTask = detectAndMeasure.DetectAndMeasureScoreTask 

379 

380 def test_detection_xy0(self): 

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

382 """ 

383 # Set up the simulated images 

384 noiseLevel = 1. 

385 staticSeed = 1 

386 fluxLevel = 500 

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

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

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

390 difference = science.clone() 

391 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

392 scienceKernel = science.psf.getKernel() 

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

394 

395 # Configure the detection Task 

396 detectionTask = self._setup_detection() 

397 

398 # Run detection and check the results 

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

400 subtractedMeasuredExposure = output.subtractedMeasuredExposure 

401 

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

403 

404 def test_measurements_finite(self): 

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

406 """ 

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

408 

409 # Set up the simulated images 

410 noiseLevel = 1. 

411 staticSeed = 1 

412 transientSeed = 6 

413 xSize = 256 

414 ySize = 256 

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

416 "xSize": xSize, "ySize": ySize} 

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

418 nSrc=1, **kwargs) 

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

420 nSrc=1, **kwargs) 

421 rng = np.random.RandomState(3) 

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

423 rng.shuffle(xLoc) 

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

425 rng.shuffle(yLoc) 

426 transients, transientSources = makeTestImage(seed=transientSeed, 

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

428 noiseLevel=noiseLevel, noiseSeed=8, 

429 xLoc=xLoc, yLoc=yLoc, 

430 **kwargs) 

431 difference = science.clone() 

432 difference.maskedImage -= matchedTemplate.maskedImage 

433 difference.maskedImage += transients.maskedImage 

434 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

435 scienceKernel = science.psf.getKernel() 

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

437 

438 # Configure the detection Task 

439 detectionTask = self._setup_detection(doForcedMeasurement=True) 

440 

441 # Run detection and check the results 

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

443 

444 for column in columnNames: 

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

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

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

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

449 

450 def test_detect_transients(self): 

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

452 """ 

453 # Set up the simulated images 

454 noiseLevel = 1. 

455 staticSeed = 1 

456 transientSeed = 6 

457 fluxLevel = 500 

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

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

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

461 scienceKernel = science.psf.getKernel() 

462 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

463 

464 # Configure the detection Task 

465 detectionTask = self._setup_detection() 

466 kwargs["seed"] = transientSeed 

467 kwargs["nSrc"] = 10 

468 kwargs["fluxLevel"] = 1000 

469 

470 # Run detection and check the results 

471 def _detection_wrapper(positive=True): 

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

473 

474 Parameters 

475 ---------- 

476 positive : `bool`, optional 

477 If set, use positive transient sources. 

478 """ 

479 

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

481 difference = science.clone() 

482 difference.maskedImage -= matchedTemplate.maskedImage 

483 if positive: 

484 difference.maskedImage += transients.maskedImage 

485 else: 

486 difference.maskedImage -= transients.maskedImage 

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

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

489 refIds = [] 

490 scale = 1. if positive else -1. 

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

492 subtractTask.config.badMaskPlanes) 

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

494 if ~goodSrcFlag: 

495 with self.assertRaises(AssertionError): 

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

497 else: 

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

499 _detection_wrapper(positive=True) 

500 _detection_wrapper(positive=False) 

501 

502 def test_detect_dipoles(self): 

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

504 """ 

505 # Set up the simulated images 

506 noiseLevel = 1. 

507 staticSeed = 1 

508 fluxLevel = 1000 

509 fluxRange = 1.5 

510 nSources = 10 

511 offset = 1 

512 xSize = 300 

513 ySize = 300 

514 kernelSize = 32 

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

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

517 templateBorderSize = kernelSize//2 

518 dipoleFlag = "ip_diffim_DipoleFit_flag_classification" 

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

520 "nSrc": nSources, "templateBorderSize": templateBorderSize, "kernelSize": kernelSize, 

521 "xSize": xSize, "ySize": ySize} 

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

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

524 difference = science.clone() 

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

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

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

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

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

530 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

531 scienceKernel = science.psf.getKernel() 

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

533 

534 # Configure the detection Task 

535 detectionTask = self._setup_detection() 

536 

537 # Run detection and check the results 

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

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

540 nSourcesDet = len(sources) 

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

542 # both a positive and a negative diaSource 

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

544 refIds = [] 

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

546 for diaSource in output.diaSources: 

547 with self.assertRaises(AssertionError): 

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

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

550 

551 detectionTask2 = self._setup_detection(doMerge=True) 

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

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

554 refIds = [] 

555 for diaSource in output2.diaSources: 

556 if diaSource[dipoleFlag]: 

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

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

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

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

561 else: 

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

563 

564 def test_sky_sources(self): 

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

566 sources and have negligible flux. 

567 """ 

568 # Set up the simulated images 

569 noiseLevel = 1. 

570 staticSeed = 1 

571 transientSeed = 6 

572 transientFluxLevel = 1000. 

573 transientFluxRange = 1.5 

574 fluxLevel = 500 

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

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

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

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

579 nSrc=10, fluxLevel=transientFluxLevel, 

580 fluxRange=transientFluxRange, 

581 noiseLevel=noiseLevel, noiseSeed=8) 

582 difference = science.clone() 

583 difference.maskedImage -= matchedTemplate.maskedImage 

584 difference.maskedImage += transients.maskedImage 

585 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

586 scienceKernel = science.psf.getKernel() 

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

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

589 

590 # Configure the detection Task 

591 detectionTask = self._setup_detection(doSkySources=True) 

592 

593 # Run detection and check the results 

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

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

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

597 for skySource in skySources: 

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

599 with self.assertRaises(AssertionError): 

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

601 with self.assertRaises(AssertionError): 

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

603 # The sky sources should have low flux levels. 

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

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

606 

607 def test_edge_detections(self): 

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

609 """ 

610 # Set up the simulated images 

611 noiseLevel = 1. 

612 staticSeed = 1 

613 transientSeed = 6 

614 fluxLevel = 500 

615 radius = 2 

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

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

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

619 

620 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask() 

621 scienceKernel = science.psf.getKernel() 

622 # Configure the detection Task 

623 detectionTask = self._setup_detection() 

624 excludeMaskPlanes = detectionTask.config.detection.excludeMaskPlanes 

625 nBad = len(excludeMaskPlanes) 

626 self.assertGreater(nBad, 0) 

627 kwargs["seed"] = transientSeed 

628 kwargs["nSrc"] = nBad 

629 kwargs["fluxLevel"] = 1000 

630 

631 # Run detection and check the results 

632 def _detection_wrapper(setFlags=True): 

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

634 difference = science.clone() 

635 difference.maskedImage -= matchedTemplate.maskedImage 

636 difference.maskedImage += transients.maskedImage 

637 if setFlags: 

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

639 srcX = int(src.getX()) 

640 srcY = int(src.getY()) 

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

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

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

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

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

646 refIds = [] 

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

648 if setFlags: 

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

650 else: 

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

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

653 if ~goodSrcFlag: 

654 with self.assertRaises(AssertionError): 

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

656 else: 

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

658 _detection_wrapper(setFlags=False) 

659 _detection_wrapper(setFlags=True) 

660 

661 

662def setup_module(module): 

663 lsst.utils.tests.init() 

664 

665 

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

667 pass 

668 

669 

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

671 lsst.utils.tests.init() 

672 unittest.main()