Coverage for python/lsst/drp/tasks/assemble_chi2_coadd.py: 34%

105 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2023-12-29 13:36 +0000

1# This file is part of drp_tasks. 

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 

22 

23import logging 

24 

25import lsst.afw.image as afwImage 

26import lsst.afw.math as afwMath 

27import lsst.afw.table as afwTable 

28import lsst.pex.config as pexConfig 

29import lsst.pipe.base as pipeBase 

30import lsst.pipe.base.connectionTypes as cT 

31import numpy as np 

32from lsst.afw.detection import Psf 

33from lsst.meas.algorithms import SourceDetectionTask 

34from lsst.meas.base import SkyMapIdGeneratorConfig 

35 

36log = logging.getLogger(__name__) 

37 

38 

39def calculateKernelSize(sigma: float, nSigmaForKernel: float = 7) -> int: 

40 """Calculate the size of the smoothing kernel. 

41 

42 Parameters 

43 ---------- 

44 sigma: 

45 Gaussian sigma of smoothing kernel. 

46 nSigmaForKernel: 

47 The multiple of `sigma` to use to set the size of the kernel. 

48 Note that that is the full width of the kernel bounding box 

49 (so a value of 7 means 3.5 sigma on either side of center). 

50 The value will be rounded up to the nearest odd integer. 

51 

52 Returns 

53 ------- 

54 size: 

55 Size of the smoothing kernel. 

56 """ 

57 return (int(sigma * nSigmaForKernel + 0.5) // 2) * 2 + 1 # make sure it is odd 

58 

59 

60def convolveImage(image: afwImage.Image, psf: Psf) -> afwImage.Image: 

61 """Convolve an image with a psf 

62 

63 This methodm and the docstring, is based off the method in 

64 `~lsst.meas.algorithms.detection.SourceDetectionTask`. 

65 

66 We convolve the image with a Gaussian approximation to the PSF, 

67 because this is separable and therefore fast. It's technically a 

68 correlation rather than a convolution, but since we use a symmetric 

69 Gaussian there's no difference. 

70 

71 Parameters 

72 ---------- 

73 image: 

74 The image to convovle. 

75 psf: 

76 The PSF to convolve the `image` with. 

77 

78 Returns 

79 ------- 

80 convolved: 

81 The result of convolving `image` with the `psf`. 

82 """ 

83 sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius() 

84 bbox = image.getBBox() 

85 

86 # Smooth using a Gaussian (which is separable, hence fast) of width sigma 

87 # Make a SingleGaussian (separable) kernel with the 'sigma' 

88 kWidth = calculateKernelSize(sigma) 

89 gaussFunc = afwMath.GaussianFunction1D(sigma) 

90 gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc) 

91 

92 convolvedImage = image.Factory(bbox) 

93 

94 afwMath.convolve(convolvedImage, image, gaussKernel, afwMath.ConvolutionControl()) 

95 

96 return convolvedImage.Factory(convolvedImage, bbox, afwImage.PARENT, False) 

97 

98 

99class AssembleChi2CoaddConnections( 

100 pipeBase.PipelineTaskConnections, 

101 dimensions=("tract", "patch", "skymap"), 

102 defaultTemplates={"inputCoaddName": "deep", "outputCoaddName": "deepChi2"}, 

103): 

104 inputCoadds = cT.Input( 

105 doc="Exposure on which to run deblending", 

106 name="{inputCoaddName}Coadd_calexp", 

107 storageClass="ExposureF", 

108 multiple=True, 

109 dimensions=("tract", "patch", "band", "skymap"), 

110 ) 

111 chi2Coadd = cT.Output( 

112 doc="Chi^2 exposure, produced by merging multiband coadds", 

113 name="{outputCoaddName}Coadd_calexp", 

114 storageClass="ExposureF", 

115 dimensions=("tract", "patch", "skymap"), 

116 ) 

117 

118 

119class AssembleChi2CoaddConfig(pipeBase.PipelineTaskConfig, pipelineConnections=AssembleChi2CoaddConnections): 

120 outputPixelatedVariance = pexConfig.Field( 

121 dtype=bool, 

122 default=False, 

123 doc="Whether to output a pixelated variance map for the generated " 

124 "chi^2 coadd, or to have a flat variance map defined by combining " 

125 "the inverse variance maps of the coadds that were combined.", 

126 ) 

127 

128 useUnionForMask = pexConfig.Field( 

129 dtype=bool, 

130 default=True, 

131 doc="Whether to calculate the union of the mask plane in each band, " 

132 "or the intersection of the mask plane in each band.", 

133 ) 

134 

135 

136class AssembleChi2CoaddTask(pipeBase.PipelineTask): 

137 """Assemble a chi^2 coadd from a collection of multi-band coadds 

138 

139 References 

140 ---------- 

141 .. [1] Szalay, A. S., Connolly, A. J., and Szokoly, G. P., “Simultaneous 

142 Multicolor Detection of Faint Galaxies in the Hubble Deep Field”, 

143 The Astronomical Journal, vol. 117, no. 1, pp. 68–74, 

144 1999. doi:10.1086/300689. 

145 

146 .. [2] Kaiser 2001 whitepaper, 

147 http://pan-starrs.ifa.hawaii.edu/project/people/kaiser/imageprocessing/im%2B%2B.pdf # noqa: E501, W505 

148 

149 .. [3] https://dmtn-015.lsst.io/ 

150 

151 .. [4] https://project.lsst.org/meetings/law/sites/lsst.org.meetings.law/files/Building%20and%20using%20coadds.pdf # noqa: E501, W505 

152 """ 

153 

154 ConfigClass = AssembleChi2CoaddConfig 

155 _DefaultName = "assembleChi2Coadd" 

156 

157 def __init__(self, initInputs, **kwargs): 

158 super().__init__(initInputs=initInputs, **kwargs) 

159 

160 def combinedMasks(self, masks: list[afwImage.MaskX]) -> afwImage.MaskX: 

161 """Combine the mask plane in each input coadd 

162 

163 Parameters 

164 ---------- 

165 mMask: 

166 The MultibandMask in each band. 

167 

168 Returns 

169 ------- 

170 result: 

171 The resulting single band mask. 

172 """ 

173 refMask = masks[0] 

174 bbox = refMask.getBBox() 

175 mask = refMask.array 

176 for _mask in masks[1:]: 

177 if self.config.useUnionForMask: 

178 mask = mask | _mask.array 

179 else: 

180 mask = mask & _mask.array 

181 result = refMask.Factory(bbox) 

182 result.array[:] = mask 

183 return result 

184 

185 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

186 inputs = butlerQC.get(inputRefs) 

187 outputs = self.run(**inputs) 

188 butlerQC.put(outputs, outputRefs) 

189 

190 def run(self, inputCoadds: list[afwImage.Exposure]) -> pipeBase.Struct: 

191 """Assemble the chi2 coadd from the multiband coadds 

192 

193 Parameters 

194 ---------- 

195 inputCoadds: 

196 The coadds to combine into a single chi2 coadd. 

197 

198 Returns 

199 ------- 

200 result: 

201 The chi2 coadd created from the input coadds. 

202 """ 

203 convControl = afwMath.ConvolutionControl() 

204 convControl.setDoNormalize(False) 

205 convControl.setDoCopyEdge(False) 

206 

207 # Set a reference exposure to use for creating the new coadd. 

208 # It doesn't matter which exposure we use, since we just need the 

209 # bounding box information and Factory to create a new expsure with 

210 # the same dtype. 

211 refExp = inputCoadds[0] 

212 bbox = refExp.getBBox() 

213 

214 image = refExp.image.Factory(bbox) 

215 variance_list = [] 

216 # Convovle the image in each band and weight by the median variance 

217 for calexp in inputCoadds: 

218 convolved = convolveImage(calexp.image, calexp.getPsf()) 

219 _variance = np.median(calexp.variance.array) 

220 convolved.array[:] /= _variance 

221 image += convolved 

222 variance_list.append(_variance) 

223 

224 variance = refExp.variance.Factory(bbox) 

225 if self.config.outputPixelatedVariance: 

226 # Write the per pixel variance to the output coadd 

227 variance.array[:] = np.sum([1 / coadd.variance for coadd in inputCoadds], axis=0) 

228 else: 

229 # Use a flat variance in each band 

230 variance.array[:] = np.sum(1 / np.array(variance_list)) 

231 # Combine the masks planes to calculate the mask plae of the new coadd 

232 mask = self.combinedMasks([coadd.mask for coadd in inputCoadds]) 

233 # Create the exposure 

234 maskedImage = refExp.maskedImage.Factory(image, mask=mask, variance=variance) 

235 chi2coadd = refExp.Factory(maskedImage, exposureInfo=refExp.getInfo()) 

236 chi2coadd.info.setFilter(None) 

237 return pipeBase.Struct(chi2Coadd=chi2coadd) 

238 

239 

240class DetectChi2SourcesConnections( 

241 pipeBase.PipelineTaskConnections, 

242 dimensions=("tract", "patch", "skymap"), 

243 defaultTemplates={"inputCoaddName": "deepChi2", "outputCoaddName": "deepChi2"}, 

244): 

245 detectionSchema = cT.InitOutput( 

246 doc="Schema of the detection catalog", 

247 name="{outputCoaddName}Coadd_det_schema", 

248 storageClass="SourceCatalog", 

249 ) 

250 exposure = cT.Input( 

251 doc="Exposure on which detections are to be performed", 

252 name="{inputCoaddName}Coadd_calexp", 

253 storageClass="ExposureF", 

254 dimensions=("tract", "patch", "skymap"), 

255 ) 

256 outputSources = cT.Output( 

257 doc="Detected sources catalog", 

258 name="{outputCoaddName}Coadd_det", 

259 storageClass="SourceCatalog", 

260 dimensions=("tract", "patch", "skymap"), 

261 ) 

262 

263 

264class DetectChi2SourcesConfig(pipeBase.PipelineTaskConfig, pipelineConnections=DetectChi2SourcesConnections): 

265 detection = pexConfig.ConfigurableField(target=SourceDetectionTask, doc="Detect sources in chi2 coadd") 

266 

267 idGenerator = SkyMapIdGeneratorConfig.make_field() 

268 

269 def setDefaults(self): 

270 super().setDefaults() 

271 self.detection.reEstimateBackground = False 

272 self.detection.thresholdValue = 3 

273 

274 

275class DetectChi2SourcesTask(pipeBase.PipelineTask): 

276 _DefaultName = "detectChi2Sources" 

277 ConfigClass = DetectChi2SourcesConfig 

278 

279 def __init__(self, schema=None, **kwargs): 

280 # N.B. Super is used here to handle the multiple inheritance of 

281 # PipelineTasks, the init tree call structure has been reviewed 

282 # carefully to be sure super will work as intended. 

283 super().__init__(**kwargs) 

284 if schema is None: 

285 schema = afwTable.SourceTable.makeMinimalSchema() 

286 self.schema = schema 

287 self.makeSubtask("detection", schema=self.schema) 

288 self.detectionSchema = afwTable.SourceCatalog(self.schema) 

289 

290 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

291 inputs = butlerQC.get(inputRefs) 

292 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId) 

293 inputs["idFactory"] = idGenerator.make_table_id_factory() 

294 inputs["expId"] = idGenerator.catalog_id 

295 outputs = self.run(**inputs) 

296 butlerQC.put(outputs, outputRefs) 

297 

298 def run(self, exposure: afwImage.Exposure, idFactory: afwTable.IdFactory, expId: int) -> pipeBase.Struct: 

299 """Run detection on a chi2 exposure. 

300 

301 Parameters 

302 ---------- 

303 exposure : 

304 Exposure on which to detect (maybe backround-subtracted and scaled, 

305 depending on configuration). 

306 idFactory : 

307 IdFactory to set source identifiers. 

308 expId : 

309 Exposure identifier (integer) for RNG seed. 

310 

311 Returns 

312 ------- 

313 result : `lsst.pipe.base.Struct` 

314 Results as a struct with attributes: 

315 ``outputSources`` 

316 Catalog of detections (`lsst.afw.table.SourceCatalog`). 

317 """ 

318 table = afwTable.SourceTable.make(self.schema, idFactory) 

319 # We override `doSmooth` since the chi2 coadd has already had an 

320 # extra PSF convolution applied to decorrelate the images 

321 # accross bands. 

322 detections = self.detection.run(table, exposure, expId=expId, doSmooth=False) 

323 sources = detections.sources 

324 return pipeBase.Struct(outputSources=sources)