From 739c2d5ce1afc57835cfd5a6ecbb2d393e02ab4a Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Wed, 22 Jul 2026 13:57:20 -0700 Subject: [PATCH 1/8] Add config files for DP2 summary whole sky plots --- ...oreRefCatMatchPhotomWholeSkyPlot_dp2Pub.py | 23 +++++++++++++++++ ...TableCoreRefCatMatchWholeSkyPlot_dp2Pub.py | 25 +++++++++++++++++++ config/objectTableCoreWholeSkyPlot_dp2Pub.py | 25 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 config/objectTableCoreRefCatMatchPhotomWholeSkyPlot_dp2Pub.py create mode 100644 config/objectTableCoreRefCatMatchWholeSkyPlot_dp2Pub.py create mode 100644 config/objectTableCoreWholeSkyPlot_dp2Pub.py 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 From 526b2d943df0c79e0b920aaa8a99390e61dce722 Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Wed, 22 Jul 2026 14:00:24 -0700 Subject: [PATCH 2/8] Add new metrics to coaddQualityCore needed for summary plots --- pipelines/coaddQualityCore.yaml | 3 ++ .../tools/atools/limitingMagnitudeMetric.py | 20 +++++-- .../lsst/analysis/tools/atools/photometry.py | 54 ++++++++++++++++++- 3 files changed, 72 insertions(+), 5 deletions(-) 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/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.""" From 5b6fe8f4163bface8487a8f0546aa1ae566bed91 Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Wed, 22 Jul 2026 14:04:04 -0700 Subject: [PATCH 3/8] Add new metrics for ref matched catalogues --- .../analysis/tools/atools/refCatMatchPlots.py | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) 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): From c8d93eb2f30c372eec3d306b70bc630718c350be Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Wed, 22 Jul 2026 14:04:41 -0700 Subject: [PATCH 4/8] Add a pipeline to make the DP2 summary plots --- pipelines/DP2Summary.yaml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pipelines/DP2Summary.yaml 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 * From 0e898508137a6b0f41f94d814d45b2bc3c62a9b8 Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Wed, 22 Jul 2026 14:07:40 -0700 Subject: [PATCH 5/8] Improve whole sky plot --- .../tools/actions/plot/wholeSkyPlot.py | 322 +++++++++--------- 1 file changed, 165 insertions(+), 157 deletions(-) diff --git a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py index 3fba4737c..340d90b8c 100644 --- a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py +++ b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py @@ -29,10 +29,11 @@ import matplotlib.patheffects as pathEffects import numpy as np +import skyproj import yaml -from matplotlib import gridspec +from matplotlib import colorizer, gridspec from matplotlib.collections import PatchCollection -from matplotlib.colors import CenteredNorm +from matplotlib.colors import CenteredNorm, Normalize from matplotlib.figure import Figure from matplotlib.patches import Patch, Polygon @@ -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)) @@ -272,34 +282,24 @@ def makePlot( 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) + if not self.publicationStyle: + ax = fig.add_subplot(gs[:3]) + sp = skyproj.McBrydeSkyproj(ax=ax) + else: + ax = fig.add_subplot(111) + sp = skyproj.McBrydeSkyproj(ax=ax, extent=[-180, 180, -90, 30]) + sp.draw_milky_way(label="Milky Way", zorder=-1) # Define color bar range. + colBarVals = data["z"] if np.sum(np.isfinite(colBarVals)) > 0: med = np.nanmedian(colBarVals) else: @@ -314,8 +314,34 @@ def makePlot( else: vmax = med + self.colorBarRange * sigmaMad + if self.fixAroundZero: + vmax = np.nanmax([np.fabs(vmin), np.fabs(vmax)]) + vmin = -1 * vmax + + # Create patches using the corners of each tract. + tracts = [] + ras = [] + decs = [] + mid_ras = [] + mid_decs = [] + 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) + sp.draw_box( + tractRas, tractDecs, facecolor=colorMap(norm(data["z"][i])), edgecolor="none", zorder=20 + ) + 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) + 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 +356,41 @@ 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: + if self.showOutliers and not self.publicationStyle: # 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) + for i, tract in enumerate(data["tract"][outlierInds]): + corners = getTractCorners(skymap, tract) + tractRas = [] + tractDecs = [] + for ra, dec in corners: + tractRas.append(ra) + tractDecs.append(dec) + sp.draw_box(tractRas, tractDecs, facecolor="none", edgecolor=outlierColor, linewidth=0.5) + outlierLine = 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", + for i, tract in enumerate(data["tract"][nanInds]): + corners = getTractCorners(skymap, tract) + tractRas = [] + tractDecs = [] + for ra, dec in corners: + tractRas.append(ra) + tractDecs.append(dec) + sp.draw_box( + tractRas, + tractDecs, + facecolor="none", edgecolor="grey", - linestyle="dotted", linewidth=0.5, - label="NaN", + linestyle="dotted", + zorder=20, ) - handles.append(nanPatch) + # Add legend information. + nanLine = 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,69 +404,68 @@ 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() + # sp.ax.set_xlim(xlim) + # sp.ax.set_ylim(ylim) - if self.showOutliers: + 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, @@ -474,31 +474,28 @@ def makePlot( 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" @@ -511,18 +508,29 @@ 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.05) + else: + fig.subplots_adjust(left=0.2, right=0.85, top=0.8, bottom=0.17, wspace=0.05) 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.22, 1.1)) + labelFontsize = 10 + sp.ax.set_xlabel(self.xAxisLabel, fontsize=labelFontsize) + sp.ax.set_ylabel(self.yAxisLabel, fontsize=labelFontsize) # This saves metadata in the PNG that allows the plot-navigator # to provide tract numbers and metric values on mouseover. @@ -554,11 +562,11 @@ def make_patch_md(patch, id_field, value, ax): # transformations to be updated to the right values. fig.canvas.draw_idle() - patch_coordinate_entries = [ - make_patch_md(patch, tract, value, ax) - for (patch, tract, value) in zip(patches, tracts, colBarVals) - ] + # patch_coordinate_entries = [ + # make_patch_md(patch, tract, value, ax) + # for (patch, tract, value) in zip(patches, tracts, colBarVals) + # ] - fig.metadata = {"label": "Tract", "boxes": json.dumps(patch_coordinate_entries)} + # fig.metadata = {"label": "Tract", "boxes": json.dumps(patch_coordinate_entries)} return fig From 80a9a9e9c455eba8e100602a9bac22597591f0f0 Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Wed, 22 Jul 2026 14:08:46 -0700 Subject: [PATCH 6/8] Make wholeSkyPlotTool work with publication style plots --- .../tools/actions/plot/wholeSkyPlot.py | 96 ++++++++++++++----- .../analysis/tools/atools/wholeSkyPlotTool.py | 18 +++- 2 files changed, 86 insertions(+), 28 deletions(-) diff --git a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py index 340d90b8c..d2029dc2f 100644 --- a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py +++ b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py @@ -32,10 +32,9 @@ import skyproj import yaml from matplotlib import colorizer, gridspec -from matplotlib.collections import PatchCollection -from matplotlib.colors import CenteredNorm, Normalize +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 @@ -275,12 +274,10 @@ 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() # Setup figure. if not self.publicationStyle: @@ -296,6 +293,8 @@ def makePlot( else: ax = fig.add_subplot(111) sp = skyproj.McBrydeSkyproj(ax=ax, extent=[-180, 180, -90, 30]) + proj = sp.ax.projection + proj.set_plot_geodesics(False) sp.draw_milky_way(label="Milky Way", zorder=-1) # Define color bar range. @@ -324,6 +323,7 @@ def makePlot( decs = [] mid_ras = [] mid_decs = [] + allPatches = [] norm = Normalize(vmin=vmin, vmax=vmax, clip=True) for i, tract in enumerate(data["tract"]): corners = getTractCorners(skymap, tract) @@ -332,9 +332,25 @@ def makePlot( for ra, dec in corners: tractRas.append(ra) tractDecs.append(dec) - sp.draw_box( - tractRas, tractDecs, facecolor=colorMap(norm(data["z"][i])), edgecolor="none", zorder=20 + minRa = np.min(tractRas) + maxRa = np.max(tractRas) + minDec = np.min(tractDecs) + maxDec = np.max(tractDecs) + width = maxRa - minRa + height = maxDec - minDec + if width > 10: + width = minRa + 360 - maxRa + 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]) @@ -365,8 +381,22 @@ def makePlot( for ra, dec in corners: tractRas.append(ra) tractDecs.append(dec) - sp.draw_box(tractRas, tractDecs, facecolor="none", edgecolor=outlierColor, linewidth=0.5) - outlierLine = sp.ax.plot([-1, -2], [-1, -2], color=outlierColor, label="Outlier") + minRa = np.min(tractRas) + maxRa = np.max(tractRas) + minDec = np.min(tractDecs) + maxDec = np.max(tractDecs) + width = maxRa - minRa + height = maxDec - minDec + if width > 10: + width = minRa + 360 - maxRa + patch = Rectangle( + [minRa, minDec], width, height, facecolor="none", edgecolor=outlierColor, zorder=20 + ) + patch.set_transform(proj) + sp.ax.add_patch(patch) + tracts.append(tract) + + sp.ax.plot([-1, -2], [-1, -2], color=outlierColor, label="Outlier") if self.showNaNs: # Plot tracts with NaN metric values. @@ -378,17 +408,31 @@ def makePlot( for ra, dec in corners: tractRas.append(ra) tractDecs.append(dec) - sp.draw_box( - tractRas, - tractDecs, + minRa = np.min(tractRas) + maxRa = np.max(tractRas) + minDec = np.min(tractDecs) + maxDec = np.max(tractDecs) + width = maxRa - minRa + height = maxDec - minDec + if width > 10: + width = minRa + 360 - maxRa + patch = Rectangle( + [minRa, minDec], + width, + height, facecolor="none", edgecolor="grey", + zorder=20, linewidth=0.5, linestyle="dotted", - zorder=20, ) + patch.set_transform(proj) + allPatches.append(patch) + sp.ax.add_patch(patch) + tracts.append(tract) + # Add legend information. - nanLine = sp.ax.plot([-1, -2], [-1, -2], color="grey", linestyle="dotted", label="NaN") + sp.ax.plot([-1, -2], [-1, -2], color="grey", linestyle="dotted", label="NaN") if self.labelTracts and not self.publicationStyle: # Label the tracts @@ -404,7 +448,6 @@ def makePlot( zorder=100, ) - axPos = ax.get_position() if not self.publicationStyle: ax1 = fig.add_axes([0.73, 0.25, 0.20, 0.47]) @@ -445,8 +488,8 @@ def makePlot( xlim, ylim = self._getAxesLimits(ras, decs) else: xlim, ylim = self.xLimits, self.yLimits - # sp.ax.set_xlim(xlim) - # sp.ax.set_ylim(ylim) + + 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, @@ -487,7 +530,6 @@ def makePlot( fig.text(0.01, 0.01, outlierText, transform=fig.transFigure, fontsize=8, alpha=0.7) # Make the color bar with a metric label. - axPos = ax.get_position() cax = fig.add_axes([0.04, 0.15, 0.03, 0.7]) cr = colorizer.Colorizer(norm=norm, cmap=colorMap) fig.colorbar( @@ -546,9 +588,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), @@ -562,11 +606,11 @@ def make_patch_md(patch, id_field, value, ax): # transformations to be updated to the right values. fig.canvas.draw_idle() - # patch_coordinate_entries = [ - # make_patch_md(patch, tract, value, ax) - # for (patch, tract, value) in zip(patches, tracts, colBarVals) - # ] + patch_coordinate_entries = [ + make_patch_md(patch, tract, value, ax) + for (patch, tract, value) in zip(allPatches, tracts, colBarVals) + ] - # fig.metadata = {"label": "Tract", "boxes": json.dumps(patch_coordinate_entries)} + fig.metadata = {"label": "Tract", "boxes": json.dumps(patch_coordinate_entries)} return fig diff --git a/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py b/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py index fabbcee1a..3a3a56cf6 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,12 @@ 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" + for seqMet in sequentialMetrics: + if seqMet in self.metric and "iff_" not in self.metric: + self.produce.plot.colorMapType = "sequential" + self.produce.plot.fixAroundZero = self.fixAroundZero super().finalize() From cef697a6f360365dcde2d1b377fce18e8728ac8e Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Wed, 9 Sep 2026 13:10:29 -0700 Subject: [PATCH 7/8] Make none publication style plots look better and use the inner corners of tracts --- .../tools/actions/plot/wholeSkyPlot.py | 144 ++++++++---------- .../analysis/tools/atools/wholeSkyPlotTool.py | 6 +- python/lsst/analysis/tools/utils.py | 11 +- 3 files changed, 73 insertions(+), 88 deletions(-) diff --git a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py index d2029dc2f..6d0bdac2a 100644 --- a/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py +++ b/python/lsst/analysis/tools/actions/plot/wholeSkyPlot.py @@ -23,6 +23,7 @@ __all__ = ("WholeSkyPlot",) +import copy import importlib.resources as importResources import json from collections.abc import Mapping @@ -286,16 +287,19 @@ def makePlot( figSize = (9, 3.5) fig: AnnotatedFigure = make_figure(dpi=300, figsize=figSize) set_rubin_plotstyle() - gs = gridspec.GridSpec(1, 4) + gs = gridspec.GridSpec(1, 20) if not self.publicationStyle: - ax = fig.add_subplot(gs[:3]) + 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]) - proj = sp.ax.projection + + # 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=-1) + sp.draw_milky_way(label="Milky Way", zorder=101, alpha=0.3) # Define color bar range. colBarVals = data["z"] @@ -317,6 +321,21 @@ def makePlot( vmax = np.nanmax([np.fabs(vmin), np.fabs(vmax)]) vmin = -1 * vmax + dataName = self.zAxisLabel.format_map(kwargs) + if self.addThresholds and dataName in metricDefs: + if "lowThreshold" in metricDefs[dataName].keys(): + lowThreshold = metricDefs[dataName]["lowThreshold"] + else: + lowThreshold = np.nan + if "highThreshold" in metricDefs[dataName].keys(): + highThreshold = metricDefs[dataName]["highThreshold"] + else: + highThreshold = np.nan + outlierInds = np.where((colBarVals < lowThreshold) | (colBarVals > highThreshold))[0] + else: + # Note tracts with metrics outside (vmin, vmax) as outliers. + outlierInds = np.where((colBarVals < vmin) | (colBarVals > vmax))[0] + # Create patches using the corners of each tract. tracts = [] ras = [] @@ -324,6 +343,7 @@ def makePlot( 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) @@ -338,8 +358,6 @@ def makePlot( maxDec = np.max(tractDecs) width = maxRa - minRa height = maxDec - minDec - if width > 10: - width = minRa + 360 - maxRa patch = Rectangle( [minRa, minDec], width, @@ -357,81 +375,36 @@ def makePlot( mid_ras.append((corners[0][0] + corners[1][0]) / 2) mid_decs.append((corners[0][1] + corners[2][1]) / 2) - dataName = self.zAxisLabel.format_map(kwargs) - if self.addThresholds and dataName in metricDefs: - if "lowThreshold" in metricDefs[dataName].keys(): - lowThreshold = metricDefs[dataName]["lowThreshold"] - else: - lowThreshold = np.nan - if "highThreshold" in metricDefs[dataName].keys(): - highThreshold = metricDefs[dataName]["highThreshold"] - else: - highThreshold = np.nan - outlierInds = np.where((colBarVals < lowThreshold) | (colBarVals > highThreshold))[0] - else: - # Note tracts with metrics outside (vmin, vmax) as outliers. - outlierInds = np.where((colBarVals < vmin) | (colBarVals > vmax))[0] - + 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: - # Plot the outlier patches. - for i, tract in enumerate(data["tract"][outlierInds]): - 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 - if width > 10: - width = minRa + 360 - maxRa - patch = Rectangle( - [minRa, minDec], width, height, facecolor="none", edgecolor=outlierColor, zorder=20 - ) - patch.set_transform(proj) - sp.ax.add_patch(patch) - tracts.append(tract) - 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] - for i, tract in enumerate(data["tract"][nanInds]): - 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 - if width > 10: - width = minRa + 360 - maxRa - patch = Rectangle( - [minRa, minDec], - width, - height, - facecolor="none", - edgecolor="grey", - zorder=20, - linewidth=0.5, - linestyle="dotted", - ) - patch.set_transform(proj) - allPatches.append(patch) - sp.ax.add_patch(patch) - tracts.append(tract) - - # Add legend information. sp.ax.plot([-1, -2], [-1, -2], color="grey", linestyle="dotted", label="NaN") if self.labelTracts and not self.publicationStyle: @@ -512,7 +485,7 @@ def makePlot( 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, @@ -540,6 +513,7 @@ def makePlot( orientation="vertical", ) cbarText = "Metric Values" + cax.tick_params(labelsize=8) text = cax.text( 0.5, @@ -557,9 +531,9 @@ def makePlot( # Finalize plot appearance. if not self.publicationStyle: addPlotInfo(fig, plotInfo) - fig.subplots_adjust(left=0.02, right=0.92, top=0.8, bottom=0.17, wspace=0.05) + 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.05) + 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']})" @@ -569,10 +543,12 @@ def makePlot( 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.22, 1.1)) - labelFontsize = 10 + 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) + 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. diff --git a/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py b/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py index 3a3a56cf6..0f5fe6635 100644 --- a/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py +++ b/python/lsst/analysis/tools/atools/wholeSkyPlotTool.py @@ -71,9 +71,11 @@ def finalize(self): if self.publicationStyle: self.produce.plot.publicationStyle = True self.produce.plot.zAxisLabel = self.metric - sequentialMetrics = ["count", "ean", "edian", "num", "igma", "tdev", "Repeat"] + sequentialMetrics = ["count", "num", "igma", "tdev", "Repeat"] for seqMet in sequentialMetrics: - if seqMet in self.metric and "iff_" not in self.metric: + 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 From 00b0f8906fa2e7abd8836767cb10ae03f236624d Mon Sep 17 00:00:00 2001 From: Sophie Reed Date: Mon, 28 Sep 2026 10:42:53 -0700 Subject: [PATCH 8/8] Update tests --- tests/test_utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 = [