Coverage for python/lsst/drp/tasks/forcedPhotCoadd.py: 32%
99 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-31 13:25 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-31 13:25 +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/>.
22import lsst.afw.table
23import lsst.pex.config
24import lsst.pipe.base as pipeBase
25from lsst.meas.base._id_generator import SkyMapIdGeneratorConfig
26from lsst.meas.base.applyApCorr import ApplyApCorrTask
27from lsst.meas.base.catalogCalculation import CatalogCalculationTask
28from lsst.meas.base.forcedMeasurement import ForcedMeasurementTask
29from lsst.meas.extensions.scarlet.io import updateCatalogFootprints
31__all__ = ("ForcedPhotCoaddConfig", "ForcedPhotCoaddTask")
34class ForcedPhotCoaddConnections(
35 pipeBase.PipelineTaskConnections,
36 dimensions=("band", "skymap", "tract", "patch"),
37 defaultTemplates={"inputCoaddName": "deep", "outputCoaddName": "deep"},
38):
39 inputSchema = pipeBase.connectionTypes.InitInput(
40 doc="Schema for the input measurement catalogs.",
41 name="{inputCoaddName}Coadd_ref_schema",
42 storageClass="SourceCatalog",
43 )
44 outputSchema = pipeBase.connectionTypes.InitOutput(
45 doc="Schema for the output forced measurement catalogs.",
46 name="{outputCoaddName}Coadd_forced_src_schema",
47 storageClass="SourceCatalog",
48 )
49 exposure = pipeBase.connectionTypes.Input(
50 doc="Input exposure to perform photometry on.",
51 name="{inputCoaddName}Coadd_calexp",
52 storageClass="ExposureF",
53 dimensions=["band", "skymap", "tract", "patch"],
54 )
55 refCat = pipeBase.connectionTypes.Input(
56 doc="Catalog of shapes and positions at which to force photometry.",
57 name="{inputCoaddName}Coadd_ref",
58 storageClass="SourceCatalog",
59 dimensions=["skymap", "tract", "patch"],
60 )
61 refCatInBand = pipeBase.connectionTypes.Input(
62 doc="Catalog of shapes and positions in the band having forced photometry done",
63 name="{inputCoaddName}Coadd_meas",
64 storageClass="SourceCatalog",
65 dimensions=("band", "skymap", "tract", "patch"),
66 )
67 footprintCatInBand = pipeBase.connectionTypes.Input(
68 doc="Catalog of footprints to attach to sources",
69 name="{inputCoaddName}Coadd_deblendedFlux",
70 storageClass="SourceCatalog",
71 dimensions=("band", "skymap", "tract", "patch"),
72 )
73 scarletModels = pipeBase.connectionTypes.Input(
74 doc="Multiband scarlet models produced by the deblender",
75 name="{inputCoaddName}Coadd_scarletModelData",
76 storageClass="ScarletModelData",
77 dimensions=("tract", "patch", "skymap"),
78 )
79 refWcs = pipeBase.connectionTypes.Input(
80 doc="Reference world coordinate system.",
81 name="{inputCoaddName}Coadd.wcs",
82 storageClass="Wcs",
83 dimensions=["band", "skymap", "tract", "patch"],
84 ) # used in place of a skymap wcs because of DM-28880
85 measCat = pipeBase.connectionTypes.Output(
86 doc="Output forced photometry catalog.",
87 name="{outputCoaddName}Coadd_forced_src",
88 storageClass="SourceCatalog",
89 dimensions=["band", "skymap", "tract", "patch"],
90 )
92 def __init__(self, *, config=None):
93 super().__init__(config=config)
94 if config.footprintDatasetName != "ScarletModelData":
95 self.inputs.remove("scarletModels")
96 if config.footprintDatasetName != "DeblendedFlux":
97 self.inputs.remove("footprintCatInBand")
100class ForcedPhotCoaddConfig(pipeBase.PipelineTaskConfig, pipelineConnections=ForcedPhotCoaddConnections):
101 measurement = lsst.pex.config.ConfigurableField(
102 target=ForcedMeasurementTask, doc="subtask to do forced measurement"
103 )
104 coaddName = lsst.pex.config.Field(
105 doc="coadd name: typically one of deep or goodSeeing",
106 dtype=str,
107 default="deep",
108 )
109 doApCorr = lsst.pex.config.Field(
110 dtype=bool, default=True, doc="Run subtask to apply aperture corrections"
111 )
112 applyApCorr = lsst.pex.config.ConfigurableField(
113 target=ApplyApCorrTask, doc="Subtask to apply aperture corrections"
114 )
115 catalogCalculation = lsst.pex.config.ConfigurableField(
116 target=CatalogCalculationTask, doc="Subtask to run catalogCalculation plugins on catalog"
117 )
118 footprintDatasetName = lsst.pex.config.Field(
119 doc="Dataset (without coadd prefix) that should be used to obtain (Heavy)Footprints for sources. "
120 "Must have IDs that match those of the reference catalog."
121 "If None, Footprints will be generated by transforming the reference Footprints.",
122 dtype=str,
123 default="ScarletModelData",
124 optional=True,
125 )
126 doConserveFlux = lsst.pex.config.Field(
127 dtype=bool,
128 default=True,
129 doc="Whether to use the deblender models as templates to re-distribute the flux "
130 "from the 'exposure' (True), or to perform measurements on the deblender model footprints. "
131 "If footprintDatasetName != 'ScarletModelData' then this field is ignored.",
132 )
133 doStripFootprints = lsst.pex.config.Field(
134 dtype=bool,
135 default=True,
136 doc="Whether to strip footprints from the output catalog before "
137 "saving to disk. "
138 "This is usually done when using scarlet models to save disk space.",
139 )
140 hasFakes = lsst.pex.config.Field(
141 dtype=bool,
142 default=False,
143 doc="Should be set to True if fake sources have been inserted into the input data.",
144 )
145 idGenerator = SkyMapIdGeneratorConfig.make_field()
147 def setDefaults(self):
148 # Docstring inherited.
149 # Make catalogCalculation a no-op by default as no modelFlux is setup
150 # by default in ForcedMeasurementTask
151 super().setDefaults()
153 self.catalogCalculation.plugins.names = []
154 self.measurement.copyColumns["id"] = "id"
155 self.measurement.copyColumns["parent"] = "parent"
156 self.measurement.plugins.names |= ["base_InputCount", "base_Variance"]
157 self.measurement.plugins["base_PixelFlags"].masksFpAnywhere = [
158 "CLIPPED",
159 "SENSOR_EDGE",
160 "REJECTED",
161 "INEXACT_PSF",
162 "STREAK",
163 ]
164 self.measurement.plugins["base_PixelFlags"].masksFpCenter = [
165 "CLIPPED",
166 "SENSOR_EDGE",
167 "REJECTED",
168 "INEXACT_PSF",
169 "STREAK",
170 ]
173class ForcedPhotCoaddTask(pipeBase.PipelineTask):
174 """A pipeline task for performing forced measurement on coadd images.
176 Parameters
177 ----------
178 refSchema : `lsst.afw.table.Schema`, optional
179 The schema of the reference catalog, passed to the constructor of the
180 references subtask. Optional, but must be specified if ``initInputs``
181 is not; if both are specified, ``initInputs`` takes precedence.
182 initInputs : `dict`
183 Dictionary that can contain a key ``inputSchema`` containing the
184 schema. If present will override the value of ``refSchema``.
185 **kwds
186 Keyword arguments are passed to the supertask constructor.
187 """
189 ConfigClass = ForcedPhotCoaddConfig
190 _DefaultName = "forcedPhotCoadd"
191 dataPrefix = "deepCoadd_"
193 def __init__(self, refSchema=None, initInputs=None, **kwds):
194 super().__init__(**kwds)
196 if initInputs is not None:
197 refSchema = initInputs["inputSchema"].schema
199 if refSchema is None:
200 raise ValueError("No reference schema provided.")
201 self.makeSubtask("measurement", refSchema=refSchema)
202 # It is necessary to get the schema internal to the forced measurement
203 # task until such a time that the schema is not owned by the
204 # measurement task, but is passed in by an external caller.
205 if self.config.doApCorr:
206 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
207 self.makeSubtask("catalogCalculation", schema=self.measurement.schema)
208 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
210 def runQuantum(self, butlerQC, inputRefs, outputRefs):
211 inputs = butlerQC.get(inputRefs)
213 refCatInBand = inputs.pop("refCatInBand")
214 if self.config.footprintDatasetName == "ScarletModelData":
215 footprintData = inputs.pop("scarletModels")
216 elif self.config.footprintDatasetName == "DeblendedFlux":
217 footprintData = inputs.pop("footprintCatIndBand")
218 else:
219 footprintData = None
220 inputs["measCat"], inputs["exposureId"] = self.generateMeasCat(
221 inputRefs.exposure.dataId,
222 inputs["exposure"],
223 inputs["refCat"],
224 refCatInBand,
225 inputs["refWcs"],
226 footprintData,
227 )
228 outputs = self.run(**inputs)
229 # Strip HeavyFootprints to save space on disk
230 if self.config.footprintDatasetName == "ScarletModelData" and self.config.doStripFootprints:
231 sources = outputs.measCat
232 for source in sources[sources["parent"] != 0]:
233 source.setFootprint(None)
234 butlerQC.put(outputs, outputRefs)
236 def generateMeasCat(self, dataId, exposure, refCat, refCatInBand, refWcs, footprintData):
237 """Generate a measurement catalog.
239 Parameters
240 ----------
241 dataId : `lsst.daf.butler.DataCoordinate`
242 Butler data ID for this image, with ``{tract, patch, band}`` keys.
243 exposure : `lsst.afw.image.exposure.Exposure`
244 Exposure to generate the catalog for.
245 refCat : `lsst.afw.table.SourceCatalog`
246 Catalog of shapes and positions at which to force photometry.
247 refCatInBand : `lsst.afw.table.SourceCatalog`
248 Catalog of shapes and position in the band forced photometry is
249 currently being performed
250 refWcs : `lsst.afw.image.SkyWcs`
251 Reference world coordinate system.
252 footprintData : `ScarletDataModel` or `lsst.afw.table.SourceCatalog`
253 Either the scarlet data models or the deblended catalog containing
254 footprints. If `footprintData` is `None` then the footprints
255 contained in `refCatInBand` are used.
257 Returns
258 -------
259 measCat : `lsst.afw.table.SourceCatalog`
260 Catalog of forced sources to measure.
261 expId : `int`
262 Unique binary id associated with the input exposure
264 Raises
265 ------
266 LookupError
267 Raised if a footprint with a given source id was in the reference
268 catalog but not in the reference catalog in band (meaning there was
269 some sort of mismatch in the two input catalogs)
270 """
271 id_generator = self.config.idGenerator.apply(dataId)
272 measCat = self.measurement.generateMeasCat(
273 exposure, refCat, refWcs, idFactory=id_generator.make_table_id_factory()
274 )
275 # attach footprints here as this can naturally live inside this method
276 if self.config.footprintDatasetName == "ScarletModelData":
277 # Load the scarlet models
278 self._attachScarletFootprints(
279 catalog=measCat, modelData=footprintData, exposure=exposure, band=dataId["band"]
280 )
281 else:
282 if self.config.footprintDatasetName is None:
283 footprintCat = refCatInBand
284 else:
285 footprintCat = footprintData
286 for srcRecord in measCat:
287 fpRecord = footprintCat.find(srcRecord.getId())
288 if fpRecord is None:
289 raise LookupError(
290 "Cannot find Footprint for source {}; please check that {} "
291 "IDs are compatible with reference source IDs".format(srcRecord.getId(), footprintCat)
292 )
293 srcRecord.setFootprint(fpRecord.getFootprint())
294 return measCat, id_generator.catalog_id
296 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
297 """Perform forced measurement on a single exposure.
299 Parameters
300 ----------
301 measCat : `lsst.afw.table.SourceCatalog`
302 The measurement catalog, based on the sources listed in the
303 reference catalog.
304 exposure : `lsst.afw.image.Exposure`
305 The measurement image upon which to perform forced detection.
306 refCat : `lsst.afw.table.SourceCatalog`
307 The reference catalog of sources to measure.
308 refWcs : `lsst.afw.image.SkyWcs`
309 The WCS for the references.
310 exposureId : `int`
311 Optional unique exposureId used for random seed in measurement
312 task.
314 Returns
315 -------
316 result : ~`lsst.pipe.base.Struct`
317 Structure with fields:
319 ``measCat``
320 Catalog of forced measurement results
321 (`lsst.afw.table.SourceCatalog`).
322 """
323 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
324 if self.config.doApCorr:
325 self.applyApCorr.run(catalog=measCat, apCorrMap=exposure.getInfo().getApCorrMap())
326 self.catalogCalculation.run(measCat)
328 return pipeBase.Struct(measCat=measCat)
330 def _attachScarletFootprints(self, catalog, modelData, exposure, band):
331 """Attach scarlet models as HeavyFootprints"""
332 if self.config.doConserveFlux:
333 redistributeImage = exposure
334 else:
335 redistributeImage = None
336 # Attach the footprints
337 updateCatalogFootprints(
338 modelData=modelData,
339 catalog=catalog,
340 band=band,
341 imageForRedistribution=redistributeImage,
342 removeScarletData=True,
343 updateFluxColumns=False,
344 )