Coverage for tests/test_dipoleFitter.py: 20%

111 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-23 02:18 -0800

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(object): 

38 """!Class to initialize test dipole image used by all tests below. 

39 

40 @var display: Display (plot) the output dipole thumbnails (matplotlib) 

41 @var verbose: be verbose during fitting 

42 @var xc: x coordinate (pixels) of center(s) of input dipole(s) 

43 @var yc: y coordinate (pixels) of center(s) of input dipole(s) 

44 @var flux: flux(es) of input dipole(s) 

45 @var gradientParams: tuple with three parameters for linear background gradient 

46 @var offsets: pixel coordinates between lobes of dipoles 

47 

48 Also stores all parameters used to generate the test image (to compare to fitting results). 

49 """ 

50 

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

52 """!Store the parameters, create the test image and run detection on it. 

53 

54 @param xc iterable x coordinate (pixels) of center(s) of input dipole(s) 

55 @param yc iterable y coordinate (pixels) of center(s) of input dipole(s) 

56 @param offsets iterable pixel coord offsets between lobes of dipole(s) 

57 @param flux iterable fluxes of pos/neg lobes of dipole(s) 

58 @param gradientParams iterable three parameters for linear background gradient 

59 """ 

60 self.display = False # Display (plot) the output dipole thumbnails (matplotlib) 

61 self.verbose = False # be verbose during fitting 

62 

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

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

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

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

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

68 

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

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

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

72 self.rtol = 0.01 

73 

74 self.generateTestImage() 

75 

76 def generateTestImage(self): 

77 self.testImage = ipUtils.DipoleTestImage( 

78 w=100, h=100, 

79 xcenPos=self.xc + self.offsets, 

80 ycenPos=self.yc + self.offsets, 

81 xcenNeg=self.xc - self.offsets, 

82 ycenNeg=self.yc - self.offsets, 

83 flux=self.flux, fluxNeg=self.flux, 

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

85 gradientParams=self.gradientParams) 

86 

87 

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

89 """!A test case for separately testing the dipole fit algorithm 

90 directly, and the single frame measurement. 

91 

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

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

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

95 """ 

96 

97 def testDipoleAlgorithm(self): 

98 """!Test the dipole fitting algorithm directly (fitDipole()). 

99 

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

101 input values for both dipoles in the image. 

102 """ 

103 params = DipoleTestImage() 

104 catalog = params.testImage.detectDipoleSources(minBinSize=32) 

105 

106 for s in catalog: 

107 fp = s.getFootprint() 

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

109 

110 rtol = params.rtol 

111 offsets = params.offsets 

112 testImage = params.testImage 

113 for i, s in enumerate(catalog): 

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

115 result, _ = alg.fitDipole( 

116 s, rel_weight=0.5, separateNegParams=False, 

117 verbose=params.verbose, display=params.display) 

118 

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

120 params.flux[i], rtol=rtol) 

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

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

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

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

125 

126 def _runDetection(self, params): 

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

128 positive and negative sources. 

129 

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

131 """ 

132 

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

134 testImage = params.testImage 

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

136 

137 measureConfig = measBase.SingleFrameMeasurementConfig() 

138 

139 measureConfig.slots.calibFlux = None 

140 measureConfig.slots.modelFlux = None 

141 measureConfig.slots.gaussianFlux = None 

142 measureConfig.slots.shape = None 

143 measureConfig.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 

144 measureConfig.doReplaceWithNoise = False 

145 

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

147 "base_PixelFlags", 

148 "base_SkyCoord", 

149 "base_PsfFlux", 

150 "ip_diffim_NaiveDipoleCentroid", 

151 "ip_diffim_NaiveDipoleFlux", 

152 "ip_diffim_PsfDipoleFlux"] 

153 

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

155 # This is an example of how to pass it a custom config. 

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

157 

158 table = afwTable.SourceTable.make(schema) 

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

160 # catalog = detectResult.sources 

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

162 

163 fpSet = detectResult.fpSets.positive 

164 fpSet.merge(detectResult.fpSets.negative, 2, 2, False) 

165 sources = afwTable.SourceCatalog(table) 

166 fpSet.makeSources(sources) 

167 

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

169 return sources 

170 

171 def _checkTaskOutput(self, params, sources, rtol=None): 

172 """!Compare the fluxes/centroids in `sources` are entered 

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

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

175 image. 

176 

177 Also test that the resulting fluxes are close to those 

178 generated by the existing ip_diffim_DipoleMeasurement task 

179 (PsfDipoleFit). 

180 """ 

181 

182 if rtol is None: 

183 rtol = params.rtol 

184 offsets = params.offsets 

185 for i, r1 in enumerate(sources): 

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

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

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

189 params.flux[i], rtol=rtol) 

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

191 params.xc[i] + offsets[i], rtol=rtol) 

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

193 params.yc[i] + offsets[i], rtol=rtol) 

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

195 params.xc[i] - offsets[i], rtol=rtol) 

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

197 params.yc[i] - offsets[i], rtol=rtol) 

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

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

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

201 

202 # compare to the original ip_diffim_PsfDipoleFlux measurements 

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

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

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

206 (result2['ip_diffim_PsfDipoleFlux_pos_instFlux'] 

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

208 rtol=rtol) 

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

210 result2['ip_diffim_PsfDipoleFlux_pos_centroid_x'], 

211 rtol=rtol) 

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

213 result2['ip_diffim_PsfDipoleFlux_pos_centroid_y'], 

214 rtol=rtol) 

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

216 result2['ip_diffim_PsfDipoleFlux_neg_centroid_x'], 

217 rtol=rtol) 

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

219 result2['ip_diffim_PsfDipoleFlux_neg_centroid_y'], 

220 rtol=rtol) 

221 

222 return result 

223 

224 def testDipoleTask(self): 

225 """!Test the dipole fitting singleFramePlugin. 

226 

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

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

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

230 

231 Also test that the resulting fluxes are close to those 

232 generated by the existing ip_diffim_DipoleMeasurement task 

233 (PsfDipoleFit). 

234 """ 

235 params = DipoleTestImage() 

236 sources = self._runDetection(params) 

237 self._checkTaskOutput(params, sources) 

238 

239 def testDipoleTaskNoPosImage(self): 

240 """!Test the dipole fitting singleFramePlugin in the case where no 

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

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

243 

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

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

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

247 

248 Also test that the resulting fluxes are close to those 

249 generated by the existing ip_diffim_DipoleMeasurement task 

250 (PsfDipoleFit). 

251 """ 

252 params = DipoleTestImage() 

253 params.testImage.posImage = None 

254 sources = self._runDetection(params) 

255 self._checkTaskOutput(params, sources) 

256 

257 def testDipoleTaskNoNegImage(self): 

258 """!Test the dipole fitting singleFramePlugin in the case where no 

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

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

261 

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

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

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

265 

266 Also test that the resulting fluxes are close to those 

267 generated by the existing ip_diffim_DipoleMeasurement task 

268 (PsfDipoleFit). 

269 """ 

270 params = DipoleTestImage() 

271 params.testImage.negImage = None 

272 sources = self._runDetection(params) 

273 self._checkTaskOutput(params, sources) 

274 

275 def testDipoleTaskNoPreSubImages(self): 

276 """!Test the dipole fitting singleFramePlugin in the case where no 

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

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

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

280 widely-separated dipoles. 

281 

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

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

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

285 

286 Also test that the resulting fluxes are close to those 

287 generated by the existing ip_diffim_DipoleMeasurement task 

288 (PsfDipoleFit). 

289 """ 

290 params = DipoleTestImage() 

291 params.testImage.posImage = params.testImage.negImage = None 

292 sources = self._runDetection(params) 

293 self._checkTaskOutput(params, sources) 

294 

295 def testDipoleEdge(self): 

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

297 singleFramePlugin. 

298 

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

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

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

302 if not measured. 

303 """ 

304 

305 params = DipoleTestImage(xc=[5.3, 4.8], yc=[4.6, 96.5]) 

306 sources = self._runDetection(params) 

307 

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

309 

310 for i, s in enumerate(sources): 

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

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

313 

314 

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

316 pass 

317 

318 

319def setup_module(module): 

320 lsst.utils.tests.init() 

321 

322 

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

324 lsst.utils.tests.init() 

325 unittest.main()