35import lsst.pipe.base
as pipeBase
36from lsst.pipe.base
import PipelineTaskConnections, NoWorkFound
37import lsst.pipe.base.connectionTypes
as cT
40__all__ = (
"ForcedPhotDetectorConfig",
"ForcedPhotDetectorTask")
44 dimensions=(
"visit",
"detector",
"tract")):
46 doc=
"Input exposure to perform photometry on.",
48 storageClass=
"ExposureF",
49 dimensions=[
"visit",
"detector"],
51 diaExposure = cT.Input(
52 doc=
"Input difference image to perform photometry on.",
53 name=
"difference_image",
54 storageClass=
"ExposureF",
55 dimensions=[
"visit",
"detector"],
58 doc=
"Catalog of shapes and positions at which to force photometry.",
60 storageClass=
"ArrowAstropy",
61 dimensions=[
"tract",
"patch"],
66 doc=
"SkyMap dataset that defines the coordinate system of the reference catalog.",
67 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
68 storageClass=
"SkyMap",
69 dimensions=[
"skymap"],
71 outputCatalog = cT.Output(
72 doc=
"Output forced photometry catalog.",
73 name=
"object_forced_source_unstandardized",
74 storageClass=
"DataFrame",
75 dimensions=(
"visit",
"detector",
"skymap",
"tract")
80 assert isinstance(config, ForcedPhotDetectorConfig)
82 if not config.doDirectPhotometry:
84 if not config.doDifferencePhotometry:
89 pipelineConnections=ForcedPhotDetectorConnections):
90 """Configuration for the ForcedPhotDetectorTask."""
91 measurement = lsst.pex.config.ConfigurableField(
92 target=SimpleForcedMeasurementTask,
93 doc=
"subtask to do forced measurement"
95 doApCorr = lsst.pex.config.Field(
98 doc=
"Run subtask to apply aperture corrections"
100 applyApCorr = lsst.pex.config.ConfigurableField(
101 target=ApplyApCorrTask,
102 doc=
"Subtask to apply aperture corrections"
104 doDirectPhotometry = lsst.pex.config.Field(
105 doc=
"Perform direct photometry on the input exposure.",
109 doDifferencePhotometry = lsst.pex.config.Field(
110 doc=
"Perform photometry on the difference image.",
114 refCatIdColumn = lsst.pex.config.Field(
118 "Name of the column that provides the object ID from the refCat connection. "
119 "measurement.copyColumns['id'] must be set to this value as well."
120 "Ignored if refCatStorageClass='SourceCatalog'."
123 refCatRaColumn = lsst.pex.config.Field(
127 "Name of the column that provides the right ascension (in floating-point degrees) from the "
128 "refCat connection. "
129 "Ignored if refCatStorageClass='SourceCatalog'."
132 refCatDecColumn = lsst.pex.config.Field(
136 "Name of the column that provides the declination (in floating-point degrees) from the "
137 "refCat connection. "
138 "Ignored if refCatStorageClass='SourceCatalog'."
141 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
145 """A pipeline task for performing forced photometry on CCD images."""
146 ConfigClass = ForcedPhotDetectorConfig
147 _DefaultName =
"forcedPhotDetector"
154 self.makeSubtask(
"measurement", refSchema=refSchema)
155 if self.config.doApCorr:
156 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
159 inputs = butlerQC.get(inputRefs)
161 if "exposure" in inputs:
162 exposure = inputs[
"exposure"]
163 bbox = exposure.getBBox()
164 wcs = exposure.getWcs()
166 raise NoWorkFound(
"Exposure has no valid WCS.")
170 if "diaExposure" in inputs:
171 diaExposure = inputs[
"diaExposure"]
173 bbox = diaExposure.getBBox()
174 wcs = diaExposure.getWcs()
176 raise NoWorkFound(
"Difference exposure has no valid WCS.")
180 raise NoWorkFound(
"No valid exposure or difference exposure provided.")
183 tract = butlerQC.quantum.dataId[
"tract"]
184 skyMap = inputs.pop(
"skyMap")
185 refWcs = skyMap[tract].getWcs()
187 self.log.info(
"Filtering ref cats: %s",
','.join([str(i.dataId)
for i
in inputs[
"refCat"]]))
200 if self.config.doDirectPhotometry:
201 id_generator = self.config.idGenerator.apply(inputRefs.exposure.dataId)
202 directCat = self.
_generateMeasCat(refCat, idFactory=id_generator.make_table_id_factory())
205 if self.config.doDifferencePhotometry:
206 id_generator = self.config.idGenerator.apply(inputRefs.diaExposure.dataId)
207 diffCat = self.
_generateMeasCat(refCat, idFactory=id_generator.make_table_id_factory())
213 objectIds=refTable[self.config.refCatIdColumn],
214 visit=butlerQC.quantum.dataId[
"visit"],
215 detector=butlerQC.quantum.dataId[
"detector"],
220 diaExposure=diaExposure,
221 band=butlerQC.quantum.dataId[
"band"],
224 butlerQC.put(outputs, outputRefs)
229 objectIds: np.ndarray,
237 band: str |
None =
None,
238 ) -> pipeBase.Struct:
239 """Run forced photometry on a single detector.
241 There is a lot of prep work in the `runQuantum` method and it is
242 expected that this taks is usually run as a pipeline task, not
243 executed as a stand alone function.
248 Reference catalog for forced photometry.
250 Array of object IDs corresponding to the reference catalog.
252 Visit ID for the observation.
254 Detector ID for the observation.
256 Reference WCS for the observation.
258 Catalog for direct photometry.
259 Only required when `config.doDirectPhotometry` is True.
261 Catalog for difference photometry.
262 Only required when `config.doDifferencePhotometry` is True.
264 Exposure for direct photometry.
265 Only required when `config.doDirectPhotometry` is True.
267 Exposure for difference photometry.
268 Only required when `config.doDifferencePhotometry` is True.
270 Band for the observation.
273 if self.config.doDirectPhotometry:
275 raise ValueError(
"`exposure` must be provided for direct photometry.")
276 if directCat
is None:
277 raise ValueError(
"`directCat` must be provided for direct photometry.")
278 self.log.info(
"Running forced measurement on %s objects", len(refCat))
280 results[
"calexp"] = directCat
282 if self.config.doDifferencePhotometry:
283 if diaExposure
is None:
284 raise ValueError(
"`diaExposure` must be provided for difference photometry.")
286 raise ValueError(
"`diffCat` must be provided for difference photometry.")
287 self.log.info(
"Running forced measurement on %s objects on difference image", len(refCat))
289 results[
"diff"] = diffCat
293 for dataset, catalog
in results.items():
294 measTbl = catalog.asAstropy()
295 measTbl[self.config.refCatIdColumn] = objectIds
296 df = measTbl.to_pandas().set_index(self.config.refCatIdColumn, drop=
False)
297 df = df.reindex(sorted(df.columns), axis=1)
300 df[
"detector"] = np.int16(detector)
301 df[
"band"] = band
if band
else pd.NA
302 df.columns = pd.MultiIndex.from_tuples([(dataset, c)
for c
in df.columns],
303 names=(
"dataset",
"column"))
307 outputCatalog = functools.reduce(
lambda d1, d2: d1.join(d2), dfs)
308 return pipeBase.Struct(outputCatalog=outputCatalog)
317 """Perform forced measurement on a single exposure.
321 refCat : `lsst.afw.table.SourceCatalog`
322 Catalog containing the reference catalog data, with columns
323 for the object ID, right ascension, and declination.
324 measCat : `lsst.afw.table.SourceCatalog`
325 Catalog containing the measurement data, with columns for the
326 object ID and measured quantities.
327 This catalog is updated in-place.
328 exposure : `lsst.afw.image.exposure.Exposure`
329 Input exposure to adjust calibrations.
330 refWcs : `lsst.afw.geom.SkyWcs`
331 Defines the X,Y coordinate system of ``refCat``.
333 self.measurement.run(
339 if self.config.doApCorr:
340 apCorrMap = exposure.getInfo().getApCorrMap()
341 if apCorrMap
is None:
342 self.log.warning(
"Forced exposure image does not have valid aperture correction; skipping.")
344 self.applyApCorr.run(
350 """Create minimal schema SourceCatalog from an Astropy Table.
352 The forced measurement subtask expects this as input.
356 table : `astropy.table.Table`
357 Table with locations and ids.
361 outputCatalog : `lsst.afw.table.SourceTable`
362 Output catalog with minimal schema.
366 outputCatalog.resize(len(table))
367 outputCatalog[
"id"] = table[self.config.refCatIdColumn]
368 outputCatalog[outputCatalog.getCoordKey().getRa()] = np.deg2rad(table[self.config.refCatRaColumn])
369 outputCatalog[outputCatalog.getCoordKey().getDec()] = np.deg2rad(table[self.config.refCatDecColumn])
373 """Prepare a merged, filtered reference catalog from ArrowAstropy
378 refTableHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
379 Handles for catalogs of shapes and positions at which to force
381 exposureBBox : `lsst.geom.Box2I`
382 Bounding box on which to select rows that overlap
383 exposureWcs : `lsst.afw.geom.SkyWcs`
384 World coordinate system to convert sky coords in ref cat to
385 pixel coords with which to compare with exposureBBox
389 refTable : `astropy.table.Table`
390 Astropy Table with only rows from the reference
391 catalogs that overlap the exposure bounding box.
394 for i
in refTableHandles:
396 table_list.append(i.get(
399 self.config.refCatIdColumn,
400 self.config.refCatRaColumn,
401 self.config.refCatDecColumn,
406 self.log.info(
"Skipping %s due to empty object table.", i.dataId)
409 raise NoWorkFound(
"All overlapping object catalogs are empty.")
410 full_table = astropy.table.vstack(table_list)
413 x, y = exposureWcs.skyToPixelArray(
414 full_table[self.config.refCatRaColumn],
415 full_table[self.config.refCatDecColumn],
419 return full_table[inBBox]
426 r"""Initialize an output catalog from the reference catalog.
430 refCat : `lsst.afw.table.SourceCatalog,`
431 Catalog of reference sources.
432 idFactory : `lsst.afw.table.IdFactory`, optional
433 Factory for creating IDs for sources.
437 meascat : `lsst.afw.table.SourceCatalog`
438 Source catalog ready for measurement.
442 This generates a new blank `~lsst.afw.table.SourceRecord` for each
443 record in ``refCat``. Note that this method does not attach any
444 `~lsst.afw.detection.Footprint`\ s, which is done in
447 if idFactory
is None:
451 table = measCat.table
452 table.setMetadata(self.measurement.algMetadata)
453 table.preallocate(len(refCat))
455 newSource = measCat.addNew()
456 newSource.assign(ref, self.measurement.mapper)
static std::shared_ptr< IdFactory > makeSimple()
static std::shared_ptr< SourceTable > make(Schema const &schema, std::shared_ptr< IdFactory > const &idFactory)
static Schema makeMinimalSchema()
__init__(self, *, config=None)
_filterRefTable(self, refTableHandles, exposureBBox, exposureWcs)
None _runForcedPhotometry(self, lsst.afw.table.SourceCatalog refCat, lsst.afw.table.SourceCatalog measCat, lsst.afw.image.Exposure exposure, lsst.afw.geom.SkyWcs refWcs)
__init__(self, initInputs=None, **kwargs)
_makeMinimalSourceCatalogFromAstropy(self, table)
runQuantum(self, butlerQC, inputRefs, outputRefs)
pipeBase.Struct run(self, lsst.afw.table.SourceCatalog refCat, np.ndarray objectIds, int visit, int detector, lsst.afw.geom.SkyWcs refWcs, lsst.afw.table.SourceCatalog|None directCat=None, lsst.afw.table.SourceCatalog|None diffCat=None, lsst.afw.image.Exposure|None exposure=None, lsst.afw.image.Exposure|None diaExposure=None, str|None band=None)
lsst.afw.table.SourceCatalog _generateMeasCat(self, lsst.afw.table.SourceCatalog refCat, lsst.afw.table.IdFactory|None idFactory=None)