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

105 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-27 12:14 +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 

24import numpy as np 

25 

26from lsst.afw.detection import Psf 

27import lsst.afw.math as afwMath 

28import lsst.afw.image as afwImage 

29import lsst.afw.table as afwTable 

30from lsst.meas.algorithms import SourceDetectionTask 

31from lsst.meas.base import SkyMapIdGeneratorConfig 

32import lsst.pex.config as pexConfig 

33import lsst.pipe.base as pipeBase 

34import lsst.pipe.base.connectionTypes as cT 

35 

36 

37log = logging.getLogger(__name__) 

38 

39 

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

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

42 

43 Parameters 

44 ---------- 

45 sigma: 

46 Gaussian sigma of smoothing kernel. 

47 nSigmaForKernel: 

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

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

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

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

52 

53 Returns 

54 ------- 

55 size: 

56 Size of the smoothing kernel. 

57 """ 

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

59 

60 

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

62 """Convolve an image with a psf 

63 

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

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

66 

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

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

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

70 Gaussian there's no difference. 

71 

72 Parameters 

73 ---------- 

74 image: 

75 The image to convovle. 

76 psf: 

77 The PSF to convolve the `image` with. 

78 

79 Returns 

80 ------- 

81 convolved: 

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

83 """ 

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

85 bbox = image.getBBox() 

86 

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

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

89 kWidth = calculateKernelSize(sigma) 

90 gaussFunc = afwMath.GaussianFunction1D(sigma) 

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

92 

93 convolvedImage = image.Factory(bbox) 

94 

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

96 

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

98 

99 

100class AssembleChi2CoaddConnections(pipeBase.PipelineTaskConnections, 

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

102 defaultTemplates={"inputCoaddName": "deep", 

103 "outputCoaddName": "deepChi2"}): 

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, 

120 pipelineConnections=AssembleChi2CoaddConnections): 

121 outputPixelatedVariance = pexConfig.Field( 

122 dtype=bool, 

123 default=False, 

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

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

126 "the inverse variance maps of the coadds that were combined." 

127 ) 

128 

129 useUnionForMask = pexConfig.Field( 

130 dtype=bool, 

131 default=True, 

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

133 "or the intersection of the mask plane in each band." 

134 ) 

135 

136 

137class AssembleChi2CoaddTask(pipeBase.PipelineTask): 

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

139 

140 References 

141 ---------- 

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

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

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

145 1999. doi:10.1086/300689. 

146 

147 .. [2] Kaiser 2001 whitepaper, 

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

149 

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

151 

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

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={ 

244 "inputCoaddName": "deepChi2", 

245 "outputCoaddName": "deepChi2" 

246 } 

247): 

248 detectionSchema = cT.InitOutput( 

249 doc="Schema of the detection catalog", 

250 name="{outputCoaddName}Coadd_det_schema", 

251 storageClass="SourceCatalog", 

252 ) 

253 exposure = cT.Input( 

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

255 name="{inputCoaddName}Coadd_calexp", 

256 storageClass="ExposureF", 

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

258 ) 

259 outputSources = cT.Output( 

260 doc="Detected sources catalog", 

261 name="{outputCoaddName}Coadd_det", 

262 storageClass="SourceCatalog", 

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

264 ) 

265 

266 

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

268 detection = pexConfig.ConfigurableField( 

269 target=SourceDetectionTask, 

270 doc="Detect sources in chi2 coadd" 

271 ) 

272 

273 idGenerator = SkyMapIdGeneratorConfig.make_field() 

274 

275 def setDefaults(self): 

276 super().setDefaults() 

277 self.detection.reEstimateBackground = False 

278 self.detection.thresholdValue = 3 

279 

280 

281class DetectChi2SourcesTask(pipeBase.PipelineTask): 

282 _DefaultName = "detectChi2Sources" 

283 ConfigClass = DetectChi2SourcesConfig 

284 

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

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

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

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

289 super().__init__(**kwargs) 

290 if schema is None: 

291 schema = afwTable.SourceTable.makeMinimalSchema() 

292 self.schema = schema 

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

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

295 

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

297 inputs = butlerQC.get(inputRefs) 

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

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

300 inputs["expId"] = idGenerator.catalog_id 

301 outputs = self.run(**inputs) 

302 butlerQC.put(outputs, outputRefs) 

303 

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

305 """Run detection on a chi2 exposure. 

306 

307 Parameters 

308 ---------- 

309 exposure : 

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

311 depending on configuration). 

312 idFactory : 

313 IdFactory to set source identifiers. 

314 expId : 

315 Exposure identifier (integer) for RNG seed. 

316 

317 Returns 

318 ------- 

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

320 Results as a struct with attributes: 

321 ``outputSources`` 

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

323 """ 

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

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

326 # extra PSF convolution applied to decorrelate the images 

327 # accross bands. 

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

329 sources = detections.sources 

330 return pipeBase.Struct(outputSources=sources)