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

51 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-28 11:05 +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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28from __future__ import annotations 

29 

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

31 

32from typing import Any, cast 

33 

34from lsst.daf.butler import DataCoordinate, DimensionPacker 

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

36 

37observation_packer_registry = makeRegistry( 

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

39) 

40 

41 

42class ObservationDimensionPackerConfig(Config): 

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

44 

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

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

47 n_detectors: Any = Field( 

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

49 "maximum detector ID, for this instrument. " 

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

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

52 dtype=int, 

53 default=None, 

54 optional=True, 

55 ) 

56 n_observations: Any = Field( 

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

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

59 "visit/exposure ID. " 

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

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

62 dtype=int, 

63 default=None, 

64 optional=True, 

65 ) 

66 

67 

68class ObservationDimensionPacker(DimensionPacker): 

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

70 

71 Parameters 

72 ---------- 

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

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

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

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

77 config : `ObservationDimensionPackerConfig`, optional 

78 Configuration for this dimension packer. 

79 is_exposure : `bool`, optional 

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

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

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

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

84 both are present. 

85 

86 Notes 

87 ----- 

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

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

90 

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

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

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

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

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

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

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

98 of its IDs to compress them into fewer bits. 

99 """ 

100 

101 ConfigClass = ObservationDimensionPackerConfig 

102 

103 def __init__( 

104 self, 

105 data_id: DataCoordinate, 

106 config: ObservationDimensionPackerConfig | None = None, 

107 is_exposure: bool | None = None, 

108 ): 

109 if config is None: 

110 config = ObservationDimensionPackerConfig() 

111 fixed = data_id.subset(data_id.universe.conform(["instrument"])) 

112 if is_exposure is None: 

113 if "visit" in data_id.dimensions.names: 

114 is_exposure = False 

115 elif "exposure" in data_id.dimensions.names: 

116 is_exposure = True 

117 else: 

118 raise ValueError( 

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

120 ) 

121 if is_exposure: 

122 dimensions = fixed.universe.conform(["instrument", "exposure", "detector"]) 

123 else: 

124 dimensions = fixed.universe.conform(["instrument", "visit", "detector"]) 

125 super().__init__(fixed, dimensions) 

126 self.is_exposure = is_exposure 

127 if config.n_detectors is not None: 

128 self._n_detectors = config.n_detectors 

129 else: 

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

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

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

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

134 if config.n_observations is not None: 

135 self._n_observations = config.n_observations 

136 elif self.is_exposure: 

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

138 else: 

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

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

141 

142 @property 

143 def maxBits(self) -> int: 

144 # Docstring inherited from DimensionPacker.maxBits 

145 return self._max_bits 

146 

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

148 # Docstring inherited from DimensionPacker._pack 

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

150 if detector_id >= self._n_detectors: 

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

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

153 if observation_id >= self._n_observations: 

154 raise ValueError( 

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

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

157 ) 

158 return detector_id + self._n_detectors * observation_id 

159 

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

161 # Docstring inherited from DimensionPacker.unpack 

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

163 return DataCoordinate.standardize( 

164 { 

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

166 "detector": detector, 

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

168 }, 

169 dimensions=self._dimensions, 

170 ) 

171 

172 

173observation_packer_registry = makeRegistry( 

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

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

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

177) 

178observation_packer_registry.register("observation", ObservationDimensionPacker)