Coverage for python/lsst/pipe/base/_observation_dimension_packer.py: 27%

49 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-25 09:14 +0000

1# This file is part of pipe_base. 

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 <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("ObservationDimensionPacker", "ObservationDimensionPackerConfig", "observation_packer_registry") 

25 

26from typing import Any, cast 

27 

28from lsst.daf.butler import DataCoordinate, DimensionPacker 

29from lsst.pex.config import Config, Field, makeRegistry 

30 

31observation_packer_registry = makeRegistry( 

32 "Configurables that can pack visit+detector or exposure+detector data IDs into integers." 

33) 

34 

35 

36class ObservationDimensionPackerConfig(Config): 

37 """Config associated with a `ObservationDimensionPacker`.""" 

38 

39 # Config fields are annotated as Any because support for better 

40 # annotations is broken on Fields with optional=True. 

41 n_detectors: Any = Field( 

42 "Number of detectors, or, more precisely, one greater than the " 

43 "maximum detector ID, for this instrument. " 

44 "Default (None) obtains this value from the instrument dimension record. " 

45 "This should rarely need to be overridden outside of tests.", 

46 dtype=int, 

47 default=None, 

48 optional=True, 

49 ) 

50 n_observations: Any = Field( 

51 "Number of observations (visits or exposures, as per 'is_exposure`) " 

52 "expected, or, more precisely, one greater than the maximum " 

53 "visit/exposure ID. " 

54 "Default (None) obtains this value from the instrument dimension record. " 

55 "This should rarely need to be overridden outside of tests.", 

56 dtype=int, 

57 default=None, 

58 optional=True, 

59 ) 

60 

61 

62class ObservationDimensionPacker(DimensionPacker): 

63 """A `DimensionPacker` for visit+detector or exposure+detector. 

64 

65 Parameters 

66 ---------- 

67 data_id : `lsst.daf.butler.DataCoordinate` 

68 Data ID that identifies at least the ``instrument`` dimension. Must 

69 have dimension records attached unless ``config.n_detectors`` and 

70 ``config.n_visits`` are both not `None`. 

71 config : `ObservationDimensionPackerConfig` 

72 Configuration for this dimension packer. 

73 is_exposure : `bool`, optional 

74 If `False`, construct a packer for visit+detector data IDs. If `True`, 

75 construct a packer for exposure+detector data IDs. If `None`, 

76 this is determined based on whether ``visit`` or ``exposure`` is 

77 present in ``data_id``, with ``visit`` checked first and hence used if 

78 both are present. 

79 

80 Notes 

81 ----- 

82 The standard pattern for constructing instances of the class is to use 

83 `Instrument.make_dimension_packer`; see that method for details. 

84 

85 This packer assumes all visit/exposure and detector IDs are sequential or 

86 otherwise densely packed between zero and their upper bound, such that 

87 ``n_detectors`` * ``n_observations`` leaves plenty of bits remaining for 

88 any other IDs that need to be included in the same integer (such as a 

89 counter for Sources detected on an image with this data ID). Instruments 

90 whose data ID values are not densely packed, should provide their own 

91 `~lsst.daf.butler.DimensionPacker` that takes advantage of the structure 

92 of its IDs to compress them into fewer bits. 

93 """ 

94 

95 ConfigClass = ObservationDimensionPackerConfig 

96 

97 def __init__( 

98 self, 

99 data_id: DataCoordinate, 

100 config: ObservationDimensionPackerConfig, 

101 is_exposure: bool | None = None, 

102 ): 

103 fixed = data_id.subset(data_id.universe.extract(["instrument"])) 

104 if is_exposure is None: 

105 if "visit" in data_id.graph.names: 

106 is_exposure = False 

107 elif "exposure" in data_id.graph.names: 

108 is_exposure = True 

109 else: 

110 raise ValueError( 

111 "'is_exposure' was not provided and 'data_id' has no visit or exposure value." 

112 ) 

113 if is_exposure: 

114 dimensions = fixed.universe.extract(["instrument", "exposure", "detector"]) 

115 else: 

116 dimensions = fixed.universe.extract(["instrument", "visit", "detector"]) 

117 super().__init__(fixed, dimensions) 

118 self.is_exposure = is_exposure 

119 if config.n_detectors is not None: 

120 self._n_detectors = config.n_detectors 

121 else: 

122 # Records accessed here should never be None; that possibility is 

123 # only for non-dimension elements like join tables that are 

124 # are sometimes not present in an expanded data ID. 

125 self._n_detectors = fixed.records["instrument"].detector_max # type: ignore[union-attr] 

126 if config.n_observations is not None: 

127 self._n_observations = config.n_observations 

128 elif self.is_exposure: 

129 self._n_observations = fixed.records["instrument"].exposure_max # type: ignore[union-attr] 

130 else: 

131 self._n_observations = fixed.records["instrument"].visit_max # type: ignore[union-attr] 

132 self._max_bits = (self._n_observations * self._n_detectors - 1).bit_length() 

133 

134 @property 

135 def maxBits(self) -> int: 

136 # Docstring inherited from DimensionPacker.maxBits 

137 return self._max_bits 

138 

139 def _pack(self, dataId: DataCoordinate) -> int: 

140 # Docstring inherited from DimensionPacker._pack 

141 detector_id = cast(int, dataId["detector"]) 

142 if detector_id >= self._n_detectors: 

143 raise ValueError(f"Detector ID {detector_id} is out of bounds; expected <{self._n_detectors}.") 

144 observation_id = cast(int, dataId["exposure" if self.is_exposure else "visit"]) 

145 if observation_id >= self._n_observations: 

146 raise ValueError( 

147 f"{'Exposure' if self.is_exposure else 'Visit'} ID {observation_id} is out of bounds; " 

148 f"expected <{self._n_observations}." 

149 ) 

150 return detector_id + self._n_detectors * observation_id 

151 

152 def unpack(self, packedId: int) -> DataCoordinate: 

153 # Docstring inherited from DimensionPacker.unpack 

154 observation, detector = divmod(packedId, self._n_detectors) 

155 return DataCoordinate.standardize( 

156 { 

157 "instrument": self.fixed["instrument"], 

158 "detector": detector, 

159 ("exposure" if self.is_exposure else "visit"): observation, 

160 }, 

161 graph=self.dimensions, 

162 ) 

163 

164 

165observation_packer_registry = makeRegistry( 

166 "Configurables that can pack visit+detector or exposure+detector data IDs into integers. " 

167 "Members of this registry should be callable with the same signature as " 

168 "`lsst.pipe.base.ObservationDimensionPacker` construction." 

169) 

170observation_packer_registry.register("observation", ObservationDimensionPacker)