Coverage for python/lsst/analysis/tools/analysisParts/base.py: 74%

92 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-03 01:36 -0700

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__ = ("BasePrep", "BaseProcess", "BaseMetricAction") 

24 

25from collections import abc 

26from typing import Mapping 

27 

28import astropy.units as apu 

29from lsst.pex.config.dictField import DictField 

30from lsst.pipe.tasks.configurableActions import ConfigurableActionStructField 

31from lsst.verify import Measurement 

32 

33from ..actions.keyedData import KeyedDataSelectorAction 

34from ..interfaces import ( 

35 AnalysisAction, 

36 KeyedData, 

37 KeyedDataAction, 

38 KeyedDataSchema, 

39 KeyedDataTypes, 

40 MetricAction, 

41 Scalar, 

42 Vector, 

43 VectorAction, 

44) 

45 

46 

47class BasePrep(KeyedDataSelectorAction): 

48 def addInputSchema(self, inputSchema: KeyedDataSchema) -> None: 

49 self._frozen = False 

50 self.vectorKeys = [name for name, _ in inputSchema] 

51 self._frozen = True 

52 

53 

54class BaseProcess(KeyedDataAction): 

55 buildActions = ConfigurableActionStructField[VectorAction | KeyedDataAction]( 

56 doc="Actions which compute a Vector which will be added to results" 

57 ) 

58 filterActions = ConfigurableActionStructField[VectorAction | KeyedDataAction]( 

59 doc="Actions which filter one or more input or build Vectors into shorter vectors" 

60 ) 

61 calculateActions = ConfigurableActionStructField[AnalysisAction]( 

62 doc="Actions which compute quantities from the input or built data" 

63 ) 

64 

65 def getInputSchema(self) -> KeyedDataSchema: 

66 inputSchema: KeyedDataTypes = {} # type: ignore 

67 buildOutputSchema: KeyedDataTypes = {} # type: ignore 

68 filterOutputSchema: KeyedDataTypes = {} # type: ignore 

69 

70 for fieldName, action in self.buildActions.items(): 

71 for name, typ in action.getInputSchema(): 

72 inputSchema[name] = typ 

73 if isinstance(action, KeyedDataAction): 73 ↛ 74line 73 didn't jump to line 74, because the condition on line 73 was never true

74 buildOutputSchema.update(action.getOutputSchema() or {}) 

75 else: 

76 buildOutputSchema[fieldName] = Vector 

77 

78 for fieldName, action in self.filterActions.items(): 

79 for name, typ in action.getInputSchema(): 

80 if name not in buildOutputSchema: 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true

81 inputSchema[name] = typ 

82 if isinstance(action, KeyedDataAction): 82 ↛ 83line 82 didn't jump to line 83, because the condition on line 82 was never true

83 filterOutputSchema.update(action.getOutputSchema() or {}) 

84 else: 

85 filterOutputSchema[fieldName] = Vector 

86 

87 for action in self.calculateActions: 

88 for name, typ in action.getInputSchema(): 

89 if name not in buildOutputSchema and name not in filterOutputSchema: 

90 inputSchema[name] = typ 

91 return ((name, typ) for name, typ in inputSchema.items()) 

92 

93 def getOutputSchema(self) -> KeyedDataSchema: 

94 for action in self.buildActions: 

95 if isinstance(action, KeyedDataAction): 

96 outSchema = action.getOutputSchema() 

97 if outSchema is not None: 

98 yield from outSchema 

99 

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

101 results = {} 

102 data = dict(data) 

103 for name, action in self.buildActions.items(): 

104 match action(data, **kwargs): 

105 case abc.Mapping() as item: 105 ↛ 106line 105 didn't jump to line 106, because the pattern on line 105 never matched

106 for key, result in item.items(): 

107 results[key] = result 

108 case item: 

109 results[name] = item 

110 view1 = data | results 

111 for name, action in self.filterActions.items(): 

112 match action(view1, **kwargs): 

113 case abc.Mapping() as item: 113 ↛ 114line 113 didn't jump to line 114, because the pattern on line 113 never matched

114 for key, result in item.items(): 

115 results[key] = result 

116 case item: 

117 results[name] = item 

118 

119 view2 = data | results 

120 for name, action in self.calculateActions.items(): 

121 match action(view2, **kwargs): 

122 case abc.Mapping() as item: 122 ↛ 125line 122 didn't jump to line 125, because the pattern on line 122 always matched

123 for key, result in item.items(): 

124 results[key] = result 

125 case item: 

126 results[name] = item 

127 return results 

128 

129 

130class BaseMetricAction(MetricAction): 

131 units = DictField[str, str]( 

132 doc="Mapping of scalar key to astropy unit string", 

133 ) 

134 newNames = DictField[str, str]( 

135 doc="Mapping of key to new name if needed prior to creating metric", 

136 default={}, 

137 ) 

138 

139 def getInputSchema(self) -> KeyedDataSchema: 

140 # Something is wrong with the typing for DictField key iteration 

141 return [(key, Scalar) for key in self.units] # type: ignore 

142 

143 def __call__(self, data: KeyedData, **kwargs) -> Mapping[str, Measurement] | Measurement: 

144 results = {} 

145 for key, unit in self.units.items(): 

146 formattedKey = key.format(**kwargs) 

147 if formattedKey not in data: 147 ↛ 148line 147 didn't jump to line 148, because the condition on line 147 was never true

148 raise ValueError(f"Key: {formattedKey} could not be found input data") 

149 value = data[formattedKey] 

150 if not isinstance(value, Scalar): 150 ↛ 151line 150 didn't jump to line 151, because the condition on line 150 was never true

151 raise ValueError(f"Data for key {key} is not a Scalar type") 

152 if newName := self.newNames.get(key): 152 ↛ 153line 152 didn't jump to line 153, because the condition on line 152 was never true

153 formattedKey = newName.format(**kwargs) 

154 results[formattedKey] = Measurement(formattedKey, value * apu.Unit(unit)) 

155 return results