Coverage for python / lsst / analysis / tools / actions / keyedData / calcCompletenessHistogram.py: 22%
76 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:32 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:32 +0000
1# This file is part of analysis_tools.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21from __future__ import annotations
23__all__ = ("CalcCompletenessHistogramAction", "MagnitudeCompletenessConfig")
25import copy
27import numpy as np
29import lsst.pex.config as pexConfig
30from lsst.pex.config.configurableActions import ConfigurableActionField
32from ...interfaces import KeyedData, KeyedDataAction, KeyedDataSchema
33from ...math import isPercent
34from ..config import MagnitudeBinConfig
35from .calcBinnedCompleteness import CalcBinnedCompletenessAction
38class MagnitudeCompletenessConfig(pexConfig.Config):
39 """Configuration for measuring magnitudes at given completeness
40 thresholds."""
42 completeness_mag_max = pexConfig.Field[float](
43 doc="Brightest magnitude to consider checking if completeness is below a percentile threshold for",
44 default=18,
45 )
46 completeness_percentiles = pexConfig.ListField[float](
47 doc="The percentiles to find the magnitude at.",
48 default=[90.0, 80.0, 50.0],
49 itemCheck=isPercent,
50 )
53class CalcCompletenessHistogramAction(KeyedDataAction):
54 """Action to calculate a histogram of completeness vs magnitude."""
56 action = ConfigurableActionField[CalcBinnedCompletenessAction](
57 doc="The action to compute completeness/purity",
58 )
59 bins = pexConfig.ConfigField[MagnitudeBinConfig](
60 doc="The magnitude bin configuration",
61 )
62 config_metrics = pexConfig.ConfigField[MagnitudeCompletenessConfig](doc="Metric definition configuration")
64 def __call__(self, data: KeyedData, **kwargs) -> KeyedData:
65 band = kwargs.get("band")
66 bins = tuple(x / 1000.0 for x in reversed(self.bins.get_bins()))
67 bin_width = self.bins.mag_width / 1000.0
68 n_bins = len(bins)
69 keys_raw = {
70 key_formatted: key
71 for key, key_formatted in self.action.getFormattedOutputKeys(**kwargs).items()
72 if not self._is_action_key_mask(key)
73 }
74 results = {key_formatted: np.zeros(n_bins, dtype=float) for key_formatted in keys_raw.keys()}
75 action = copy.copy(self.action)
76 name_completeness = action.name_completeness
77 if band is not None:
78 name_completeness = name_completeness.format(band=band)
80 percentile_completeness = {pc: np.nan for pc in sorted(self.config_metrics.completeness_percentiles)}
81 percentiles = list(percentile_completeness.keys())
82 n_percentiles = len(percentiles)
83 idx_percentile = 0
84 # isfinite won't work on None
85 completeness_last, median_last = np.nan, np.nan
87 for idx_rev, minimum in enumerate(bins):
88 maximum = minimum + bin_width
89 median = (maximum + minimum) / 2.0
90 action.selector_range_ref.minimum = minimum
91 action.selector_range_ref.maximum = maximum
92 action.selector_range_target.minimum = minimum
93 action.selector_range_target.maximum = maximum
94 result = action(data, **kwargs)
95 for key_formatted, array in results.items():
96 value = result[key_formatted]
97 # The implicit float conversion will generate a warning if the
98 # value is masked, so check for that first
99 array[n_bins - idx_rev - 1] = np.nan if np.ma.is_masked(value) else value
100 if median >= self.config_metrics.completeness_mag_max:
101 completeness = result[name_completeness] * 100
102 if completeness:
103 if idx_percentile > 0:
104 if completeness < percentiles[idx_percentile - 1]:
105 idx_percentile -= 1
106 while (idx_percentile < n_percentiles) and (completeness > percentiles[idx_percentile]):
107 # Crude linear interpolation
108 # TODO: Replace with a spline or anything better
109 if np.isfinite(completeness_last) and np.isfinite(median_last):
110 percentile = percentiles[idx_percentile]
111 width = completeness - completeness_last
112 # The abs should be unnecessary but just in case
113 magnitude = (
114 abs(completeness - percentile) / width * median_last
115 + abs(percentile - completeness_last) / width * median
116 )
117 else:
118 magnitude = median
119 percentile_completeness[percentiles[idx_percentile]] = magnitude
120 idx_percentile += 1
121 completeness_last, median_last = completeness, median
123 for percentile, magnitude in percentile_completeness.items():
124 name_percentile = self.getPercentileName(percentile)
125 key = action.name_mag_completeness(name_percentile)
126 if band is not None:
127 key = key.format(band=band)
128 results[key] = magnitude
130 return results
132 def _is_action_key_mask(self, key: str):
133 is_mask = key.startswith(f"{self.action.name_prefix}mask")
134 return is_mask
136 def getInputSchema(self) -> KeyedDataSchema:
137 yield from self.action.getInputSchema()
139 def getOutputSchema(self) -> KeyedDataSchema:
140 result = {
141 (key, typ) for key, typ in self.action.getOutputSchema() if not self._is_action_key_mask(key)
142 }
143 return result
145 def getPercentileName(self, percentile: float) -> str:
146 return f"{percentile:.2f}_pct".replace(".", "p")