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

70 statements  

« prev     ^ index     » next       coverage.py v7.2.6, created at 2023-05-24 09:26 +0000

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.pex.config as pexConfig 

30import lsst.pipe.base as pipeBase 

31import lsst.pipe.base.connectionTypes as cT 

32import lsst.utils as utils 

33 

34 

35log = logging.getLogger(__name__) 

36 

37 

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

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

40 

41 Parameters 

42 ---------- 

43 sigma: 

44 Gaussian sigma of smoothing kernel. 

45 nSigmaForKernel: 

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

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

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

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

50 

51 Returns 

52 ------- 

53 size: 

54 Size of the smoothing kernel. 

55 """ 

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

57 

58 

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

60 """Convolve an image with a psf 

61 

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

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

64 

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

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

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

68 Gaussian there's no difference. 

69 

70 Parameters 

71 ---------- 

72 image: 

73 The image to convovle. 

74 psf: 

75 The PSF to convolve the `image` with. 

76 

77 Returns 

78 ------- 

79 convolved: 

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

81 """ 

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

83 bbox = image.getBBox() 

84 

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

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

87 kWidth = calculateKernelSize(sigma) 

88 gaussFunc = afwMath.GaussianFunction1D(sigma) 

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

90 

91 convolvedImage = image.Factory(bbox) 

92 

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

94 

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

96 

97 

98class AssembleChi2CoaddConnections(pipeBase.PipelineTaskConnections, 

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

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

101 "outputCoaddName": "deep"}): 

102 inputCoadds = cT.Input( 

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

104 name="{inputCoaddName}Coadd_calexp", 

105 storageClass="ExposureF", 

106 multiple=True, 

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

108 ) 

109 chi2Coadd = cT.Output( 

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

111 name="{outputCoaddName}Chi2Coadd", 

112 storageClass="ExposureF", 

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

114 ) 

115 

116 

117class AssembleChi2CoaddConfig(pipeBase.PipelineTaskConfig, 

118 pipelineConnections=AssembleChi2CoaddConnections): 

119 outputPixelatedVariance = pexConfig.Field( 

120 dtype=bool, 

121 default=False, 

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

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

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

125 ) 

126 

127 useUnionForMask = pexConfig.Field( 

128 dtype=bool, 

129 default=True, 

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

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

132 ) 

133 

134 

135class AssembleChi2CoaddTask(pipeBase.Task): 

136 """Assemble a chi^2 (Kaiser) coadd from a collection of multi-band coadds 

137 

138 See Kaiser 2001 for more information. 

139 """ 

140 ConfigClass = AssembleChi2CoaddConfig 

141 _DefaultName = "assembleChi2Coadd" 

142 

143 def __init__(self, *args, **kwargs): 

144 super().__init__(*args, **kwargs) 

145 

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

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

148 

149 Parameters 

150 ---------- 

151 mMask: 

152 The MultibandMask in each band. 

153 

154 Returns 

155 ------- 

156 result: 

157 The resulting single band mask. 

158 """ 

159 refMask = masks[0] 

160 bbox = refMask.getBBox() 

161 mask = refMask.array 

162 for _mask in masks[1:]: 

163 if self.config.useUnionForMask: 

164 mask = mask | _mask.array 

165 else: 

166 mask = mask & _mask.array 

167 result = refMask.Factory(bbox) 

168 result.array[:] = mask 

169 return result 

170 

171 @utils.inheritDoc(pipeBase.PipelineTask) 

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

173 inputs = butlerQC.get(inputRefs) 

174 outputs = self.run(**inputs) 

175 butlerQC.put(outputs, outputRefs) 

176 

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

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

179 

180 Parameters 

181 ---------- 

182 inputCoadds: 

183 The coadds to combine into a single chi2 coadd. 

184 

185 Returns 

186 ------- 

187 result: 

188 The chi2 coadd created from the input coadds. 

189 """ 

190 convControl = afwMath.ConvolutionControl() 

191 convControl.setDoNormalize(False) 

192 convControl.setDoCopyEdge(False) 

193 

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

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

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

197 # the same dtype. 

198 refExp = inputCoadds[0] 

199 bbox = refExp.getBBox() 

200 

201 image = refExp.image.Factory(bbox) 

202 variance_list = [] 

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

204 for calexp in inputCoadds: 

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

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

207 convolved.array[:] /= _variance 

208 image += convolved 

209 variance_list.append(_variance) 

210 

211 variance = refExp.variance.Factory(bbox) 

212 if self.config.outputPixelatedVariance: 

213 # Write the per pixel variance to the output coadd 

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

215 else: 

216 # Use a flat variance in each band 

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

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

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

220 # Create the exposure 

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

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

223 return pipeBase.Struct(chi2Coadd=chi2coadd)