Coverage for tests/test_CompensatedGaussianFlux.py: 11%

155 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-03 03:00 -0700

1# This file is part of meas_base. 

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 unittest 

23 

24import numpy as np 

25 

26import lsst.geom 

27import lsst.afw.geom 

28import lsst.meas.base 

29import lsst.utils.tests 

30from lsst.meas.base.tests import AlgorithmTestCase 

31from lsst.meas.base._measBaseLib import _compensatedGaussianFiltInnerProduct 

32 

33 

34class CompensatedGaussianFluxTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

35 def setUp(self): 

36 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-20, -30), 

37 lsst.geom.Extent2I(1000, 1000)) 

38 

39 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox) 

40 self.psf_size = 2.0 

41 self.dataset.psfShape = lsst.afw.geom.Quadrupole(self.psf_size**2., self.psf_size**2., 0.0) 

42 

43 # We want a set of point sources at various flux levels. 

44 self.dataset.addSource(10000.0, lsst.geom.Point2D(50.1, 49.8)) 

45 self.dataset.addSource(20000.0, lsst.geom.Point2D(100.5, 100.4)) 

46 self.dataset.addSource(30000.0, lsst.geom.Point2D(150.4, 149.6)) 

47 self.dataset.addSource(40000.0, lsst.geom.Point2D(200.2, 200.3)) 

48 self.dataset.addSource(50000.0, lsst.geom.Point2D(250.3, 250.1)) 

49 self.dataset.addSource(60000.0, lsst.geom.Point2D(300.4, 300.2)) 

50 self.dataset.addSource(70000.0, lsst.geom.Point2D(350.5, 350.6)) 

51 self.dataset.addSource(80000.0, lsst.geom.Point2D(400.6, 400.0)) 

52 self.dataset.addSource(90000.0, lsst.geom.Point2D(450.0, 450.0)) 

53 self.dataset.addSource(100000.0, lsst.geom.Point2D(500.7, 500.8)) 

54 

55 # Small test for Monte Carlo 

56 self.bbox_single = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

57 lsst.geom.Extent2I(101, 101)) 

58 self.dataset_single = lsst.meas.base.tests.TestDataset(self.bbox_single) 

59 self.dataset_single.psfShape = self.dataset.psfShape 

60 

61 self.dataset_single.addSource(100000.0, lsst.geom.Point2D(50.0, 50.0)) 

62 

63 self.all_widths = (2, 3, 5) 

64 self.larger_widths = (3, 5) 

65 self.all_ts = (1.5, 2.0) 

66 

67 def tearDown(self): 

68 del self.bbox 

69 del self.dataset 

70 del self.bbox_single 

71 del self.dataset_single 

72 

73 def makeAlgorithm(self, config=None): 

74 """Construct an algorithm and return both it and its schema. 

75 """ 

76 schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema() 

77 if config is None: 

78 config = lsst.meas.base.SingleFrameCompensatedGaussianFluxConfig() 

79 algorithm = lsst.meas.base.SingleFrameCompensatedGaussianFluxPlugin( 

80 config, 

81 "base_CompensatedGaussianFlux", 

82 schema, 

83 None 

84 ) 

85 return algorithm, schema 

86 

87 def _calcNumpyCompensatedGaussian(self, arr, var_arr, x_cent, y_cent, width, t): 

88 """Calculate the compensated Gaussian using numpy (for testing). 

89 

90 Parameters 

91 ---------- 

92 arr : `np.ndarray` 

93 Array of pixel values. 

94 var_arr : `np.ndarray` 

95 Array of variance values. 

96 x_cent : `float` 

97 x value of centroid. 

98 y_cent : `float` 

99 y value of centroid. 

100 width : `float` 

101 Width of inner kernel. 

102 t : `float` 

103 Scaling factor for outer kernel (outer_width = width*t). 

104 

105 Returns 

106 ------- 

107 flux : `float` 

108 variance : `float` 

109 """ 

110 xx, yy = np.meshgrid(np.arange(arr.shape[0]), np.arange(arr.shape[1])) 

111 # Compute the inner and outer normalized Gaussian weights. 

112 inner = (1./(2.*np.pi*width**2.))*np.exp(-0.5*(((xx - x_cent)/width)**2. + ((yy - y_cent)/width)**2.)) 

113 outer = (1./(2.*np.pi*(t*width)**2.))*np.exp(-0.5*(((xx - x_cent)/(t*width))**2. 

114 + ((yy - y_cent)/(t*width))**2.)) 

115 weight = inner - outer 

116 

117 # Compute the weighted sum of the pixels. 

118 flux = np.sum(weight*arr) 

119 # And the normalization term, derived in Lupton et al. (in prep). 

120 flux *= 4.*np.pi*(width**2.)*(t**2. + 1)/(t**2. - 1) 

121 

122 # And compute the variance 

123 variance = np.sum(weight*weight*var_arr) 

124 variance /= np.sum(weight*weight) 

125 

126 # The variance normalization term, derived in Lupton et al. (in prep). 

127 variance *= 4.*np.pi*(width**2.)*(t**2 + 1)/t**2. 

128 

129 return flux, variance 

130 

131 def testCompensatedGaussianInnerProduct(self): 

132 """Test using the inner product routine directly.""" 

133 

134 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

135 lsst.geom.Extent2I(101, 101)) 

136 dataset = lsst.meas.base.tests.TestDataset(bbox) 

137 psf_size = 2.0 

138 dataset.psfShape = lsst.afw.geom.Quadrupole(psf_size**2., psf_size**2., 0.0) 

139 centroid = lsst.geom.Point2D(50.0, 50.0) 

140 true_flux = 100000.0 

141 dataset.addSource(true_flux, centroid) 

142 

143 # We need to set up the task in order to create the test dataset. 

144 task = self.makeSingleFrameMeasurementTask("base_CompensatedGaussianFlux") 

145 

146 for width in self.all_widths: 

147 for t in self.all_ts: 

148 flux_0 = None 

149 var_0 = None 

150 for dc_offset in (0.0, -1.0, 1.0): 

151 exposure, catalog = dataset.realize(10.0, task.schema, randomSeed=1000) 

152 

153 exposure.image.array += dc_offset 

154 

155 flux, var = _compensatedGaussianFiltInnerProduct( 

156 exposure.image.array, 

157 exposure.variance.array, 

158 centroid.getX(), 

159 centroid.getY(), 

160 width, 

161 t, 

162 ) 

163 

164 flux_numpy, var_numpy = self._calcNumpyCompensatedGaussian( 

165 exposure.image.array, 

166 exposure.variance.array, 

167 centroid.getX(), 

168 centroid.getY(), 

169 width, 

170 t, 

171 ) 

172 

173 # Compare values from c++ code to numpy code. 

174 self.assertFloatsAlmostEqual(flux, flux_numpy, rtol=1e-6) 

175 self.assertFloatsAlmostEqual(var, var_numpy, rtol=1e-6) 

176 

177 # If the kernel width is equal to the simulated PSF then it 

178 # should be nearly unbiased. 

179 if width == psf_size: 

180 self.assertFloatsAlmostEqual(flux, true_flux, rtol=1e-3) 

181 

182 # And check biases with non-zero DC offset; these should be 

183 # equal with some floating point tolerance. 

184 if dc_offset == 0.0: 

185 flux_0 = flux 

186 var_0 = var 

187 else: 

188 self.assertFloatsAlmostEqual(flux, flux_0, rtol=1e-7) 

189 self.assertFloatsAlmostEqual(var, var_0, rtol=1e-7) 

190 

191 def testCompensatedGaussianSubPixels(self): 

192 """Test for correct instFlux as a function of sub-pixel position.""" 

193 np.random.seed(12345) 

194 

195 n_points = 100 

196 

197 x_sub = np.random.uniform(low=0.0, high=1.0, size=n_points) 

198 y_sub = np.random.uniform(low=0.0, high=1.0, size=n_points) 

199 

200 # We need to set up the task in order to create the test dataset. 

201 task = self.makeSingleFrameMeasurementTask("base_CompensatedGaussianFlux") 

202 

203 for width in self.all_widths: 

204 for t in self.all_ts: 

205 fluxes = np.zeros(n_points) 

206 

207 for i in range(n_points): 

208 dataset = lsst.meas.base.tests.TestDataset(self.bbox_single) 

209 centroid = lsst.geom.Point2D(50.0 + x_sub[i], 50.0 + y_sub[i]) 

210 dataset.addSource(50000.0, centroid) 

211 

212 exposure, catalog = dataset.realize(10.0, task.schema, randomSeed=i) 

213 flux, var = _compensatedGaussianFiltInnerProduct( 

214 exposure.image.array, 

215 exposure.variance.array, 

216 centroid.getX(), 

217 centroid.getY(), 

218 width, 

219 t, 

220 ) 

221 

222 fluxes[i] = flux 

223 

224 # Check for no correlation with x_sub and y_sub. 

225 fit_x = np.polyfit(x_sub, fluxes/50000.0, 1) 

226 self.assertLess(np.abs(fit_x[0]), 1e-3) 

227 fit_y = np.polyfit(y_sub, fluxes/50000.0, 1) 

228 self.assertLess(np.abs(fit_y[0]), 1e-3) 

229 

230 def testCompensatedGaussianPlugin(self): 

231 """Test for correct instFlux given known position and shape. 

232 """ 

233 # In the z-band, HSC images have a noise of about 40.0 ADU, and a background 

234 # offset of ~ -0.6 ADU/pixel. This determines our test levels. 

235 for width in self.all_widths: 

236 for t in self.all_ts: 

237 for dc_offset in (0.0, -1.0, 1.0): 

238 config = self.makeSingleFrameMeasurementConfig("base_CompensatedGaussianFlux") 

239 config.algorithms["base_CompensatedGaussianFlux"].kernel_widths = [width] 

240 config.algorithms["base_CompensatedGaussianFlux"].t = t 

241 

242 task = self.makeSingleFrameMeasurementTask(config=config) 

243 exposure, catalog = self.dataset.realize(40.0, task.schema, randomSeed=0) 

244 exposure.image.array += dc_offset 

245 task.run(catalog, exposure) 

246 

247 filter_flux = catalog[f"base_CompensatedGaussianFlux_{width}_instFlux"] 

248 filter_err = catalog[f"base_CompensatedGaussianFlux_{width}_instFluxErr"] 

249 truth_flux = catalog["truth_instFlux"] 

250 

251 if width == self.psf_size: 

252 # When the filter matches the PSF, we should get close to the true flux. 

253 tol = np.sqrt((filter_err/filter_flux)**2. + 0.02**2.) 

254 self.assertFloatsAlmostEqual(filter_flux, truth_flux, rtol=tol) 

255 elif width > self.psf_size: 

256 # When the filter is larger than the PSF, the filter flux will be 

257 # greater than the truth flux. 

258 np.testing.assert_array_less(truth_flux, filter_flux) 

259 

260 if dc_offset == 0.0: 

261 # Use the no-offset run as a comparison for offset runs. 

262 flux_0 = filter_flux 

263 else: 

264 # Note: this tolerance is determined empirically, but this is 

265 # larger than preferable. 

266 self.assertFloatsAlmostEqual(filter_flux, flux_0, rtol=5e-3) 

267 

268 # The ratio of the filter flux to the truth flux should be consistent. 

269 # I'm not sure how to scale this with the error, so this is a loose 

270 # tolerance now. 

271 ratio = filter_flux / truth_flux 

272 self.assertLess(np.std(ratio), 0.04) 

273 

274 def testMonteCarlo(self): 

275 """Test an ideal simulation, with no noise. 

276 

277 Demonstrate that: 

278 

279 - We get exactly the right answer, and 

280 - The reported uncertainty agrees with a Monte Carlo test of the noise. 

281 """ 

282 nSamples = 500 

283 

284 for width in self.larger_widths: 

285 for t in self.all_ts: 

286 config = lsst.meas.base.SingleFrameCompensatedGaussianFluxConfig() 

287 config.kernel_widths = [width] 

288 config.t = t 

289 

290 algorithm, schema = self.makeAlgorithm(config=config) 

291 

292 # Make a noiseless catalog. 

293 exposure, catalog = self.dataset_single.realize(1E-8, schema, randomSeed=1) 

294 

295 # Only use the high-flux source for the error tests. 

296 record = catalog[0] 

297 algorithm.measure(record, exposure) 

298 inst_flux = record[f"base_CompensatedGaussianFlux_{width}_instFlux"] 

299 

300 for noise in (0.001, 0.01, 0.1): 

301 fluxes = np.zeros(nSamples) 

302 errs = np.zeros_like(fluxes) 

303 

304 for repeat in range(nSamples): 

305 # By using ``repeat`` to seed the RNG, we get results which 

306 # fall within the tolerances defined below. If we allow this 

307 # test to be truly random, passing becomes RNG-dependent. 

308 exposure_samp, catalog_samp = self.dataset_single.realize( 

309 noise*inst_flux, 

310 schema, 

311 randomSeed=repeat, 

312 ) 

313 record_samp = catalog_samp[0] 

314 algorithm.measure(record_samp, exposure_samp) 

315 fluxes[repeat] = record_samp[f"base_CompensatedGaussianFlux_{width}_instFlux"] 

316 errs[repeat] = record_samp[f"base_CompensatedGaussianFlux_{width}_instFluxErr"] 

317 

318 err_mean = np.mean(errs) 

319 flux_std = np.std(fluxes) 

320 self.assertFloatsAlmostEqual(err_mean, flux_std, rtol=0.10) 

321 

322 

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

324 pass 

325 

326 

327def setup_module(module): 

328 lsst.utils.tests.init() 

329 

330 

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

332 lsst.utils.tests.init() 

333 unittest.main()