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