Coverage for tests/test_dipoleFitter.py: 20%

117 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-03 03:33 -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, maxFootprintArea=None): 

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 if maxFootprintArea: 

154 measureTask.config.plugins["ip_diffim_DipoleFit"].maxFootprintArea = maxFootprintArea 

155 

156 table = afwTable.SourceTable.make(schema) 

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

158 # catalog = detectResult.sources 

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

160 

161 fpSet = detectResult.positive 

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

163 sources = afwTable.SourceCatalog(table) 

164 fpSet.makeSources(sources) 

165 

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

167 return sources 

168 

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

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

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

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

173 image. 

174 

175 Also test that the resulting fluxes are close to those 

176 generated by the existing ip_diffim_DipoleMeasurement task 

177 (PsfDipoleFit). 

178 """ 

179 

180 if rtol is None: 

181 rtol = dipoleTestImage.rtol 

182 offsets = dipoleTestImage.offsets 

183 for i, r1 in enumerate(sources): 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

199 

200 # compare to the original ip_diffim_PsfDipoleFlux measurements 

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

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

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

204 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux'] 

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

206 rtol=rtol) 

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

208 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'], 

209 rtol=rtol) 

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

211 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'], 

212 rtol=rtol) 

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

214 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'], 

215 rtol=rtol) 

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

217 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'], 

218 rtol=rtol) 

219 

220 return result 

221 

222 def testDipoleTask(self): 

223 """Test the dipole fitting singleFramePlugin. 

224 

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

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

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

228 

229 Also test that the resulting fluxes are close to those 

230 generated by the existing ip_diffim_DipoleMeasurement task 

231 (PsfDipoleFit). 

232 """ 

233 dipoleTestImage = DipoleTestImage() 

234 sources = self._runDetection(dipoleTestImage) 

235 self._checkTaskOutput(dipoleTestImage, sources) 

236 

237 def testDipoleTaskNoPosImage(self): 

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

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

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

241 

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

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

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

245 

246 Also test that the resulting fluxes are close to those 

247 generated by the existing ip_diffim_DipoleMeasurement task 

248 (PsfDipoleFit). 

249 """ 

250 dipoleTestImage = DipoleTestImage() 

251 dipoleTestImage.testImage.posImage = None 

252 sources = self._runDetection(dipoleTestImage) 

253 self._checkTaskOutput(dipoleTestImage, sources) 

254 

255 def testDipoleTaskNoNegImage(self): 

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

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

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

259 

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

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

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

263 

264 Also test that the resulting fluxes are close to those 

265 generated by the existing ip_diffim_DipoleMeasurement task 

266 (PsfDipoleFit). 

267 """ 

268 dipoleTestImage = DipoleTestImage() 

269 dipoleTestImage.testImage.negImage = None 

270 sources = self._runDetection(dipoleTestImage) 

271 self._checkTaskOutput(dipoleTestImage, sources) 

272 

273 def testDipoleTaskNoPreSubImages(self): 

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

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

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

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

278 widely-separated dipoles. 

279 

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

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

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

283 

284 Also test that the resulting fluxes are close to those 

285 generated by the existing ip_diffim_DipoleMeasurement task 

286 (PsfDipoleFit). 

287 """ 

288 dipoleTestImage = DipoleTestImage() 

289 dipoleTestImage.testImage.posImage = dipoleTestImage.testImage.negImage = None 

290 sources = self._runDetection(dipoleTestImage) 

291 self._checkTaskOutput(dipoleTestImage, sources) 

292 

293 def testDipoleEdge(self): 

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

295 singleFramePlugin. 

296 

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

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

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

300 if not measured. 

301 """ 

302 

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

304 sources = self._runDetection(dipoleTestImage) 

305 

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

307 

308 for i, s in enumerate(sources): 

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

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

311 

312 def testDipoleFootprintTooLarge(self): 

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

314 

315 dipoleTestImage = DipoleTestImage() 

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

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

318 

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

320 

321 

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

323 pass 

324 

325 

326def setup_module(module): 

327 lsst.utils.tests.init() 

328 

329 

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

331 lsst.utils.tests.init() 

332 unittest.main()