Coverage for python/lsst/drp/tasks/forcedPhotCoadd.py: 31%
98 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-22 11:15 +0000
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-22 11:15 +0000
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 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
32__all__ = ("ForcedPhotCoaddConfig", "ForcedPhotCoaddTask")
35class ForcedPhotCoaddConnections(pipeBase.PipelineTaskConnections,
36 dimensions=("band", "skymap", "tract", "patch"),
37 defaultTemplates={"inputCoaddName": "deep",
38 "outputCoaddName": "deep"}):
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,
101 pipelineConnections=ForcedPhotCoaddConnections):
102 measurement = lsst.pex.config.ConfigurableField(
103 target=ForcedMeasurementTask,
104 doc="subtask to do forced measurement"
105 )
106 coaddName = lsst.pex.config.Field(
107 doc="coadd name: typically one of deep or goodSeeing",
108 dtype=str,
109 default="deep",
110 )
111 doApCorr = lsst.pex.config.Field(
112 dtype=bool,
113 default=True,
114 doc="Run subtask to apply aperture corrections"
115 )
116 applyApCorr = lsst.pex.config.ConfigurableField(
117 target=ApplyApCorrTask,
118 doc="Subtask to apply aperture corrections"
119 )
120 catalogCalculation = lsst.pex.config.ConfigurableField(
121 target=CatalogCalculationTask,
122 doc="Subtask to run catalogCalculation plugins on catalog"
123 )
124 footprintDatasetName = lsst.pex.config.Field(
125 doc="Dataset (without coadd prefix) that should be used to obtain (Heavy)Footprints for sources. "
126 "Must have IDs that match those of the reference catalog."
127 "If None, Footprints will be generated by transforming the reference Footprints.",
128 dtype=str,
129 default="ScarletModelData",
130 optional=True
131 )
132 doConserveFlux = lsst.pex.config.Field(
133 dtype=bool,
134 default=True,
135 doc="Whether to use the deblender models as templates to re-distribute the flux "
136 "from the 'exposure' (True), or to perform measurements on the deblender model footprints. "
137 "If footprintDatasetName != 'ScarletModelData' then this field is ignored.")
138 doStripFootprints = lsst.pex.config.Field(
139 dtype=bool,
140 default=True,
141 doc="Whether to strip footprints from the output catalog before "
142 "saving to disk. "
143 "This is usually done when using scarlet models to save disk space.")
144 hasFakes = lsst.pex.config.Field(
145 dtype=bool,
146 default=False,
147 doc="Should be set to True if fake sources have been inserted into the input data."
148 )
149 idGenerator = SkyMapIdGeneratorConfig.make_field()
151 def setDefaults(self):
152 # Docstring inherited.
153 # Make catalogCalculation a no-op by default as no modelFlux is setup
154 # by default in ForcedMeasurementTask
155 super().setDefaults()
157 self.catalogCalculation.plugins.names = []
158 self.measurement.copyColumns["id"] = "id"
159 self.measurement.copyColumns["parent"] = "parent"
160 self.measurement.plugins.names |= ['base_InputCount', 'base_Variance']
161 self.measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['CLIPPED', 'SENSOR_EDGE',
162 'REJECTED', 'INEXACT_PSF']
163 self.measurement.plugins['base_PixelFlags'].masksFpCenter = ['CLIPPED', 'SENSOR_EDGE',
164 'REJECTED', 'INEXACT_PSF']
167class ForcedPhotCoaddTask(pipeBase.PipelineTask):
168 """A pipeline task for performing forced measurement on coadd images.
170 Parameters
171 ----------
172 refSchema : `lsst.afw.table.Schema`, optional
173 The schema of the reference catalog, passed to the constructor of the
174 references subtask. Optional, but must be specified if ``initInputs``
175 is not; if both are specified, ``initInputs`` takes precedence.
176 initInputs : `dict`
177 Dictionary that can contain a key ``inputSchema`` containing the
178 schema. If present will override the value of ``refSchema``.
179 **kwds
180 Keyword arguments are passed to the supertask constructor.
181 """
183 ConfigClass = ForcedPhotCoaddConfig
184 _DefaultName = "forcedPhotCoadd"
185 dataPrefix = "deepCoadd_"
187 def __init__(self, refSchema=None, initInputs=None, **kwds):
188 super().__init__(**kwds)
190 if initInputs is not None:
191 refSchema = initInputs['inputSchema'].schema
193 if refSchema is None:
194 raise ValueError("No reference schema provided.")
195 self.makeSubtask("measurement", refSchema=refSchema)
196 # It is necessary to get the schema internal to the forced measurement
197 # task until such a time that the schema is not owned by the
198 # measurement task, but is passed in by an external caller.
199 if self.config.doApCorr:
200 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
201 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
202 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
204 def runQuantum(self, butlerQC, inputRefs, outputRefs):
205 inputs = butlerQC.get(inputRefs)
207 refCatInBand = inputs.pop('refCatInBand')
208 if self.config.footprintDatasetName == "ScarletModelData":
209 footprintData = inputs.pop("scarletModels")
210 elif self.config.footprintDatasetName == "DeblendedFlux":
211 footprintData = inputs.pop("footprintCatIndBand")
212 else:
213 footprintData = None
214 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId,
215 inputs['exposure'],
216 inputs['refCat'],
217 refCatInBand,
218 inputs['refWcs'],
219 footprintData)
220 outputs = self.run(**inputs)
221 # Strip HeavyFootprints to save space on disk
222 if self.config.footprintDatasetName == "ScarletModelData" and self.config.doStripFootprints:
223 sources = outputs.measCat
224 for source in sources[sources["parent"] != 0]:
225 source.setFootprint(None)
226 butlerQC.put(outputs, outputRefs)
228 def generateMeasCat(self, dataId, exposure, refCat, refCatInBand, refWcs, footprintData):
229 """Generate a measurement catalog.
231 Parameters
232 ----------
233 dataId : `lsst.daf.butler.DataCoordinate`
234 Butler data ID for this image, with ``{tract, patch, band}`` keys.
235 exposure : `lsst.afw.image.exposure.Exposure`
236 Exposure to generate the catalog for.
237 refCat : `lsst.afw.table.SourceCatalog`
238 Catalog of shapes and positions at which to force photometry.
239 refCatInBand : `lsst.afw.table.SourceCatalog`
240 Catalog of shapes and position in the band forced photometry is
241 currently being performed
242 refWcs : `lsst.afw.image.SkyWcs`
243 Reference world coordinate system.
244 footprintData : `ScarletDataModel` or `lsst.afw.table.SourceCatalog`
245 Either the scarlet data models or the deblended catalog containing
246 footprints. If `footprintData` is `None` then the footprints
247 contained in `refCatInBand` are used.
249 Returns
250 -------
251 measCat : `lsst.afw.table.SourceCatalog`
252 Catalog of forced sources to measure.
253 expId : `int`
254 Unique binary id associated with the input exposure
256 Raises
257 ------
258 LookupError
259 Raised if a footprint with a given source id was in the reference
260 catalog but not in the reference catalog in band (meaning there was
261 some sort of mismatch in the two input catalogs)
262 """
263 id_generator = self.config.idGenerator.apply(dataId)
264 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
265 idFactory=id_generator.make_table_id_factory())
266 # attach footprints here as this can naturally live inside this method
267 if self.config.footprintDatasetName == "ScarletModelData":
268 # Load the scarlet models
269 self._attachScarletFootprints(
270 catalog=measCat,
271 modelData=footprintData,
272 exposure=exposure,
273 band=dataId["band"]
274 )
275 else:
276 if self.config.footprintDatasetName is None:
277 footprintCat = refCatInBand
278 else:
279 footprintCat = footprintData
280 for srcRecord in measCat:
281 fpRecord = footprintCat.find(srcRecord.getId())
282 if fpRecord is None:
283 raise LookupError("Cannot find Footprint for source {}; please check that {} "
284 "IDs are compatible with reference source IDs"
285 .format(srcRecord.getId(), footprintCat))
286 srcRecord.setFootprint(fpRecord.getFootprint())
287 return measCat, id_generator.catalog_id
289 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
290 """Perform forced measurement on a single exposure.
292 Parameters
293 ----------
294 measCat : `lsst.afw.table.SourceCatalog`
295 The measurement catalog, based on the sources listed in the
296 reference catalog.
297 exposure : `lsst.afw.image.Exposure`
298 The measurement image upon which to perform forced detection.
299 refCat : `lsst.afw.table.SourceCatalog`
300 The reference catalog of sources to measure.
301 refWcs : `lsst.afw.image.SkyWcs`
302 The WCS for the references.
303 exposureId : `int`
304 Optional unique exposureId used for random seed in measurement
305 task.
307 Returns
308 -------
309 result : ~`lsst.pipe.base.Struct`
310 Structure with fields:
312 ``measCat``
313 Catalog of forced measurement results
314 (`lsst.afw.table.SourceCatalog`).
315 """
316 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
317 if self.config.doApCorr:
318 self.applyApCorr.run(
319 catalog=measCat,
320 apCorrMap=exposure.getInfo().getApCorrMap()
321 )
322 self.catalogCalculation.run(measCat)
324 return pipeBase.Struct(measCat=measCat)
326 def _attachScarletFootprints(self, catalog, modelData, exposure, band):
327 """Attach scarlet models as HeavyFootprints
328 """
329 if self.config.doConserveFlux:
330 redistributeImage = exposure.image
331 else:
332 redistributeImage = None
333 # Attach the footprints
334 modelData.updateCatalogFootprints(
335 catalog=catalog,
336 band=band,
337 psfModel=exposure.getPsf(),
338 redistributeImage=redistributeImage,
339 removeScarletData=True,
340 updateFluxColumns=False,
341 )