23from deprecated.sphinx
import deprecated
40import lsst.pipe.base.connectionTypes
as cT
45from .forcedMeasurement
import ForcedMeasurementTask
46from .applyApCorr
import ApplyApCorrTask
47from .catalogCalculation
import CatalogCalculationTask
48from ._id_generator
import DetectorVisitIdGeneratorConfig
50__all__ = (
"ForcedPhotCcdConfig",
"ForcedPhotCcdTask",
51 "ForcedPhotCcdFromDataFrameTask",
"ForcedPhotCcdFromDataFrameConfig")
54@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
55 "This task will be removed after v30.",
56 version=
"v29.0", category=FutureWarning)
58 dimensions=(
"instrument",
"visit",
"detector",
"skymap",
"tract"),
59 defaultTemplates={
"inputCoaddName":
"deep",
60 "inputName":
"calexp"}):
61 inputSchema = cT.InitInput(
62 doc=
"Schema for the input measurement catalogs.",
63 name=
"{inputCoaddName}Coadd_ref_schema",
64 storageClass=
"SourceCatalog",
66 outputSchema = cT.InitOutput(
67 doc=
"Schema for the output forced measurement catalogs.",
68 name=
"forced_src_schema",
69 storageClass=
"SourceCatalog",
72 doc=
"Input exposure to perform photometry on.",
74 storageClass=
"ExposureF",
75 dimensions=[
"instrument",
"visit",
"detector"],
78 doc=
"Catalog of shapes and positions at which to force photometry.",
79 name=
"{inputCoaddName}Coadd_ref",
80 storageClass=
"SourceCatalog",
81 dimensions=[
"skymap",
"tract",
"patch"],
86 doc=
"SkyMap dataset that defines the coordinate system of the reference catalog.",
87 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
88 storageClass=
"SkyMap",
89 dimensions=[
"skymap"],
92 doc=
"Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
94 storageClass=
"Background",
95 dimensions=(
"instrument",
"visit",
"detector"),
97 visitSummary = cT.Input(
98 doc=
"Input visit-summary catalog with updated calibration objects.",
99 name=
"finalVisitSummary",
100 storageClass=
"ExposureCatalog",
101 dimensions=(
"instrument",
"visit"),
104 doc=
"Output forced photometry catalog.",
106 storageClass=
"SourceCatalog",
107 dimensions=[
"instrument",
"visit",
"detector",
"skymap",
"tract"],
110 def __init__(self, *, config=None):
111 super().__init__(config=config)
112 if not config.doApplySkyCorr:
114 if not config.useVisitSummary:
115 del self.visitSummary
116 if config.refCatStorageClass !=
"SourceCatalog":
120 self.refCat = dataclasses.replace(self.refCat, storageClass=config.refCatStorageClass)
123@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
124 "This task will be removed after v30.",
125 version=
"v29.0", category=FutureWarning)
126class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig,
127 pipelineConnections=ForcedPhotCcdConnections):
128 """Config class for forced measurement driver task."""
130 target=ForcedMeasurementTask,
131 doc=
"subtask to do forced measurement"
134 doc=
"coadd name: typically one of deep or goodSeeing",
141 doc=
"Run subtask to apply aperture corrections"
144 target=ApplyApCorrTask,
145 doc=
"Subtask to apply aperture corrections"
148 target=CatalogCalculationTask,
149 doc=
"Subtask to run catalogCalculation plugins on catalog"
154 doc=
"Apply sky correction?",
160 "Use updated WCS, PhotoCalib, ApCorr, and PSF from visit summary? "
161 "This should be False if and only if the input image already has the best-available calibration "
168 "SourceCatalog":
"Read an lsst.afw.table.SourceCatalog.",
169 "DataFrame":
"Read a pandas.DataFrame.",
170 "ArrowAstropy":
"Read an astropy.table.Table saved to Parquet.",
172 default=
"SourceCatalog",
174 "The butler storage class for the refCat connection. "
175 "If set to something other than 'SourceCatalog', the "
176 "'inputSchema' connection will be ignored."
181 default=
"diaObjectId",
183 "Name of the column that provides the object ID from the refCat connection. "
184 "measurement.copyColumns['id'] must be set to this value as well."
185 "Ignored if refCatStorageClass='SourceCatalog'."
192 "Name of the column that provides the right ascension (in floating-point degrees) from the "
193 "refCat connection. "
194 "Ignored if refCatStorageClass='SourceCatalog'."
201 "Name of the column that provides the declination (in floating-point degrees) from the "
202 "refCat connection. "
203 "Ignored if refCatStorageClass='SourceCatalog'."
210 doc=
"Add photometric calibration variance to warp variance plane?",
211 deprecated=
"Deprecated and unused; will be removed after v29.",
215 doc=
"Where to obtain footprints to install in the measurement catalog, prior to measurement.",
217 "transformed":
"Transform footprints from the reference catalog (downgrades HeavyFootprints).",
218 "psf": (
"Use the scaled shape of the PSF at the position of each source (does not generate "
219 "HeavyFootprints)."),
222 default=
"transformed",
226 doc=
"Scaling factor to apply to the PSF shape when footprintSource='psf' (ignored otherwise).",
229 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
231 def setDefaults(self):
233 super().setDefaults()
236 self.measurement.doReplaceWithNoise =
False
239 self.measurement.plugins.names = [
"base_PixelFlags",
240 "base_TransformedCentroid",
242 "base_LocalBackground",
243 "base_LocalPhotoCalib",
246 self.measurement.slots.psfFlux =
"base_PsfFlux"
247 self.measurement.slots.shape =
None
250 self.catalogCalculation.plugins.names = []
254 if self.refCatStorageClass !=
"SourceCatalog":
255 if self.footprintSource ==
"transformed":
256 raise ValueError(
"Cannot transform footprints from reference catalog, because "
257 f
"{self.config.refCatStorageClass} datasets can't hold footprints.")
258 if self.measurement.copyColumns[
"id"] != self.refCatIdColumn:
260 f
"measurement.copyColumns['id'] should be set to {self.refCatIdColumn} "
261 f
"(refCatIdColumn) when refCatStorageClass={self.refCatStorageClass}."
264 def configureParquetRefCat(self, refCatStorageClass: str =
"ArrowAstropy"):
265 """Set the refCatStorageClass option to a Parquet-based type, and
266 reconfigure the measurement subtask and footprintSources accordingly.
268 self.refCatStorageClass = refCatStorageClass
269 self.footprintSource =
"psf"
270 self.measurement.doReplaceWithNoise =
False
271 self.measurement.plugins.names -= {
"base_TransformedCentroid"}
272 self.measurement.plugins.names |= {
"base_TransformedCentroidFromCoord"}
273 self.measurement.copyColumns[
"id"] = self.refCatIdColumn
274 self.measurement.copyColumns.pop(
"deblend_nChild",
None)
275 self.measurement.slots.centroid =
"base_TransformedCentroidFromCoord"
278@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
279 "This task will be removed after v30.",
280 version=
"v29.0", category=FutureWarning)
281class ForcedPhotCcdTask(pipeBase.PipelineTask):
282 """A pipeline task for performing forced measurement on CCD images.
286 refSchema : `lsst.afw.table.Schema`, optional
287 The schema of the reference catalog, passed to the constructor of the
288 references subtask. Optional, but must be specified if ``initInputs``
289 is not; if both are specified, ``initInputs`` takes precedence.
291 Dictionary that can contain a key ``inputSchema`` containing the
292 schema. If present will override the value of ``refSchema``.
294 Keyword arguments are passed to the supertask constructor.
297 ConfigClass = ForcedPhotCcdConfig
298 _DefaultName =
"forcedPhotCcd"
301 def __init__(self, refSchema=None, initInputs=None, **kwargs):
302 super().__init__(**kwargs)
305 refSchema = initInputs[
'inputSchema'].schema
307 if refSchema
is None:
310 self.makeSubtask(
"measurement", refSchema=refSchema)
314 if self.config.doApCorr:
315 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
316 self.makeSubtask(
'catalogCalculation', schema=self.measurement.schema)
319 def runQuantum(self, butlerQC, inputRefs, outputRefs):
320 inputs = butlerQC.get(inputRefs)
322 tract = butlerQC.quantum.dataId[
'tract']
323 skyMap = inputs.pop(
'skyMap')
324 inputs[
'refWcs'] = skyMap[tract].getWcs()
327 skyCorr = inputs.pop(
'skyCorr',
None)
329 inputs[
'exposure'] = self.prepareCalibratedExposure(
332 visitSummary=inputs.pop(
"visitSummary",
None),
335 if inputs[
"exposure"].getWcs()
is None:
336 raise NoWorkFound(
"Exposure has no WCS.")
338 match self.config.refCatStorageClass:
339 case
"SourceCatalog":
340 prepFunc = self._prepSourceCatalogRefCat
342 prepFunc = self._prepDataFrameRefCat
344 prepFunc = self._prepArrowAstropyRefCat
346 raise AssertionError(
"Configuration should not have passed validation.")
347 self.log.info(
"Filtering ref cats: %s",
','.join([str(i.dataId)
for i
in inputs[
'refCat']]))
348 inputs[
'refCat'] = prepFunc(
350 inputs[
'exposure'].getBBox(),
351 inputs[
'exposure'].getWcs(),
355 inputs[
'measCat'], inputs[
'exposureId'] = self.generateMeasCat(
356 inputRefs.exposure.dataId, inputs[
'exposure'], inputs[
'refCat'], inputs[
'refWcs']
360 self.attachFootprints(inputs[
"measCat"], inputs[
"refCat"], inputs[
"exposure"], inputs[
"refWcs"])
361 outputs = self.run(**inputs)
362 butlerQC.put(outputs, outputRefs)
364 def prepareCalibratedExposure(self, exposure, skyCorr=None, visitSummary=None):
365 """Prepare a calibrated exposure and apply external calibrations
366 and sky corrections if so configured.
370 exposure : `lsst.afw.image.exposure.Exposure`
371 Input exposure to adjust calibrations.
372 skyCorr : `lsst.afw.math.backgroundList`, optional
373 Sky correction frame to apply if doApplySkyCorr=True.
374 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
375 Exposure catalog with update calibrations; any not-None calibration
376 objects attached will be used. These are applied first and may be
377 overridden by other arguments.
381 exposure : `lsst.afw.image.exposure.Exposure`
382 Exposure with adjusted calibrations.
384 detectorId = exposure.getInfo().getDetector().getId()
386 if visitSummary
is not None:
387 row = visitSummary.find(detectorId)
389 raise RuntimeError(f
"Detector id {detectorId} not found in visitSummary.")
390 if (photoCalib := row.getPhotoCalib())
is not None:
391 exposure.setPhotoCalib(photoCalib)
392 if (skyWcs := row.getWcs())
is not None:
393 exposure.setWcs(skyWcs)
394 if (psf := row.getPsf())
is not None:
396 if (apCorrMap := row.getApCorrMap())
is not None:
397 exposure.info.setApCorrMap(apCorrMap)
399 if skyCorr
is not None:
400 exposure.maskedImage -= skyCorr.getImage()
404 def generateMeasCat(self, dataId, exposure, refCat, refWcs):
405 """Generate a measurement catalog.
409 dataId : `lsst.daf.butler.DataCoordinate`
410 Butler data ID for this image, with ``{visit, detector}`` keys.
411 exposure : `lsst.afw.image.exposure.Exposure`
412 Exposure to generate the catalog for.
413 refCat : `lsst.afw.table.SourceCatalog`
414 Catalog of shapes and positions at which to force photometry.
415 refWcs : `lsst.afw.image.SkyWcs`
416 Reference world coordinate system.
417 This parameter is not currently used.
421 measCat : `lsst.afw.table.SourceCatalog`
422 Catalog of forced sources to measure.
424 Unique binary id associated with the input exposure
426 id_generator = self.config.idGenerator.apply(dataId)
427 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
428 idFactory=id_generator.make_table_id_factory())
429 return measCat, id_generator.catalog_id
431 def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
432 """Perform forced measurement on a single exposure.
436 measCat : `lsst.afw.table.SourceCatalog`
437 The measurement catalog, based on the sources listed in the
439 exposure : `lsst.afw.image.Exposure`
440 The measurement image upon which to perform forced detection.
441 refCat : `lsst.afw.table.SourceCatalog`
442 The reference catalog of sources to measure.
443 refWcs : `lsst.afw.image.SkyWcs`
444 The WCS for the references.
446 Optional unique exposureId used for random seed in measurement
451 result : `lsst.pipe.base.Struct`
452 Structure with fields:
455 Catalog of forced measurement results
456 (`lsst.afw.table.SourceCatalog`).
458 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
459 if self.config.doApCorr:
460 apCorrMap = exposure.getInfo().getApCorrMap()
461 if apCorrMap
is None:
462 self.log.warning(
"Forced exposure image does not have valid aperture correction; skipping.")
464 self.applyApCorr.run(
468 self.catalogCalculation.run(measCat)
470 return pipeBase.Struct(measCat=measCat)
472 def attachFootprints(self, sources, refCat, exposure, refWcs):
473 """Attach footprints to blank sources prior to measurements.
477 `~lsst.afw.detection.Footprint` objects for forced photometry must
478 be in the pixel coordinate system of the image being measured, while
479 the actual detections may start out in a different coordinate system.
481 Subclasses of this class may implement this method to define how
482 those `~lsst.afw.detection.Footprint` objects should be generated.
484 This default implementation transforms depends on the
485 ``footprintSource`` configuration parameter.
487 if self.config.footprintSource ==
"transformed":
488 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
489 elif self.config.footprintSource ==
"psf":
490 return self.measurement.attachPsfShapeFootprints(sources, exposure,
491 scaling=self.config.psfFootprintScaling)
493 def _prepSourceCatalogRefCat(self, refCatHandles, exposureBBox, exposureWcs):
494 """Prepare a merged, filtered reference catalog from SourceCatalog
499 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
500 Handles for catalogs of shapes and positions at which to force
502 exposureBBox : `lsst.geom.Box2I`
503 Bounding box on which to select rows that overlap
504 exposureWcs : `lsst.afw.geom.SkyWcs`
505 World coordinate system to convert sky coords in ref cat to
506 pixel coords with which to compare with exposureBBox
510 refSources : `lsst.afw.table.SourceCatalog`
511 Filtered catalog of forced sources to measure.
515 The majority of this code is based on the methods of
516 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
524 expBoxCorners = expBBox.getCorners()
525 expSkyCorners = [exposureWcs.pixelToSky(corner).getVector()
for corner
in expBoxCorners]
533 for refCat
in refCatHandles:
534 refCat = refCat.get()
535 if mergedRefCat
is None:
538 containedIds = np.array([0])
540 coordKey = refCat.getCoordKey()
541 inside = expPolygon.contains(lon=refCat[coordKey.getRa()], lat=refCat[coordKey.getDec()])
542 parentIds = refCat[refCat.getParentKey()]
543 inside &= np.isin(parentIds, containedIds)
545 mergedRefCat.extend(refCat[inside])
546 containedIds = np.union1d(containedIds, refCat[refCat.getIdKey()][inside])
548 if mergedRefCat
is None:
549 raise RuntimeError(
"No reference objects for forced photometry.")
553 def _prepDataFrameRefCat(self, refCatHandles, exposureBBox, exposureWcs):
554 """Prepare a merged, filtered reference catalog from DataFrame
559 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
560 Handles for catalogs of shapes and positions at which to force
562 exposureBBox : `lsst.geom.Box2I`
563 Bounding box on which to select rows that overlap
564 exposureWcs : `lsst.afw.geom.SkyWcs`
565 World coordinate system to convert sky coords in ref cat to
566 pixel coords with which to compare with exposureBBox
570 refCat : `lsst.afw.table.SourceTable`
571 Source Catalog with minimal schema that overlaps exposureBBox
577 self.config.refCatIdColumn,
578 self.config.refCatRaColumn,
579 self.config.refCatDecColumn,
583 for i
in refCatHandles
585 df = pd.concat(dfList)
588 x, y = exposureWcs.skyToPixelArray(
589 df[self.config.refCatRaColumn].values,
590 df[self.config.refCatDecColumn].values,
594 refCat = self._makeMinimalSourceCatalogFromDataFrame(df[inBBox])
597 def _prepArrowAstropyRefCat(self, refCatHandles, exposureBBox, exposureWcs):
598 """Prepare a merged, filtered reference catalog from ArrowAstropy
603 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle`
604 Handles for catalogs of shapes and positions at which to force
606 exposureBBox : `lsst.geom.Box2I`
607 Bounding box on which to select rows that overlap
608 exposureWcs : `lsst.afw.geom.SkyWcs`
609 World coordinate system to convert sky coords in ref cat to
610 pixel coords with which to compare with exposureBBox
614 refCat : `lsst.afw.table.SourceTable`
615 Source Catalog with minimal schema that overlaps exposureBBox
621 self.config.refCatIdColumn,
622 self.config.refCatRaColumn,
623 self.config.refCatDecColumn,
627 for i
in refCatHandles
629 full_table = astropy.table.vstack(table_list)
632 x, y = exposureWcs.skyToPixelArray(
633 full_table[self.config.refCatRaColumn],
634 full_table[self.config.refCatDecColumn],
638 refCat = self._makeMinimalSourceCatalogFromAstropy(full_table[inBBox])
641 def _makeMinimalSourceCatalogFromDataFrame(self, df):
642 """Create minimal schema SourceCatalog from a pandas DataFrame.
644 The forced measurement subtask expects this as input.
648 df : `pandas.DataFrame`
649 Table with locations and ids.
653 outputCatalog : `lsst.afw.table.SourceTable`
654 Output catalog with minimal schema.
658 outputCatalog.reserve(len(df))
660 for objectId, ra, dec
in df[[
'ra',
'dec']].itertuples():
661 outputRecord = outputCatalog.addNew()
662 outputRecord.setId(objectId)
666 def _makeMinimalSourceCatalogFromAstropy(self, table):
667 """Create minimal schema SourceCatalog from an Astropy Table.
669 The forced measurement subtask expects this as input.
673 table : `astropy.table.Table`
674 Table with locations and ids.
678 outputCatalog : `lsst.afw.table.SourceTable`
679 Output catalog with minimal schema.
683 outputCatalog.reserve(len(table))
685 for objectId, ra, dec
in table.iterrows():
686 outputRecord = outputCatalog.addNew()
687 outputRecord.setId(objectId)
692@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
693 "This task will be removed after v30.",
694 version=
"v29.0", category=FutureWarning)
696 dimensions=(
"instrument",
"visit",
"detector",
"skymap",
"tract"),
697 defaultTemplates={
"inputCoaddName":
"goodSeeing",
698 "inputName":
"calexp",
703@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
704 "This task will be removed after v30.",
705 version=
"v29.0", category=FutureWarning)
706class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig,
707 pipelineConnections=ForcedPhotCcdFromDataFrameConnections):
708 def setDefaults(self):
709 super().setDefaults()
710 self.configureParquetRefCat(
"DataFrame")
711 self.connections.refCat =
"{inputCoaddName}Diff_fullDiaObjTable"
712 self.connections.outputSchema =
"forced_src_diaObject_schema"
713 self.connections.measCat =
"forced_src_diaObject"
716@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
717 "This task will be removed after v30.",
718 version=
"v29.0", category=FutureWarning)
719class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask):
720 """Force Photometry on a per-detector exposure with coords from a DataFrame
722 Uses input from a DataFrame instead of SourceCatalog
723 like the base class ForcedPhotCcd does.
724 Writes out a SourceCatalog so that the downstream
725 WriteForcedSourceTableTask can be reused with output from this Task.
727 _DefaultName =
"forcedPhotCcdFromDataFrame"
728 ConfigClass = ForcedPhotCcdFromDataFrameConfig
static Schema makeMinimalSchema()
Return a minimal schema for Source tables and records.
static Key< RecordId > getParentKey()
Key for the parent ID.
A floating-point coordinate rectangle geometry.
Point in an unspecified spherical coordinate system.
ConvexPolygon is a closed convex polygon on the unit sphere.