Coverage for tests/test_detectAndMeasure.py: 10%

156 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-05 02:45 -0800

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 

27from lsst.ip.diffim.utils import makeTestImage 

28import lsst.utils.tests 

29 

30 

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

32 

33 def test_detection_runs(self): 

34 """Basic smoke test. 

35 """ 

36 noiseLevel = 1. 

37 staticSeed = 1 

38 fluxLevel = 500 

39 kwargs = {"seed": staticSeed, "psfSize": 2.4, "fluxLevel": fluxLevel, "x0": 0, "y0": 0} 

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

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

42 difference = science.clone() 

43 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass() 

44 config.doApCorr = False 

45 task = detectAndMeasure.DetectAndMeasureTask(config=config) 

46 output = task.run(science, matchedTemplate, difference) 

47 subtractedMeasuredExposure = output.subtractedMeasuredExposure 

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

49 

50 def test_detection_xy0(self): 

51 """Basic smoke test with non-zero x0 and y0. 

52 """ 

53 noiseLevel = 1. 

54 staticSeed = 1 

55 fluxLevel = 500 

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

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

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

59 difference = science.clone() 

60 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass() 

61 config.doApCorr = False 

62 config.doMerge = False 

63 config.doSkySources = False 

64 config.doForcedMeasurement = False 

65 task = detectAndMeasure.DetectAndMeasureTask(config=config) 

66 output = task.run(science, matchedTemplate, difference) 

67 subtractedMeasuredExposure = output.subtractedMeasuredExposure 

68 

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

70 

71 def test_detect_transients(self): 

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

73 """ 

74 noiseLevel = 1. 

75 staticSeed = 1 

76 transientSeed = 6 

77 fluxLevel = 500 

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

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

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

81 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass() 

82 config.doApCorr = False 

83 config.doMerge = False 

84 config.doSkySources = False 

85 config.doForcedMeasurement = False 

86 detectionTask = detectAndMeasure.DetectAndMeasureTask(config=config) 

87 

88 def _detection_wrapper(polarity=1): 

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

90 nSrc=10, fluxLevel=1000., 

91 noiseLevel=noiseLevel, noiseSeed=8) 

92 difference = science.clone() 

93 difference.maskedImage -= matchedTemplate.maskedImage 

94 if polarity > 0: 

95 difference.maskedImage += transients.maskedImage 

96 else: 

97 difference.maskedImage -= transients.maskedImage 

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

99 refIds = [] 

100 for diaSource in output.diaSources: 

101 self._check_diaSource(transientSources, diaSource, refIds=refIds, scale=polarity) 

102 _detection_wrapper(polarity=1) 

103 _detection_wrapper(polarity=-1) 

104 

105 def test_detect_dipoles(self): 

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

107 """ 

108 noiseLevel = 1. 

109 staticSeed = 1 

110 fluxLevel = 1000 

111 fluxRange = 1.5 

112 nSources = 10 

113 offset = 1 

114 dipoleFlag = "ip_diffim_DipoleFit_flag_classification" 

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

116 "nSrc": nSources} 

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

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

119 difference = science.clone() 

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

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

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

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

124 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass() 

125 config.doApCorr = False 

126 config.doMerge = False 

127 config.doSkySources = False 

128 config.doForcedMeasurement = False 

129 detectionTask = detectAndMeasure.DetectAndMeasureTask(config=config) 

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

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

132 nSourcesDet = len(sources) 

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

134 refIds = [] 

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

136 for diaSource in output.diaSources: 

137 with self.assertRaises(AssertionError): 

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

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

140 

141 config.doMerge = True 

142 detectionTask2 = detectAndMeasure.DetectAndMeasureTask(config=config) 

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

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

145 refIds = [] 

146 for diaSource in output2.diaSources: 

147 if diaSource[dipoleFlag]: 

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

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

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

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

152 else: 

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

154 

155 def test_sky_sources(self): 

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

157 sources and have negligible flux. 

158 """ 

159 noiseLevel = 1. 

160 staticSeed = 1 

161 transientSeed = 6 

162 transientFluxLevel = 1000. 

163 transientFluxRange = 1.5 

164 fluxLevel = 500 

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

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

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

168 config = detectAndMeasure.DetectAndMeasureTask.ConfigClass() 

169 config.doApCorr = False 

170 config.doMerge = False 

171 config.doSkySources = True 

172 config.doForcedMeasurement = False 

173 config.skySources.nSources = 5 

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

175 detectionTask = detectAndMeasure.DetectAndMeasureTask(config=config) 

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

177 nSrc=10, fluxLevel=transientFluxLevel, 

178 fluxRange=transientFluxRange, 

179 noiseLevel=noiseLevel, noiseSeed=8) 

180 difference = science.clone() 

181 difference.maskedImage -= matchedTemplate.maskedImage 

182 difference.maskedImage += transients.maskedImage 

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

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

185 self.assertEqual(len(skySources), config.skySources.nSources) 

186 for skySource in skySources: 

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

188 with self.assertRaises(AssertionError): 

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

190 with self.assertRaises(AssertionError): 

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

192 # The sky sources should have low flux levels. 

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

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

195 

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

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

198 rtol=0.02, atol=None): 

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

200 and compare properties. 

201 """ 

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

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

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

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

206 if refIds is not None: 

207 # Check that the same source was not previously associated 

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

209 refIds.append(src.getId()) 

210 if atol is None: 

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

212 if usePsfFlux: 

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

214 rtol=rtol, atol=atol) 

215 else: 

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

217 rtol=rtol, atol=atol) 

218 

219 

220def setup_module(module): 

221 lsst.utils.tests.init() 

222 

223 

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

225 pass 

226 

227 

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

229 lsst.utils.tests.init() 

230 unittest.main()