30from lsst.pipe.base import PipelineTaskConnections, PipelineTaskConfig
31import lsst.pipe.base.connectionTypes
as cT
35 dimensions=(
"skymap",
"tract",
"patch"),
36 defaultTemplates={
"inputCoaddName":
"deep",
37 "outputCoaddName":
"deep"}):
38 inputSchema = cT.InitInput(
39 doc=
"Schema for the output merged measurement catalog.",
40 name=
"{inputCoaddName}Coadd_meas_schema",
41 storageClass=
"SourceCatalog",
43 outputSchema = cT.InitOutput(
44 doc=
"Schema for the output merged measurement catalog.",
45 name=
"{outputCoaddName}Coadd_ref_schema",
46 storageClass=
"SourceCatalog",
49 doc=
"Input catalogs to merge.",
50 name=
"{inputCoaddName}Coadd_meas",
52 storageClass=
"SourceCatalog",
53 dimensions=[
"band",
"skymap",
"tract",
"patch"],
55 mergedCatalog = cT.Output(
56 doc=
"Output merged catalog.",
57 name=
"{outputCoaddName}Coadd_ref",
58 storageClass=
"SourceCatalog",
59 dimensions=[
"skymap",
"tract",
"patch"],
63class MergeMeasurementsConfig(PipelineTaskConfig, pipelineConnections=MergeMeasurementsConnections):
65 @anchor MergeMeasurementsConfig_
67 @brief Configuration parameters
for the MergeMeasurementsTask
69 pseudoFilterList = pexConfig.ListField(
72 doc=
"Names of filters which may have no associated detection\n"
73 "(N.b. should include MergeDetectionsConfig.skyFilterName)"
75 snName = pexConfig.Field(
77 default=
"base_PsfFlux",
78 doc=
"Name of flux measurement for calculating the S/N when choosing the reference band."
80 minSN = pexConfig.Field(
83 doc=
"If the S/N from the priority band is below this value (and the S/N "
84 "is larger than minSNDiff compared to the priority band), use the band with "
85 "the largest S/N as the reference band."
87 minSNDiff = pexConfig.Field(
90 doc=
"If the difference in S/N between another band and the priority band is larger "
91 "than this value (and the S/N in the priority band is less than minSN) "
92 "use the band with the largest S/N as the reference band"
94 flags = pexConfig.ListField(
96 doc=
"Require that these flags, if available, are not set",
97 default=[
"base_PixelFlags_flag_interpolatedCenter",
"base_PsfFlux_flag",
98 "ext_photometryKron_KronFlux_flag",
"modelfit_CModel_flag", ]
100 priorityList = pexConfig.ListField(
103 doc=
"Priority-ordered list of filter bands for the merge."
105 coaddName = pexConfig.Field(
113 if len(self.priorityList) == 0:
114 raise RuntimeError(
"No priority list provided")
125class MergeMeasurementsTask(pipeBase.PipelineTask):
126 """Merge measurements from multiple bands.
131 Compatibility parameter. Should always be `
None`.
133 The schema of the detection catalogs used
as input to this task.
134 initInputs : `dict`, optional
135 Dictionary that can contain a key ``inputSchema`` containing the
136 input schema. If present will override the value of ``schema``.
138 _DefaultName = "mergeCoaddMeasurements"
139 ConfigClass = MergeMeasurementsConfig
141 inputDataset =
"meas"
142 outputDataset =
"ref"
144 def __init__(self, butler=None, schema=None, initInputs=None, **kwargs):
145 super().__init__(**kwargs)
147 if butler
is not None:
148 warnings.warn(
"The 'butler' parameter is no longer used and can be safely removed.",
149 category=FutureWarning, stacklevel=2)
152 if initInputs
is not None:
153 schema = initInputs[
'inputSchema'].schema
156 raise ValueError(
"No input schema or initInputs['inputSchema'] provided.")
160 self.schemaMapper = afwTable.SchemaMapper(inputSchema,
True)
161 self.schemaMapper.addMinimalSchema(inputSchema,
True)
162 self.instFluxKey = inputSchema.find(self.config.snName +
"_instFlux").getKey()
163 self.instFluxErrKey = inputSchema.find(self.config.snName +
"_instFluxErr").getKey()
164 self.fluxFlagKey = inputSchema.find(self.config.snName +
"_flag").getKey()
167 for band
in self.config.priorityList:
168 outputKey = self.schemaMapper.editOutputSchema().addField(
169 "merge_measurement_%s" % band,
171 doc=
"Flag field set if the measurements here are from the %s filter" % band
173 peakKey = inputSchema.find(
"merge_peak_%s" % band).key
174 footprintKey = inputSchema.find(
"merge_footprint_%s" % band).key
175 self.flagKeys[band] = pipeBase.Struct(peak=peakKey, footprint=footprintKey, output=outputKey)
176 self.schema = self.schemaMapper.getOutputSchema()
178 self.pseudoFilterKeys = []
179 for filt
in self.config.pseudoFilterList:
181 self.pseudoFilterKeys.append(self.schema.find(
"merge_peak_%s" % filt).getKey())
182 except Exception
as e:
183 self.log.warning(
"merge_peak is not set for pseudo-filter %s: %s", filt, e)
186 for flag
in self.config.flags:
188 self.badFlags[flag] = self.schema.find(flag).getKey()
189 except KeyError
as exc:
190 self.log.warning(
"Can't find flag %s in schema: %s", flag, exc)
191 self.outputSchema = afwTable.SourceCatalog(self.schema)
193 def runQuantum(self, butlerQC, inputRefs, outputRefs):
194 inputs = butlerQC.get(inputRefs)
195 dataIds = (ref.dataId
for ref
in inputRefs.catalogs)
196 catalogDict = {dataId[
'band']: cat
for dataId, cat
in zip(dataIds, inputs[
'catalogs'])}
197 inputs[
'catalogs'] = catalogDict
198 outputs = self.run(**inputs)
199 butlerQC.put(outputs, outputRefs)
201 def run(self, catalogs):
203 Merge measurement catalogs to create a single reference catalog for forced photometry
205 @param[
in] catalogs: the catalogs to be merged
207 For parent sources, we choose the first band
in config.priorityList
for which the
208 merge_footprint flag
for that band
is is True.
210 For child sources, the logic
is the same,
except that we use the merge_peak flags.
213 orderedCatalogs = [catalogs[band]
for band
in self.config.priorityList
if band
in catalogs.keys()]
214 orderedKeys = [self.flagKeys[band]
for band
in self.config.priorityList
if band
in catalogs.keys()]
216 mergedCatalog = afwTable.SourceCatalog(self.schema)
217 mergedCatalog.reserve(len(orderedCatalogs[0]))
219 idKey = orderedCatalogs[0].table.getIdKey()
220 for catalog
in orderedCatalogs[1:]:
221 if numpy.any(orderedCatalogs[0].get(idKey) != catalog.get(idKey)):
222 raise ValueError(
"Error in inputs to MergeCoaddMeasurements: source IDs do not match")
226 for orderedRecords
in zip(*orderedCatalogs):
231 priorityRecord =
None
232 priorityFlagKeys =
None
234 hasPseudoFilter =
False
238 for inputRecord, flagKeys
in zip(orderedRecords, orderedKeys):
239 parent = (inputRecord.getParent() == 0
and inputRecord.get(flagKeys.footprint))
240 child = (inputRecord.getParent() != 0
and inputRecord.get(flagKeys.peak))
242 if not (parent
or child):
243 for pseudoFilterKey
in self.pseudoFilterKeys:
244 if inputRecord.get(pseudoFilterKey):
245 hasPseudoFilter =
True
246 priorityRecord = inputRecord
247 priorityFlagKeys = flagKeys
252 isBad = any(inputRecord.get(flag)
for flag
in self.badFlags)
253 if isBad
or inputRecord.get(self.fluxFlagKey)
or inputRecord.get(self.instFluxErrKey) == 0:
256 sn = inputRecord.get(self.instFluxKey)/inputRecord.get(self.instFluxErrKey)
257 if numpy.isnan(sn)
or sn < 0.:
259 if (parent
or child)
and priorityRecord
is None:
260 priorityRecord = inputRecord
261 priorityFlagKeys = flagKeys
264 maxSNRecord = inputRecord
265 maxSNFlagKeys = flagKeys
278 bestRecord = priorityRecord
279 bestFlagKeys = priorityFlagKeys
280 elif (prioritySN < self.config.minSN
and (maxSN - prioritySN) > self.config.minSNDiff
281 and maxSNRecord
is not None):
282 bestRecord = maxSNRecord
283 bestFlagKeys = maxSNFlagKeys
284 elif priorityRecord
is not None:
285 bestRecord = priorityRecord
286 bestFlagKeys = priorityFlagKeys
288 if bestRecord
is not None and bestFlagKeys
is not None:
289 outputRecord = mergedCatalog.addNew()
290 outputRecord.assign(bestRecord, self.schemaMapper)
291 outputRecord.set(bestFlagKeys.output,
True)
293 raise ValueError(
"Error in inputs to MergeCoaddMeasurements: no valid reference for %s" %
297 for inputCatalog
in orderedCatalogs:
298 if len(mergedCatalog) != len(inputCatalog):
299 raise ValueError(
"Mismatch between catalog sizes: %s != %s" %
300 (len(mergedCatalog), len(orderedCatalogs)))
302 return pipeBase.Struct(
303 mergedCatalog=mergedCatalog