Coverage for python/lsst/drp/tasks/assemble_chi2_coadd.py: 34%
105 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-26 04:18 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-26 04:18 -0700
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/>.
23import logging
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
36log = logging.getLogger(__name__)
39def calculateKernelSize(sigma: float, nSigmaForKernel: float = 7) -> int:
40 """Calculate the size of the smoothing kernel.
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.
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
60def convolveImage(image: afwImage.Image, psf: Psf) -> afwImage.Image:
61 """Convolve an image with a psf
63 This methodm and the docstring, is based off the method in
64 `~lsst.meas.algorithms.detection.SourceDetectionTask`.
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.
71 Parameters
72 ----------
73 image:
74 The image to convovle.
75 psf:
76 The PSF to convolve the `image` with.
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()
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)
92 convolvedImage = image.Factory(bbox)
94 afwMath.convolve(convolvedImage, image, gaussKernel, afwMath.ConvolutionControl())
96 return convolvedImage.Factory(convolvedImage, bbox, afwImage.PARENT, False)
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 )
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 )
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 )
136class AssembleChi2CoaddTask(pipeBase.PipelineTask):
137 """Assemble a chi^2 coadd from a collection of multi-band coadds
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.
146 .. [2] Kaiser 2001 whitepaper,
147 http://pan-starrs.ifa.hawaii.edu/project/people/kaiser/imageprocessing/im%2B%2B.pdf # noqa: E501, W505
149 .. [3] https://dmtn-015.lsst.io/
151 .. [4] https://project.lsst.org/meetings/law/sites/lsst.org.meetings.law/files/Building%20and%20using%20coadds.pdf # noqa: E501, W505
152 """
154 ConfigClass = AssembleChi2CoaddConfig
155 _DefaultName = "assembleChi2Coadd"
157 def __init__(self, initInputs, **kwargs):
158 super().__init__(initInputs=initInputs, **kwargs)
160 def combinedMasks(self, masks: list[afwImage.MaskX]) -> afwImage.MaskX:
161 """Combine the mask plane in each input coadd
163 Parameters
164 ----------
165 mMask:
166 The MultibandMask in each band.
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
185 def runQuantum(self, butlerQC, inputRefs, outputRefs):
186 inputs = butlerQC.get(inputRefs)
187 outputs = self.run(**inputs)
188 butlerQC.put(outputs, outputRefs)
190 def run(self, inputCoadds: list[afwImage.Exposure]) -> pipeBase.Struct:
191 """Assemble the chi2 coadd from the multiband coadds
193 Parameters
194 ----------
195 inputCoadds:
196 The coadds to combine into a single chi2 coadd.
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)
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()
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)
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)
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 )
264class DetectChi2SourcesConfig(pipeBase.PipelineTaskConfig, pipelineConnections=DetectChi2SourcesConnections):
265 detection = pexConfig.ConfigurableField(target=SourceDetectionTask, doc="Detect sources in chi2 coadd")
267 idGenerator = SkyMapIdGeneratorConfig.make_field()
269 def setDefaults(self):
270 super().setDefaults()
271 self.detection.reEstimateBackground = False
272 self.detection.thresholdValue = 3
275class DetectChi2SourcesTask(pipeBase.PipelineTask):
276 _DefaultName = "detectChi2Sources"
277 ConfigClass = DetectChi2SourcesConfig
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)
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)
298 def run(self, exposure: afwImage.Exposure, idFactory: afwTable.IdFactory, expId: int) -> pipeBase.Struct:
299 """Run detection on a chi2 exposure.
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.
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)