Coverage for python/lsst/pipe/tasks/assembleChi2Coadd.py: 36%

107 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-11 02:43 -0700

1# This file is part of pipe_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 

35import lsst.utils as utils 

36 

37 

38log = logging.getLogger(__name__) 

39 

40 

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

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

43 

44 Parameters 

45 ---------- 

46 sigma: 

47 Gaussian sigma of smoothing kernel. 

48 nSigmaForKernel: 

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

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

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

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

53 

54 Returns 

55 ------- 

56 size: 

57 Size of the smoothing kernel. 

58 """ 

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

60 

61 

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

63 """Convolve an image with a psf 

64 

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

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

67 

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

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

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

71 Gaussian there's no difference. 

72 

73 Parameters 

74 ---------- 

75 image: 

76 The image to convovle. 

77 psf: 

78 The PSF to convolve the `image` with. 

79 

80 Returns 

81 ------- 

82 convolved: 

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

84 """ 

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

86 bbox = image.getBBox() 

87 

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

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

90 kWidth = calculateKernelSize(sigma) 

91 gaussFunc = afwMath.GaussianFunction1D(sigma) 

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

93 

94 convolvedImage = image.Factory(bbox) 

95 

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

97 

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

99 

100 

101class AssembleChi2CoaddConnections(pipeBase.PipelineTaskConnections, 

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

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

104 "outputCoaddName": "deepChi2"}): 

105 inputCoadds = cT.Input( 

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

107 name="{inputCoaddName}Coadd_calexp", 

108 storageClass="ExposureF", 

109 multiple=True, 

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

111 ) 

112 chi2Coadd = cT.Output( 

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

114 name="{outputCoaddName}Coadd_calexp", 

115 storageClass="ExposureF", 

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

117 ) 

118 

119 

120class AssembleChi2CoaddConfig(pipeBase.PipelineTaskConfig, 

121 pipelineConnections=AssembleChi2CoaddConnections): 

122 outputPixelatedVariance = pexConfig.Field( 

123 dtype=bool, 

124 default=False, 

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

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

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

128 ) 

129 

130 useUnionForMask = pexConfig.Field( 

131 dtype=bool, 

132 default=True, 

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

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

135 ) 

136 

137 

138class AssembleChi2CoaddTask(pipeBase.PipelineTask): 

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

140 

141 References 

142 ---------- 

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

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

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

146 1999. doi:10.1086/300689. 

147 

148 .. [2] Kaiser 2001 whitepaper, 

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

150 

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

152 

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

154 """ 

155 ConfigClass = AssembleChi2CoaddConfig 

156 _DefaultName = "assembleChi2Coadd" 

157 

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

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

160 

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

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

163 

164 Parameters 

165 ---------- 

166 mMask: 

167 The MultibandMask in each band. 

168 

169 Returns 

170 ------- 

171 result: 

172 The resulting single band mask. 

173 """ 

174 refMask = masks[0] 

175 bbox = refMask.getBBox() 

176 mask = refMask.array 

177 for _mask in masks[1:]: 

178 if self.config.useUnionForMask: 

179 mask = mask | _mask.array 

180 else: 

181 mask = mask & _mask.array 

182 result = refMask.Factory(bbox) 

183 result.array[:] = mask 

184 return result 

185 

186 @utils.inheritDoc(pipeBase.PipelineTask) 

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

188 inputs = butlerQC.get(inputRefs) 

189 outputs = self.run(**inputs) 

190 butlerQC.put(outputs, outputRefs) 

191 

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

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

194 

195 Parameters 

196 ---------- 

197 inputCoadds: 

198 The coadds to combine into a single chi2 coadd. 

199 

200 Returns 

201 ------- 

202 result: 

203 The chi2 coadd created from the input coadds. 

204 """ 

205 convControl = afwMath.ConvolutionControl() 

206 convControl.setDoNormalize(False) 

207 convControl.setDoCopyEdge(False) 

208 

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

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

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

212 # the same dtype. 

213 refExp = inputCoadds[0] 

214 bbox = refExp.getBBox() 

215 

216 image = refExp.image.Factory(bbox) 

217 variance_list = [] 

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

219 for calexp in inputCoadds: 

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

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

222 convolved.array[:] /= _variance 

223 image += convolved 

224 variance_list.append(_variance) 

225 

226 variance = refExp.variance.Factory(bbox) 

227 if self.config.outputPixelatedVariance: 

228 # Write the per pixel variance to the output coadd 

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

230 else: 

231 # Use a flat variance in each band 

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

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

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

235 # Create the exposure 

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

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

238 chi2coadd.info.setFilter(None) 

239 return pipeBase.Struct(chi2Coadd=chi2coadd) 

240 

241 

242class DetectChi2SourcesConnections( 

243 pipeBase.PipelineTaskConnections, 

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

245 defaultTemplates={ 

246 "inputCoaddName": "deepChi2", 

247 "outputCoaddName": "deepChi2" 

248 } 

249): 

250 detectionSchema = cT.InitOutput( 

251 doc="Schema of the detection catalog", 

252 name="{outputCoaddName}Coadd_det_schema", 

253 storageClass="SourceCatalog", 

254 ) 

255 exposure = cT.Input( 

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

257 name="{inputCoaddName}Coadd_calexp", 

258 storageClass="ExposureF", 

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

260 ) 

261 outputSources = cT.Output( 

262 doc="Detected sources catalog", 

263 name="{outputCoaddName}Coadd_det", 

264 storageClass="SourceCatalog", 

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

266 ) 

267 

268 

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

270 detection = pexConfig.ConfigurableField( 

271 target=SourceDetectionTask, 

272 doc="Detect sources in chi2 coadd" 

273 ) 

274 

275 idGenerator = SkyMapIdGeneratorConfig.make_field() 

276 

277 def setDefaults(self): 

278 super().setDefaults() 

279 self.detection.reEstimateBackground = False 

280 self.detection.thresholdValue = 3 

281 

282 

283class DetectChi2SourcesTask(pipeBase.PipelineTask): 

284 _DefaultName = "detectChi2Sources" 

285 ConfigClass = DetectChi2SourcesConfig 

286 

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

288 # N.B. Super is used here to handle the multiple inheritance of PipelineTasks, the init tree 

289 # call structure has been reviewed carefully to be sure super will work as intended. 

290 super().__init__(**kwargs) 

291 if schema is None: 

292 schema = afwTable.SourceTable.makeMinimalSchema() 

293 self.schema = schema 

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

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

296 

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

298 inputs = butlerQC.get(inputRefs) 

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

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

301 inputs["expId"] = idGenerator.catalog_id 

302 outputs = self.run(**inputs) 

303 butlerQC.put(outputs, outputRefs) 

304 

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

306 """Run detection on a chi2 exposure. 

307 

308 Parameters 

309 ---------- 

310 exposure : 

311 Exposure on which to detect (may be backround-subtracted and scaled, 

312 depending on configuration). 

313 idFactory : 

314 IdFactory to set source identifiers. 

315 expId : 

316 Exposure identifier (integer) for RNG seed. 

317 

318 Returns 

319 ------- 

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

321 Results as a struct with attributes: 

322 ``outputSources`` 

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

324 """ 

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

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

327 # extra PSF convolution applied to decorrelate the images 

328 # accross bands. 

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

330 sources = detections.sources 

331 return pipeBase.Struct(outputSources=sources)