Coverage for tests/test_CompensatedGaussianFlux.py: 11%

155 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-12 11:28 -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# import math 

24 

25import numpy as np 

26 

27import lsst.geom 

28import lsst.afw.geom 

29import lsst.meas.base 

30import lsst.utils.tests 

31from lsst.meas.base.tests import AlgorithmTestCase 

32from lsst.meas.base._measBaseLib import _compensatedGaussianFiltInnerProduct 

33 

34 

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

36 def setUp(self): 

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

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

39 

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

41 self.psf_size = 2.0 

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

43 

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

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

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

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

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

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

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

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

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

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

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

55 

56 # Small test for Monte Carlo 

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

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

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

60 self.dataset_single.psfShape = self.dataset.psfShape 

61 

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

63 

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

65 self.larger_widths = (3, 5) 

66 self.all_ts = (1.5, 2.0) 

67 

68 def tearDown(self): 

69 del self.bbox 

70 del self.dataset 

71 del self.bbox_single 

72 del self.dataset_single 

73 

74 def makeAlgorithm(self, config=None): 

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

76 """ 

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

78 if config is None: 

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

80 algorithm = lsst.meas.base.SingleFrameCompensatedGaussianFluxPlugin( 

81 config, 

82 "base_CompensatedGaussianFlux", 

83 schema, 

84 None 

85 ) 

86 return algorithm, schema 

87 

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

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

90 

91 Parameters 

92 ---------- 

93 arr : `np.ndarray` 

94 Array of pixel values. 

95 var_arr : `np.ndarray` 

96 Array of variance values. 

97 x_cent : `float` 

98 x value of centroid. 

99 y_cent : `float` 

100 y value of centroid. 

101 width : `float` 

102 Width of inner kernel. 

103 t : `float` 

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

105 

106 Returns 

107 ------- 

108 flux : `float` 

109 variance : `float` 

110 """ 

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

112 # Compute the inner and outer normalized Gaussian weights. 

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

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

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

116 weight = inner - outer 

117 

118 # Compute the weighted sum of the pixels. 

119 flux = np.sum(weight*arr) 

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

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

122 

123 # And compute the variance 

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

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

126 

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

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

129 

130 return flux, variance 

131 

132 def testCompensatedGaussianInnerProduct(self): 

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

134 

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

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

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

138 psf_size = 2.0 

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

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

141 true_flux = 100000.0 

142 dataset.addSource(true_flux, centroid) 

143 

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

145 task = self.makeSingleFrameMeasurementTask("base_CompensatedGaussianFlux") 

146 

147 for width in self.all_widths: 

148 for t in self.all_ts: 

149 flux_0 = None 

150 var_0 = None 

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

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

153 

154 exposure.image.array += dc_offset 

155 

156 flux, var = _compensatedGaussianFiltInnerProduct( 

157 exposure.image.array, 

158 exposure.variance.array, 

159 centroid.getX(), 

160 centroid.getY(), 

161 width, 

162 t, 

163 ) 

164 

165 flux_numpy, var_numpy = self._calcNumpyCompensatedGaussian( 

166 exposure.image.array, 

167 exposure.variance.array, 

168 centroid.getX(), 

169 centroid.getY(), 

170 width, 

171 t, 

172 ) 

173 

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

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

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

177 

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

179 # should be nearly unbiased. 

180 if width == psf_size: 

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

182 

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

184 # equal with some floating point tolerance. 

185 if dc_offset == 0.0: 

186 flux_0 = flux 

187 var_0 = var 

188 else: 

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

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

191 

192 def testCompensatedGaussianSubPixels(self): 

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

194 np.random.seed(12345) 

195 

196 n_points = 100 

197 

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

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

200 

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

202 task = self.makeSingleFrameMeasurementTask("base_CompensatedGaussianFlux") 

203 

204 for width in self.all_widths: 

205 for t in self.all_ts: 

206 fluxes = np.zeros(n_points) 

207 

208 for i in range(n_points): 

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

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

211 dataset.addSource(50000.0, centroid) 

212 

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

214 flux, var = _compensatedGaussianFiltInnerProduct( 

215 exposure.image.array, 

216 exposure.variance.array, 

217 centroid.getX(), 

218 centroid.getY(), 

219 width, 

220 t, 

221 ) 

222 

223 fluxes[i] = flux 

224 

225 # Check for no correlation with x_sub and y_sub. 

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

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

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

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

230 

231 def testCompensatedGaussianPlugin(self): 

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

233 """ 

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

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

236 for width in self.all_widths: 

237 for t in self.all_ts: 

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

239 config = self.makeSingleFrameMeasurementConfig("base_CompensatedGaussianFlux") 

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

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

242 

243 task = self.makeSingleFrameMeasurementTask(config=config) 

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

245 exposure.image.array += dc_offset 

246 task.run(catalog, exposure) 

247 

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

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

250 truth_flux = catalog["truth_instFlux"] 

251 

252 if width == self.psf_size: 

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

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

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

256 elif width > self.psf_size: 

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

258 # greater than the truth flux. 

259 np.testing.assert_array_less(truth_flux, filter_flux) 

260 

261 if dc_offset == 0.0: 

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

263 flux_0 = filter_flux 

264 else: 

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

266 # larger than preferable. 

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

268 

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

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

271 # tolerance now. 

272 ratio = filter_flux / truth_flux 

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

274 

275 def testMonteCarlo(self): 

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

277 

278 Demonstrate that: 

279 

280 - We get exactly the right answer, and 

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

282 """ 

283 nSamples = 500 

284 

285 for width in self.larger_widths: 

286 for t in self.all_ts: 

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

288 config.kernel_widths = [width] 

289 config.t = t 

290 

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

292 

293 # Make a noiseless catalog. 

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

295 

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

297 record = catalog[0] 

298 algorithm.measure(record, exposure) 

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

300 

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

302 fluxes = np.zeros(nSamples) 

303 errs = np.zeros_like(fluxes) 

304 

305 for repeat in range(nSamples): 

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

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

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

309 exposure_samp, catalog_samp = self.dataset_single.realize( 

310 noise*inst_flux, 

311 schema, 

312 randomSeed=repeat, 

313 ) 

314 record_samp = catalog_samp[0] 

315 algorithm.measure(record_samp, exposure_samp) 

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

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

318 

319 err_mean = np.mean(errs) 

320 flux_std = np.std(fluxes) 

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

322 

323 

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

325 pass 

326 

327 

328def setup_module(module): 

329 lsst.utils.tests.init() 

330 

331 

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

333 lsst.utils.tests.init() 

334 unittest.main()