diff --git a/pipelines/coaddQualityCore.yaml b/pipelines/coaddQualityCore.yaml index 6d8f0098e..50e913b32 100644 --- a/pipelines/coaddQualityCore.yaml +++ b/pipelines/coaddQualityCore.yaml @@ -35,3 +35,18 @@ tasks: class: lsst.analysis.tools.tasks.catalogMatch.CatalogMatchTask refCatObjectTract: class: lsst.analysis.tools.tasks.refCatObjectAnalysis.RefCatObjectAnalysisTask + plotPropertyMapTract: + class: lsst.analysis.tools.tasks.PropertyMapTractAnalysisTask + config: + connections.outputName: propertyMapTract + atools.healSparseMap: PropertyMapTool + zoomFactors: [2, 8] + python: | + from lsst.analysis.tools.atools import * + config.properties["exposure_time"] = PropertyMapConfig + config.properties["exposure_time"].coaddName= "deep" + config.properties["exposure_time"].operations = ["sum"] + config.properties["psf_size"] = PropertyMapConfig + config.properties["psf_size"].operations = ["weighted_mean"] + config.properties["sky_background"] = PropertyMapConfig + config.properties["sky_background"].operations = ["weighted_mean"] diff --git a/python/lsst/analysis/tools/actions/plot/__init__.py b/python/lsst/analysis/tools/actions/plot/__init__.py index 7c68b4e39..82b80f827 100644 --- a/python/lsst/analysis/tools/actions/plot/__init__.py +++ b/python/lsst/analysis/tools/actions/plot/__init__.py @@ -4,6 +4,7 @@ from .focalPlanePlot import * from .histPlot import * from .multiVisitCoveragePlot import * +from .propertyMapPlot import * from .rhoStatisticsPlot import * from .scatterplotWithTwoHists import * from .skyPlot import * diff --git a/python/lsst/analysis/tools/actions/plot/propertyMapPlot.py b/python/lsst/analysis/tools/actions/plot/propertyMapPlot.py new file mode 100644 index 000000000..a47a79e60 --- /dev/null +++ b/python/lsst/analysis/tools/actions/plot/propertyMapPlot.py @@ -0,0 +1,441 @@ +# 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__ = ("PropertyMapPlot",) + +import logging +from typing import Iterable, Mapping, Optional + +import lsst.pex.config as pexConfig +import matplotlib.patheffects as mpl_path_effects +import matplotlib.pyplot as plt +import numpy as np +import skyproj +from healsparse.healSparseMap import HealSparseMap +from lsst.skymap.tractInfo import ExplicitTractInfo +from matplotlib.figure import Figure + +from ...interfaces import KeyedData, PlotAction + +_LOG = logging.getLogger(__name__) + + +class PropertyMapPlot(PlotAction): + plotName = pexConfig.Field[str](doc="The name for the plotting task.", optional=True) + + def __call__( + self, data: KeyedData, tract: ExplicitTractInfo, zoomFactors: pexConfig.listField.List, **kwargs + ) -> Mapping[str, Figure]: + self._validateInput(data, tract, zoomFactors, **kwargs) + return self.makePlot(data, tract, zoomFactors, **kwargs) + + def _validateInput( + self, data: KeyedData, tract: ExplicitTractInfo, zoomFactors: pexConfig.listField.List, **kwargs + ) -> None: + """Validate the input data.""" + + if not isinstance(tract, ExplicitTractInfo): + raise TypeError(f"Input `tract` type must be {ExplicitTractInfo} not {type(tract)}.") + + isListOfFloats = isinstance(zoomFactors, pexConfig.listField.List) and all( + isinstance(zf, float) for zf in zoomFactors + ) + if not (isListOfFloats and len(zoomFactors) == 2): + raise TypeError("`zoomFactors` must be a two-element `lsst.pex.config.listField.List` of floats.") + + # Identify any invalid entries in `data`. + invalidEntries = { + key: type(value) for key, value in data.items() if not isinstance(value, HealSparseMap) + } + + # If any invalid entries are found, raise a TypeError with details. + if invalidEntries: + error_message = ", ".join( + f"`{key}` should be {HealSparseMap}, got {type_}" for key, type_ in invalidEntries.items() + ) + raise TypeError(f"Invalid input types found: {error_message}") + + def addPlotInfo(self, fig: Figure, plotInfo: Mapping[str, str], mapName: Mapping[str, str]) -> Figure: + """Add useful information to the plot. + + Parameters + ---------- + fig : `matplotlib.figure.Figure` + The figure to add the information to. + + plotInfo : `dict` + A dictionary of the plot information. + + mapName : `str` + The name of the map being plotted. + + Returns + ------- + fig : `matplotlib.figure.Figure` + The figure with the information added. + """ + + dataIdText = f"Tract: {plotInfo['tract']}" + mapText = ( + f", Property: {plotInfo['property']}, " + f"Operation: {plotInfo['operation']}, " + f"Coadd: {plotInfo['coaddName']}" + ) + + bandText = f", Band: {plotInfo['band']}" + infoText = f"{dataIdText}{bandText}{mapText}" + extraText = f"Valid area: {plotInfo['valid_area']:.2f} sq. deg., " \ + f"nside_coverage: {plotInfo['nside_coverage']}, nside_sparse: {plotInfo['nside_sparse']}" + + fig.text( + 0.036, + 0.965, + f'{plotInfo["plotName"]}: {mapName}', + fontsize=20, + transform=fig.transFigure, + ha="left", + va="top", + ) + fig.text( + 0.036, 0.94, infoText, fontsize=15, transform=fig.transFigure, alpha=0.6, ha="left", va="top" + ) + fig.text( + 0.036, 0.92, extraText, fontsize=15, transform=fig.transFigure, alpha=0.6, ha="left", va="top" + ) + return fig + + def makePlot( + self, + data: KeyedData, + tract: ExplicitTractInfo, + zoomFactors: list, + plotInfo: Optional[Mapping[str, str]] = None, + **kwargs, + ) -> Mapping[str, Figure]: + """Make the survey property map plot. + + Parameters + ---------- + data : `KeyedData` + The HealSparseMap to plot the points from. + + tract: `lsst.skymap.tractInfo.ExplicitTractInfo` + The tract info object. + + plotInfo : `dict` + A dictionary of information about the data being plotted. + + **kwargs : + Additional keyword arguments to pass to the plot. + + Returns + ------- + figDict : `dict` [`~matplotlib.figure.Figure`] + The resulting figures. + """ + + figDict: dict[str, Figure] = {} + + # Plotting customization. + colorbarTickLabelSize = 14 + colorBarAspect = 16 + nBinsHist = 100 + rcparams = { + "axes.labelsize": 18, + "axes.linewidth": 1.8, + "xtick.labelsize": 13, + "ytick.labelsize": 13, + } + plt.rcParams.update(rcparams) + + # Muted green, red and blue for labeling the full and zoomed-in maps. + zoomColors = ["#265D40", "#8B0000", "#00008B"] + + for mapName, mapData in data.items(): + fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 16)) + + # Reduce whitespace but leave some room at the top for `plotInfo`. + plt.subplots_adjust(left=0.064, right=0.96, top=0.855, bottom=0.07, wspace=0.18, hspace=0.24) + + # Get the values for the valid pixels of the full tract. + values = mapData[mapData.valid_pixels] + goodValues = np.isfinite(values) + values = values[goodValues] # As a precaution. + + # Make a concise human-readable label for the plot. + plotInfo["coaddName"] = mapName.split("Coadd_")[0] + plotInfo["operation"] = self.getLongestSuffixMatch( + mapName, ["min", "max", "mean", "weighted_mean", "sum"] + ) + plotInfo["property"] = mapName[ + len(f"{plotInfo['coaddName']}Coadd_") : -len(plotInfo["operation"]) + ].strip("_") + label = plotInfo["property"].replace("_", " ").title().replace("Psf", "PSF") + + fullExtent = None + zoomIdx = [] + for ax, zoom, zoomFactor, zoomColor in zip( + [ax1, ax3, ax4], [True, False, False], [None, *zoomFactors], zoomColors + ): + extent = self.getZoomedExtent(fullExtent, zoomFactor) + sp = skyproj.GnomonicSkyproj( + ax=ax, + lon_0=tract.ctr_coord.getRa().asDegrees(), + lat_0=tract.ctr_coord.getDec().asDegrees(), + extent=extent, + rcparams=rcparams, + ) + sp.draw_hspmap(mapData, zoom=zoom) + sp.set_xlabel("RA (deg)") + sp.set_ylabel("Dec (deg)") + cbar = sp.draw_colorbar(location="right", fraction=0.15, aspect=colorBarAspect, pad=0) + cbar.ax.tick_params(labelsize=colorbarTickLabelSize) + cbarText = ( + "Full Tract" if zoomFactor is None else f"{self.prettyPrintFloat(zoomFactor)}X Zoom" + ) + self.addTextToColorbar(cbar, cbarText, color=zoomColor) + if zoomFactor is None: + # Save the skyproj object of the full-tract plot. + # Will be used in drawing zoom rectangles etc. + spf = sp + # Get the extent of the full tract. + fullExtent = spf.get_extent() + else: + # Create a rectangle for the zoomed-in region. + x0, x1, y0, y1 = extent + for c, ls, lw in zip(["white", zoomColor], ["solid", "dashed"], [3.5, 1.5]): + spf.draw_polygon( + [x0, x0, x1, x1], + [y0, y1, y1, y0], + facecolor="none", + edgecolor=c, + linestyle=ls, + linewidth=lw, + alpha=0.8, + ) + zoomText = spf._ax.text( + (x0 + x1) / 2, + y0, + f"{self.prettyPrintFloat(zoomFactor)}X", + color=zoomColor, + fontsize=14, + fontweight="bold", + alpha=0.8, + ha="center", + va="bottom", + ) + # Add a distinct outline around the text for better + # visibility in various backgrounds. + zoomText.set_path_effects( + [ + mpl_path_effects.Stroke(linewidth=4, foreground="white", alpha=0.8), + mpl_path_effects.Normal(), + ] + ) + # Get the indices of pixels in the zoomed-in region. + pos = mapData.valid_pixels_pos() + # Reversed axes consideration. + xmin, xmax = sorted([x0, x1]) + idx = (pos[0] > xmin) & (pos[0] < xmax) & (pos[1] > y0) & (pos[1] < y1) + zoomIdx.append(idx[goodValues]) + + # Calculate weights for each bin to ensure that the peak of the + # histogram reaches 1. + weights = np.ones_like(values) / np.histogram(values, bins=nBinsHist)[0].max() + + # Compute full-tract histogram and get its bins. + bins = ax2.hist( + values, + bins=nBinsHist, + label="Full Tract", + color=zoomColors[0], + weights=weights, + alpha=0.7, + )[1] + + # Align the histogram (top right panel) with the skyproj plots. + pos1 = spf._ax.get_position() # Top left. + pos4 = sp._ax.get_position() # Bottom right. + cbarWidth = cbar.ax.get_position().height / colorBarAspect + # NOTE: cbarWidth != cbar.ax.get_position().width + ax2.set_position([pos4.x0, pos1.y0, pos4.width + cbarWidth, pos1.height]) + + # `exposure_time` histograms are bar-like, not suited for + # overplotting, so we exclude them. + if "exposure_time" not in mapName: + # Overplot the histograms for the zoomed-in plots. + for zoomFactor, zidx, color in zip(zoomFactors, zoomIdx, zoomColors[1:]): + weights = np.ones_like(values[zidx]) / np.histogram(values[zidx], bins=bins)[0].max() + histLabel = f"{self.prettyPrintFloat(zoomFactor)}X Zoom" + ax2.hist( + values[zidx], + bins=bins, + label=histLabel, + color=color, + weights=weights, + alpha=0.6, + ) + + # Set labels and legend. + ax2.set_xlabel(label) + ax2.set_ylabel("Normalized Count") + legend = ax2.legend(loc="best", frameon=False, fontsize=15) + for line, text in zip(legend.legendHandles, legend.get_texts()): + text.set_color(line.get_facecolor()) + + # Add nside info to `plotInfo`. + plotInfo["nside_coverage"] = mapData.nside_coverage + plotInfo["nside_sparse"] = mapData.nside_sparse + plotInfo["valid_area"] = mapData.get_valid_area() + + # Add useful information to the plot. + figDict[mapName] = self.addPlotInfo(fig, plotInfo, mapName) + + _LOG.info( + f"Made property map plot for dataset type {mapName}, tract: {plotInfo['tract']}, " + f"band: '{plotInfo['band']}'." + ) + + return figDict + + def getOutputNames(self, config=None) -> Iterable[str]: + # Docstring inherited. + + # Names needed for making corresponding output connections for the maps + # that are configured to run. + outputNames: tuple[str] = () + for propertyName in config.properties: + coaddName = config.properties[propertyName].coaddName + for operationName in config.properties[propertyName].operations: + outputNames += (f"{coaddName}Coadd_{propertyName}_{operationName}",) + + return outputNames + + @staticmethod + def getZoomedExtent(fullExtent, n): + """Get zoomed extent centered on the original full plot. + + Parameters + ---------- + fullExtent : `tuple` of `float` + The full extent defined by (lon_min, lon_max, lat_min, lat_max): + + lon_min : `float` + Minimum longitude of the original extent. + + lon_max : `float` + Maximum longitude of the original extent. + + lat_min : `float` + Minimum latitude of the original extent. + + lat_max : `float` + Maximum latitude of the original extent. + + n : `float`, optional + Zoom factor; for instance, n=2 means zooming in 2 times at the + center. If None, the function returns None. + + Returns + ------- + Results : `tuple` [`float`] + New extent as (new_lon_min, new_lon_max, new_lat_min, new_lat_max). + """ + if n is None: + return None + lon_min, lon_max, lat_min, lat_max = fullExtent + lon_center, lat_center = (lon_min + lon_max) / 2, (lat_min + lat_max) / 2 + half_lon = (lon_max - lon_min) / (2 * n) + half_lat = (lat_max - lat_min) / (2 * n) + return lon_center - half_lon, lon_center + half_lon, lat_center - half_lat, lat_center + half_lat + + @staticmethod + def prettyPrintFloat(n): + if n.is_integer(): + return str(int(n)) + return str(n) + + @staticmethod + def addTextToColorbar(cb, text, orientation="vertical", color="black", fontsize=14, fontweight="bold"): + """Helper method to add text inside the horizontal colorbar. + + Parameters + ---------- + cb : `matplotlib.colorbar.Colorbar` + The colorbar object. + + text : `str` + The text to add. + + fontsize : `int`, optional + The fontsize of the text. + + fontweight : `str`, optional + The fontweight of the text. + + Returns + ------- + Results : `None` + The text is added to the colorbar in place. + + """ + if color is None: + color = "black" + vmid = (cb.vmin + cb.vmax) / 2 + positions = {"vertical": (0.5, vmid), "horizontal": (vmid, 0.5)} + cbtext = cb.ax.text( + *positions[orientation], + text, + color=color, + va="center", + ha="center", + fontsize=fontsize, + fontweight=fontweight, + rotation=orientation, + alpha=0.8, + ) + # Add a distinct outline around the text for better visibility in + # various backgrounds. + cbtext.set_path_effects( + [mpl_path_effects.Stroke(linewidth=4, foreground="white", alpha=0.8), mpl_path_effects.Normal()] + ) + + @staticmethod + def getLongestSuffixMatch(s, options): + """Find the longest suffix. + TODO: polish docstring. + + Parameters + ---------- + s : `str` + The string to match. + + options : `list` [`str`] + The list of options to match. + + Returns + ------- + Results : `str` + The longest string of s that matches any of the options. + """ + return next((opt for opt in sorted(options, key=len, reverse=True) if s.endswith(opt)), None) diff --git a/python/lsst/analysis/tools/atools/__init__.py b/python/lsst/analysis/tools/atools/__init__.py index 5541bbfdf..e25b05bfd 100644 --- a/python/lsst/analysis/tools/atools/__init__.py +++ b/python/lsst/analysis/tools/atools/__init__.py @@ -13,6 +13,7 @@ from .numericalValidity import * from .photometricRepeatability import * from .photometry import * +from .propertyMap import * from .seeingMetric import * from .shapes import * from .simpleDiaPlot import * diff --git a/python/lsst/analysis/tools/atools/propertyMap.py b/python/lsst/analysis/tools/atools/propertyMap.py new file mode 100644 index 000000000..dd446d2cc --- /dev/null +++ b/python/lsst/analysis/tools/atools/propertyMap.py @@ -0,0 +1,63 @@ +# 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__ = ("PropertyMapTool",) + +from typing import Any, Dict, Iterable, MutableMapping + +from healsparse.healSparseMap import HealSparseMap +from lsst.pex.config import Field + +from ..actions.plot.propertyMapPlot import PropertyMapPlot +from ..interfaces import AnalysisAction, AnalysisTool + + +class LoadHealSparseMap(AnalysisAction): + """Load a selection of HealSparseMaps configured for plotting.""" + + mapsKey = Field[str]( + doc="The key used to access the dictionary of requested HealSparseMap objects to be loaded." + ) + + def getInputSchema(self) -> Iterable[tuple[str, Dict]]: + return [(self.mapsKey, Dict[str, HealSparseMap])] + + def __call__( + self, data: MutableMapping[str, Dict[str, HealSparseMap]], **kwds: Any + ) -> Dict[str, HealSparseMap]: + return data[self.mapsKey] + + +class PropertyMapTool(AnalysisTool): + """An `AnalysisTool` for plotting property maps.""" + + # Make the getOutputNames() method in the plot action config-aware. + dynamicOutputNames: bool = True + + # Do not iterate over multiple bands in a parameterized manner. + parameterizedBand: bool = False + + def setDefaults(self): + super().setDefaults() + self.process.buildActions.data = LoadHealSparseMap() + self.process.buildActions.data.mapsKey = "maps" + self.produce.plot = PropertyMapPlot() diff --git a/python/lsst/analysis/tools/tasks/__init__.py b/python/lsst/analysis/tools/tasks/__init__.py index c9e85dbd2..06cf75209 100644 --- a/python/lsst/analysis/tools/tasks/__init__.py +++ b/python/lsst/analysis/tools/tasks/__init__.py @@ -6,4 +6,5 @@ from .diffMatchedAnalysis import * from .objectTableSurveyAnalysis import * from .objectTableTractAnalysis import * +from .propertyMapTractAnalysis import * from .sourceTableVisitAnalysis import * diff --git a/python/lsst/analysis/tools/tasks/propertyMapTractAnalysis.py b/python/lsst/analysis/tools/tasks/propertyMapTractAnalysis.py new file mode 100644 index 000000000..5098a3a2b --- /dev/null +++ b/python/lsst/analysis/tools/tasks/propertyMapTractAnalysis.py @@ -0,0 +1,179 @@ +# 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__ = [ + "PropertyMapTractAnalysisConfig", + "PropertyMapTractAnalysisTask", +] + +from typing import Mapping + +from lsst.daf.butler import DataCoordinate +from lsst.pex.config import Config, ConfigDictField, Field, ListField +from lsst.pipe.base import ButlerQuantumContext, InputQuantizedConnection, OutputQuantizedConnection +from lsst.pipe.base import connectionTypes as ct +from lsst.skymap import BaseSkyMap + +from ..interfaces import AnalysisBaseConfig, AnalysisBaseConnections, AnalysisPipelineTask + + +class PropertyMapConfig(Config): + coaddName = Field( + dtype=str, + doc="Coadd name: typically one of deep or goodSeeing.", + default="deep", + ) + operations = ListField( + dtype=str, + doc="List of operations whose corresponding maps should be retrieved.", + default=["min", "max", "mean", "weighted_mean", "sum"], + ) + + +class PropertyMapTractAnalysisConnections( + AnalysisBaseConnections, + dimensions=("skymap", "tract", "band"), + defaultTemplates={"outputName": "propertyMapTract"}, +): + healSparsePropertyMapsConfig = ct.Input( + doc="Configuration parameters for HealSparseInputMapTask in pipe_tasks.", + name="healSparsePropertyMaps_config", + storageClass="Config", + dimensions=(), + ) + + skymap = ct.Input( + doc="The skymap that covers the tract that the data is from.", + name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, + storageClass="SkyMap", + dimensions=("skymap",), + ) + + def __init__(self, *, config=None): + super().__init__(config=config) + + operationNameLookup = { + "min": "Minimum", + "max": "Maximum", + "mean": "Mean", + "weighted_mean": "Weighted mean", + "sum": "Sum", + } + + # Making connections for the maps that are configured to run. + for propertyName in config.properties: + coaddName = config.properties[propertyName].coaddName + for operationName in config.properties[propertyName].operations: + operationLongName = operationNameLookup[operationName] + name = f"{coaddName}Coadd_{propertyName}_map_{operationName}" + setattr( + self, + f"{coaddName}Coadd_{propertyName}_{operationName}", + ct.Input( + doc=f"{operationLongName}-value map of {{propertyLongName}}", + name=name, + storageClass="HealSparseMap", + dimensions=("tract", "skymap", "band"), + multiple=False, + deferLoad=False, + ), + ) + + +class PropertyMapTractAnalysisConfig( + AnalysisBaseConfig, pipelineConnections=PropertyMapTractAnalysisConnections +): + zoomFactors = ListField( + dtype=float, + doc="Two-element list of zoom factors to use when plotting the maps.", + default=[2, 8], + ) + + properties = ConfigDictField( + doc="A configurable dictionary describing the property maps to be plotted, and the coadd name and " + "operations for each map. The available properties include 'exposure_time', 'psf_size', 'psf_e1', " + "'psf_e2', 'psf_maglim', 'sky_noise', 'sky_background', 'dcr_dra', 'dcr_ddec', 'dcr_e1', 'dcr_e2', " + "and 'epoch'.", + keytype=str, + itemtype=PropertyMapConfig, + default={}, + ) + + +class PropertyMapTractAnalysisTask(AnalysisPipelineTask): + ConfigClass = PropertyMapTractAnalysisConfig + _DefaultName = "propertyMapTractAnalysisTask" + + def parsePlotInfo(self, dataId: DataCoordinate | None) -> Mapping[str, str]: + """Parse plot info from the dataId. + + Parameters + ---------- + dataId : `dict` + The dataId of the data to be plotted. + + Returns + ------- + plotInfo : `dict` + A dictionary of the plot information. + + Notes + ----- + We customized this method to fit our needs. 'tract' can be obtained + from dataId, but 'run' and 'tableName' are not relevant for + HealSparseMaps. + """ + + # Initialize the plot info dictionary. + plotInfo = {} + + self._populatePlotInfoWithDataId(plotInfo, dataId) + return plotInfo + + def runQuantum( + self, + butlerQC: ButlerQuantumContext, + inputRefs: InputQuantizedConnection, + outputRefs: OutputQuantizedConnection, + ) -> None: + # Docstring inherited. + + inputs = butlerQC.get(inputRefs) + + if "skymap" in inputs.keys(): + skymap = inputs["skymap"] + else: + skymap = None + dataId = butlerQC.quantum.dataId + + plotInfo = self.parsePlotInfo(dataId) + tract = skymap[dataId["tract"]] + + mapsDict = {} + for key, value in inputs.items(): + if key in ["skymap", "healSparsePropertyMapsConfig"]: + continue + mapsDict[key] = value + + kwargs = {"tract": tract, "zoomFactors": self.config.zoomFactors, "plotInfo": plotInfo} + outputs = self.run(data={"maps": mapsDict}, **kwargs) + butlerQC.put(outputs, outputRefs)