lsst.meas.base g683338f4e5+db3f74d584
Loading...
Searching...
No Matches
forcedPhotCoadd.py
Go to the documentation of this file.
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/>.
21
22import warnings
23
24import lsst.pex.config
25import lsst.afw.table
26
27import lsst.pipe.base as pipeBase
28
29from ._id_generator import SkyMapIdGeneratorConfig
30from .forcedMeasurement import ForcedMeasurementTask
31from .applyApCorr import ApplyApCorrTask
32from .catalogCalculation import CatalogCalculationTask
33
34__all__ = ("ForcedPhotCoaddConfig", "ForcedPhotCoaddTask")
35
36
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 )
93
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")
100
101
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()
152
153 def setDefaults(self):
154 # Docstring inherited.
155 # Make catalogCalculation a no-op by default as no modelFlux is setup by default in
156 # ForcedMeasurementTask
157 super().setDefaults()
158
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']
167
168
169class ForcedPhotCoaddTask(pipeBase.PipelineTask):
170 """A pipeline task for performing forced measurement on coadd images.
171
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 """
186
187 ConfigClass = ForcedPhotCoaddConfig
188 _DefaultName = "forcedPhotCoadd"
189 dataPrefix = "deepCoadd_"
190
191 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
192 super().__init__(**kwds)
193
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
198
199 if initInputs is not None:
200 refSchema = initInputs['inputSchema'].schema
201
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 task until such a time
206 # that the schema is not owned by the measurement task, but is passed in by an external caller
207 if self.config.doApCorr:
208 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
209 self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
210 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
211
212 def runQuantum(self, butlerQC, inputRefs, outputRefs):
213 inputs = butlerQC.get(inputRefs)
214
215 refCatInBand = inputs.pop('refCatInBand')
216 if self.config.footprintDatasetName == "ScarletModelData":
217 footprintData = inputs.pop("scarletModels")
218 elif self.config.footprintDatasetName == "DeblendedFlux":
219 footprintData = inputs.pop("footprintCatIndBand")
220 else:
221 footprintData = None
222 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId,
223 inputs['exposure'],
224 inputs['refCat'],
225 refCatInBand,
226 inputs['refWcs'],
227 footprintData)
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)
235
236 def generateMeasCat(self, dataId, exposure, refCat, refCatInBand, refWcs, footprintData):
237 """Generate a measurement catalog.
238
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.
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.
256
257 Returns
258 -------
260 Catalog of forced sources to measure.
261 expId : `int`
262 Unique binary id associated with the input exposure
263
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(exposure, refCat, refWcs,
273 idFactory=id_generator.make_table_id_factory())
274 # attach footprints here as this can naturally live inside this method
275 if self.config.footprintDatasetName == "ScarletModelData":
276 # Load the scarlet models
277 self._attachScarletFootprints(
278 catalog=measCat,
279 modelData=footprintData,
280 exposure=exposure,
281 band=dataId["band"]
282 )
283 else:
284 if self.config.footprintDatasetName is None:
285 footprintCat = refCatInBand
286 else:
287 footprintCat = footprintData
288 for srcRecord in measCat:
289 fpRecord = footprintCat.find(srcRecord.getId())
290 if fpRecord is None:
291 raise LookupError("Cannot find Footprint for source {}; please check that {} "
292 "IDs are compatible with reference source IDs"
293 .format(srcRecord.getId(), footprintCat))
294 srcRecord.setFootprint(fpRecord.getFootprint())
295 return measCat, id_generator.catalog_id
296
297 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
298 """Perform forced measurement on a single exposure.
299
300 Parameters
301 ----------
303 The measurement catalog, based on the sources listed in the
304 reference catalog.
305 exposure : `lsst.afw.image.Exposure`
306 The measurement image upon which to perform forced detection.
308 The reference catalog of sources to measure.
309 refWcs : `lsst.afw.image.SkyWcs`
310 The WCS for the references.
311 exposureId : `int`
312 Optional unique exposureId used for random seed in measurement
313 task.
314
315 Returns
316 -------
317 result : ~`lsst.pipe.base.Struct`
318 Structure with fields:
319
320 ``measCat``
321 Catalog of forced measurement results
323 """
324 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
325 if self.config.doApCorr:
326 self.applyApCorr.run(
327 catalog=measCat,
328 apCorrMap=exposure.getInfo().getApCorrMap()
329 )
330 self.catalogCalculation.run(measCat)
331
332 return pipeBase.Struct(measCat=measCat)
333
334 def _attachScarletFootprints(self, catalog, modelData, exposure, band):
335 """Attach scarlet models as HeavyFootprints
336 """
337 if self.config.doConserveFlux:
338 redistributeImage = exposure.image
339 else:
340 redistributeImage = None
341 # Attach the footprints
342 modelData.updateCatalogFootprints(
343 catalog=catalog,
344 band=band,
345 psfModel=exposure.getPsf(),
346 redistributeImage=redistributeImage,
347 removeScarletData=True,
348 updateFluxColumns=False,
349 )