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