Coverage for tests/test_convolved.py: 19%

148 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-16 11:34 +0000

1# 

2# LSST Data Management System 

3# Copyright 2017 LSST/AURA. 

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 <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22from __future__ import absolute_import, division, print_function 

23 

24import math 

25import unittest 

26import lsst.utils.tests 

27import lsst.daf.base as dafBase 

28import lsst.afw.detection as afwDetection 

29import lsst.afw.geom as afwGeom 

30import lsst.afw.geom.ellipses as afwEll 

31import lsst.afw.table as afwTable 

32import lsst.afw.image as afwImage 

33import lsst.geom as geom 

34import lsst.meas.base as measBase 

35import lsst.meas.extensions.convolved # Load flux.convolved algorithm 

36 

37import lsst.afw.display as afwDisplay 

38 

39try: 

40 type(display) 

41except NameError: 

42 display = False 

43 frame = 1 

44 

45SIGMA_TO_FWHM = 2.0*math.sqrt(2.0*math.log(2.0)) 

46 

47 

48def makeExposure(bbox, scale, psfFwhm, flux): 

49 """Make a fake exposure 

50 

51 Parameters 

52 ---------- 

53 

54 bbox : `lsst.geom.Box2I` 

55 Bounding box for image. 

56 scale : `lsst.geom.Angle` 

57 Pixel scale. 

58 psfFwhm : `float` 

59 PSF FWHM (arcseconds) 

60 flux : `float` 

61 PSF flux (ADU) 

62 

63 Returns 

64 ------- 

65 exposure : `lsst.afw.image.ExposureF` 

66 Fake exposure. 

67 center : `lsst.geom.Point2D` 

68 Position of fake source. 

69 """ 

70 image = afwImage.ImageF(bbox) 

71 image.set(0) 

72 center = geom.Box2D(bbox).getCenter() 

73 psfSigma = psfFwhm/SIGMA_TO_FWHM/scale.asArcseconds() 

74 psfWidth = 2*int(4.0*psfSigma) + 1 

75 psf = afwDetection.GaussianPsf(psfWidth, psfWidth, psfSigma) 

76 psfImage = psf.computeImage(center).convertF() 

77 psfFlux = psfImage.getArray().sum() 

78 psfImage *= flux/psfFlux 

79 

80 subImage = afwImage.ImageF(image, psfImage.getBBox(afwImage.PARENT), afwImage.PARENT) 

81 subImage += psfImage 

82 

83 exp = afwImage.makeExposure(afwImage.makeMaskedImage(image)) 

84 exp.setPsf(psf) 

85 exp.getMaskedImage().getVariance().set(1.0) 

86 exp.getMaskedImage().getMask().set(0) 

87 

88 cdMatrix = afwGeom.makeCdMatrix(scale=scale) 

89 exp.setWcs(afwGeom.makeSkyWcs(crpix=center, 

90 crval=geom.SpherePoint(0.0, 0.0, geom.degrees), 

91 cdMatrix=cdMatrix)) 

92 return exp, center 

93 

94 

95class ConvolvedFluxTestCase(lsst.utils.tests.TestCase): 

96 """A test case for measuring convolved fluxes""" 

97 

98 def checkSchema(self, schema, names): 

99 """Check that the schema includes flux, fluxErr and flag elements for each measurement 

100 

101 Also checks for the presence of the corresponding undeblended measurements. 

102 

103 Parameters 

104 ---------- 

105 schema : `lsst.afw.table.Schema` 

106 Schema to check. 

107 names : `list` of `str` 

108 List of measurement algorithm names 

109 """ 

110 for name in names: 

111 self.assertIn(name + "_instFlux", schema) 

112 self.assertIn(name + "_instFluxErr", schema) 

113 self.assertIn(name + "_flag", schema) 

114 self.assertIn("undeblended_" + name + "_instFlux", schema) 

115 self.assertIn("undeblended_" + name + "_instFluxErr", schema) 

116 self.assertIn("undeblended_" + name + "_flag", schema) 

117 

118 def check(self, psfFwhm=0.5, flux=1000.0, forced=False): 

119 """Check that we can measure convolved fluxes 

120 

121 We create an image with a Gaussian PSF and a single point source. 

122 Measurements of the point source should match expectations for a 

123 Gaussian of the known sigma and known aperture radius. 

124 

125 Parameters 

126 ---------- 

127 psfFwhm : `float` 

128 PSF FWHM (arcsec) 

129 flux : `float` 

130 Source flux (ADU) 

131 forced : `bool` 

132 Forced measurement? 

133 """ 

134 bbox = geom.Box2I(geom.Point2I(12345, 6789), geom.Extent2I(200, 300)) 

135 

136 # We'll only achieve the target accuracy if the pixel scale is rather smaller than Gaussians 

137 # involved. Otherwise it's important to consider the convolution with the pixel grid, and we're 

138 # not doing that here. 

139 scale = 0.1*geom.arcseconds 

140 

141 TaskClass = measBase.ForcedMeasurementTask if forced else measBase.SingleFrameMeasurementTask 

142 

143 exposure, center = makeExposure(bbox, scale, psfFwhm, flux) 

144 measConfig = TaskClass.ConfigClass() 

145 algName = "ext_convolved_ConvolvedFlux" 

146 measConfig.plugins.names.add(algName) 

147 if not forced: 

148 measConfig.plugins.names.add("ext_photometryKron_KronFlux") 

149 else: 

150 measConfig.copyColumns = {"id": "objectId", "parent": "parentObjectId"} 

151 values = [ii/scale.asArcseconds() for ii in (0.6, 0.8, 1.0, 1.2)] 

152 algConfig = measConfig.plugins[algName] 

153 algConfig.seeing = values 

154 algConfig.aperture.radii = values 

155 algConfig.aperture.maxSincRadius = max(values) + 1 # Get as exact as we can 

156 

157 if forced: 

158 offset = geom.Extent2D(-12.3, 45.6) 

159 kronRadiusName = "my_Kron_Radius" 

160 kronRadius = 12.345 

161 refWcs = exposure.getWcs().copyAtShiftedPixelOrigin(offset) 

162 measConfig.plugins[algName].kronRadiusName = kronRadiusName 

163 refSchema = afwTable.SourceTable.makeMinimalSchema() 

164 centroidKey = afwTable.Point2DKey.addFields(refSchema, "my_centroid", doc="centroid", 

165 unit="pixel") 

166 shapeKey = afwTable.QuadrupoleKey.addFields(refSchema, "my_shape", "shape") 

167 refSchema.getAliasMap().set("slot_Centroid", "my_centroid") 

168 refSchema.getAliasMap().set("slot_Shape", "my_shape") 

169 refSchema.addField("my_centroid_flag", type="Flag", doc="centroid flag") 

170 refSchema.addField("my_shape_flag", type="Flag", doc="shape flag") 

171 refSchema.addField(kronRadiusName, type=float, doc="my custom kron radius", units="pixel") 

172 refCat = afwTable.SourceCatalog(refSchema) 

173 refSource = refCat.addNew() 

174 refSource.set(centroidKey, center + offset) 

175 refSource.set(shapeKey, afwEll.Quadrupole(afwEll.Axes(kronRadius, kronRadius, 0))) 

176 refSource.set(kronRadiusName, kronRadius) 

177 refSource.setCoord(refWcs.pixelToSky(refSource.get(centroidKey))) 

178 taskInitArgs = (refSchema,) 

179 taskRunArgs = (refCat, refWcs) 

180 else: 

181 taskInitArgs = (afwTable.SourceTable.makeMinimalSchema(),) 

182 taskRunArgs = () 

183 

184 # Activate undeblended measurement with the same configuration 

185 measConfig.undeblended.names.add(algName) 

186 measConfig.undeblended[algName] = measConfig.plugins[algName] 

187 

188 algMetadata = dafBase.PropertyList() 

189 task = TaskClass(*taskInitArgs, config=measConfig, algMetadata=algMetadata) 

190 

191 schema = task.schema 

192 measCat = afwTable.SourceCatalog(schema) 

193 source = measCat.addNew() 

194 source.getTable().setMetadata(algMetadata) 

195 ss = afwDetection.FootprintSet(exposure.getMaskedImage(), afwDetection.Threshold(0.1)) 

196 fp = ss.getFootprints()[0] 

197 source.setFootprint(fp) 

198 

199 task.run(measCat, exposure, *taskRunArgs) 

200 

201 disp = afwDisplay.Display(frame) 

202 disp.mtv(exposure) 

203 disp.dot("x", *center, origin=afwImage.PARENT, title="psfFwhm=%f" % (psfFwhm,)) 

204 

205 self.checkSchema(schema, algConfig.getAllApertureResultNames()) 

206 self.checkSchema(schema, algConfig.getAllKronResultNames()) 

207 self.checkSchema(schema, algConfig.getAllResultNames()) 

208 

209 if not forced: 

210 kronRadius = source.get("ext_photometryKron_KronFlux_radius") 

211 

212 self.assertFalse(source.get(algName + "_flag")) # algorithm succeeded 

213 originalSeeing = psfFwhm/scale.asArcseconds() 

214 for ii, targetSeeing in enumerate(algConfig.seeing): 

215 deconvolve = targetSeeing < originalSeeing 

216 seeing = originalSeeing if deconvolve else targetSeeing 

217 

218 def expected(radius, sigma=seeing/SIGMA_TO_FWHM): 

219 """Return expected flux for 2D Gaussian with nominated sigma""" 

220 return flux*(1.0 - math.exp(-0.5*(radius/sigma)**2)) 

221 

222 for prefix in ("", "undeblended_"): 

223 self.assertEqual(source.get(prefix + algName + "_%d_deconv" % ii), deconvolve) 

224 

225 # Kron succeeded and match expectation 

226 if not forced: 

227 kronName = algConfig.getKronResultName(targetSeeing) 

228 kronApRadius = algConfig.kronRadiusForFlux*kronRadius 

229 self.assertFloatsAlmostEqual(source.get(prefix + kronName + "_instFlux"), 

230 expected(kronApRadius), rtol=1.0e-3) 

231 self.assertGreater(source.get(prefix + kronName + "_instFluxErr"), 0) 

232 self.assertFalse(source.get(prefix + kronName + "_flag")) 

233 

234 # Aperture measurements succeeded and match expectation 

235 for jj, radius in enumerate(measConfig.algorithms[algName].aperture.radii): 

236 name = algConfig.getApertureResultName(targetSeeing, radius) 

237 self.assertFloatsAlmostEqual(source.get(prefix + name + "_instFlux"), expected(radius), 

238 rtol=1.0e-3) 

239 self.assertFalse(source.get(prefix + name + "_flag")) 

240 self.assertGreater(source.get(prefix + name + "_instFluxErr"), 0) 

241 

242 def testConvolvedFlux(self): 

243 for forced in (True, False): 

244 for psfFwhm in (0.5, # Smaller than all target seeings 

245 0.9, # Larger than half the target seeings 

246 1.3, # Larger than all the target seeings 

247 ): 

248 self.check(psfFwhm=psfFwhm, forced=forced) 

249 

250 

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

252 pass 

253 

254 

255def setup_module(module, backend="virtualDevice"): 

256 lsst.utils.tests.init() 

257 try: 

258 afwDisplay.setDefaultBackend(backend) 

259 except Exception: 

260 print("Unable to configure display backend: %s" % backend) 

261 

262 

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

264 import sys 

265 

266 from argparse import ArgumentParser 

267 parser = ArgumentParser() 

268 parser.add_argument('--backend', type=str, default="virtualDevice", 

269 help="The backend to use, e.g. 'ds9'. Be sure to 'setup display_<backend>'") 

270 args = parser.parse_args() 

271 

272 setup_module(sys.modules[__name__], backend=args.backend) 

273 unittest.main()