Coverage for tests/test_dipoleFitter.py: 21%

107 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-15 03:21 -0700

1# 

2# LSST Data Management System 

3# Copyright 2008-2017 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"""Tests of the DipoleFitAlgorithm and its related tasks and plugins. 

23 

24Each test generates a fake image with two synthetic dipoles as input data. 

25""" 

26import unittest 

27 

28import numpy as np 

29 

30import lsst.utils.tests 

31import lsst.afw.table as afwTable 

32from lsst.ip.diffim.dipoleFitTask import (DipoleFitAlgorithm, DipoleFitTask) 

33import lsst.ip.diffim.utils as ipUtils 

34 

35 

36class DipoleTestImage: 

37 """Create a test dipole image and store the parameters used to make it, 

38 for comparison with the fitted results. 

39 

40 Parameters 

41 ---------- 

42 xc, yc : `list` [`float`] 

43 x, y coordinate (pixels) of center(s) of input dipole(s). 

44 flux: `list` [`float`] 

45 Flux(es) of input dipole(s). 

46 gradientParams : `tuple` 

47 Tuple with three parameters for linear background gradient. 

48 offsets : `list` [`float`] 

49 Pixel coordinates between lobes of dipoles. 

50 """ 

51 

52 def __init__(self, xc=None, yc=None, flux=None, offsets=None, gradientParams=None): 

53 self.xc = xc if xc is not None else [65.3, 24.2] 

54 self.yc = yc if yc is not None else [38.6, 78.5] 

55 self.offsets = offsets if offsets is not None else np.array([-2., 2.]) 

56 self.flux = flux if flux is not None else [2500., 2345.] 

57 self.gradientParams = gradientParams if gradientParams is not None else [10., 3., 5.] 

58 

59 # The default tolerance for comparisons of fitted parameters with input values. 

60 # Given the noise in the input images (default noise value of 2.), this is a 

61 # useful test of algorithm robustness, and will guard against future regressions. 

62 self.rtol = 0.01 

63 

64 self.generateTestImage() 

65 

66 def generateTestImage(self): 

67 self.testImage = ipUtils.DipoleTestImage( 

68 w=100, h=100, 

69 xcenPos=self.xc + self.offsets, 

70 ycenPos=self.yc + self.offsets, 

71 xcenNeg=self.xc - self.offsets, 

72 ycenNeg=self.yc - self.offsets, 

73 flux=self.flux, fluxNeg=self.flux, 

74 noise=2., # Note the input noise - this affects the relative tolerances used. 

75 gradientParams=self.gradientParams) 

76 

77 

78class DipoleFitTest(lsst.utils.tests.TestCase): 

79 """A test case for separately testing the dipole fit algorithm 

80 directly, and the single frame measurement. 

81 

82 In each test, create a simulated diffim with two dipoles, noise, 

83 and a linear background gradient in the pre-sub images then 

84 compare the input fluxes/centroids with the fitted results. 

85 """ 

86 

87 def testDipoleAlgorithm(self): 

88 """Test the dipole fitting algorithm directly (fitDipole()). 

89 

90 Test that the resulting fluxes/centroids are very close to the 

91 input values for both dipoles in the image. 

92 """ 

93 # Display (plot) the output dipole thumbnails with matplotlib. 

94 display = False 

95 # Be verbose during fitting, including the lmfit internal details. 

96 verbose = False 

97 

98 dipoleTestImage = DipoleTestImage() 

99 catalog = dipoleTestImage.testImage.detectDipoleSources(minBinSize=32) 

100 

101 for s in catalog: 

102 fp = s.getFootprint() 

103 self.assertTrue(len(fp.getPeaks()) == 2) 

104 

105 rtol = dipoleTestImage.rtol 

106 offsets = dipoleTestImage.offsets 

107 testImage = dipoleTestImage.testImage 

108 for i, s in enumerate(catalog): 

109 alg = DipoleFitAlgorithm(testImage.diffim, testImage.posImage, testImage.negImage) 

110 result, _ = alg.fitDipole( 

111 s, rel_weight=0.5, separateNegParams=False, 

112 verbose=verbose, display=display) 

113 

114 self.assertFloatsAlmostEqual((result.posFlux + abs(result.negFlux))/2., 

115 dipoleTestImage.flux[i], rtol=rtol) 

116 self.assertFloatsAlmostEqual(result.posCentroidX, dipoleTestImage.xc[i] + offsets[i], rtol=rtol) 

117 self.assertFloatsAlmostEqual(result.posCentroidY, dipoleTestImage.yc[i] + offsets[i], rtol=rtol) 

118 self.assertFloatsAlmostEqual(result.negCentroidX, dipoleTestImage.xc[i] - offsets[i], rtol=rtol) 

119 self.assertFloatsAlmostEqual(result.negCentroidY, dipoleTestImage.yc[i] - offsets[i], rtol=rtol) 

120 

121 def _runDetection(self, dipoleTestImage, maxFootprintArea=None): 

122 """Run 'diaSource' detection on the diffim, including merging of 

123 positive and negative sources. 

124 

125 Then run DipoleFitTask on the image and return the resulting catalog. 

126 """ 

127 # Create the various tasks and schema -- avoid code reuse. 

128 testImage = dipoleTestImage.testImage 

129 detectTask, schema = testImage.detectDipoleSources(doMerge=False, minBinSize=32) 

130 

131 config = DipoleFitTask.ConfigClass() 

132 # Also run the older C++ DipoleFlux algorithm for comparison purposes. 

133 config.plugins.names |= ["ip_diffim_PsfDipoleFlux"] 

134 if maxFootprintArea: 

135 config.plugins["ip_diffim_DipoleFit"].maxFootprintArea = maxFootprintArea 

136 measureTask = DipoleFitTask(schema=schema, config=config) 

137 

138 table = afwTable.SourceTable.make(schema) 

139 detectResult = detectTask.run(table, testImage.diffim) 

140 fpSet = detectResult.positive 

141 fpSet.merge(detectResult.negative, 2, 2, False) 

142 sources = afwTable.SourceCatalog(table) 

143 fpSet.makeSources(sources) 

144 

145 measureTask.run(sources, testImage.diffim, testImage.posImage, testImage.negImage) 

146 return sources 

147 

148 def _checkTaskOutput(self, dipoleTestImage, sources, rtol=None): 

149 """Compare the fluxes/centroids in `sources` are entered 

150 into the correct slots of the catalog, and have values that 

151 are very close to the input values for both dipoles in the 

152 image. 

153 

154 Also test that the resulting fluxes are close to those 

155 generated by the existing ip_diffim_DipoleMeasurement task 

156 (PsfDipoleFit). 

157 """ 

158 

159 if rtol is None: 

160 rtol = dipoleTestImage.rtol 

161 offsets = dipoleTestImage.offsets 

162 for i, r1 in enumerate(sources): 

163 result = r1.extract("ip_diffim_DipoleFit*") 

164 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux'] 

165 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2., 

166 dipoleTestImage.flux[i], rtol=rtol) 

167 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_x'], 

168 dipoleTestImage.xc[i] + offsets[i], rtol=rtol) 

169 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_y'], 

170 dipoleTestImage.yc[i] + offsets[i], rtol=rtol) 

171 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_x'], 

172 dipoleTestImage.xc[i] - offsets[i], rtol=rtol) 

173 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_y'], 

174 dipoleTestImage.yc[i] - offsets[i], rtol=rtol) 

175 # Note this is dependent on the noise (variance) being realistic in the image. 

176 # otherwise it throws off the chi2 estimate, which is used for classification: 

177 self.assertTrue(result['ip_diffim_DipoleFit_flag_classification']) 

178 

179 # compare to the original ip_diffim_PsfDipoleFlux measurements 

180 result2 = r1.extract("ip_diffim_PsfDipoleFlux*") 

181 self.assertFloatsAlmostEqual((result['ip_diffim_DipoleFit_pos_instFlux'] 

182 + abs(result['ip_diffim_DipoleFit_neg_instFlux']))/2., 

183 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux'] 

184 + abs(result2['ip_diffim_PsfDipoleFlux_neg_instFlux']))/2., 

185 rtol=rtol) 

186 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_x'], 

187 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'], 

188 rtol=rtol) 

189 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_y'], 

190 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'], 

191 rtol=rtol) 

192 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_x'], 

193 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'], 

194 rtol=rtol) 

195 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_y'], 

196 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'], 

197 rtol=rtol) 

198 

199 return result 

200 

201 def testDipoleTask(self): 

202 """Test the dipole fitting singleFramePlugin. 

203 

204 Test that the resulting fluxes/centroids are entered into the 

205 correct slots of the catalog, and have values that are very 

206 close to the input values for both dipoles in the image. 

207 

208 Also test that the resulting fluxes are close to those 

209 generated by the existing ip_diffim_DipoleMeasurement task 

210 (PsfDipoleFit). 

211 """ 

212 dipoleTestImage = DipoleTestImage() 

213 sources = self._runDetection(dipoleTestImage) 

214 self._checkTaskOutput(dipoleTestImage, sources) 

215 

216 def testDipoleTaskNoPosImage(self): 

217 """Test the dipole fitting singleFramePlugin in the case where no 

218 `posImage` is provided. It should be the same as above because 

219 `posImage` can be constructed from `diffim+negImage`. 

220 

221 Test that the resulting fluxes/centroids are entered into the 

222 correct slots of the catalog, and have values that are very 

223 close to the input values for both dipoles in the image. 

224 

225 Also test that the resulting fluxes are close to those 

226 generated by the existing ip_diffim_DipoleMeasurement task 

227 (PsfDipoleFit). 

228 """ 

229 dipoleTestImage = DipoleTestImage() 

230 dipoleTestImage.testImage.posImage = None 

231 sources = self._runDetection(dipoleTestImage) 

232 self._checkTaskOutput(dipoleTestImage, sources) 

233 

234 def testDipoleTaskNoNegImage(self): 

235 """Test the dipole fitting singleFramePlugin in the case where no 

236 `negImage` is provided. It should be the same as above because 

237 `negImage` can be constructed from `posImage-diffim`. 

238 

239 Test that the resulting fluxes/centroids are entered into the 

240 correct slots of the catalog, and have values that are very 

241 close to the input values for both dipoles in the image. 

242 

243 Also test that the resulting fluxes are close to those 

244 generated by the existing ip_diffim_DipoleMeasurement task 

245 (PsfDipoleFit). 

246 """ 

247 dipoleTestImage = DipoleTestImage() 

248 dipoleTestImage.testImage.negImage = None 

249 sources = self._runDetection(dipoleTestImage) 

250 self._checkTaskOutput(dipoleTestImage, sources) 

251 

252 def testDipoleTaskNoPreSubImages(self): 

253 """Test the dipole fitting singleFramePlugin in the case where no 

254 pre-subtraction data (`posImage` or `negImage`) are provided. 

255 In this case it just fits a dipole model to the diffim 

256 (dipole) image alone. Note that this test will only pass for 

257 widely-separated dipoles. 

258 

259 Test that the resulting fluxes/centroids are entered into the 

260 correct slots of the catalog, and have values that are very 

261 close to the input values for both dipoles in the image. 

262 

263 Also test that the resulting fluxes are close to those 

264 generated by the existing ip_diffim_DipoleMeasurement task 

265 (PsfDipoleFit). 

266 """ 

267 dipoleTestImage = DipoleTestImage() 

268 dipoleTestImage.testImage.posImage = dipoleTestImage.testImage.negImage = None 

269 sources = self._runDetection(dipoleTestImage) 

270 self._checkTaskOutput(dipoleTestImage, sources) 

271 

272 def testDipoleEdge(self): 

273 """Test the too-close-to-image-edge scenario for dipole fitting 

274 singleFramePlugin. 

275 

276 Test that the dipoles which are too close to the edge are 

277 not detected. 

278 """ 

279 

280 dipoleTestImage = DipoleTestImage(xc=[5.3, 4.8], yc=[4.6, 96.5]) 

281 sources = self._runDetection(dipoleTestImage) 

282 

283 self.assertTrue(len(sources) == 0) 

284 

285 def testDipoleFootprintTooLarge(self): 

286 """Test that the footprint area cut flags sources.""" 

287 

288 dipoleTestImage = DipoleTestImage() 

289 # This area is smaller than the area of the test sources (~750). 

290 sources = self._runDetection(dipoleTestImage, maxFootprintArea=500) 

291 

292 self.assertTrue(np.all(sources["ip_diffim_DipoleFit_flag"])) 

293 

294 

295class TestMemory(lsst.utils.tests.MemoryTestCase): 

296 pass 

297 

298 

299def setup_module(module): 

300 lsst.utils.tests.init() 

301 

302 

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

304 lsst.utils.tests.init() 

305 unittest.main()