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
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/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
diff --git a/python/lsst/analysis/tools/actions/vector/selectors.py b/python/lsst/analysis/tools/actions/vector/selectors.py
index 04d09a799..15a2bc2f3 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[int](
+ 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."""
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
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"