Coverage for python / lsst / analysis / tools / actions / vector / calcBinnedStats.py: 39%

72 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-01 08:55 +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 

22 

23__all__ = ("CalcBinnedStatsAction",) 

24 

25from functools import cached_property 

26from typing import cast 

27 

28import numpy as np 

29 

30from lsst.pex.config import Field 

31from lsst.pex.config.configurableActions import ConfigurableActionField 

32 

33from ...interfaces import KeyedData, KeyedDataAction, KeyedDataSchema, Scalar, Vector 

34from ..keyedData.summaryStatistics import SummaryStatisticAction 

35from .selectors import RangeSelector 

36 

37 

38class CalcBinnedStatsAction(KeyedDataAction): 

39 key_vector = Field[str](doc="Vector on which to compute statistics") 

40 name_prefix = Field[str](default="", doc="Field name to append stat names to") 

41 name_suffix = Field[str](default="", doc="Field name to append to stat names") 

42 selector_range = ConfigurableActionField[RangeSelector](doc="Range selector") 

43 return_minmax = Field[bool](default=True, doc="Whether to return the bin minimum and maximum") 

44 

45 def getInputSchema(self, **kwargs) -> KeyedDataSchema: 

46 yield (self.key_vector, Vector) 

47 yield from self.selector_range.getInputSchema() 

48 

49 def getOutputSchema(self) -> KeyedDataSchema: 

50 yield from ( 

51 (self.name_mask, Vector), 

52 (self.name_median, Scalar), 

53 (self.name_sigmaMad, Scalar), 

54 (self.name_count, Scalar), 

55 ) 

56 if self.return_minmax: 

57 yield (self.name_select_maximum, Scalar) 

58 yield (self.name_select_median, Scalar), 

59 if self.return_minmax: 

60 yield (self.name_select_minimum, Scalar), 

61 yield from ( 

62 ("range_maximum", Scalar), 

63 ("range_minimum", Scalar), 

64 ) 

65 

66 @cached_property 

67 def name_count(self): 

68 return f"{self.name_prefix}count{self.name_suffix}" 

69 

70 @cached_property 

71 def name_mask(self): 

72 return f"{self.name_prefix}mask{self.name_suffix}" 

73 

74 @cached_property 

75 def name_median(self): 

76 return f"{self.name_prefix}median{self.name_suffix}" 

77 

78 @cached_property 

79 def name_select_maximum(self): 

80 return f"{self.name_prefix}select_maximum{self.name_suffix}" 

81 

82 @cached_property 

83 def name_select_median(self): 

84 return f"{self.name_prefix}select_median{self.name_suffix}" 

85 

86 @cached_property 

87 def name_select_minimum(self): 

88 return f"{self.name_prefix}select_minimum{self.name_suffix}" 

89 

90 @cached_property 

91 def name_sigmaMad(self): 

92 return f"{self.name_prefix}sigmaMad{self.name_suffix}" 

93 

94 def __call__(self, data: KeyedData, **kwargs) -> KeyedData: 

95 has_band = "band" in kwargs 

96 kwargs_format = {} 

97 if has_band: 

98 kwargs_format["band"] = kwargs["band"] 

99 prefix_band = f"{kwargs['band']}_" if has_band else "" 

100 

101 results = {} 

102 mask = self.selector_range(data, **kwargs) 

103 results[self.name_mask.format(**kwargs_format)] = mask 

104 kwargs["mask"] = mask 

105 

106 action = SummaryStatisticAction(vectorKey=self.key_vector) 

107 # this is sad, but pex_config seems to have broken behavior that 

108 # is dangerous to fix 

109 action.setDefaults() 

110 

111 for name, value in action(data, **kwargs).items(): 

112 results[getattr(self, f"name_{name}").format(**kwargs_format)] = value 

113 

114 values = cast(Vector, data[self.selector_range.vectorKey][mask]) # type: ignore 

115 valid = np.sum(np.isfinite(values)) > 0 

116 

117 if self.return_minmax: 

118 results[self.name_select_maximum.format(**kwargs_format)] = cast( 

119 Scalar, float(np.nanmax(values)) if valid else np.nan 

120 ) 

121 results[self.name_select_median.format(**kwargs_format)] = cast( 

122 Scalar, float(np.nanmedian(values)) if valid else np.nan 

123 ) 

124 if self.return_minmax: 

125 results[self.name_select_minimum.format(**kwargs_format)] = cast( 

126 Scalar, float(np.nanmin(values)) if valid else np.nan 

127 ) 

128 results[f"{prefix_band}range_maximum"] = self.selector_range.maximum 

129 results[f"{prefix_band}range_minimum"] = self.selector_range.minimum 

130 

131 return results