From d6bfd7ca666ed326dc8a71ade6a8194140ec3c4d Mon Sep 17 00:00:00 2001 From: Erin Howard Date: Wed, 18 Dec 2024 13:07:00 -0800 Subject: [PATCH 1/5] Add vector action to return unique items. --- .../tools/actions/vector/vectorActions.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/python/lsst/analysis/tools/actions/vector/vectorActions.py b/python/lsst/analysis/tools/actions/vector/vectorActions.py index a13e8edd4..ac6d13e70 100644 --- a/python/lsst/analysis/tools/actions/vector/vectorActions.py +++ b/python/lsst/analysis/tools/actions/vector/vectorActions.py @@ -36,6 +36,7 @@ "RAcosDec", "AngularSeparation", "IsMatchedObjectSameClass", + "UniqueAction", ) import logging @@ -441,3 +442,17 @@ def getInputSchema(self) -> KeyedDataSchema: yield self.key_is_ref_star, Vector yield self.key_is_target_galaxy, Vector yield self.key_is_target_star, Vector + + +class UniqueAction(VectorAction): + """Return the unique items from a vector.""" + + vectorKey = Field[str](doc="The vector key to return the unique values from.") + + def __call__(self, data: KeyedData, **kwargs) -> Vector: + mask = kwargs.get("mask") + result = data[self.vectorKey][mask] + return np.array(set(list(result[0]))) + + def getInputSchema(self) -> KeyedDataSchema: + yield self.vectorKey, Vector From a7becfd2baeeffdb9a120c8fb0c310acb30ce022 Mon Sep 17 00:00:00 2001 From: Erin Howard Date: Wed, 4 Jun 2025 23:51:30 -0700 Subject: [PATCH 2/5] Add patch selector. --- .../tools/actions/vector/selectors.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/python/lsst/analysis/tools/actions/vector/selectors.py b/python/lsst/analysis/tools/actions/vector/selectors.py index 04d09a799..66d5216de 100644 --- a/python/lsst/analysis/tools/actions/vector/selectors.py +++ b/python/lsst/analysis/tools/actions/vector/selectors.py @@ -38,6 +38,7 @@ "VisitPlotFlagSelector", "ThresholdSelector", "BandSelector", + "PatchSelector", "MatchingFlagSelector", "MagSelector", "InjectedClassSelector", @@ -552,6 +553,37 @@ def __call__(self, data: KeyedData, **kwargs) -> Vector: return cast(Vector, mask) +class PatchSelector(VectorAction): + """Makes a mask for sources observed in a specified set of patches.""" + + vectorKey = Field[str](doc="Key of the Vector which defines the patch.", default="patch") + patches = ListField[str]( + doc="The patches to select. `None` indicates no patch selection applied.", + default=[], + ) + + def getInputSchema(self) -> KeyedDataSchema: + return ((self.vectorKey, Vector),) + + def __call__(self, data: KeyedData, **kwargs) -> Vector: + patches: Optional[tuple[str, ...]] + match kwargs: + case {"patch": patch} if not self.patches and self.patches == []: + patches = (patch,) + case {"patches": patches} if not self.patches and self.patches == []: + patches = patches + case _ if self.patches: + patches = tuple(self.patches) + case _: + patches = None + if patches: + mask = np.isin(data[self.vectorKey], patches).flatten() + else: + # No patch selection is applied, i.e., select all rows + mask = np.full(len(data[self.vectorKey]), True) # type: ignore + return cast(Vector, mask) + + class ParentObjectSelector(FlagSelector): """Select only parent objects that are not sky objects.""" From 6e7534a2868019e01c2c3d3a15d37a5a50cadac1 Mon Sep 17 00:00:00 2001 From: Erin Howard Date: Wed, 4 Jun 2025 20:20:32 -0700 Subject: [PATCH 3/5] Add plot action for coadd depth. --- .../analysis/tools/actions/plot/__init__.py | 1 + .../tools/actions/plot/coaddDepthPlot.py | 159 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 python/lsst/analysis/tools/actions/plot/coaddDepthPlot.py diff --git a/python/lsst/analysis/tools/actions/plot/__init__.py b/python/lsst/analysis/tools/actions/plot/__init__.py index af77f1712..d67cec138 100644 --- a/python/lsst/analysis/tools/actions/plot/__init__.py +++ b/python/lsst/analysis/tools/actions/plot/__init__.py @@ -1,5 +1,6 @@ from .barPlots import * from .calculateRange import * +from .coaddDepthPlot import * from .colorColorFitPlot import * from .completenessPlot import * from .diaSkyPlot import * diff --git a/python/lsst/analysis/tools/actions/plot/coaddDepthPlot.py b/python/lsst/analysis/tools/actions/plot/coaddDepthPlot.py new file mode 100644 index 000000000..ba573fb2c --- /dev/null +++ b/python/lsst/analysis/tools/actions/plot/coaddDepthPlot.py @@ -0,0 +1,159 @@ +# This file is part of analysis_tools. +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import annotations + +__all__ = ("CoaddDepthPlot",) + +from typing import TYPE_CHECKING, Any, Mapping + +from matplotlib.figure import Figure +from matplotlib.lines import Line2D +import matplotlib.pyplot as plt + +from lsst.utils.plotting import publication_plots, set_rubin_plotstyle + +from ...interfaces import PlotAction, Vector +from ..vector import BandSelector, PatchSelector +from .plotUtils import addPlotInfo + +if TYPE_CHECKING: + from ...interfaces import KeyedData, KeyedDataSchema + +bands_dict = publication_plots.get_band_dicts() + + +class CoaddDepthPlot(PlotAction): + """Make a plot of pixels per coadd depth.""" + + def setDefaults(self): + super().setDefaults() + + def getInputSchema(self) -> KeyedDataSchema: + base: list[tuple[str, type[Vector]]] = [] + base.append(("patch", Vector)) + base.append(("band", Vector)) + base.append(("depth", Vector)) + base.append(("pixels", Vector)) + return base + + def __call__(self, data: KeyedData, **kwargs) -> Figure: + self._validateInput(data) + return self.makePlot(data, **kwargs) + + def _validateInput(self, data: KeyedData) -> None: + needed = set(k[0] for k in self.getInputSchema()) + if not needed.issubset(data.keys()): + raise ValueError(f"Input data does not contain all required keys: {self.getInputSchema()}") + + def makePlot(self, data: KeyedData, plotInfo: Mapping[str, str] | None = None, **kwargs: Any) -> Figure: + """Make the plot. + + Parameters + ---------- + `KeyedData` + The catalog to plot the points from. + + plotInfo : `dict` + A dictionary of the plot information. + + Returns + ------- + fig : `~matplotlib.figure.Figure` + The resulting figure. + """ + set_rubin_plotstyle() + fig = plt.figure(dpi=300, figsize=(20, 20)) + + max_depth = max(data['depth']) + max_pixels = max(data['pixels']) + + plt.subplots_adjust(hspace=0, wspace=0) + + patch_counter = 90 # The top left corner of a tract is patch 90 + m = 0 # subplot index + while patch_counter >= 0: + for n in range(10): # column index + ax = plt.subplot(10, 10, m + 1) # there are 10x10 patches per tract + patchSelector = PatchSelector(vectorKey='patch', patches=[patch_counter]) + patch_mask = patchSelector(data) + + if patch_counter in data['patch']: + uniqueBands = set(data['band'][patch_mask]) + for band in uniqueBands: + color = bands_dict['colors'][band] + markerstyle = bands_dict['symbols'][band] + bandSelector = BandSelector(vectorKey='band', bands=[band]) + band_mask = bandSelector(data) + + tot_mask = (patch_mask) & (band_mask) + + ax.plot(data['depth'][tot_mask], data['pixels'][tot_mask], + color=color, linewidth=0, ls=None, + marker=markerstyle, ms=4, alpha=0.75, + label=f'{band}') + ax.grid(alpha=0.5) + + # Chart formatting + # Need a solution for ax.set_xscale when max_depth is high, + # but not all patches/bands have quality coverage. + ax.set_yscale('log') + ax.set_xlim(0, max_depth + 5) + ax.set_ylim(5, max_pixels) + # Can we somehow generalize ax.set_xticks? + # ax.set_xticks(np.arange(0, max_depth, 20)) + ax.tick_params(axis="both", which="minor") + ax.tick_params(axis='both', which="both", top=False, right=False) + + # Only label axes of the farmost left and bottom row of plots + if (n != 0): + ax.set_yticklabels([]) + ax.tick_params(axis='y', which='both', length=0) + if (patch_counter not in range(10)): + ax.set_xticklabels([]) + ax.tick_params(axis='x', which='both', length=0) + + ax.set_title(f"patch {patch_counter}", y=0.85) + patch_counter += 1 + m += 1 + patch_counter -= 2*(n+1) + fig.supxlabel('Number of input visits (n_image depth)', y=0.075) + fig.supylabel('Count (pixels)', x=0.075) + legend_elements = [ + Line2D([0], [0], color=bands_dict['colors']['u'], + lw=0, marker=bands_dict['symbols']['u'], label='u'), + Line2D([0], [0], color=bands_dict['colors']['g'], + lw=0, marker=bands_dict['symbols']['g'], label='g'), + Line2D([0], [0], color=bands_dict['colors']['r'], + lw=0, marker=bands_dict['symbols']['r'], label='r'), + Line2D([0], [0], color=bands_dict['colors']['i'], + lw=0, marker=bands_dict['symbols']['i'], label='i'), + Line2D([0], [0], color=bands_dict['colors']['z'], + lw=0, marker=bands_dict['symbols']['z'], label='z'), + Line2D([0], [0], color=bands_dict['colors']['y'], + lw=0, marker=bands_dict['symbols']['y'], label='y'), + ] + plt.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(1.05, 10)) + + if plotInfo is not None: + fig = addPlotInfo(fig, plotInfo) + + return fig From cd8808e1d6d1375083ee4eb587fd3176576888f1 Mon Sep 17 00:00:00 2001 From: Erin Howard Date: Wed, 11 Dec 2024 13:05:03 -0800 Subject: [PATCH 4/5] Add tasks and pipeline for template metrics. --- pipelines/templateQualityCore.yaml | 22 +++ .../tools/actions/vector/selectors.py | 2 +- .../analysis/tools/atools/coaddInputCount.py | 99 ++++++++++++- python/lsst/analysis/tools/tasks/__init__.py | 3 + .../tools/tasks/coaddDepthSummaryAnalysis.py | 138 ++++++++++++++++++ .../tools/tasks/coaddDepthSummaryPlot.py | 94 ++++++++++++ .../tasks/coaddDepthTableTractAnalysis.py | 58 ++++++++ 7 files changed, 411 insertions(+), 5 deletions(-) create mode 100644 pipelines/templateQualityCore.yaml create mode 100644 python/lsst/analysis/tools/tasks/coaddDepthSummaryAnalysis.py create mode 100644 python/lsst/analysis/tools/tasks/coaddDepthSummaryPlot.py create mode 100644 python/lsst/analysis/tools/tasks/coaddDepthTableTractAnalysis.py diff --git a/pipelines/templateQualityCore.yaml b/pipelines/templateQualityCore.yaml new file mode 100644 index 000000000..ac12d7d17 --- /dev/null +++ b/pipelines/templateQualityCore.yaml @@ -0,0 +1,22 @@ +description: | + Tier1 plots and metrics to assess template quality. +tasks: + analyzeCoaddDepthCore: + class: lsst.analysis.tools.tasks.CoaddDepthSummaryAnalysisTask + config: + connections.coaddType: template + coaddDepthMetricTract: + class: lsst.analysis.tools.tasks.CoaddDepthTableTractAnalysisTask + config: + atools.coadd_depth_summary_metrics: CoaddQualityCheck + python: | + from lsst.analysis.tools.atools import CoaddQualityCheck + coaddDepthPlot: + class: lsst.analysis.tools.tasks.CoaddDepthSummaryPlotTask + config: + connections.coaddType: template + # plot will be called n_image_template_CoaddDepthPlot + # 'n_image' originates from the task outputName; 'template' originates from the line below + atools.template: CoaddQualityPlot + python: | + from lsst.analysis.tools.atools import CoaddQualityPlot diff --git a/python/lsst/analysis/tools/actions/vector/selectors.py b/python/lsst/analysis/tools/actions/vector/selectors.py index 66d5216de..15a2bc2f3 100644 --- a/python/lsst/analysis/tools/actions/vector/selectors.py +++ b/python/lsst/analysis/tools/actions/vector/selectors.py @@ -557,7 +557,7 @@ class PatchSelector(VectorAction): """Makes a mask for sources observed in a specified set of patches.""" vectorKey = Field[str](doc="Key of the Vector which defines the patch.", default="patch") - patches = ListField[str]( + patches = ListField[int]( doc="The patches to select. `None` indicates no patch selection applied.", default=[], ) diff --git a/python/lsst/analysis/tools/atools/coaddInputCount.py b/python/lsst/analysis/tools/atools/coaddInputCount.py index 18bff6166..09a933b15 100644 --- a/python/lsst/analysis/tools/atools/coaddInputCount.py +++ b/python/lsst/analysis/tools/atools/coaddInputCount.py @@ -20,18 +20,24 @@ # along with this program. If not, see . from __future__ import annotations -__all__ = ("CoaddInputCount",) +__all__ = ("CoaddInputCount", "CoaddQualityCheck", "CoaddQualityPlot") from ..actions.plot.calculateRange import MinMax from ..actions.plot.skyPlot import SkyPlot -from ..actions.scalar.scalarActions import MeanAction, MedianAction, SigmaMadAction +from ..actions.plot.coaddDepthPlot import CoaddDepthPlot +from ..actions.scalar.scalarActions import MeanAction, MedianAction, SigmaMadAction, StdevAction from ..actions.vector import CoaddPlotFlagSelector, LoadVector, SnSelector from ..interfaces import AnalysisTool +from lsst.pex.config import ListField + class CoaddInputCount(AnalysisTool): - """skyPlot and associated metrics indicating the number - of exposures that have gone into creating a coadd. + """Coadd-wide metrics pertaining to how many exposures have gone into + a deep coadd. + + This AnalysisTool is designed to run on an object table, which is only + created for deep coadds, not template coadds. """ def setDefaults(self): @@ -86,3 +92,88 @@ def setDefaults(self): "mean": "{band}_inputCount_mean", "sigmaMad": "{band}_inputCount_sigmaMad", } + + +class CoaddQualityCheck(AnalysisTool): + """Compute the percentage of each coadd that has a number of input + exposures exceeding a threshold. + + This AnalysisTool is designed to run on any coadd, provided a + coadd_depth_table is created first (via CoaddDepthSummaryAnalysisTask). + + For example, if exactly half of a coadd patch contains 15 overlapping + constituent visits and half contains fewer, the value computed for + `depth_above_threshold_12` would be 50. + + These values come from the n_image data product, which is an image + identical to the coadd but with pixel values of the number of input + images instead of flux or counts. n_images are persisted during + coadd assembly. + """ + + threshold_list = ListField( + default=[1, 3, 5, 12], + dtype=int, + doc="The n_image pixel value thresholds.", + ) + + quantile_list = ListField( + default=[5, 10, 25, 50, 75, 90, 95], + dtype=int, + doc="The percentiles at which to compute n_image values, in ascending order.", + ) + + def setDefaults(self): + super().setDefaults() + + self.process.buildActions.patch = LoadVector(vectorKey="patch") + self.process.buildActions.bands = LoadVector(vectorKey="band") + + def finalize(self): + for threshold in self.threshold_list: + name = f"depth_above_threshold_{threshold}" + setattr(self.process.buildActions, + name, + LoadVector(vectorKey=name)) + setattr(self.process.calculateActions, + f"{name}_median", + MedianAction(vectorKey=name)) + setattr(self.process.calculateActions, + f"{name}_stdev", + StdevAction(vectorKey=name)) + + # The units for the quantity are dimensionless (percentage) + self.produce.metric.units[f"{name}_median"] = "" + self.produce.metric.units[f"{name}_stdev"] = "" + + for quantile in self.quantile_list: + name = f"depth_{quantile}_percentile" + setattr(self.process.buildActions, + name, + LoadVector(vectorKey=name)) + setattr(self.process.calculateActions, + f"{name}_median", + MedianAction(vectorKey=name)) + setattr(self.process.calculateActions, + f"{name}_stdev", + StdevAction(vectorKey=name)) + + # The units for the quantity are dimensionless (depth) + self.produce.metric.units[f"{name}_median"] = "" + self.produce.metric.units[f"{name}_stdev"] = "" + + +class CoaddQualityPlot(AnalysisTool): + """Make a plot of coadd depth. + """ + + parameterizedBand: bool = False + + def setDefaults(self): + super().setDefaults() + self.process.buildActions.patch = LoadVector(vectorKey="patch") + self.process.buildActions.band = LoadVector(vectorKey="band") + self.process.buildActions.depth = LoadVector(vectorKey="depth") + self.process.buildActions.pixels = LoadVector(vectorKey="pixels") + + self.produce.plot = CoaddDepthPlot() diff --git a/python/lsst/analysis/tools/tasks/__init__.py b/python/lsst/analysis/tools/tasks/__init__.py index bfa9b9d66..0f7374ca8 100644 --- a/python/lsst/analysis/tools/tasks/__init__.py +++ b/python/lsst/analysis/tools/tasks/__init__.py @@ -6,6 +6,9 @@ from .calibrationAnalysis import * from .catalogMatch import * from .ccdVisitTableAnalysis import * +from .coaddDepthSummaryAnalysis import * +from .coaddDepthSummaryPlot import * +from .coaddDepthTableTractAnalysis import * from .diaFakesDetectorVisitAnalysis import * from .diaFakesVisitAnalysis import * from .diaObjectDetectorVisitAnalysis import * diff --git a/python/lsst/analysis/tools/tasks/coaddDepthSummaryAnalysis.py b/python/lsst/analysis/tools/tasks/coaddDepthSummaryAnalysis.py new file mode 100644 index 000000000..7174e0295 --- /dev/null +++ b/python/lsst/analysis/tools/tasks/coaddDepthSummaryAnalysis.py @@ -0,0 +1,138 @@ +# This file is part of analysis_tools. +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations +from astropy.table import Table + +import numpy as np + +__all__ = ( + "CoaddDepthSummaryAnalysisConfig", + "CoaddDepthSummaryAnalysisTask", +) + +from lsst.pex.config import ListField +from lsst.pipe.base import connectionTypes as cT +from lsst.pipe.base import Struct + +from ..interfaces import AnalysisBaseConfig, AnalysisBaseConnections, AnalysisPipelineTask + + +class CoaddDepthSummaryAnalysisConnections( + AnalysisBaseConnections, + dimensions=("tract", "skymap"), + defaultTemplates={"coaddType": "", # set as either deep or template in the pipeline + "outputName": "coadd_depth_table"}, +): + data = cT.Input( + doc="Coadd n_image to load from the butler (pixel values are the number of input images).", + name="{coaddType}_coadd_n_image", + storageClass="ImageU", + multiple=True, + dimensions=("tract", "patch", "band", "skymap"), + deferLoad=True, + ) + + statTable = cT.Output( + doc="Table with resulting n_image based depth statistics.", + name="{outputName}", + storageClass="ArrowAstropy", + dimensions=("tract", "skymap"), + ) + + +class CoaddDepthSummaryAnalysisConfig(AnalysisBaseConfig, + pipelineConnections=CoaddDepthSummaryAnalysisConnections): + threshold_list = ListField( + default=[1, 3, 5, 12], + dtype=int, + doc="The n_image pixel value thresholds, in ascending order.", + ) + + quantile_list = ListField( + default=[5, 10, 25, 50, 75, 90, 95], + dtype=int, + doc="The percentiles at which to compute n_image values, in ascending order.", + ) + + +class CoaddDepthSummaryAnalysisTask(AnalysisPipelineTask): + ConfigClass = CoaddDepthSummaryAnalysisConfig + _DefaultName = "coaddDepthSummaryAnalysis" + + def runQuantum(self, butlerQC, inputRefs, outputRefs): + inputs = butlerQC.get(inputRefs) + outputs = self.run(inputs) + butlerQC.put(outputs, outputRefs) + + def run(self, inputs): + t = Table() + bands = [] + patches = [] + medians = [] + stdevs = [] + stats = [] + quantiles = [] + + for n_image_handle in inputs["data"]: + n_image = n_image_handle.get() + data_id = n_image_handle.dataId + band = str(data_id.band.name) + patch = int(data_id.patch.id) + median = np.nanmedian(n_image.array) + stdev = np.nanstd(n_image.array) + + bands.append(band) + patches.append(patch) + medians.append(median) + stdevs.append(stdev) + + band_patch_stats = [] + for threshold in self.config.threshold_list: + # Calculate the percentage of the image with an image depth + # above the given threshold. + stat = np.sum(n_image.array > threshold) * 100 / ( + n_image.getHeight() * n_image.getWidth()) + band_patch_stats.append(stat) + + stats.append(band_patch_stats) + + # Calculate the quantiles for image depth + # across the whole n_image array. + quantile = list(np.percentile(n_image.array, + q=self.config.quantile_list)) + quantiles.append(quantile) + + threshold_col_names = [f"depth_above_threshold_{threshold}" + for threshold in self.config.threshold_list] + quantile_col_names = [f"depth_{q}_percentile" for q in self.config.quantile_list] + + # Construct the Astropy table + data = [patches, bands, medians, stdevs] + \ + list(zip(*stats)) + \ + list(zip(*quantiles)) + names = ["patch", "band", "medians", "stdevs"] + \ + threshold_col_names + \ + quantile_col_names + dtype = ["int", "str", "float", "float"] + \ + ["float" for x in range(len(list(zip(*stats))))] + \ + ["int" for y in range(len(list(zip(*quantiles))))] + t = Table(data=data, names=names, dtype=dtype) + return Struct(statTable=t) diff --git a/python/lsst/analysis/tools/tasks/coaddDepthSummaryPlot.py b/python/lsst/analysis/tools/tasks/coaddDepthSummaryPlot.py new file mode 100644 index 000000000..caaf60ce3 --- /dev/null +++ b/python/lsst/analysis/tools/tasks/coaddDepthSummaryPlot.py @@ -0,0 +1,94 @@ +# This file is part of analysis_tools. +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations + +import numpy as np + +__all__ = ( + "CoaddDepthSummaryPlotConfig", + "CoaddDepthSummaryPlotTask", +) +from lsst.pipe.base import connectionTypes as cT +from lsst.pipe.base import Struct + +from ..interfaces import AnalysisBaseConfig, AnalysisBaseConnections, AnalysisPipelineTask, KeyedData + + +class CoaddDepthSummaryPlotConnections( + AnalysisBaseConnections, + dimensions=("tract", "skymap"), + defaultTemplates={"coaddType": "", + "outputName": "n_image", }, +): + + n_image_data = cT.Input( + doc="Coadd n_image to load from the butler (pixel values are the number of input images).", + name="{coaddType}_coadd_n_image", + storageClass="ImageU", + multiple=True, + dimensions=("tract", "patch", "band", "skymap"), + deferLoad=True, + ) + + +class CoaddDepthSummaryPlotConfig(AnalysisBaseConfig, + pipelineConnections=CoaddDepthSummaryPlotConnections): + pass + + +class CoaddDepthSummaryPlotTask(AnalysisPipelineTask): + ConfigClass = CoaddDepthSummaryPlotConfig + _DefaultName = "coaddDepthSummaryPlot" + + def runQuantum(self, butlerQC, inputRefs, outputRefs): + inputs = butlerQC.get(inputRefs) + outputs = self.run(data={'n_image_data': inputs['n_image_data']}) + butlerQC.put(outputs, outputRefs) + + def run(self, *, data: KeyedData | None = None, **kwargs) -> Struct: + """Load n_images and use them to make a plot illustrating coadd depth. + """ + bands = [] + patches = [] + + depths = [] + pixels = [] + + for n_image_handle in data['n_image_data']: + n_image = n_image_handle.get() + data_id = n_image_handle.dataId + band = str(data_id.band.name) + patch = int(data_id.patch.id) + + depth, pixel = np.unique(n_image.array, return_counts=True) + + depths.extend(depth) + pixels.extend(pixel) + + for i in range(len(depth)): + bands.append(band) + patches.append(patch) + + pixel_data = {'patch': patches, 'band': bands, 'depth': depths, 'pixels': pixels} + + outputs = super().run(data=pixel_data, **kwargs) # this creates a struct for the output + + return outputs diff --git a/python/lsst/analysis/tools/tasks/coaddDepthTableTractAnalysis.py b/python/lsst/analysis/tools/tasks/coaddDepthTableTractAnalysis.py new file mode 100644 index 000000000..6f6225881 --- /dev/null +++ b/python/lsst/analysis/tools/tasks/coaddDepthTableTractAnalysis.py @@ -0,0 +1,58 @@ +# This file is part of analysis_tools. +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations + +__all__ = ( + "CoaddDepthTableTractAnalysisConnections", + "CoaddDepthTableTractAnalysisConfig", + "CoaddDepthTableTractAnalysisTask", +) + +import lsst.pex.config as pexConfig +from lsst.pipe.base import connectionTypes as cT + +from ..interfaces import AnalysisBaseConfig, AnalysisBaseConnections, AnalysisPipelineTask + + +class CoaddDepthTableTractAnalysisConnections( + AnalysisBaseConnections, + dimensions=("tract", "skymap"), + defaultTemplates={"inputName": "coadd_depth_table", + "outputName": "coadd_depth_summary"}, +): + data = cT.Input( + doc="Table with coadd depth statistics based on n_image values.", + name="{inputName}", + storageClass="ArrowAstropy", + dimensions=("tract", "skymap"), + deferLoad=True, + ) + + +class CoaddDepthTableTractAnalysisConfig( + AnalysisBaseConfig, pipelineConnections=CoaddDepthTableTractAnalysisConnections +): + load_skymap = pexConfig.Field[bool](doc="Whether to load the skymap.", default=True) + + +class CoaddDepthTableTractAnalysisTask(AnalysisPipelineTask): + ConfigClass = CoaddDepthTableTractAnalysisConfig + _DefaultName = "coaddDepthTableTractAnalysis" From cf9d4ed04e7936caa10d2c7381cab488864190bc Mon Sep 17 00:00:00 2001 From: Erin Howard Date: Fri, 27 Jun 2025 17:10:50 -0700 Subject: [PATCH 5/5] Add deep_coadd equivalent template metrics pipeline. --- pipelines/deepCoaddQualityCore.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pipelines/deepCoaddQualityCore.yaml diff --git a/pipelines/deepCoaddQualityCore.yaml b/pipelines/deepCoaddQualityCore.yaml new file mode 100644 index 000000000..4ea79bb46 --- /dev/null +++ b/pipelines/deepCoaddQualityCore.yaml @@ -0,0 +1,22 @@ +description: | + Tier1 plots and metrics to assess deep_coadd quality. +tasks: + analyzeCoaddDepthCore: + class: lsst.analysis.tools.tasks.CoaddDepthSummaryAnalysisTask + config: + connections.coaddType: deep + coaddDepthMetricTract: + class: lsst.analysis.tools.tasks.CoaddDepthTableTractAnalysisTask + config: + atools.coadd_depth_summary_metrics: CoaddQualityCheck + python: | + from lsst.analysis.tools.atools import CoaddQualityCheck + coaddDepthPlot: + class: lsst.analysis.tools.tasks.CoaddDepthSummaryPlotTask + config: + connections.coaddType: deep + # plot will be called n_image_deep_CoaddDepthPlot + # 'n_image' originates from the task outputName; 'deep' originates from the line below + atools.deep: CoaddQualityPlot + python: | + from lsst.analysis.tools.atools import CoaddQualityPlot