diff --git a/config/objectTableCoreRefCatMatchPhotomWholeSkyPlot_dp2Pub.py b/config/objectTableCoreRefCatMatchPhotomWholeSkyPlot_dp2Pub.py new file mode 100644 index 000000000..734221638 --- /dev/null +++ b/config/objectTableCoreRefCatMatchPhotomWholeSkyPlot_dp2Pub.py @@ -0,0 +1,23 @@ +# Configuration for instances of lsst.analysis.tools.tasks.WholeSkyAnalysisTask +# that aggregates metrics from objectTableCoreRefCatMatch analysis. + +from lsst.analysis.tools.atools import WholeSkyPlotTool + +# Keys with `band` +keysWithBand = [ + "photomDiffMetrics_{band}_ref_photom_offset_coadd", + "photomDiffMetrics_{band}_ref_photom_offset_sigmaMad_coadd", +] +if hasattr(parameters, "objectTableCoreRefCatMatchWholeSkyPlotKeysWithBand"): + keysWithBand = parameters.objectTableCoreRefCatMatchWholeSkyPlotKeysWithBand + +for keyWithBand in keysWithBand: + atoolName = keyWithBand.replace("_{band}", "") + setattr(config.atools, atoolName, WholeSkyPlotTool) + atool = getattr(config.atools, atoolName) + setattr(atool, "metric", keyWithBand) + setattr(atool, "publicationStyle", True) + if "sigmaMad" not in atoolName: + setattr(atool, "fixAroundZero", True) + +config.addOutputNamePrefix = True diff --git a/config/objectTableCoreRefCatMatchWholeSkyPlot_dp2Pub.py b/config/objectTableCoreRefCatMatchWholeSkyPlot_dp2Pub.py new file mode 100644 index 000000000..4ef9ef8da --- /dev/null +++ b/config/objectTableCoreRefCatMatchWholeSkyPlot_dp2Pub.py @@ -0,0 +1,25 @@ +# Configuration for instances of lsst.analysis.tools.tasks.WholeSkyAnalysisTask +# that aggregates metrics from objectTableCoreRefCatMatch analysis. + +from lsst.analysis.tools.atools import WholeSkyPlotTool + +# Keys with `band` +keysWithBand = [ + "astromDiffRAScatterPlot_{band}_ref_ra_offset_coadd", + "astromDiffRAScatterPlot_{band}_ref_ra_offset_sigmaMad_coadd", + "astromDiffDecScatterPlot_{band}_ref_dec_offset_coadd", + "astromDiffDecScatterPlot_{band}_ref_dec_offset_sigmaMad_coadd", +] +if hasattr(parameters, "objectTableCoreRefCatMatchWholeSkyPlotKeysWithBand"): + keysWithBand = parameters.objectTableCoreRefCatMatchWholeSkyPlotKeysWithBand + +for keyWithBand in keysWithBand: + atoolName = keyWithBand.replace("_{band}", "") + setattr(config.atools, atoolName, WholeSkyPlotTool) + atool = getattr(config.atools, atoolName) + setattr(atool, "metric", keyWithBand) + setattr(atool, "publicationStyle", True) + if "sigmaMad" not in atoolName: + setattr(atool, "fixAroundZero", True) + +config.addOutputNamePrefix = True diff --git a/config/objectTableCoreWholeSkyPlot_dp2Pub.py b/config/objectTableCoreWholeSkyPlot_dp2Pub.py new file mode 100644 index 000000000..c24eb1ddc --- /dev/null +++ b/config/objectTableCoreWholeSkyPlot_dp2Pub.py @@ -0,0 +1,25 @@ +# Configuration for instances of lsst.analysis.tools.tasks.WholeSkyAnalysisTask +# that aggregates metrics from objectTableCore analysis. + +from lsst.analysis.tools.atools import WholeSkyPlotTool + +# Keys with `band` +keysWithBand = [ + "psfCModelScatter_{band}_psf_cModel_diff_median", + "shapeSizeFractionalDiff_{band}_highSNStars_median", + "psfSersicScatter_{band}_psf_sersic_diff_median", + "fiveSigmaDepth_{band}_median5sigmaDepth", +] +if hasattr(parameters, "objectTableCoreWholeSkyPlotKeysWithBand"): + keysWithBand = parameters.objectTableCoreWholeSkyPlotKeysWithBand + +for keyWithBand in keysWithBand: + atoolName = keyWithBand.replace("_{band}", "") + setattr(config.atools, atoolName, WholeSkyPlotTool) + atool = getattr(config.atools, atoolName) + setattr(atool, "metric", keyWithBand) + setattr(atool, "publicationStyle", True) + if "Depth" not in atoolName: + setattr(atool, "fixAroundZero", True) + +config.addOutputNamePrefix = True diff --git a/pipelines/DP2Summary.yaml b/pipelines/DP2Summary.yaml new file mode 100644 index 000000000..6109d957b --- /dev/null +++ b/pipelines/DP2Summary.yaml @@ -0,0 +1,39 @@ +description: | + Tier1 plots and metrics to assess the data quality across the whole sky +tasks: + makeMetricTableObjectTableCore: + class: lsst.analysis.tools.tasks.MakeMetricTableTask + config: + connections.metricBundleName: "object_metrics" + connections.outputTableName: "object_metrics_table" + objectTableCoreWholeSkyPlot: + class: lsst.analysis.tools.tasks.WholeSkyAnalysisTask + config: + connections.inputName: "object_metrics_table" + file: $ANALYSIS_TOOLS_DIR/config/objectTableCoreWholeSkyPlot_dp2Pub.py + makeMetricTableObjectTableCoreRefCatMatchPhotom: + class: lsst.analysis.tools.tasks.MakeMetricTableTask + config: + connections.metricBundleName: "object_ref_match_photom_metrics" + connections.outputTableName: "object_ref_match_photom_metrics_table" + objectTableCoreRefCatMatchPhotomWholeSkyPlot: + class: lsst.analysis.tools.tasks.WholeSkyAnalysisTask + config: + connections.inputName: "object_ref_match_photom_metrics_table" + connections.outputName: "objectTableCoreRefCatMatchPhotom_wholeSky" + file: $ANALYSIS_TOOLS_DIR/config/objectTableCoreRefCatMatchPhotomWholeSkyPlot_dp2Pub.py + python: | + from lsst.analysis.tools.atools import * + makeMetricTableObjectTableCoreRefCatMatch: + class: lsst.analysis.tools.tasks.MakeMetricTableTask + config: + connections.metricBundleName: "object_ref_match_astrom_metrics" + connections.outputTableName: "object_ref_match_astrom_metrics_table" + objectTableCoreRefCatMatchWholeSkyPlot: + class: lsst.analysis.tools.tasks.WholeSkyAnalysisTask + config: + connections.inputName: "object_ref_match_astrom_metrics_table" + connections.outputName: "objectTableCoreRefCatMatchAstrom_wholeSky" + file: $ANALYSIS_TOOLS_DIR/config/objectTableCoreRefCatMatchWholeSkyPlot_dp2Pub.py + python: | + from lsst.analysis.tools.atools import * diff --git a/pipelines/coaddQualityCore.yaml b/pipelines/coaddQualityCore.yaml index 49c750132..b5324b7c1 100644 --- a/pipelines/coaddQualityCore.yaml +++ b/pipelines/coaddQualityCore.yaml @@ -19,6 +19,7 @@ tasks: atools.skyObjectSky: SkyObjectSkyPlot atools.skyObjectFlux: SkyObjectHistPlot atools.psfCModelScatter: PsfCModelScatterPlot # photometry.py + atools.psfSersicScatter: PsfSersicScatterPlot # photometry.py atools.shapeSizeDetRadiusVsCmodelMag: SizeMagnitudePlot atools.shapeSizeDetRadiusVsCmodelMag.size_type: "determinantRadius" atools.shapeSizeDetRadiusVsCmodelMag.mag_x: "cmodel_err" @@ -95,6 +96,8 @@ tasks: atools.sersicIndexVsSersicMag.applyContext: CoaddContext atools.coaddInputCount: CoaddInputCount atools.coaddPatchCount: CountPatches # counts.py + atools.fiveSigmaDepth: FiveSigmaPointSourceDepthMetric # limitingMagnitudeMetric + atools.fiveSigmaDepth.applyContext: CoaddContext python: | from lsst.analysis.tools.atools import * from lsst.analysis.tools.contexts import * diff --git a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py index 3fba4737c..6d0bdac2a 100644 --- a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py +++ b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py @@ -23,18 +23,19 @@ __all__ = ("WholeSkyPlot",) +import copy import importlib.resources as importResources import json from collections.abc import Mapping import matplotlib.patheffects as pathEffects import numpy as np +import skyproj import yaml -from matplotlib import gridspec -from matplotlib.collections import PatchCollection -from matplotlib.colors import CenteredNorm +from matplotlib import colorizer, gridspec +from matplotlib.colors import Normalize from matplotlib.figure import Figure -from matplotlib.patches import Patch, Polygon +from matplotlib.patches import Rectangle import lsst.analysis.tools from lsst.pex.config import ChoiceField, Field, ListField @@ -68,13 +69,12 @@ class WholeSkyPlot(PlotAction): The default axes limits and figure size were chosen to plot HSC PDR2. """ - xAxisLabel = Field[str](doc="Label to use for the x axis.", default="RA (degrees)") - yAxisLabel = Field[str](doc="Label to use for the y axis.", default="Dec (degrees)") + xAxisLabel = Field[str](doc="Label to use for the x axis.", default="R.A.") + yAxisLabel = Field[str](doc="Label to use for the y axis.", default="Dec.") zAxisLabel = Field[str](doc="Label to use for the z axis.", default="") autoAxesLimits = Field[bool](doc="Find axes limits automatically.", default=True) xLimits = ListField[float](doc="Plotting limits for the x axis.", default=[-5.0, 365.0]) yLimits = ListField[float](doc="Plotting limits for the y axis.", default=[-10.0, 60.0]) - autoAxesLimits = Field[bool](doc="Find axes limits automatically.", default=True) colorBarMin = Field[float](doc="The minimum value of the color bar.", optional=True) colorBarMax = Field[float](doc="The minimum value of the color bar.", optional=True) colorBarRange = Field[float]( @@ -104,6 +104,16 @@ class WholeSkyPlot(PlotAction): default=True, ) + publicationStyle = Field[bool]( + doc="Make a simplified plot for publication use.", + default=False, + ) + + fixAroundZero = Field[bool]( + doc="Fix the colorbar to be symmetric around zero.", + default=False, + ) + def getInputSchema(self, **kwargs) -> KeyedDataSchema: base = [] base.append(("z", Vector)) @@ -265,41 +275,34 @@ def makePlot( if colorMap is None: colorMap = stars_cmap() outlierColor = "red" - norm = None case "divergent": if colorMap is None: colorMap = divergent_cmap() outlierColor = "fuchsia" - norm = CenteredNorm() - - # Create patches using the corners of each tract. - patches = [] - colBarVals = [] - tracts = [] - ras = [] - decs = [] - mid_ras = [] - mid_decs = [] - for i, tract in enumerate(data["tract"]): - corners = getTractCorners(skymap, tract) - patches.append(Polygon(corners, closed=True)) - colBarVals.append(data["z"][i]) - tracts.append(tract) - ras.append(corners[0][0]) - decs.append(corners[0][1]) - mid_ras.append((corners[0][0] + corners[1][0]) / 2) - mid_decs.append((corners[0][1] + corners[2][1]) / 2) # Setup figure. - fig: AnnotatedFigure = make_figure(dpi=300, figsize=(12, 3.5)) + if not self.publicationStyle: + figSize = (11, 3.5) + else: + figSize = (9, 3.5) + fig: AnnotatedFigure = make_figure(dpi=300, figsize=figSize) set_rubin_plotstyle() - gs = gridspec.GridSpec(1, 4) - ax = fig.add_subplot(gs[:3]) - # Add colored patches showing tract metric values. - patchCollection = PatchCollection(patches, cmap=colorMap, norm=norm) - ax.add_collection(patchCollection) + gs = gridspec.GridSpec(1, 20) + if not self.publicationStyle: + ax = fig.add_subplot(gs[3:15]) + sp = skyproj.McBrydeSkyproj(ax=ax) + else: + ax = fig.add_subplot(111) + sp = skyproj.McBrydeSkyproj(ax=ax, extent=[-180, 180, -90, 30]) + + # This step is necessary to fix a race condition in skyproj + # It is fixed in a later version that is not yet in our env + proj = copy.copy(sp.ax.projection) + proj.set_plot_geodesics(False) + sp.draw_milky_way(label="Milky Way", zorder=101, alpha=0.3) # Define color bar range. + colBarVals = data["z"] if np.sum(np.isfinite(colBarVals)) > 0: med = np.nanmedian(colBarVals) else: @@ -314,8 +317,11 @@ def makePlot( else: vmax = med + self.colorBarRange * sigmaMad + if self.fixAroundZero: + vmax = np.nanmax([np.fabs(vmin), np.fabs(vmax)]) + vmin = -1 * vmax + dataName = self.zAxisLabel.format_map(kwargs) - colBarVals = np.array(colBarVals) if self.addThresholds and dataName in metricDefs: if "lowThreshold" in metricDefs[dataName].keys(): lowThreshold = metricDefs[dataName]["lowThreshold"] @@ -330,66 +336,78 @@ def makePlot( # Note tracts with metrics outside (vmin, vmax) as outliers. outlierInds = np.where((colBarVals < vmin) | (colBarVals > vmax))[0] - # Initialize legend handles. - handles = [] - - if self.showOutliers: - # Plot the outlier patches. - outlierPatches = [] - if len(outlierInds) > 0: - for ind in outlierInds: - outlierPatches.append(patches[ind]) - outlierPatchCollection = PatchCollection( - outlierPatches, - cmap=colorMap, - norm=norm, - facecolors="none", - edgecolors=outlierColor, - linewidths=0.5, - zorder=100, - ) - ax.add_collection(outlierPatchCollection) - # Add legend information. - outlierPatch = Patch( - facecolor="none", - edgecolor=outlierColor, - linewidth=0.5, - label="Outlier", - ) - handles.append(outlierPatch) + # Create patches using the corners of each tract. + tracts = [] + ras = [] + decs = [] + mid_ras = [] + mid_decs = [] + allPatches = [] + nanCount = 0 + norm = Normalize(vmin=vmin, vmax=vmax, clip=True) + for i, tract in enumerate(data["tract"]): + corners = getTractCorners(skymap, tract) + tractRas = [] + tractDecs = [] + for ra, dec in corners: + tractRas.append(ra) + tractDecs.append(dec) + minRa = np.min(tractRas) + maxRa = np.max(tractRas) + minDec = np.min(tractDecs) + maxDec = np.max(tractDecs) + width = maxRa - minRa + height = maxDec - minDec + patch = Rectangle( + [minRa, minDec], + width, + height, + facecolor=colorMap(norm(data["z"][i])), + edgecolor="none", + zorder=20, + ) + patch.set_transform(proj) + allPatches.append(patch) + sp.ax.add_patch(patch) + tracts.append(tract) + ras.append(corners[0][0]) + decs.append(corners[0][1]) + mid_ras.append((corners[0][0] + corners[1][0]) / 2) + mid_decs.append((corners[0][1] + corners[2][1]) / 2) + if self.showOutliers and not self.publicationStyle: + # Plot the outlier patches. + if i in outlierInds: + patch = Rectangle( + [minRa, minDec], width, height, facecolor="none", edgecolor=outlierColor, zorder=20 + ) + patch.set_transform(proj) + sp.ax.add_patch(patch) + + if self.showNaNs: + # Plot tracts with NaN metric values + if not np.isfinite(colBarVals[i]): + nanCount += 1 + patch = Rectangle( + [minRa, minDec], + width, + height, + facecolor="none", + edgecolor="grey", + zorder=20, + linewidth=0.5, + linestyle="dotted", + ) + patch.set_transform(proj) + sp.ax.add_patch(patch) + + # Add legend information. + if self.showOutliers and not self.publicationStyle: + sp.ax.plot([-1, -2], [-1, -2], color=outlierColor, label="Outlier") if self.showNaNs: - # Plot tracts with NaN metric values. - nanInds = np.where(~np.isfinite(colBarVals))[0] - nanPatches = [] - if len(nanInds) > 0: - for ind in nanInds: - nanPatches.append(patches[ind]) - nanPatchCollection = PatchCollection( - nanPatches, - cmap=None, - norm=norm, - facecolors="white", - edgecolors="grey", - linestyles="dotted", - linewidths=0.5, - zorder=100, - ) - ax.add_collection(nanPatchCollection) - # Add legend information. - nanPatch = Patch( - facecolor="white", - edgecolor="grey", - linestyle="dotted", - linewidth=0.5, - label="NaN", - ) - handles.append(nanPatch) + sp.ax.plot([-1, -2], [-1, -2], color="grey", linestyle="dotted", label="NaN") - if len(handles) > 0: - fig.legend(handles=handles) - - if self.labelTracts: + if self.labelTracts and not self.publicationStyle: # Label the tracts for i, tract in enumerate(tracts): ax.text( @@ -403,104 +421,99 @@ def makePlot( zorder=100, ) - ax.set_aspect("equal") - axPos = ax.get_position() - ax1 = fig.add_axes([0.73, 0.25, 0.20, 0.47]) - - if np.sum(np.isfinite(data["z"])) > 0: - ax1.hist(data["z"], bins=len(data["z"] / 10), color=stars_color(), histtype="step") - else: - ax1.text(0.5, 0.5, "Data all NaN/Inf") - ax1.set_xlabel("Metric Values") - ax1.set_ylabel("Number") - ax1.yaxis.set_label_position("right") - ax1.yaxis.tick_right() + if not self.publicationStyle: + ax1 = fig.add_axes([0.73, 0.25, 0.20, 0.47]) - if self.addThresholds and dataName in metricDefs: - # Check the thresholds are finite and set them to - # the min/max of the data if they aren't to calculate - # the x range of the plot - if np.isfinite(lowThreshold): - ax1.axvline(lowThreshold, color=accent_color()) - else: - lowThreshold = np.nanmin(colBarVals) - if np.isfinite(highThreshold): - ax1.axvline(highThreshold, color=accent_color()) + if np.sum(np.isfinite(data["z"])) > 0: + ax1.hist(data["z"], bins=len(data["z"] / 10), color=stars_color(), histtype="step") else: - highThreshold = np.nanmax(colBarVals) + ax1.text(0.5, 0.5, "Data all NaN/Inf") + ax1.set_xlabel("Metric Values") + ax1.set_ylabel("Number") + ax1.yaxis.set_label_position("right") + ax1.yaxis.tick_right() + + if self.addThresholds and dataName in metricDefs and not self.publicationStyle: + # Check the thresholds are finite and set them to + # the min/max of the data if they aren't to calculate + # the x range of the plot + if np.isfinite(lowThreshold): + ax1.axvline(lowThreshold, color=accent_color()) + else: + lowThreshold = np.nanmin(colBarVals) + if np.isfinite(highThreshold): + ax1.axvline(highThreshold, color=accent_color()) + else: + highThreshold = np.nanmax(colBarVals) + + widthThreshold = highThreshold - lowThreshold + upperLim = highThreshold + 0.5 * widthThreshold + lowerLim = lowThreshold - 0.5 * widthThreshold + ax1.set_xlim(lowerLim, upperLim) + numOutside = np.sum((data["z"] > upperLim) | (data["z"] < lowerLim)) + ax1.set_title("Outside plot limits: " + str(numOutside)) - widthThreshold = highThreshold - lowThreshold - upperLim = highThreshold + 0.5 * widthThreshold - lowerLim = lowThreshold - 0.5 * widthThreshold - ax1.set_xlim(lowerLim, upperLim) - numOutside = np.sum((data["z"] > upperLim) | (data["z"] < lowerLim)) - ax1.set_title("Outside plot limits: " + str(numOutside)) - - else: - if vmin != vmax and np.isfinite(vmin) and np.isfinite(vmax): - ax1.set_xlim(vmin, vmax) + else: + if vmin != vmax and np.isfinite(vmin) and np.isfinite(vmax): + ax1.set_xlim(vmin, vmax) if self.autoAxesLimits: xlim, ylim = self._getAxesLimits(ras, decs) else: xlim, ylim = self.xLimits, self.yLimits - ax.set_xlim(xlim) - ax.set_ylim(ylim) - ax.set_xlabel(self.xAxisLabel) - ax.set_ylabel(self.yAxisLabel) - ax.invert_xaxis() - if self.showOutliers: + sp.set_extent([xlim[0], xlim[1], ylim[0], ylim[1]]) + + if self.showOutliers and not self.publicationStyle: # Add text boxes to show the number of tracts, number of NaNs, # median, sigma MAD, and the five largest outlier values. outlierText = self._getMaxOutlierVals(self.colorBarRange, tracts, colBarVals, outlierInds) - # Make vertical text spacing readable for different figure sizes. - multiplier = 3.5 / fig.get_size_inches()[1] - verticalSpacing = 0.028 * multiplier - fig.text( - 0.01, - 0.01 + 3 * verticalSpacing, - f"Num tracts: {len(tracts)}", - transform=fig.transFigure, - fontsize=8, - alpha=0.7, - ) - if self.showNaNs: + if not self.publicationStyle: + # Make vertical text spacing readable for different figure sizes. + multiplier = 3.5 / fig.get_size_inches()[1] + verticalSpacing = 0.028 * multiplier + fig.text( + 0.01, + 0.01 + 3 * verticalSpacing, + f"Num tracts: {len(tracts)}", + transform=fig.transFigure, + fontsize=8, + alpha=0.7, + ) + + if self.showNaNs and not self.publicationStyle: fig.text( 0.01, 0.01 + 2 * verticalSpacing, - f"Num nans: {len(nanInds)}", + f"Num nans: {nanCount}", transform=fig.transFigure, fontsize=8, alpha=0.7, ) - fig.text( - 0.01, - 0.01 + verticalSpacing, - f"Median: {med:.3f}; " + r"$\sigma_{MAD}$" + f": {sigmaMad:.3f}", - transform=fig.transFigure, - fontsize=8, - alpha=0.7, - ) - if self.showOutliers: + if not self.publicationStyle: + fig.text( + 0.01, + 0.01 + verticalSpacing, + f"Median: {med:.3f}; " + r"$\sigma_{MAD}$" + f": {sigmaMad:.3f}", + transform=fig.transFigure, + fontsize=8, + alpha=0.7, + ) + if self.showOutliers and not self.publicationStyle: fig.text(0.01, 0.01, outlierText, transform=fig.transFigure, fontsize=8, alpha=0.7) - # Truncate the color range to (vmin, vmax). - if vmin != vmax and np.isfinite(vmin) and np.isfinite(vmax): - colBarVals = np.clip(np.array(colBarVals), vmin, vmax) - patchCollection.set_array(colBarVals) # Make the color bar with a metric label. - axPos = ax.get_position() - cax = fig.add_axes([0.084, axPos.y1 + 0.02, 0.62, 0.07]) + cax = fig.add_axes([0.04, 0.15, 0.03, 0.7]) + cr = colorizer.Colorizer(norm=norm, cmap=colorMap) fig.colorbar( - patchCollection, + colorizer.ColorizingArtist(cr), cax=cax, shrink=0.7, extend="both", - location="top", - orientation="horizontal", + orientation="vertical", ) cbarText = "Metric Values" + cax.tick_params(labelsize=8) text = cax.text( 0.5, @@ -511,18 +524,31 @@ def makePlot( va="center", fontsize=10, zorder=100, + rotation="vertical", ) text.set_path_effects([pathEffects.Stroke(linewidth=3, foreground="w"), pathEffects.Normal()]) # Finalize plot appearance. - ax.grid() - ax.set_axisbelow(True) - addPlotInfo(fig, plotInfo) - fig.subplots_adjust(left=0.08, right=0.92, top=0.8, bottom=0.17, wspace=0.05) + if not self.publicationStyle: + addPlotInfo(fig, plotInfo) + fig.subplots_adjust(left=0.02, right=0.92, top=0.8, bottom=0.17, wspace=0.1) + else: + fig.subplots_adjust(left=0.2, right=0.85, top=0.8, bottom=0.17, wspace=0.1) titleText = self.zAxisLabel.format_map(kwargs) if "zUnit" in data and data["zUnit"] != "": titleText += f" ({data['zUnit']})" - fig.suptitle("Metric: " + titleText, fontsize=20) + fig.suptitle("Metric: " + titleText, fontsize=12) + + if self.publicationStyle: + sp.ax.legend(loc="upper left", bbox_to_anchor=(0.9, 0.1)) + labelFontsize = 15 + else: + sp.ax.legend(loc="upper right", bbox_to_anchor=(1.47, 1.56), ncols=2) + labelFontsize = 15 + sp.ax.set_xlabel(self.xAxisLabel, fontsize=labelFontsize) + sp.ax.set_ylabel(self.yAxisLabel, fontsize=labelFontsize, labelpad=10) + sp.ax.tick_params(labelsize=8) + sp.ax.tick_params(axis="y", labelsize=8) # This saves metadata in the PNG that allows the plot-navigator # to provide tract numbers and metric values on mouseover. @@ -538,9 +564,11 @@ def makePlot( # - value: the region's metric, as a string. # def make_patch_md(patch, id_field, value, ax): - path = ax.transData.transform_path(patch.get_path()) - x_path = [int(x) for x in path.vertices[:, 0].tolist()] - y_path = [int(y) for y in path.vertices[:, 1].tolist()] + trans = patch.get_transform() + path = trans.transform_path(patch.get_path()) + scale = fig.canvas.device_pixel_ratio + x_path = [int(x) for x in (path.vertices[:, 0] / scale).tolist()] + y_path = [int(y) for y in (path.vertices[:, 1] / scale).tolist()] return { "min_x": min(x_path), "max_x": max(x_path), @@ -556,7 +584,7 @@ def make_patch_md(patch, id_field, value, ax): patch_coordinate_entries = [ make_patch_md(patch, tract, value, ax) - for (patch, tract, value) in zip(patches, tracts, colBarVals) + for (patch, tract, value) in zip(allPatches, tracts, colBarVals) ] fig.metadata = {"label": "Tract", "boxes": json.dumps(patch_coordinate_entries)} diff --git a/python/lsst/analysis/tools/atools/limitingMagnitudeMetric.py b/python/lsst/analysis/tools/atools/limitingMagnitudeMetric.py index 432af3f21..5e7ba6e59 100644 --- a/python/lsst/analysis/tools/atools/limitingMagnitudeMetric.py +++ b/python/lsst/analysis/tools/atools/limitingMagnitudeMetric.py @@ -36,21 +36,33 @@ class FiveSigmaPointSourceDepthMetric(AnalysisTool): and mean 5-sigma depths are returned. """ - parameterizedBand: bool = False + def coaddContext(self) -> None: + + self.prep.selectors.starSelector.vectorKey = "{band}_extendedness" + self.prep.selectors.snSelector.fluxType = "{band}_psfFlux" + self.process.buildActions.mags = ConvertFluxToMag(vectorKey="{band}_psfFlux") + + self.produce.metric.newNames = { + "median5sigmaDepth": "{band}_median5sigmaDepth", + "mean5sigmaDepth": "{band}_mean5sigmaDepth", + } + + def visitContext(self) -> None: + self.parameterizedBand = False + self.prep.selectors.starSelector.vectorKey = "extendedness" + self.prep.selectors.snSelector.fluxType = "psfFlux" + self.process.buildActions.mags = ConvertFluxToMag(vectorKey="psfFlux") def setDefaults(self): super().setDefaults() self.prep.selectors.starSelector = StarSelector() - self.prep.selectors.starSelector.vectorKey = "extendedness" self.prep.selectors.snSelector = SnSelector() - self.prep.selectors.snSelector.fluxType = "psfFlux" # Select between 4.75 < SNR < 5.25 to get a decent sample: self.prep.selectors.snSelector.threshold = 4.75 self.prep.selectors.snSelector.maxSN = 5.25 - self.process.buildActions.mags = ConvertFluxToMag(vectorKey="psfFlux") self.process.calculateActions.median5sigmaDepth = MedianAction(vectorKey="mags") self.process.calculateActions.mean5sigmaDepth = MeanAction(vectorKey="mags") diff --git a/python/lsst/analysis/tools/atools/photometry.py b/python/lsst/analysis/tools/atools/photometry.py index 6c48c0268..c1bdb14f0 100644 --- a/python/lsst/analysis/tools/atools/photometry.py +++ b/python/lsst/analysis/tools/atools/photometry.py @@ -20,7 +20,7 @@ # along with this program. If not, see . from __future__ import annotations -__all__ = ("Ap12PsfSkyPlot", "PsfCModelSkyPlot", "PsfCModelScatterPlot", "PsfApRatio") +__all__ = ("Ap12PsfSkyPlot", "PsfCModelSkyPlot", "PsfCModelScatterPlot", "PsfApRatio", "PsfSersicScatterPlot") from ..actions.plot.histPlot import HistPanel, HistPlot from ..actions.plot.scatterplotWithTwoHists import ScatterPlotStatsAction, ScatterPlotWithTwoHists @@ -180,6 +180,58 @@ def setDefaults(self): } +class PsfSersicScatterPlot(AnalysisTool): + """Creates a scatterPlot showing the difference between + PSF and CModel mags""" + + def setDefaults(self): + super().setDefaults() + self.prep.selectors.flagSelector = CoaddPlotFlagSelector() + self.prep.selectors.flagSelector.bands = [] + + self.prep.selectors.snSelector = SnSelector() + self.prep.selectors.snSelector.fluxType = "{band}_psfFlux" + self.prep.selectors.snSelector.threshold = 300 + + self.prep.selectors.starSelector = StarSelector() + self.prep.selectors.starSelector.vectorKey = "{band}_extendedness" + + self.process.buildActions.xStars = ConvertFluxToMag() + self.process.buildActions.xStars.vectorKey = "{band}_psfFlux" + self.process.buildActions.yStars = MagDiff() + self.process.buildActions.yStars.col1 = "{band}_psfFlux" + self.process.buildActions.yStars.col2 = "{band}_sersicFlux" + self.process.buildActions.patch = LoadVector(vectorKey="patch") + + self.process.calculateActions.stars = ScatterPlotStatsAction(vectorKey="yStars") + self.process.calculateActions.stars.lowSNSelector.fluxType = "{band}_psfFlux" + self.process.calculateActions.stars.highSNSelector.fluxType = "{band}_psfFlux" + self.process.calculateActions.stars.fluxType = "{band}_psfFlux" + + self.process.calculateActions.median = MedianAction() + self.process.calculateActions.median.vectorKey = "yStars" + + self.process.calculateActions.mean = MeanAction() + self.process.calculateActions.mean.vectorKey = "yStars" + + self.process.calculateActions.sigmaMad = SigmaMadAction() + self.process.calculateActions.sigmaMad.vectorKey = "yStars" + + self.produce.plot = ScatterPlotWithTwoHists() + self.produce.plot.plotTypes = ["stars"] + self.produce.plot.xAxisLabel = "PSF Magnitude (mag)" + self.produce.plot.yAxisLabel = "PSF - Sersic (mmag)" + self.produce.plot.magLabel = "PSF Magnitude (mag)" + + self.produce.metric.units = {"median": "mmag", "sigmaMad": "mmag", "mean": "mmag"} + + self.produce.metric.newNames = { + "median": "{band}_psf_sersic_diff_median", + "mean": "{band}_psf_sersic_diff_mean", + "sigmaMad": "{band}_psf_sersic_diff_sigmaMad", + } + + class PsfApRatio(AnalysisTool): """Base class for plots or metrics which use PSF/Aperture Ratios.""" diff --git a/python/lsst/analysis/tools/atools/refCatMatchPlots.py b/python/lsst/analysis/tools/atools/refCatMatchPlots.py index 945d54a18..7f2a7d251 100644 --- a/python/lsst/analysis/tools/atools/refCatMatchPlots.py +++ b/python/lsst/analysis/tools/atools/refCatMatchPlots.py @@ -143,10 +143,21 @@ def setDefaults(self): self.process.calculateActions.stars.highSNSelector.fluxType = "{band}_psfFlux_target" self.process.calculateActions.stars.fluxType = "{band}_psfFlux_target" - self.produce = ScatterPlotWithTwoHists() - self.produce.plotTypes = ["stars"] - self.produce.magLabel = "PSF Magnitude (mag$_{{AB}}$)" - self.produce.xAxisLabel = "PSF Magnitude (mag$_{{AB}}$)" + # Calculate median, std, sigmaMad, RMS, and number counts: + self.process.calculateActions.ref_offset = MedianAction(vectorKey="yStars") + self.process.calculateActions.ref_offset_mean = MeanAction(vectorKey="yStars") + self.process.calculateActions.ref_offset_stdev = StdevAction(vectorKey="yStars") + self.process.calculateActions.ref_offset_sigmaMad = SigmaMadAction(vectorKey="yStars") + self.process.calculateActions.ref_offset_rms = RmsAction(vectorKey="yStars") + self.process.calculateActions.ref_offset_nstars = CountAction(vectorKey="yStars") + self.process.calculateActions.ref_offset_gradient = MedianGradientAction( + xsVectorKey="xStars", ysVectorKey="yStars" + ) + + self.produce.plot = ScatterPlotWithTwoHists() + self.produce.plot.plotTypes = ["stars"] + self.produce.plot.magLabel = "PSF Magnitude (mag$_{{AB}}$)" + self.produce.plot.xAxisLabel = "PSF Magnitude (mag$_{{AB}}$)" self.applyContext(CoaddContext) self.applyContext(RefMatchContext) @@ -294,7 +305,27 @@ def setDefaults(self): ) self.process.buildActions.yStars.buildAction.actionB = RAcosDec(raKey="ra_ref", decKey="dec_ref") - self.produce.yAxisLabel = "RA$_{{target}}$ - RA$_{{ref}}$ (mas)" + self.produce.plot.yAxisLabel = "RA$_{{target}}$ - RA$_{{ref}}$ (mas)" + + self.produce.metric.units = { + "ref_offset": "mas", + "ref_offset_mean": "mas", + "ref_offset_stdev": "mas", + "ref_offset_sigmaMad": "mas", + "ref_offset_rms": "mas", + "ref_offset_nstars": "", + "ref_offset_gradient": "mas / mag", + } + + self.produce.metric.newNames = { + "ref_offset": "{band}_ref_ra_offset_coadd", + "ref_offset_mean": "{band}_ref_ra_offset_mean_coadd", + "ref_offset_stdev": "{band}_ref_ra_offset_stdev_coadd", + "ref_offset_sigmaMad": "{band}_ref_ra_offset_sigmaMad_coadd", + "ref_offset_rms": "{band}_ref_ra_offset_rms_coadd", + "ref_offset_nstars": "{band}_ref_ra_offset_nstars_coadd", + "ref_offset_gradient": "{band}_ref_ra_offset_gradient_coadd", + } class TargetRefCatDeltaRAScatterVisitPlot(TargetRefCatDeltaScatterAstromVisit): @@ -335,7 +366,27 @@ def setDefaults(self): self.process.buildActions.yStars.buildAction.actionA = LoadVector(vectorKey="coord_dec_target") self.process.buildActions.yStars.buildAction.actionB = LoadVector(vectorKey="dec_ref") - self.produce.yAxisLabel = "Dec$_{{target}}$ - Dec$_{{ref}}$ (mas)" + self.produce.plot.yAxisLabel = "Dec$_{{target}}$ - Dec$_{{ref}}$ (mas)" + + self.produce.metric.units = { + "ref_offset": "mas", + "ref_offset_mean": "mas", + "ref_offset_stdev": "mas", + "ref_offset_sigmaMad": "mas", + "ref_offset_rms": "mas", + "ref_offset_nstars": "", + "ref_offset_gradient": "mas / mag", + } + + self.produce.metric.newNames = { + "ref_offset": "{band}_ref_dec_offset_coadd", + "ref_offset_mean": "{band}_ref_dec_offset_mean_coadd", + "ref_offset_stdev": "{band}_ref_dec_offset_stdev_coadd", + "ref_offset_sigmaMad": "{band}_ref_dec_offset_sigmaMad_coadd", + "ref_offset_rms": "{band}_ref_dec_offset_rms_coadd", + "ref_offset_nstars": "{band}_ref_dec_offset_nstars_coadd", + "ref_offset_gradient": "{band}_ref_dec_offset_gradient_coadd", + } class TargetRefCatDeltaSkyPlot(TargetRefCatDelta): diff --git a/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py b/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py index fabbcee1a..0f5fe6635 100644 --- a/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py +++ b/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py @@ -48,6 +48,16 @@ class WholeSkyPlotTool(AnalysisTool): default="e1Diff_{band}_highSNStars_median", ) + publicationStyle = Field[bool]( + doc="Plot a simplified plot for publication use.", + default=False, + ) + + fixAroundZero = Field[bool]( + doc="Fix the colorbar to be symmetric around zero.", + default=False, + ) + def setDefaults(self): super().setDefaults() @@ -58,8 +68,14 @@ def setDefaults(self): def finalize(self): self.process.buildActions.z = LoadVector(vectorKey=self.metric) self.process.buildActions.zUnit = KeyedDataUnitAccessAction(key=self.metric) + if self.publicationStyle: + self.produce.plot.publicationStyle = True self.produce.plot.zAxisLabel = self.metric - sequentialMetrics = ["count", "ean", "edian", "num", "igma", "tdev", "Repeat"] - if any(sequentialMetric in self.metric for sequentialMetric in sequentialMetrics): - self.produce.plot.colorMapType = "sequential" + sequentialMetrics = ["count", "num", "igma", "tdev", "Repeat"] + for seqMet in sequentialMetrics: + if seqMet in self.metric: + self.produce.plot.colorMapType = "sequential" + self.produce.plot.fixAroundZero = self.fixAroundZero + if self.fixAroundZero: + self.produce.plot.colorMapType = "divergent" super().finalize() diff --git a/python/lsst/analysis/tools/utils.py b/python/lsst/analysis/tools/utils.py index f0e7e4d5b..f45a682e9 100644 --- a/python/lsst/analysis/tools/utils.py +++ b/python/lsst/analysis/tools/utils.py @@ -54,8 +54,15 @@ def getTractCorners(skymap, tractId): ----- Corners are returned in degrees and wrapped in ra. """ - tractCorners = skymap[tractId].getVertexList() - corners = _wrapRa([(corner.getRa().asDegrees(), corner.getDec().asDegrees()) for corner in tractCorners]) + # Get bounding box is needed if the skymap isn't rings + # The discrete skymaps don't return a box from + # getInnerSkyRegion + tractBox = skymap[tractId].getInnerSkyRegion().getBoundingBox() + minRa, maxRa = tractBox.getLon().getA().asDegrees(), tractBox.getLon().getB().asDegrees() + minDec, maxDec = tractBox.getLat().getA().asDegrees(), tractBox.getLat().getB().asDegrees() + + tractCorners = [(minRa, minDec), (maxRa, minDec), (maxRa, maxDec), (minRa, minDec)] + corners = _wrapRa(tractCorners) return corners diff --git a/tests/test_utils.py b/tests/test_utils.py index b2e2cf119..93593f985 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -48,16 +48,16 @@ def setUp(self): self.tractCorners = [ [ - (358.8900906668926, -2.1096270745156804), - (361.10981685029367, -2.1096270745156804), - (361.10981685029367, 0.11009493220816949), - (358.8900906668926, 0.11009493220816949), + (360.0, -2.110023112350562), + (360.0, -2.110023112350562), + (360.0, 0.11011561234816987), + (360.0, 0.11011561234816987), ], [ - (181.10981686420706, -0.11000243565052142), - (178.89009065297807, -0.110002449565178), - (178.88933977564471, 2.109719508502047), - (181.1105676789934, 2.1097195571483582), + (178.88933974699682, -0.11002311235142387), + (181.11056770764125, -0.11002311235142387), + (181.11056770764125, 2.110115612347309), + (178.88933974699682, -0.11002311235142387), ], ] self.patchCorners = [