Coverage for tests/test_dipoleFitter.py: 20%

111 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-01 02:41 -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 

32import lsst.meas.base as measBase 

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

34import lsst.ip.diffim.utils as ipUtils 

35 

36 

37class DipoleTestImage: 

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

39 for comparison with the fitted results. 

40 

41 Parameters 

42 ---------- 

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

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

45 flux: `list` [`float`] 

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

47 gradientParams : `tuple` 

48 Tuple with three parameters for linear background gradient. 

49 offsets : `list` [`float`] 

50 Pixel coordinates between lobes of dipoles. 

51 """ 

52 

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

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

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

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

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

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

59 

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

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

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

63 self.rtol = 0.01 

64 

65 self.generateTestImage() 

66 

67 def generateTestImage(self): 

68 self.testImage = ipUtils.DipoleTestImage( 

69 w=100, h=100, 

70 xcenPos=self.xc + self.offsets, 

71 ycenPos=self.yc + self.offsets, 

72 xcenNeg=self.xc - self.offsets, 

73 ycenNeg=self.yc - self.offsets, 

74 flux=self.flux, fluxNeg=self.flux, 

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

76 gradientParams=self.gradientParams) 

77 

78 

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

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

81 directly, and the single frame measurement. 

82 

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

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

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

86 """ 

87 

88 def testDipoleAlgorithm(self): 

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

90 

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

92 input values for both dipoles in the image. 

93 """ 

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

95 display = False 

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

97 verbose = False 

98 

99 dipoleTestImage = DipoleTestImage() 

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

101 

102 for s in catalog: 

103 fp = s.getFootprint() 

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

105 

106 rtol = dipoleTestImage.rtol 

107 offsets = dipoleTestImage.offsets 

108 testImage = dipoleTestImage.testImage 

109 for i, s in enumerate(catalog): 

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

111 result, _ = alg.fitDipole( 

112 s, rel_weight=0.5, separateNegParams=False, 

113 verbose=verbose, display=display) 

114 

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

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

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

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

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

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

121 

122 def _runDetection(self, dipoleTestImage): 

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

124 positive and negative sources. 

125 

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

127 """ 

128 

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

130 testImage = dipoleTestImage.testImage 

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

132 

133 measureConfig = measBase.SingleFrameMeasurementConfig() 

134 

135 measureConfig.slots.calibFlux = None 

136 measureConfig.slots.modelFlux = None 

137 measureConfig.slots.gaussianFlux = None 

138 measureConfig.slots.shape = None 

139 measureConfig.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 

140 measureConfig.doReplaceWithNoise = False 

141 

142 measureConfig.plugins.names = ["base_CircularApertureFlux", 

143 "base_PixelFlags", 

144 "base_SkyCoord", 

145 "base_PsfFlux", 

146 "ip_diffim_NaiveDipoleCentroid", 

147 "ip_diffim_NaiveDipoleFlux", 

148 "ip_diffim_PsfDipoleFlux"] 

149 

150 # Here is where we make the dipole fitting task. It can run the other measurements as well. 

151 measureTask = DipoleFitTask(config=measureConfig, schema=schema) 

152 

153 table = afwTable.SourceTable.make(schema) 

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

155 # catalog = detectResult.sources 

156 # deblendTask.run(self.dipole, catalog, psf=self.dipole.getPsf()) 

157 

158 fpSet = detectResult.positive 

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

160 sources = afwTable.SourceCatalog(table) 

161 fpSet.makeSources(sources) 

162 

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

164 return sources 

165 

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

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

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

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

170 image. 

171 

172 Also test that the resulting fluxes are close to those 

173 generated by the existing ip_diffim_DipoleMeasurement task 

174 (PsfDipoleFit). 

175 """ 

176 

177 if rtol is None: 

178 rtol = dipoleTestImage.rtol 

179 offsets = dipoleTestImage.offsets 

180 for i, r1 in enumerate(sources): 

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

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

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

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

185 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'], 

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

187 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'], 

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

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

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

191 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'], 

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

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

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

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

196 

197 # compare to the original ip_diffim_PsfDipoleFlux measurements 

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

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

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

201 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux'] 

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

203 rtol=rtol) 

204 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_x'], 

205 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'], 

206 rtol=rtol) 

207 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_pos_centroid_y'], 

208 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'], 

209 rtol=rtol) 

210 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_x'], 

211 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'], 

212 rtol=rtol) 

213 self.assertFloatsAlmostEqual(result['ip_diffim_DipoleFit_neg_centroid_y'], 

214 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'], 

215 rtol=rtol) 

216 

217 return result 

218 

219 def testDipoleTask(self): 

220 """Test the dipole fitting singleFramePlugin. 

221 

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

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

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

225 

226 Also test that the resulting fluxes are close to those 

227 generated by the existing ip_diffim_DipoleMeasurement task 

228 (PsfDipoleFit). 

229 """ 

230 dipoleTestImage = DipoleTestImage() 

231 sources = self._runDetection(dipoleTestImage) 

232 self._checkTaskOutput(dipoleTestImage, sources) 

233 

234 def testDipoleTaskNoPosImage(self): 

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

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

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

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.posImage = None 

249 sources = self._runDetection(dipoleTestImage) 

250 self._checkTaskOutput(dipoleTestImage, sources) 

251 

252 def testDipoleTaskNoNegImage(self): 

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

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

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

256 

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

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

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

260 

261 Also test that the resulting fluxes are close to those 

262 generated by the existing ip_diffim_DipoleMeasurement task 

263 (PsfDipoleFit). 

264 """ 

265 dipoleTestImage = DipoleTestImage() 

266 dipoleTestImage.testImage.negImage = None 

267 sources = self._runDetection(dipoleTestImage) 

268 self._checkTaskOutput(dipoleTestImage, sources) 

269 

270 def testDipoleTaskNoPreSubImages(self): 

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

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

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

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

275 widely-separated dipoles. 

276 

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

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

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

280 

281 Also test that the resulting fluxes are close to those 

282 generated by the existing ip_diffim_DipoleMeasurement task 

283 (PsfDipoleFit). 

284 """ 

285 dipoleTestImage = DipoleTestImage() 

286 dipoleTestImage.testImage.posImage = dipoleTestImage.testImage.negImage = None 

287 sources = self._runDetection(dipoleTestImage) 

288 self._checkTaskOutput(dipoleTestImage, sources) 

289 

290 def testDipoleEdge(self): 

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

292 singleFramePlugin. 

293 

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

295 flagged as such in the catalog and do not raise an error that is 

296 not caught. Make sure both diaSources are actually detected, 

297 if not measured. 

298 """ 

299 

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

301 sources = self._runDetection(dipoleTestImage) 

302 

303 self.assertTrue(len(sources) == 2) 

304 

305 for i, s in enumerate(sources): 

306 result = s.extract("ip_diffim_DipoleFit*") 

307 self.assertTrue(result.get("ip_diffim_DipoleFit_flag")) 

308 

309 

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

311 pass 

312 

313 

314def setup_module(module): 

315 lsst.utils.tests.init() 

316 

317 

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

319 lsst.utils.tests.init() 

320 unittest.main()