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

49 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-19 10:39 +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` 

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, 

107 is_exposure: bool | None = None, 

108 ): 

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

110 if is_exposure is None: 

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

112 is_exposure = False 

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

114 is_exposure = True 

115 else: 

116 raise ValueError( 

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

118 ) 

119 if is_exposure: 

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

121 else: 

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

123 super().__init__(fixed, dimensions) 

124 self.is_exposure = is_exposure 

125 if config.n_detectors is not None: 

126 self._n_detectors = config.n_detectors 

127 else: 

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

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

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

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

132 if config.n_observations is not None: 

133 self._n_observations = config.n_observations 

134 elif self.is_exposure: 

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

136 else: 

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

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

139 

140 @property 

141 def maxBits(self) -> int: 

142 # Docstring inherited from DimensionPacker.maxBits 

143 return self._max_bits 

144 

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

146 # Docstring inherited from DimensionPacker._pack 

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

148 if detector_id >= self._n_detectors: 

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

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

151 if observation_id >= self._n_observations: 

152 raise ValueError( 

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

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

155 ) 

156 return detector_id + self._n_detectors * observation_id 

157 

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

159 # Docstring inherited from DimensionPacker.unpack 

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

161 return DataCoordinate.standardize( 

162 { 

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

164 "detector": detector, 

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

166 }, 

167 graph=self.dimensions, 

168 ) 

169 

170 

171observation_packer_registry = makeRegistry( 

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

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

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

175) 

176observation_packer_registry.register("observation", ObservationDimensionPacker)