22__all__ = [
"WriteObjectTableConfig",
"WriteObjectTableTask",
23 "WriteSourceTableConfig",
"WriteSourceTableTask",
24 "WriteRecalibratedSourceTableConfig",
"WriteRecalibratedSourceTableTask",
25 "PostprocessAnalysis",
26 "TransformCatalogBaseConfig",
"TransformCatalogBaseTask",
27 "TransformObjectCatalogConfig",
"TransformObjectCatalogTask",
28 "ConsolidateObjectTableConfig",
"ConsolidateObjectTableTask",
29 "TransformSourceTableConfig",
"TransformSourceTableTask",
30 "ConsolidateVisitSummaryConfig",
"ConsolidateVisitSummaryTask",
31 "ConsolidateSourceTableConfig",
"ConsolidateSourceTableTask",
32 "MakeCcdVisitTableConfig",
"MakeCcdVisitTableTask",
33 "MakeVisitTableConfig",
"MakeVisitTableTask",
34 "WriteForcedSourceTableConfig",
"WriteForcedSourceTableTask",
35 "TransformForcedSourceTableConfig",
"TransformForcedSourceTableTask",
36 "ConsolidateTractConfig",
"ConsolidateTractTask",
37 "ComputeColumnsAction",
"ModelExtendednessColumnAction",
40from collections
import defaultdict
42from deprecated.sphinx
import deprecated
47from typing
import Iterable
52from numpy.typing
import NDArray
56import lsst.pipe.base
as pipeBase
58from lsst.daf.butler.formatters.parquet
import pandas_to_astropy
60from lsst.pipe.base
import NoWorkFound, UpstreamFailureNoWorkFound, connectionTypes
63from lsst.meas.base import SingleFrameMeasurementTask, DetectorVisitIdGeneratorConfig
64from lsst.obs.base.utils
import strip_provenance_from_fits_header, TableVStack
66from .coaddBase
import reorderRefs
67from .functors
import CompositeFunctor, Column
68from .schemaUtils
import convertDataFrameToSdmSchema, readSdmSchemaFile
70log = logging.getLogger(__name__)
73def flattenFilters(df, noDupCols=["coord_ra", "coord_dec"], camelCase=False, inputBands=None):
74 """Flattens a dataframe with multilevel column index.
76 newDf = pd.DataFrame()
78 dfBands = df.columns.unique(level=0).values
81 columnFormat =
"{0}{1}" if camelCase
else "{0}_{1}"
82 newColumns = {c: columnFormat.format(band, c)
83 for c
in subdf.columns
if c
not in noDupCols}
84 cols = list(newColumns.keys())
85 newDf = pd.concat([newDf, subdf[cols].rename(columns=newColumns)], axis=1)
88 presentBands = dfBands
if inputBands
is None else list(set(inputBands).intersection(dfBands))
90 noDupDf = df[presentBands[0]][noDupCols]
91 newDf = pd.concat([noDupDf, newDf], axis=1)
96 defaultTemplates={
"coaddName":
"deep"},
97 dimensions=(
"tract",
"patch",
"skymap")):
98 inputCatalogMeas = connectionTypes.Input(
99 doc=
"Catalog of source measurements on the deepCoadd.",
100 dimensions=(
"tract",
"patch",
"band",
"skymap"),
101 storageClass=
"SourceCatalog",
102 name=
"{coaddName}Coadd_meas",
105 inputCatalogForcedSrc = connectionTypes.Input(
106 doc=
"Catalog of forced measurements (shape and position parameters held fixed) on the deepCoadd.",
107 dimensions=(
"tract",
"patch",
"band",
"skymap"),
108 storageClass=
"SourceCatalog",
109 name=
"{coaddName}Coadd_forced_src",
112 inputCatalogPsfsMultiprofit = connectionTypes.Input(
113 doc=
"Catalog of Gaussian mixture model fit parameters for the PSF model at each object centroid.",
114 dimensions=(
"tract",
"patch",
"band",
"skymap"),
115 storageClass=
"ArrowAstropy",
116 name=
"{coaddName}Coadd_psfs_multiprofit",
119 outputCatalog = connectionTypes.Output(
120 doc=
"A vertical concatenation of the deepCoadd_{ref|meas|forced_src} catalogs, "
121 "stored as a DataFrame with a multi-level column index per-patch.",
122 dimensions=(
"tract",
"patch",
"skymap"),
123 storageClass=
"DataFrame",
124 name=
"{coaddName}Coadd_obj"
128class WriteObjectTableConfig(pipeBase.PipelineTaskConfig,
129 pipelineConnections=WriteObjectTableConnections):
130 coaddName = pexConfig.Field(
137class WriteObjectTableTask(pipeBase.PipelineTask):
138 """Write filter-merged object tables as a DataFrame in parquet format.
140 _DefaultName =
"writeObjectTable"
141 ConfigClass = WriteObjectTableConfig
144 outputDataset =
"obj"
146 def runQuantum(self, butlerQC, inputRefs, outputRefs):
147 inputs = butlerQC.get(inputRefs)
149 catalogs = defaultdict(dict)
150 for dataset, connection
in (
151 (
"meas",
"inputCatalogMeas"),
152 (
"forced_src",
"inputCatalogForcedSrc"),
153 (
"psfs_multiprofit",
"inputCatalogPsfsMultiprofit"),
155 for ref, cat
in zip(getattr(inputRefs, connection), inputs[connection]):
156 catalogs[ref.dataId[
"band"]][dataset] = cat
158 dataId = butlerQC.quantum.dataId
159 df = self.run(catalogs=catalogs, tract=dataId[
"tract"], patch=dataId[
"patch"])
160 outputs = pipeBase.Struct(outputCatalog=df)
161 butlerQC.put(outputs, outputRefs)
163 def run(self, catalogs, tract, patch):
164 """Merge multiple catalogs.
169 Mapping from filter names to dict of catalogs.
171 tractId to use for the tractId column.
173 patchId to use for the patchId column.
177 catalog : `pandas.DataFrame`
183 Raised if any of the catalogs is of an unsupported type.
186 for filt, tableDict
in catalogs.items():
187 for dataset, table
in tableDict.items():
189 if isinstance(table, pd.DataFrame):
191 elif isinstance(table, afwTable.SourceCatalog):
192 df = table.asAstropy().to_pandas()
193 elif isinstance(table, astropy.table.Table):
194 df = table.to_pandas()
196 raise ValueError(f
"{dataset=} has unsupported {type(table)=}")
197 df.set_index(
"id", drop=
True, inplace=
True)
200 df = df.reindex(sorted(df.columns), axis=1)
201 df = df.assign(tractId=tract, patchId=patch)
204 df.columns = pd.MultiIndex.from_tuples([(dataset, filt, c)
for c
in df.columns],
205 names=(
"dataset",
"band",
"column"))
210 catalog = functools.reduce(
lambda d1, d2: d1.join(d2), dfs)
214class WriteSourceTableConnections(pipeBase.PipelineTaskConnections,
215 defaultTemplates={
"catalogType":
""},
216 dimensions=(
"instrument",
"visit",
"detector")):
218 catalog = connectionTypes.Input(
219 doc=
"Input full-depth catalog of sources produced by CalibrateTask",
220 name=
"{catalogType}src",
221 storageClass=
"SourceCatalog",
222 dimensions=(
"instrument",
"visit",
"detector")
224 outputCatalog = connectionTypes.Output(
225 doc=
"Catalog of sources, `src` in Astropy/Parquet format. Columns are unchanged.",
226 name=
"{catalogType}source",
227 storageClass=
"ArrowAstropy",
228 dimensions=(
"instrument",
"visit",
"detector")
232class WriteSourceTableConfig(pipeBase.PipelineTaskConfig,
233 pipelineConnections=WriteSourceTableConnections):
237class WriteSourceTableTask(pipeBase.PipelineTask):
238 """Write source table to DataFrame Parquet format.
240 _DefaultName =
"writeSourceTable"
241 ConfigClass = WriteSourceTableConfig
243 def runQuantum(self, butlerQC, inputRefs, outputRefs):
244 inputs = butlerQC.get(inputRefs)
245 inputs[
"visit"] = butlerQC.quantum.dataId[
"visit"]
246 inputs[
"detector"] = butlerQC.quantum.dataId[
"detector"]
247 result = self.run(**inputs)
248 outputs = pipeBase.Struct(outputCatalog=result.table)
249 butlerQC.put(outputs, outputRefs)
251 def run(self, catalog, visit, detector, **kwargs):
252 """Convert `src` catalog to an Astropy table.
256 catalog: `afwTable.SourceCatalog`
257 catalog to be converted
258 visit, detector: `int`
259 Visit and detector ids to be added as columns.
261 Additional keyword arguments are ignored as a convenience for
262 subclasses that pass the same arguments to several different
267 result : `~lsst.pipe.base.Struct`
269 `astropy.table.Table` version of the input catalog
271 self.log.info(
"Generating DataFrame from src catalog visit,detector=%i,%i", visit, detector)
272 tbl = catalog.asAstropy()
275 tbl[
"detector"] = np.int16(detector)
277 return pipeBase.Struct(table=tbl)
280class WriteRecalibratedSourceTableConnections(WriteSourceTableConnections,
281 defaultTemplates={
"catalogType":
""},
282 dimensions=(
"instrument",
"visit",
"detector",
"skymap")):
283 visitSummary = connectionTypes.Input(
284 doc=
"Input visit-summary catalog with updated calibration objects.",
285 name=
"finalVisitSummary",
286 storageClass=
"ExposureCatalog",
287 dimensions=(
"instrument",
"visit",),
290 def __init__(self, config):
298 self.catalog = dataclasses.replace(self.catalog, deferGraphConstraint=
True)
301class WriteRecalibratedSourceTableConfig(WriteSourceTableConfig,
302 pipelineConnections=WriteRecalibratedSourceTableConnections):
304 doReevaluatePhotoCalib = pexConfig.Field(
307 doc=(
"Add or replace local photoCalib columns"),
309 doReevaluateSkyWcs = pexConfig.Field(
312 doc=(
"Add or replace local WCS columns and update the coord columns, coord_ra and coord_dec"),
316class WriteRecalibratedSourceTableTask(WriteSourceTableTask):
317 """Write source table to DataFrame Parquet format.
319 _DefaultName =
"writeRecalibratedSourceTable"
320 ConfigClass = WriteRecalibratedSourceTableConfig
322 def runQuantum(self, butlerQC, inputRefs, outputRefs):
323 inputs = butlerQC.get(inputRefs)
325 inputs[
"visit"] = butlerQC.quantum.dataId[
"visit"]
326 inputs[
"detector"] = butlerQC.quantum.dataId[
"detector"]
328 if self.config.doReevaluatePhotoCalib
or self.config.doReevaluateSkyWcs:
329 exposure = ExposureF()
330 inputs[
"exposure"] = self.prepareCalibratedExposure(
332 visitSummary=inputs[
"visitSummary"],
333 detectorId=butlerQC.quantum.dataId[
"detector"]
335 inputs[
"catalog"] = self.addCalibColumns(**inputs)
337 result = self.run(**inputs)
338 outputs = pipeBase.Struct(outputCatalog=result.table)
339 butlerQC.put(outputs, outputRefs)
341 def prepareCalibratedExposure(self, exposure, detectorId, visitSummary=None):
342 """Prepare a calibrated exposure and apply external calibrations
347 exposure : `lsst.afw.image.exposure.Exposure`
348 Input exposure to adjust calibrations. May be an empty Exposure.
350 Detector ID associated with the exposure.
351 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
352 Exposure catalog with all calibration objects. WCS and PhotoCalib
353 are always applied if ``visitSummary`` is provided and those
354 components are not `None`.
358 exposure : `lsst.afw.image.exposure.Exposure`
359 Exposure with adjusted calibrations.
361 if visitSummary
is not None:
362 row = visitSummary.find(detectorId)
364 raise pipeBase.NoWorkFound(f
"Visit summary for detector {detectorId} is missing.")
365 if (photoCalib := row.getPhotoCalib())
is None:
366 self.log.warning(
"Detector id %s has None for photoCalib in visit summary; "
367 "skipping reevaluation of photoCalib.", detectorId)
368 exposure.setPhotoCalib(
None)
370 exposure.setPhotoCalib(photoCalib)
371 if (skyWcs := row.getWcs())
is None:
372 self.log.warning(
"Detector id %s has None for skyWcs in visit summary; "
373 "skipping reevaluation of skyWcs.", detectorId)
374 exposure.setWcs(
None)
376 exposure.setWcs(skyWcs)
380 def addCalibColumns(self, catalog, exposure, **kwargs):
381 """Add replace columns with calibs evaluated at each centroid
383 Add or replace 'base_LocalWcs' and 'base_LocalPhotoCalib' columns in
384 a source catalog, by rerunning the plugins.
388 catalog : `lsst.afw.table.SourceCatalog`
389 catalog to which calib columns will be added
390 exposure : `lsst.afw.image.exposure.Exposure`
391 Exposure with attached PhotoCalibs and SkyWcs attributes to be
392 reevaluated at local centroids. Pixels are not required.
394 Additional keyword arguments are ignored to facilitate passing the
395 same arguments to several methods.
399 newCat: `lsst.afw.table.SourceCatalog`
400 Source Catalog with requested local calib columns
402 measureConfig = SingleFrameMeasurementTask.ConfigClass()
403 measureConfig.doReplaceWithNoise =
False
406 for slot
in measureConfig.slots:
407 setattr(measureConfig.slots, slot,
None)
409 measureConfig.plugins.names = []
410 if self.config.doReevaluateSkyWcs:
411 measureConfig.plugins.names.add(
"base_LocalWcs")
412 self.log.info(
"Re-evaluating base_LocalWcs plugin")
413 if self.config.doReevaluatePhotoCalib:
414 measureConfig.plugins.names.add(
"base_LocalPhotoCalib")
415 self.log.info(
"Re-evaluating base_LocalPhotoCalib plugin")
416 pluginsNotToCopy = tuple(measureConfig.plugins.names)
420 aliasMap = catalog.schema.getAliasMap()
421 mapper = afwTable.SchemaMapper(catalog.schema)
422 for item
in catalog.schema:
423 if not item.field.getName().startswith(pluginsNotToCopy):
424 mapper.addMapping(item.key)
426 schema = mapper.getOutputSchema()
427 measurement = SingleFrameMeasurementTask(config=measureConfig, schema=schema)
428 schema.setAliasMap(aliasMap)
429 newCat = afwTable.SourceCatalog(schema)
430 newCat.extend(catalog, mapper=mapper)
436 if self.config.doReevaluateSkyWcs
and exposure.wcs
is not None:
437 afwTable.updateSourceCoords(exposure.wcs, newCat)
438 wcsPlugin = measurement.plugins[
"base_LocalWcs"]
442 if self.config.doReevaluatePhotoCalib
and exposure.getPhotoCalib()
is not None:
443 pcPlugin = measurement.plugins[
"base_LocalPhotoCalib"]
448 if wcsPlugin
is not None:
449 wcsPlugin.measure(row, exposure)
450 if pcPlugin
is not None:
451 pcPlugin.measure(row, exposure)
456class PostprocessAnalysis(object):
457 """Calculate columns from DataFrames or handles storing DataFrames.
459 This object manages and organizes an arbitrary set of computations
460 on a catalog. The catalog is defined by a
461 `DeferredDatasetHandle` or `InMemoryDatasetHandle` object
462 (or list thereof), such as a ``deepCoadd_obj`` dataset, and the
463 computations are defined by a collection of
464 `~lsst.pipe.tasks.functors.Functor` objects (or, equivalently, a
465 ``CompositeFunctor``).
467 After the object is initialized, accessing the ``.df`` attribute (which
468 holds the `pandas.DataFrame` containing the results of the calculations)
469 triggers computation of said dataframe.
471 One of the conveniences of using this object is the ability to define a
472 desired common filter for all functors. This enables the same functor
473 collection to be passed to several different `PostprocessAnalysis` objects
474 without having to change the original functor collection, since the ``filt``
475 keyword argument of this object triggers an overwrite of the ``filt``
476 property for all functors in the collection.
478 This object also allows a list of refFlags to be passed, and defines a set
479 of default refFlags that are always included even if not requested.
481 If a list of DataFrames or Handles is passed, rather than a single one,
482 then the calculations will be mapped over all the input catalogs. In
483 principle, it should be straightforward to parallelize this activity, but
484 initial tests have failed (see TODO in code comments).
488 handles : `~lsst.daf.butler.DeferredDatasetHandle` or
489 `~lsst.pipe.base.InMemoryDatasetHandle` or
491 Source catalog(s) for computation.
492 functors : `list`, `dict`, or `~lsst.pipe.tasks.functors.CompositeFunctor`
493 Computations to do (functors that act on ``handles``).
494 If a dict, the output
495 DataFrame will have columns keyed accordingly.
496 If a list, the column keys will come from the
497 ``.shortname`` attribute of each functor.
499 filt : `str`, optional
500 Filter in which to calculate. If provided,
501 this will overwrite any existing ``.filt`` attribute
502 of the provided functors.
504 flags : `list`, optional
505 List of flags (per-band) to include in output table.
506 Taken from the ``meas`` dataset if applied to a multilevel Object Table.
508 refFlags : `list`, optional
509 List of refFlags (only reference band) to include in output table.
511 forcedFlags : `list`, optional
512 List of flags (per-band) to include in output table.
513 Taken from the ``forced_src`` dataset if applied to a
514 multilevel Object Table. Intended for flags from measurement plugins
515 only run during multi-band forced-photometry.
517 _defaultRefFlags = []
520 def __init__(self, handles, functors, filt=None, flags=None, refFlags=None, forcedFlags=None):
521 self.handles = handles
522 self.functors = functors
525 self.flags = list(flags)
if flags
is not None else []
526 self.forcedFlags = list(forcedFlags)
if forcedFlags
is not None else []
527 self.refFlags = list(self._defaultRefFlags)
528 if refFlags
is not None:
529 self.refFlags += list(refFlags)
534 def defaultFuncs(self):
535 funcs = dict(self._defaultFuncs)
540 additionalFuncs = self.defaultFuncs
541 additionalFuncs.update({flag:
Column(flag, dataset=
"forced_src")
for flag
in self.forcedFlags})
542 additionalFuncs.update({flag:
Column(flag, dataset=
"ref")
for flag
in self.refFlags})
543 additionalFuncs.update({flag:
Column(flag, dataset=
"meas")
for flag
in self.flags})
545 if isinstance(self.functors, CompositeFunctor):
550 func.funcDict.update(additionalFuncs)
551 func.filt = self.filt
557 return [name
for name, func
in self.func.funcDict.items()
if func.noDup]
565 def compute(self, dropna=False, pool=None):
567 if type(self.handles)
in (list, tuple):
569 dflist = [self.func(handle, dropna=dropna)
for handle
in self.handles]
573 dflist = pool.map(functools.partial(self.func, dropna=dropna), self.handles)
574 self._df = pd.concat(dflist)
576 self._df = self.func(self.handles, dropna=dropna)
581class TransformCatalogBaseConnections(pipeBase.PipelineTaskConnections,
583 """Expected Connections for subclasses of TransformCatalogBaseTask.
587 inputCatalog = connectionTypes.Input(
589 storageClass=
"DataFrame",
591 outputCatalog = connectionTypes.Output(
593 storageClass=
"ArrowAstropy",
597class TransformCatalogBaseConfig(pipeBase.PipelineTaskConfig,
598 pipelineConnections=TransformCatalogBaseConnections):
599 functorFile = pexConfig.Field(
601 doc=
"Path to YAML file specifying Science Data Model functors to use "
602 "when copying columns and computing calibrated values.",
606 primaryKey = pexConfig.Field(
608 doc=
"Name of column to be set as the DataFrame index. If None, the index"
609 "will be named `id`",
613 columnsFromDataId = pexConfig.ListField(
617 doc=
"Columns to extract from the dataId",
621class TransformCatalogBaseTask(pipeBase.PipelineTask):
622 """Base class for transforming/standardizing a catalog by applying functors
623 that convert units and apply calibrations.
625 The purpose of this task is to perform a set of computations on an input
626 ``DeferredDatasetHandle`` or ``InMemoryDatasetHandle`` that holds a
627 ``DataFrame`` dataset (such as ``deepCoadd_obj``), and write the results to
628 a new dataset (which needs to be declared in an ``outputDataset``
631 The calculations to be performed are defined in a YAML file that specifies
632 a set of functors to be computed, provided as a ``--functorFile`` config
633 parameter. An example of such a YAML file is the following:
640 args: slot_Centroid_x
643 args: slot_Centroid_y
645 functor: LocalNanojansky
647 - slot_PsfFlux_instFlux
648 - slot_PsfFlux_instFluxErr
649 - base_LocalPhotoCalib
650 - base_LocalPhotoCalibErr
652 functor: LocalNanojanskyErr
654 - slot_PsfFlux_instFlux
655 - slot_PsfFlux_instFluxErr
656 - base_LocalPhotoCalib
657 - base_LocalPhotoCalibErr
661 The names for each entry under "func" will become the names of columns in
662 the output dataset. All the functors referenced are defined in
663 `~lsst.pipe.tasks.functors`. Positional arguments to be passed to each
664 functor are in the `args` list, and any additional entries for each column
665 other than "functor" or "args" (e.g., ``'filt'``, ``'dataset'``) are
666 treated as keyword arguments to be passed to the functor initialization.
668 The "flags" entry is the default shortcut for `Column` functors.
669 All columns listed under "flags" will be copied to the output table
670 untransformed. They can be of any datatype.
671 In the special case of transforming a multi-level oject table with
672 band and dataset indices (deepCoadd_obj), these will be taked from the
673 ``meas`` dataset and exploded out per band.
675 There are two special shortcuts that only apply when transforming
676 multi-level Object (deepCoadd_obj) tables:
677 - The "refFlags" entry is shortcut for `Column` functor
678 taken from the ``ref`` dataset if transforming an ObjectTable.
679 - The "forcedFlags" entry is shortcut for `Column` functors.
680 taken from the ``forced_src`` dataset if transforming an ObjectTable.
681 These are expanded out per band.
684 This task uses the `lsst.pipe.tasks.postprocess.PostprocessAnalysis` object
685 to organize and excecute the calculations.
688 def _DefaultName(self):
689 raise NotImplementedError(
"Subclass must define the \"_DefaultName\" attribute.")
692 def outputDataset(self):
693 raise NotImplementedError(
"Subclass must define the \"outputDataset\" attribute.")
696 def inputDataset(self):
697 raise NotImplementedError(
"Subclass must define \"inputDataset\" attribute.")
700 def ConfigClass(self):
701 raise NotImplementedError(
"Subclass must define \"ConfigClass\" attribute.")
703 def __init__(self, *args, **kwargs):
704 super().__init__(*args, **kwargs)
705 if self.config.functorFile:
706 self.log.info(
"Loading tranform functor definitions from %s",
707 self.config.functorFile)
708 self.
funcs = CompositeFunctor.from_file(self.config.functorFile)
709 self.
funcs.update(dict(PostprocessAnalysis._defaultFuncs))
713 def runQuantum(self, butlerQC, inputRefs, outputRefs):
714 inputs = butlerQC.get(inputRefs)
715 if self.
funcs is None:
716 raise ValueError(
"config.functorFile is None. "
717 "Must be a valid path to yaml in order to run Task as a PipelineTask.")
718 result = self.run(handle=inputs[
"inputCatalog"], funcs=self.
funcs,
719 dataId=dict(outputRefs.outputCatalog.dataId.mapping))
720 butlerQC.put(result, outputRefs)
722 def run(self, handle, funcs=None, dataId=None, band=None):
723 """Do postprocessing calculations
725 Takes a ``DeferredDatasetHandle`` or ``InMemoryDatasetHandle`` or
726 ``DataFrame`` object and dataId,
727 returns a dataframe with results of postprocessing calculations.
731 handles : `~lsst.daf.butler.DeferredDatasetHandle` or
732 `~lsst.pipe.base.InMemoryDatasetHandle` or
733 `~pandas.DataFrame`, or list of these.
734 DataFrames from which calculations are done.
735 funcs : `~lsst.pipe.tasks.functors.Functor`
736 Functors to apply to the table's columns
737 dataId : dict, optional
738 Used to add a `patchId` column to the output dataframe.
739 band : `str`, optional
740 Filter band that is being processed.
744 result : `lsst.pipe.base.Struct`
745 Result struct, with a single ``outputCatalog`` attribute holding
746 the transformed catalog.
748 self.log.info(
"Transforming/standardizing the source table dataId: %s", dataId)
750 df = self.transform(band, handle, funcs, dataId).df
751 self.log.info(
"Made a table of %d columns and %d rows", len(df.columns), len(df))
754 raise UpstreamFailureNoWorkFound(
755 "Input catalog is empty, so there is nothing to transform/standardize",
758 result = pipeBase.Struct(outputCatalog=pandas_to_astropy(df))
761 def getFunctors(self):
764 def getAnalysis(self, handles, funcs=None, band=None):
767 analysis = PostprocessAnalysis(handles, funcs, filt=band)
770 def transform(self, band, handles, funcs, dataId):
771 analysis = self.getAnalysis(handles, funcs=funcs, band=band)
773 if dataId
and self.config.columnsFromDataId:
774 for key
in self.config.columnsFromDataId:
776 if key ==
"detector":
778 df[key] = np.int16(dataId[key])
780 df[key] = dataId[key]
782 raise ValueError(f
"'{key}' in config.columnsFromDataId not found in dataId: {dataId}")
784 if self.config.primaryKey:
785 if df.index.name != self.config.primaryKey
and self.config.primaryKey
in df:
786 df.reset_index(inplace=
True, drop=
True)
787 df.set_index(self.config.primaryKey, inplace=
True)
789 return pipeBase.Struct(
796 defaultTemplates={
"coaddName":
"deep"},
797 dimensions=(
"tract",
"patch",
"skymap")):
798 inputCatalog = connectionTypes.Input(
799 doc=
"The vertical concatenation of the {coaddName}_{meas|forced_src|psfs_multiprofit} catalogs, "
800 "stored as a DataFrame with a multi-level column index per-patch.",
801 dimensions=(
"tract",
"patch",
"skymap"),
802 storageClass=
"DataFrame",
803 name=
"{coaddName}Coadd_obj",
806 inputCatalogRef = connectionTypes.Input(
807 doc=
"Catalog marking the primary detection (which band provides a good shape and position)"
808 "for each detection in deepCoadd_mergeDet.",
809 dimensions=(
"tract",
"patch",
"skymap"),
810 storageClass=
"SourceCatalog",
811 name=
"{coaddName}Coadd_ref",
814 inputCatalogExpMultiprofit = connectionTypes.Input(
815 doc=
"Catalog of multiband Exponential fits.",
816 dimensions=(
"tract",
"patch",
"skymap"),
817 storageClass=
"ArrowAstropy",
818 name=
"{coaddName}Coadd_Exp_multiprofit",
821 inputCatalogSersicMultiprofit = connectionTypes.Input(
822 doc=
"Catalog of multiband Sersic fits.",
823 dimensions=(
"tract",
"patch",
"skymap"),
824 storageClass=
"ArrowAstropy",
825 name=
"{coaddName}Coadd_Sersic_multiprofit",
828 inputCatalogEpoch = connectionTypes.Input(
829 doc=
"Catalog of mean epochs for each object per band.",
830 dimensions=(
"tract",
"patch",
"skymap"),
831 storageClass=
"ArrowAstropy",
835 outputCatalog = connectionTypes.Output(
836 doc=
"Per-Patch Object Table of columns transformed from the deepCoadd_obj table per the standard "
838 dimensions=(
"tract",
"patch",
"skymap"),
839 storageClass=
"ArrowAstropy",
843 def __init__(self, *, config=None):
844 super().__init__(config=config)
845 if config.multilevelOutput:
846 self.outputCatalog = dataclasses.replace(self.outputCatalog, storageClass=
"DataFrame")
849class TransformObjectCatalogConfig(TransformCatalogBaseConfig,
850 pipelineConnections=TransformObjectCatalogConnections):
851 coaddName = pexConfig.Field(
856 outputBands = pexConfig.ListField(
860 doc=(
"These bands and only these bands will appear in the output,"
861 " NaN-filled if the input does not include them."
862 " If None, then use all bands found in the input.")
864 camelCase = pexConfig.Field(
867 doc=(
"Write per-band columns names with camelCase, else underscore "
868 "For example: gPsFlux instead of g_PsFlux.")
870 multilevelOutput = pexConfig.Field(
873 doc=(
"Whether results dataframe should have a multilevel column index (True) or be flat "
874 "and name-munged (False). If True, the output storage class will be "
875 "set to DataFrame, since astropy tables do not support multi-level indexing."),
876 deprecated=
"Support for multi-level outputs is deprecated and will be removed after v29.",
878 goodFlags = pexConfig.ListField(
881 doc=(
"List of 'good' flags that should be set False when populating empty tables. "
882 "All other flags are considered to be 'bad' flags and will be set to True.")
884 floatFillValue = pexConfig.Field(
887 doc=
"Fill value for float fields when populating empty tables."
889 integerFillValue = pexConfig.Field(
892 doc=
"Fill value for integer fields when populating empty tables."
895 def setDefaults(self):
896 super().setDefaults()
897 self.
functorFile = os.path.join(
"$PIPE_TASKS_DIR",
"schemas",
"Object.yaml")
898 self.primaryKey =
"objectId"
899 self.columnsFromDataId = [
"tract",
"patch"]
900 self.goodFlags = [
"calib_astrometry_used",
901 "calib_photometry_reserved",
902 "calib_photometry_used",
903 "calib_psf_candidate",
904 "calib_psf_reserved",
908class TransformObjectCatalogTask(TransformCatalogBaseTask):
909 """Produce a flattened Object Table to match the format specified in
912 Do the same set of postprocessing calculations on all bands.
914 This is identical to `TransformCatalogBaseTask`, except for that it does
915 the specified functor calculations for all filters present in the
916 input `deepCoadd_obj` table. Any specific ``"filt"`` keywords specified
917 by the YAML file will be superceded.
919 _DefaultName =
"transformObjectCatalog"
920 ConfigClass = TransformObjectCatalogConfig
923 datasets_multiband = (
"ref",
"epoch",
"Exp_multiprofit",
"Sersic_multiprofit")
925 def runQuantum(self, butlerQC, inputRefs, outputRefs):
926 inputs = butlerQC.get(inputRefs)
927 if self.
funcs is None:
928 raise ValueError(
"config.functorFile is None. "
929 "Must be a valid path to yaml in order to run Task as a PipelineTask.")
930 result = self.run(handle=inputs[
"inputCatalog"], funcs=self.
funcs,
931 dataId=dict(outputRefs.outputCatalog.dataId.mapping),
932 handle_ref=inputs[
"inputCatalogRef"],
933 handle_epoch=inputs[
"inputCatalogEpoch"],
934 handle_Exp_multiprofit=inputs[
"inputCatalogExpMultiprofit"],
935 handle_Sersic_multiprofit=inputs[
"inputCatalogSersicMultiprofit"],
937 butlerQC.put(result, outputRefs)
939 def run(self, handle, funcs=None, dataId=None, band=None, **kwargs):
943 if isinstance(funcs, CompositeFunctor):
944 funcDict_in = funcs.funcDict
945 elif isinstance(funcs, dict):
947 elif isinstance(funcs, list):
948 funcDict_in = {idx: v
for idx, v
in enumerate(funcs)}
950 raise TypeError(f
"Unsupported {type(funcs)=}")
953 funcDicts_multiband = {}
954 for dataset
in self.datasets_multiband:
955 if (handle_multi := kwargs.get(f
"handle_{dataset}"))
is None:
956 raise RuntimeError(f
"Missing required handle_{dataset} kwarg")
957 handles_multi[dataset] = handle_multi
958 funcDicts_multiband[dataset] = {}
962 templateDf = pd.DataFrame()
964 columns = handle.get(component=
"columns")
965 inputBands = columns.unique(level=1).values
967 outputBands = self.config.outputBands
if self.config.outputBands
else inputBands
972 columns_add_from_ref = defaultdict(list)
974 for name, func
in funcDict_in.items():
975 if func.dataset
in funcDicts_multiband:
977 if band := getattr(func,
"band_to_check",
None):
978 if band
not in outputBands:
981 elif hasattr(func,
"bands"):
986 func.bands = tuple(inputBands)
987 if hasattr(func,
"columns_ref"):
988 columns_add_from_ref[func.dataset].extend(func.columns_ref)
990 funcDict = funcDicts_multiband.get(func.dataset, funcDict_band)
991 funcDict[name] = func
996 for inputBand
in inputBands:
997 if inputBand
not in outputBands:
998 self.log.info(
"Ignoring %s band data in the input", inputBand)
1000 self.log.info(
"Transforming the catalog of band %s", inputBand)
1001 result = self.transform(inputBand, handle, funcs_band, dataId)
1002 dfDict[inputBand] = result.df
1003 analysisDict[inputBand] = result.analysis
1004 if templateDf.empty:
1005 templateDf = result.df
1008 for filt
in outputBands:
1009 if filt
not in dfDict:
1010 self.log.info(
"Adding empty columns for band %s", filt)
1011 dfTemp = templateDf.copy()
1012 for col
in dfTemp.columns:
1013 testValue = dfTemp[col].values[0]
1014 if isinstance(testValue, (np.bool_, pd.BooleanDtype)):
1016 if col
in self.config.goodFlags:
1020 elif isinstance(testValue, numbers.Integral):
1024 if isinstance(testValue, np.unsignedinteger):
1025 raise ValueError(
"Parquet tables may not have unsigned integer columns.")
1027 fillValue = self.config.integerFillValue
1029 fillValue = self.config.floatFillValue
1030 dfTemp[col].values[:] = fillValue
1031 dfDict[filt] = dfTemp
1034 df = pd.concat(dfDict, axis=1, names=[
"band",
"column"])
1035 name_index = df.index.name
1038 if not self.config.multilevelOutput:
1039 noDupCols = list(set.union(*[set(v.noDupCols)
for v
in analysisDict.values()]))
1040 if self.config.primaryKey
in noDupCols:
1041 noDupCols.remove(self.config.primaryKey)
1042 if dataId
and self.config.columnsFromDataId:
1043 noDupCols += self.config.columnsFromDataId
1044 df =
flattenFilters(df, noDupCols=noDupCols, camelCase=self.config.camelCase,
1045 inputBands=inputBands)
1051 for dataset, funcDict
in funcDicts_multiband.items():
1052 handle_multiband = handles_multi[dataset]
1053 df_dataset = handle_multiband.get()
1054 if isinstance(df_dataset, astropy.table.Table):
1056 if name_index
not in df_dataset.colnames:
1057 if self.config.primaryKey
in df_dataset.colnames:
1058 name_index_ap = self.config.primaryKey
1061 f
"Neither of {name_index=} nor {self.config.primaryKey=} appear in"
1062 f
" {df_dataset.colnames=} for {dataset=}"
1065 name_index_ap = name_index
1066 df_dataset = df_dataset.to_pandas().set_index(name_index_ap, drop=
False)
1067 elif isinstance(df_dataset, afwTable.SourceCatalog):
1068 df_dataset = df_dataset.asAstropy().to_pandas().set_index(name_index, drop=
False)
1070 if dataset ==
"ref":
1074 raise RuntimeError(f
"ref must be the first dataset, not {dataset}")
1076 for column
in {key:
None for key
in columns_add_from_ref.get(dataset, [])}:
1077 df_dataset[column] = df_ref[column]
1081 result = self.transform(
1083 pipeBase.InMemoryDatasetHandle(df_dataset, storageClass=
"DataFrame"),
1087 result.df.index.name = name_index
1089 if self.config.columnsFromDataId:
1090 columns_drop = [column
for column
in self.config.columnsFromDataId
if column
in result.df]
1092 result.df.drop(columns_drop, axis=1, inplace=
True)
1096 to_concat = pd.concat(
1097 {band: result.df
for band
in self.config.outputBands}, axis=1, names=[
"band",
"column"]
1098 )
if self.config.multilevelOutput
else result.df
1099 df = pd.concat([df, to_concat], axis=1)
1100 analysisDict[dataset] = result.analysis
1103 df.index.name = self.config.primaryKey
1105 if not self.config.multilevelOutput:
1106 tbl = pandas_to_astropy(df)
1110 self.log.info(
"Made a table of %d columns and %d rows", len(tbl.columns), len(tbl))
1112 return pipeBase.Struct(outputCatalog=tbl)
1115class ComputeColumnsAction(ConfigurableAction):
1116 """An action that computes multiple vectors from an input.
1118 This class is meant to be compatible with analysis_tools'
1119 AnalysisAction class, which cannot be a dependency of pipe_tasks."""
1121 def getInputSchema(self) -> dict[str, type[NDArray]]:
1122 """Return the required inputs for this action.
1124 This function is meant to be compatible with
1126 raise NotImplementedError(
"This method must be overloaded in subclasses")
1128 def __call__(self, table: astropy.table.Table) -> dict[str, NDArray]:
1129 """This method must return a dict of computed columns."""
1130 raise NotImplementedError(
"This method must be overloaded in subclasses")
1133class ExtendednessColumnActionBase(ComputeColumnsAction):
1134 bands = pexConfig.ListField[str](
1135 doc=
"The bands to make single-band outputs for.",
1136 default=[
"u",
"g",
"r",
"i",
"z",
"y"]
1138 bands_combined = pexConfig.DictField[str, str](
1139 doc=
"Multiband classification column specialization. Keys specify the"
1140 " name of the column and values are a comma-separated list of"
1141 " bands, all of which must be contained in the bands listed.",
1142 default={
"griz":
"g,r,i,z"},
1143 itemCheck=
lambda x: (len(y := x.split(
",")) > 1) & (len(set(y)) == len(y)),
1145 model_column_flux = pexConfig.Field[str](
1146 doc=
"The model flux column to use for computing the difference to"
1147 " to the S/N flux. Must contain the {band} and {model} templates.",
1148 default=
"{band}_{model}Flux",
1149 check=
lambda x: (
"{band}" in x)
and (
"{model}" in x,),
1151 model_column_flux_err = pexConfig.Field[str](
1152 doc=
"The model flux error column to use for computing the difference"
1153 " to the S/N flux. Must contain the {band} and {model} templates.",
1154 default=
"{band}_{model}FluxErr",
1155 check=
lambda x: (
"{band}" in x)
and (
"{model}" in x,),
1157 model_flux_name = pexConfig.Field[str](
1158 doc=
"The extended object model to use to compared to PSF model fluxes",
1161 output_column = pexConfig.Field[str](
1162 doc=
"Name of the output column. Must contain the {band} template",
1163 default=
"{band}_model_extendedness",
1164 check=
lambda x:
"{band}" in x,
1166 psf_column_flux = pexConfig.Field[str](
1167 doc=
"The name of the PSF flux column. Must contain the {band} template.",
1168 default=
"{band}_psfFlux",
1169 check=
lambda x:
"{band}" in x,
1171 psf_column_flux_err = pexConfig.Field[str](
1172 doc=
"The name of the PSF flux error column. Must contain the {band} template.",
1173 default=
"{band}_psfFluxErr",
1174 check=
lambda x:
"{band}" in x,
1176 size_column = pexConfig.Field[str](
1177 doc=
"The column to use for applying size cuts. Must contain the {axis} template.",
1178 default=
"exponential_reff_{axis}",
1181 def getInputSchema(self) -> Iterable[tuple[str, type[NDArray]]]:
1182 size_column = self.size_column
1184 (size_column.format(axis=axis), NDArray[float])
for axis
in (
"x",
"y")
1186 model = self.model_flux_name
1188 self.psf_column_flux, self.psf_column_flux_err,
1189 self.model_column_flux, self.model_column_flux_err,
1192 (column.format(band=band, model=model), NDArray[float])
for band
in self.bands
1200 for name, band_combined
in self.bands_combined.items():
1201 bands = band_combined.split(
",")
1202 bands_missing = [band
for band
in bands
if band
not in self.bands]
1205 f
"self.bands_combined[{name}] contains bands={bands_missing} not in {self.bands=}"
1208 raise ValueError(f
"Validation failed due to errors: {'; '.join(errors)}")
1210 def _get_fluxes(self, table, band: str):
1211 model = self.model_flux_name
1212 flux_psf, fluxerr_psf, flux_model, fluxerr_model = (
1213 np.array(table[column.format(band=band, model=model)])
1215 self.psf_column_flux, self.psf_column_flux_err,
1216 self.model_column_flux, self.model_column_flux_err,
1219 return flux_psf, fluxerr_psf, flux_model, fluxerr_model
1222class ModelExtendednessColumnAction(ExtendednessColumnActionBase):
1223 fluxerr_coefficent = pexConfig.Field[float](
1224 doc=
"The coefficient to multiply the flux error by when adding to the model flux.",
1226 check=
lambda x: x >= 0,
1228 fluxerr_stretch = pexConfig.Field[float](
1229 doc=
"The factor to multiply flux error-scaled ratios by to derive extendedness.",
1231 check=
lambda x: x > 0,
1233 good_sn_min = pexConfig.Field[float](
1234 doc=
"Minimum PSF S/N to include objects if"
1235 " min_n_good_to_shift_flux_ratio is > 0; ignored otherwise.",
1238 max_reff_compact = pexConfig.Field[float](
1239 doc=
"The maximum effective radius in pixels below which an object is"
1240 " classified as not extended, regardless of other parameter values.",
1243 min_n_good_to_shift_flux_ratio = pexConfig.Field[int](
1244 doc=
"Minimum number of objects with PSF S/N > good_sn_min and with "
1245 " size larger than max_reff_compact to use to compute the median "
1246 " PSF-to-model flux ratio, which is assumed to be 1 otherwise."
1247 " If this value is not >0, the median flux ratio will be kept 1.",
1251 def __call__(self, table: astropy.table.Table) -> dict[str, NDArray]:
1252 size_column = self.size_column
1253 size_model = np.sqrt(
1254 0.5*(table[size_column.format(axis=
'x')]**2 + table[size_column.format(axis=
'y')]**2)
1256 small = size_model < self.max_reff_compact
1258 band_mappings_to_process = {band: [band]
for band
in self.bands}
1259 band_mappings_to_process.update({k: v.split(
",")
for k, v
in self.bands_combined.items()})
1261 for output_band, input_bands
in band_mappings_to_process.items():
1262 if len(input_bands) > 1:
1263 flux_psf, fluxerr_psf_sq, flux_model, fluxerr_model_sq = (
1264 np.zeros(n_obj, dtype=float)
for _
in range(4))
1265 for input_band
in input_bands:
1266 flux_psf_b, fluxerr_psf_b, flux_model_b, fluxerr_model_b = self._get_fluxes(
1267 table, band=input_band)
1269 good = np.isfinite(flux_psf_b) & np.isfinite(flux_model_b) & (
1270 flux_psf_b > 0) & (flux_model_b > 0) & (fluxerr_psf_b > 0) & (fluxerr_model_b > 0)
1271 flux_psf[good] += flux_psf_b[good]
1272 flux_model[good] += flux_model_b[good]
1273 fluxerr_psf_sq[good] += fluxerr_psf_b[good]**2
1274 fluxerr_model_sq[good] += fluxerr_model_b[good]**2
1275 fluxerr_psf = np.sqrt(fluxerr_psf_sq)
1276 fluxerr_model = np.sqrt(fluxerr_model_sq)
1277 fluxerr_psf[fluxerr_psf == 0] = np.inf
1278 fluxerr_model[fluxerr_model == 0] = np.inf
1280 flux_psf, fluxerr_psf, flux_model, fluxerr_model = self._get_fluxes(
1281 table, band=input_bands[0])
1283 psf_sn = flux_psf/fluxerr_psf
1284 flux_ratio = np.array(flux_psf / flux_model)
1286 if self.min_n_good_to_shift_flux_ratio > 0:
1287 good = small & (psf_sn > self.good_sn_min)
1290 if np.sum(good ==
True) > self.min_n_good_to_shift_flux_ratio:
1291 flux_ratio *= 1./np.nanmedian(flux_ratio[good])
1293 flux_ratio_err = np.sqrt(
1294 (fluxerr_psf/flux_model)**2 + (fluxerr_model*fluxerr_psf/flux_model**2)**2
1296 extendedness = (1 - flux_ratio) + self.fluxerr_coefficent*flux_ratio_err
1297 extendedness *= np.sqrt(size_model/self.max_reff_compact)
1298 extendedness[(extendedness < 0) & (extendedness > -np.inf)] = 0
1300 stretch = self.fluxerr_stretch
1301 extendedness *= stretch
1302 extendedness = np.clip((stretch + 1)/stretch*extendedness/(1 + extendedness), 0, 1)
1304 column_out = self.output_column.format(band=output_band)
1305 output[column_out] = extendedness
1310 dimensions=(
"tract",
"skymap")):
1311 inputCatalogs = connectionTypes.Input(
1312 doc=
"Per-Patch objectTables conforming to the standard data model.",
1314 storageClass=
"ArrowAstropy",
1315 dimensions=(
"tract",
"patch",
"skymap"),
1319 outputCatalog = connectionTypes.Output(
1320 doc=
"Pre-tract horizontal concatenation of the input objectTables",
1321 name=
"objectTable_tract",
1322 storageClass=
"ArrowAstropy",
1323 dimensions=(
"tract",
"skymap"),
1328 pipelineConnections=ConsolidateObjectTableConnections):
1329 actions = ConfigurableActionStructField[ComputeColumnsAction](
1330 doc=
"Actions to add columns to the final object table",
1332 coaddName = pexConfig.Field[str](
1339 self.
actions.extendedness = ModelExtendednessColumnAction()
1343 """Write patch-merged source tables to a tract-level DataFrame Parquet file.
1345 Concatenates `objectTable` list into a per-visit `objectTable_tract`.
1347 _DefaultName =
"consolidateObjectTable"
1348 ConfigClass = ConsolidateObjectTableConfig
1350 inputDataset =
"objectTable"
1351 outputDataset =
"objectTable_tract"
1354 inputs = butlerQC.get(inputRefs)
1355 self.log.info(
"Concatenating %s per-patch Object Tables",
1356 len(inputs[
"inputCatalogs"]))
1357 table = TableVStack.vstack_handles(inputs[
"inputCatalogs"])
1358 for action
in self.config.actions:
1359 computed = action(table)
1360 for key, values
in computed.items():
1361 table[key] = values.astype(np.float32)
if values.dtype == np.float64
else values
1362 butlerQC.put(pipeBase.Struct(outputCatalog=table), outputRefs)
1366 defaultTemplates={
"catalogType":
""},
1367 dimensions=(
"instrument",
"visit",
"detector")):
1369 inputCatalog = connectionTypes.Input(
1370 doc=
"Wide input catalog of sources produced by WriteSourceTableTask",
1371 name=
"{catalogType}source",
1372 storageClass=
"DataFrame",
1373 dimensions=(
"instrument",
"visit",
"detector"),
1376 outputCatalog = connectionTypes.Output(
1377 doc=
"Narrower, per-detector Source Table transformed and converted per a "
1378 "specified set of functors",
1379 name=
"{catalogType}sourceTable",
1380 storageClass=
"ArrowAstropy",
1381 dimensions=(
"instrument",
"visit",
"detector")
1386 pipelineConnections=TransformSourceTableConnections):
1390 self.
functorFile = os.path.join(
"$PIPE_TASKS_DIR",
"schemas",
"Source.yaml")
1396 """Transform/standardize a source catalog
1398 _DefaultName =
"transformSourceTable"
1399 ConfigClass = TransformSourceTableConfig
1403 dimensions=(
"instrument",
"visit",),
1404 defaultTemplates={
"calexpType":
""}):
1405 calexp = connectionTypes.Input(
1406 doc=
"Processed exposures used for metadata",
1408 storageClass=
"ExposureF",
1409 dimensions=(
"instrument",
"visit",
"detector"),
1413 visitSummary = connectionTypes.Output(
1414 doc=(
"Per-visit consolidated exposure metadata. These catalogs use "
1415 "detector id for the id and are sorted for fast lookups of a "
1417 name=
"visitSummary",
1418 storageClass=
"ExposureCatalog",
1419 dimensions=(
"instrument",
"visit"),
1421 visitSummarySchema = connectionTypes.InitOutput(
1422 doc=
"Schema of the visitSummary catalog",
1423 name=
"visitSummary_schema",
1424 storageClass=
"ExposureCatalog",
1429 pipelineConnections=ConsolidateVisitSummaryConnections):
1430 """Config for ConsolidateVisitSummaryTask"""
1432 full = pexConfig.Field(
1433 "Whether to propate all exposure components. "
1434 "This adds PSF, aperture correction map, transmission curve, and detector, which can increase file "
1435 "size by more than factor of 10, but it makes the visit summaries produced by this task fully usable"
1436 "by tasks that were designed to run downstream of lsst.drp.tasks.UpdateVisitSummaryTask.",
1443 """Task to consolidate per-detector visit metadata.
1445 This task aggregates the following metadata from all the detectors in a
1446 single visit into an exposure catalog:
1450 - The physical_filter and band (if available).
1452 - The aperture correction map.
1453 - The transmission curve.
1454 - The psf size, shape, and effective area at the center of the detector.
1455 - The corners of the bounding box in right ascension/declination.
1457 Tests for this task are performed in ci_hsc_gen3.
1459 _DefaultName =
"consolidateVisitSummary"
1460 ConfigClass = ConsolidateVisitSummaryConfig
1464 self.
schema = afwTable.ExposureTable.makeMinimalSchema()
1465 self.
schema.addField(
"visit", type=
"L", doc=
"Visit number")
1466 self.
schema.addField(
"physical_filter", type=
"String", size=32, doc=
"Physical filter")
1467 self.
schema.addField(
"band", type=
"String", size=32, doc=
"Name of band")
1468 ExposureSummaryStats.update_schema(self.
schema)
1472 dataRefs = butlerQC.get(inputRefs.calexp)
1473 visit = dataRefs[0].dataId[
"visit"]
1475 self.log.debug(
"Concatenating metadata from %d per-detector calexps (visit %d)",
1476 len(dataRefs), visit)
1480 butlerQC.put(expCatalog, outputRefs.visitSummary)
1483 """Make a combined exposure catalog from a list of dataRefs.
1484 These dataRefs must point to exposures with wcs, summaryStats,
1485 and other visit metadata.
1490 Visit identification number.
1491 dataRefs : `list` of `lsst.daf.butler.DeferredDatasetHandle`
1492 List of dataRefs in visit.
1496 visitSummary : `lsst.afw.table.ExposureCatalog`
1497 Exposure catalog with per-detector summary information.
1499 cat = afwTable.ExposureCatalog(self.
schema)
1500 cat.resize(len(dataRefs))
1502 cat[
"visit"] = visit
1504 for i, dataRef
in enumerate(dataRefs):
1505 visitInfo = dataRef.get(component=
"visitInfo")
1506 filterLabel = dataRef.get(component=
"filter")
1507 summaryStats = dataRef.get(component=
"summaryStats")
1508 detector = dataRef.get(component=
"detector")
1509 wcs = dataRef.get(component=
"wcs")
1510 photoCalib = dataRef.get(component=
"photoCalib")
1511 bbox = dataRef.get(component=
"bbox")
1512 validPolygon = dataRef.get(component=
"validPolygon")
1516 rec.setVisitInfo(visitInfo)
1518 rec.setPhotoCalib(photoCalib)
1519 rec.setValidPolygon(validPolygon)
1521 if self.config.full:
1522 rec.setPsf(dataRef.get(component=
"psf"))
1523 rec.setApCorrMap(dataRef.get(component=
"apCorrMap"))
1524 rec.setTransmissionCurve(dataRef.get(component=
"transmissionCurve"))
1526 rec[
"physical_filter"] = filterLabel.physicalLabel
if filterLabel.hasPhysicalLabel()
else ""
1527 rec[
"band"] = filterLabel.bandLabel
if filterLabel.hasBandLabel()
else ""
1528 rec.setId(detector.getId())
1529 summaryStats.update_record(rec)
1532 raise pipeBase.NoWorkFound(
1533 "No detectors had sufficient information to make a visit summary row."
1536 metadata = dafBase.PropertyList()
1537 metadata.add(
"COMMENT",
"Catalog id is detector id, sorted.")
1539 metadata.add(
"COMMENT",
"Only detectors with data have entries.")
1540 cat.setMetadata(metadata)
1547 defaultTemplates={
"catalogType":
""},
1548 dimensions=(
"instrument",
"visit")):
1549 inputCatalogs = connectionTypes.Input(
1550 doc=
"Input per-detector Source Tables",
1551 name=
"{catalogType}sourceTable",
1552 storageClass=
"ArrowAstropy",
1553 dimensions=(
"instrument",
"visit",
"detector"),
1557 outputCatalog = connectionTypes.Output(
1558 doc=
"Per-visit concatenation of Source Table",
1559 name=
"{catalogType}sourceTable_visit",
1560 storageClass=
"ArrowAstropy",
1561 dimensions=(
"instrument",
"visit")
1566 pipelineConnections=ConsolidateSourceTableConnections):
1571 """Concatenate `sourceTable` list into a per-visit `sourceTable_visit`
1573 _DefaultName =
"consolidateSourceTable"
1574 ConfigClass = ConsolidateSourceTableConfig
1576 inputDataset =
"sourceTable"
1577 outputDataset =
"sourceTable_visit"
1581 detectorOrder = [ref.dataId[
"detector"]
for ref
in inputRefs.inputCatalogs]
1582 detectorOrder.sort()
1583 inputRefs = reorderRefs(inputRefs, detectorOrder, dataIdKey=
"detector")
1584 inputs = butlerQC.get(inputRefs)
1585 self.log.info(
"Concatenating %s per-detector Source Tables",
1586 len(inputs[
"inputCatalogs"]))
1587 table = TableVStack.vstack_handles(inputs[
"inputCatalogs"])
1588 butlerQC.put(pipeBase.Struct(outputCatalog=table), outputRefs)
1592 dimensions=(
"instrument",),
1593 defaultTemplates={
"calexpType":
""}):
1594 visitSummaryRefs = connectionTypes.Input(
1595 doc=
"Data references for per-visit consolidated exposure metadata",
1596 name=
"finalVisitSummary",
1597 storageClass=
"ExposureCatalog",
1598 dimensions=(
"instrument",
"visit"),
1602 outputCatalog = connectionTypes.Output(
1603 doc=
"CCD and Visit metadata table",
1604 name=
"ccdVisitTable",
1605 storageClass=
"ArrowAstropy",
1606 dimensions=(
"instrument",)
1611 pipelineConnections=MakeCcdVisitTableConnections):
1612 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
1616 """Produce a `ccdVisitTable` from the visit summary exposure catalogs.
1618 _DefaultName =
"makeCcdVisitTable"
1619 ConfigClass = MakeCcdVisitTableConfig
1621 def run(self, visitSummaryRefs):
1622 """Make a table of ccd information from the visit summary catalogs.
1626 visitSummaryRefs : `list` of `lsst.daf.butler.DeferredDatasetHandle`
1627 List of DeferredDatasetHandles pointing to exposure catalogs with
1628 per-detector summary information.
1632 result : `~lsst.pipe.base.Struct`
1633 Results struct with attribute:
1636 Catalog of ccd and visit information.
1639 for visitSummaryRef
in visitSummaryRefs:
1640 visitSummary = visitSummaryRef.get()
1641 if not visitSummary:
1643 visitInfo = visitSummary[0].getVisitInfo()
1646 strip_provenance_from_fits_header(visitSummary.metadata)
1649 summaryTable = visitSummary.asAstropy()
1650 selectColumns = [
"id",
"visit",
"physical_filter",
"band",
"ra",
"dec",
1651 "pixelScale",
"zenithDistance",
1652 "expTime",
"zeroPoint",
"psfSigma",
"skyBg",
"skyNoise",
1653 "astromOffsetMean",
"astromOffsetStd",
"nPsfStar",
1654 "psfStarDeltaE1Median",
"psfStarDeltaE2Median",
1655 "psfStarDeltaE1Scatter",
"psfStarDeltaE2Scatter",
1656 "psfStarDeltaSizeMedian",
"psfStarDeltaSizeScatter",
1657 "psfStarScaledDeltaSizeScatter",
"psfTraceRadiusDelta",
1658 "psfApFluxDelta",
"psfApCorrSigmaScaledDelta",
1659 "maxDistToNearestPsf",
"starEMedian",
"starUnNormalizedEMedian",
1660 "effTime",
"effTimePsfSigmaScale",
1661 "effTimeSkyBgScale",
"effTimeZeroPointScale",
1663 ccdEntry = summaryTable[selectColumns]
1668 ccdEntry.rename_column(
"visit",
"visitId")
1669 ccdEntry.rename_column(
"id",
"detectorId")
1673 ccdEntry[
"decl"] = ccdEntry[
"dec"]
1675 ccdEntry[
"ccdVisitId"] = [
1676 self.config.idGenerator.apply(
1677 visitSummaryRef.dataId,
1678 detector=detector_id,
1685 for detector_id
in summaryTable[
"id"]
1687 ccdEntry[
"detector"] = summaryTable[
"id"]
1688 ccdEntry[
"seeing"] = (
1689 visitSummary[
"psfSigma"] * visitSummary[
"pixelScale"] * np.sqrt(8 * np.log(2))
1691 ccdEntry[
"skyRotation"] = visitInfo.getBoresightRotAngle().asDegrees()
1692 ccdEntry[
"expMidpt"] = np.datetime64(visitInfo.date.nsecs(scale=dafBase.DateTime.TAI),
"ns")
1693 ccdEntry[
"expMidptMJD"] = visitInfo.getDate().get(dafBase.DateTime.MJD)
1694 expTime = visitInfo.getExposureTime()
1695 ccdEntry[
"obsStart"] = (
1696 ccdEntry[
"expMidpt"] - 0.5 * np.timedelta64(int(expTime * 1E9),
"ns")
1698 expTime_days = expTime / (60*60*24)
1699 ccdEntry[
"obsStartMJD"] = ccdEntry[
"expMidptMJD"] - 0.5 * expTime_days
1700 ccdEntry[
"darkTime"] = visitInfo.getDarkTime()
1701 ccdEntry[
"xSize"] = summaryTable[
"bbox_max_x"] - summaryTable[
"bbox_min_x"]
1702 ccdEntry[
"ySize"] = summaryTable[
"bbox_max_y"] - summaryTable[
"bbox_min_y"]
1703 ccdEntry[
"llcra"] = summaryTable[
"raCorners"][:, 0]
1704 ccdEntry[
"llcdec"] = summaryTable[
"decCorners"][:, 0]
1705 ccdEntry[
"ulcra"] = summaryTable[
"raCorners"][:, 1]
1706 ccdEntry[
"ulcdec"] = summaryTable[
"decCorners"][:, 1]
1707 ccdEntry[
"urcra"] = summaryTable[
"raCorners"][:, 2]
1708 ccdEntry[
"urcdec"] = summaryTable[
"decCorners"][:, 2]
1709 ccdEntry[
"lrcra"] = summaryTable[
"raCorners"][:, 3]
1710 ccdEntry[
"lrcdec"] = summaryTable[
"decCorners"][:, 3]
1714 ccdEntries.append(ccdEntry)
1716 outputCatalog = astropy.table.vstack(ccdEntries, join_type=
"exact")
1717 return pipeBase.Struct(outputCatalog=outputCatalog)
1721 dimensions=(
"instrument",),
1722 defaultTemplates={
"calexpType":
""}):
1723 visitSummaries = connectionTypes.Input(
1724 doc=
"Per-visit consolidated exposure metadata",
1725 name=
"finalVisitSummary",
1726 storageClass=
"ExposureCatalog",
1727 dimensions=(
"instrument",
"visit",),
1731 outputCatalog = connectionTypes.Output(
1732 doc=
"Visit metadata table",
1734 storageClass=
"ArrowAstropy",
1735 dimensions=(
"instrument",)
1740 pipelineConnections=MakeVisitTableConnections):
1745 """Produce a `visitTable` from the visit summary exposure catalogs.
1747 _DefaultName =
"makeVisitTable"
1748 ConfigClass = MakeVisitTableConfig
1750 def run(self, visitSummaries):
1751 """Make a table of visit information from the visit summary catalogs.
1755 visitSummaries : `list` of `lsst.afw.table.ExposureCatalog`
1756 List of exposure catalogs with per-detector summary information.
1759 result : `~lsst.pipe.base.Struct`
1760 Results struct with attribute:
1763 Catalog of visit information.
1766 for visitSummary
in visitSummaries:
1767 visitSummary = visitSummary.get()
1768 if not visitSummary:
1770 visitRow = visitSummary[0]
1771 visitInfo = visitRow.getVisitInfo()
1774 visitEntry[
"visitId"] = visitRow[
"visit"]
1775 visitEntry[
"visit"] = visitRow[
"visit"]
1776 visitEntry[
"physical_filter"] = visitRow[
"physical_filter"]
1777 visitEntry[
"band"] = visitRow[
"band"]
1778 raDec = visitInfo.getBoresightRaDec()
1779 visitEntry[
"ra"] = raDec.getRa().asDegrees()
1780 visitEntry[
"dec"] = raDec.getDec().asDegrees()
1784 visitEntry[
"decl"] = visitEntry[
"dec"]
1786 visitEntry[
"skyRotation"] = visitInfo.getBoresightRotAngle().asDegrees()
1787 azAlt = visitInfo.getBoresightAzAlt()
1788 visitEntry[
"azimuth"] = azAlt.getLongitude().asDegrees()
1789 visitEntry[
"altitude"] = azAlt.getLatitude().asDegrees()
1790 visitEntry[
"zenithDistance"] = 90 - azAlt.getLatitude().asDegrees()
1791 visitEntry[
"airmass"] = visitInfo.getBoresightAirmass()
1792 expTime = visitInfo.getExposureTime()
1793 visitEntry[
"expTime"] = expTime
1794 visitEntry[
"expMidpt"] = np.datetime64(visitInfo.date.nsecs(scale=dafBase.DateTime.TAI),
"ns")
1795 visitEntry[
"expMidptMJD"] = visitInfo.getDate().get(dafBase.DateTime.MJD)
1796 visitEntry[
"obsStart"] = visitEntry[
"expMidpt"] - 0.5 * np.timedelta64(int(expTime * 1E9),
"ns")
1797 expTime_days = expTime / (60*60*24)
1798 visitEntry[
"obsStartMJD"] = visitEntry[
"expMidptMJD"] - 0.5 * expTime_days
1799 visitEntries.append(visitEntry)
1805 outputCatalog = astropy.table.Table(rows=visitEntries)
1806 return pipeBase.Struct(outputCatalog=outputCatalog)
1809@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
1810 "This task will be removed after v30.",
1811 version=
"v29.0", category=FutureWarning)
1813 dimensions=(
"instrument",
"visit",
"detector",
"skymap",
"tract")):
1815 inputCatalog = connectionTypes.Input(
1816 doc=
"Primary per-detector, single-epoch forced-photometry catalog. "
1817 "By default, it is the output of ForcedPhotCcdTask on calexps",
1819 storageClass=
"SourceCatalog",
1820 dimensions=(
"instrument",
"visit",
"detector",
"skymap",
"tract")
1822 inputCatalogDiff = connectionTypes.Input(
1823 doc=
"Secondary multi-epoch, per-detector, forced photometry catalog. "
1824 "By default, it is the output of ForcedPhotCcdTask run on image differences.",
1826 storageClass=
"SourceCatalog",
1827 dimensions=(
"instrument",
"visit",
"detector",
"skymap",
"tract")
1829 outputCatalog = connectionTypes.Output(
1830 doc=
"InputCatalogs horizonatally joined on `objectId` in DataFrame parquet format",
1831 name=
"mergedForcedSource",
1832 storageClass=
"DataFrame",
1833 dimensions=(
"instrument",
"visit",
"detector",
"skymap",
"tract")
1837@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
1838 "This task will be removed after v30.",
1839 version=
"v29.0", category=FutureWarning)
1841 pipelineConnections=WriteForcedSourceTableConnections):
1842 key = lsst.pex.config.Field(
1843 doc=
"Column on which to join the two input tables on and make the primary key of the output",
1849@deprecated(reason=
"This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. "
1850 "This task will be removed after v30.",
1851 version=
"v29.0", category=FutureWarning)
1853 """Merge and convert per-detector forced source catalogs to DataFrame Parquet format.
1855 Because the predecessor ForcedPhotCcdTask operates per-detector,
1856 per-tract, (i.e., it has tract in its dimensions), detectors
1857 on the tract boundary may have multiple forced source catalogs.
1859 The successor task TransformForcedSourceTable runs per-patch
1860 and temporally-aggregates overlapping mergedForcedSource catalogs from all
1861 available multiple epochs.
1863 _DefaultName =
"writeForcedSourceTable"
1864 ConfigClass = WriteForcedSourceTableConfig
1867 inputs = butlerQC.get(inputRefs)
1868 inputs[
"visit"] = butlerQC.quantum.dataId[
"visit"]
1869 inputs[
"detector"] = butlerQC.quantum.dataId[
"detector"]
1870 inputs[
"band"] = butlerQC.quantum.dataId[
"band"]
1871 outputs = self.
run(**inputs)
1872 butlerQC.put(outputs, outputRefs)
1874 def run(self, inputCatalog, inputCatalogDiff, visit, detector, band=None):
1876 for table, dataset,
in zip((inputCatalog, inputCatalogDiff), (
"calexp",
"diff")):
1877 df = table.asAstropy().to_pandas().set_index(self.config.key, drop=
False)
1878 df = df.reindex(sorted(df.columns), axis=1)
1881 df[
"detector"] = np.int16(detector)
1882 df[
"band"] = band
if band
else pd.NA
1883 df.columns = pd.MultiIndex.from_tuples([(dataset, c)
for c
in df.columns],
1884 names=(
"dataset",
"column"))
1888 outputCatalog = functools.reduce(
lambda d1, d2: d1.join(d2), dfs)
1889 return pipeBase.Struct(outputCatalog=outputCatalog)
1893 dimensions=(
"instrument",
"skymap",
"patch",
"tract")):
1895 inputCatalogs = connectionTypes.Input(
1896 doc=
"DataFrames of merged ForcedSources produced by WriteForcedSourceTableTask",
1897 name=
"mergedForcedSource",
1898 storageClass=
"DataFrame",
1899 dimensions=(
"instrument",
"visit",
"detector",
"skymap",
"tract"),
1903 referenceCatalog = connectionTypes.Input(
1904 doc=
"Reference catalog which was used to seed the forcedPhot. Columns "
1905 "objectId, detect_isPrimary, detect_isTractInner, detect_isPatchInner "
1908 storageClass=
"DataFrame",
1909 dimensions=(
"tract",
"patch",
"skymap"),
1912 outputCatalog = connectionTypes.Output(
1913 doc=
"Narrower, temporally-aggregated, per-patch ForcedSource Table transformed and converted per a "
1914 "specified set of functors",
1915 name=
"forcedSourceTable",
1916 storageClass=
"ArrowAstropy",
1917 dimensions=(
"tract",
"patch",
"skymap")
1922 pipelineConnections=TransformForcedSourceTableConnections):
1923 referenceColumns = pexConfig.ListField(
1925 default=[
"detect_isPrimary",
"detect_isTractInner",
"detect_isPatchInner"],
1927 doc=
"Columns to pull from reference catalog",
1929 keyRef = lsst.pex.config.Field(
1930 doc=
"Column on which to join the two input tables on and make the primary key of the output",
1934 key = lsst.pex.config.Field(
1935 doc=
"Rename the output DataFrame index to this name",
1937 default=
"forcedSourceId",
1942 self.
functorFile = os.path.join(
"$PIPE_TASKS_DIR",
"schemas",
"ForcedSource.yaml")
1947 """Transform/standardize a ForcedSource catalog
1949 Transforms each wide, per-detector forcedSource DataFrame per the
1950 specification file (per-camera defaults found in ForcedSource.yaml).
1951 All epochs that overlap the patch are aggregated into one per-patch
1952 narrow-DataFrame file.
1954 No de-duplication of rows is performed. Duplicate resolutions flags are
1955 pulled in from the referenceCatalog: `detect_isPrimary`,
1956 `detect_isTractInner`,`detect_isPatchInner`, so that user may de-duplicate
1957 for analysis or compare duplicates for QA.
1959 The resulting table includes multiple bands. Epochs (MJDs) and other useful
1960 per-visit rows can be retreived by joining with the CcdVisitTable on
1963 _DefaultName =
"transformForcedSourceTable"
1964 ConfigClass = TransformForcedSourceTableConfig
1967 inputs = butlerQC.get(inputRefs)
1968 if self.funcs
is None:
1969 raise ValueError(
"config.functorFile is None. "
1970 "Must be a valid path to yaml in order to run Task as a PipelineTask.")
1971 outputs = self.
run(inputs[
"inputCatalogs"], inputs[
"referenceCatalog"], funcs=self.funcs,
1972 dataId=dict(outputRefs.outputCatalog.dataId.mapping))
1974 butlerQC.put(outputs, outputRefs)
1976 def run(self, inputCatalogs, referenceCatalog, funcs=None, dataId=None, band=None):
1978 refColumns = list(self.config.referenceColumns)
1979 refColumns.append(self.config.keyRef)
1980 ref = referenceCatalog.get(parameters={
"columns": refColumns})
1981 if ref.index.name != self.config.keyRef:
1987 ref.set_index(self.config.keyRef, inplace=
True)
1988 self.log.info(
"Aggregating %s input catalogs" % (len(inputCatalogs)))
1989 for handle
in inputCatalogs:
1990 result = self.transform(
None, handle, funcs, dataId)
1992 dfs.append(result.df.join(ref, how=
"inner"))
1994 outputCatalog = pd.concat(dfs)
1996 if outputCatalog.empty:
1997 raise NoWorkFound(f
"No forced photometry rows for {dataId}.")
2001 outputCatalog.index.rename(self.config.keyRef, inplace=
True)
2003 outputCatalog.reset_index(inplace=
True)
2005 if "forcedSourceId" in outputCatalog.columns:
2008 outputCatalog.set_index(
"forcedSourceId", inplace=
True, verify_integrity=
True)
2010 outputCatalog.index.rename(self.config.key, inplace=
True)
2012 self.log.info(
"Made a table of %d columns and %d rows",
2013 len(outputCatalog.columns), len(outputCatalog))
2014 return pipeBase.Struct(outputCatalog=pandas_to_astropy(outputCatalog))
2018 defaultTemplates={
"catalogType":
""},
2019 dimensions=(
"instrument",
"tract")):
2020 inputCatalogs = connectionTypes.Input(
2021 doc=
"Input per-patch DataFrame Tables to be concatenated",
2022 name=
"{catalogType}ForcedSourceTable",
2023 storageClass=
"DataFrame",
2024 dimensions=(
"tract",
"patch",
"skymap"),
2028 outputCatalog = connectionTypes.Output(
2029 doc=
"Output per-tract concatenation of DataFrame Tables",
2030 name=
"{catalogType}ForcedSourceTable_tract",
2031 storageClass=
"DataFrame",
2032 dimensions=(
"tract",
"skymap"),
2037 pipelineConnections=ConsolidateTractConnections):
2039 doUseSchema = pexConfig.Field(
2042 doc=
"Use an existing schema to coerce the data types of the output columns."
2044 schemaDir = pexConfig.Field(
2046 doc=
"Path to the directory containing schema definitions.",
2047 default=os.path.join(
"${SDM_SCHEMAS_DIR}",
2051 schemaFile = pexConfig.Field(
2053 doc=
"Yaml file specifying the schema of the output catalog.",
2056 tableName = pexConfig.Field(
2058 doc=
"Name of the table in the schema file to read.",
2064 """Concatenate any per-patch, dataframe list into a single
2065 per-tract DataFrame.
2067 _DefaultName =
"ConsolidateTract"
2068 ConfigClass = ConsolidateTractConfig
2071 inputs = butlerQC.get(inputRefs)
2074 self.log.info(
"Concatenating %s per-patch %s Tables",
2075 len(inputs[
"inputCatalogs"]),
2076 inputRefs.inputCatalogs[0].datasetType.name)
2077 df = pd.concat(inputs[
"inputCatalogs"])
2078 if self.config.doUseSchema:
2079 schemaFile = os.path.join(self.config.schemaDir, self.config.schemaFile)
2080 schema = readSdmSchemaFile(schemaFile)
2081 df = convertDataFrameToSdmSchema(schema, df, tableName=self.config.tableName)
2082 butlerQC.put(pipeBase.Struct(outputCatalog=df), outputRefs)
2086 pipeBase.PipelineTaskConnections,
2087 dimensions=(
"instrument",
"tract")
2089 inputCatalogs = connectionTypes.Input(
2090 doc=
"Parents of the deblended objects",
2091 name=
"object_parent_patch",
2092 storageClass=
"SourceCatalog",
2093 dimensions=(
"tract",
"patch",
"skymap"),
2097 outputCatalog = connectionTypes.Output(
2098 doc=
"Output per-tract concatenation of DataFrame Tables",
2099 name=
"object_parent",
2100 storageClass=
"ArrowAstropy",
2101 dimensions=(
"tract",
"skymap"),
2106 pipeBase.PipelineTaskConfig,
2107 pipelineConnections=ConsolidateParentTractConnections,
2113 """Concatenate any per-patch, dataframe list into a single
2114 per-tract DataFrame.
2116 _DefaultName =
"ConsolidateTract"
2117 ConfigClass = ConsolidateParentTractConfig
2120 self.log.info(
"Concatenating %s per-patch %s Tables",
2121 len(inputRefs.inputCatalogs),
2122 inputRefs.inputCatalogs[0].datasetType.name)
2125 for ref
in inputRefs.inputCatalogs:
2126 catalog = butlerQC.get(ref)
2127 table = catalog.asAstropy()
2130 table.rename_column(
"id",
"objectId")
2131 table.rename_column(
"parent",
"parentObjectId")
2132 table.rename_column(
"merge_peak_sky",
"sky_object")
2135 table[
"tract"] = ref.dataId[
"tract"]
2136 table[
"patch"] = ref.dataId[
"patch"]
2138 tables.append(table)
2139 outputTable = astropy.table.vstack(tables, join_type=
"exact")
2140 butlerQC.put(pipeBase.Struct(outputCatalog=outputTable), outputRefs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
_combineExposureMetadata(self, visit, dataRefs)
run(self, visitSummaryRefs)
run(self, visitSummaries)
run(self, inputCatalogs, referenceCatalog, funcs=None, dataId=None, band=None)
runQuantum(self, butlerQC, inputRefs, outputRefs)
runQuantum(self, butlerQC, inputRefs, outputRefs)
run(self, inputCatalog, inputCatalogDiff, visit, detector, band=None)
flattenFilters(df, noDupCols=["coord_ra", "coord_dec"], camelCase=False, inputBands=None)