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-02 02:17 -0700

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 fields are annotated as Any because support for better 

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

39 n_detectors: Any = Field( 

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

41 "maximum detector ID, for this instrument. " 

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

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

44 dtype=int, 

45 default=None, 

46 optional=True, 

47 ) 

48 n_observations: Any = Field( 

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

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

51 "visit/exposure ID. " 

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

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

54 dtype=int, 

55 default=None, 

56 optional=True, 

57 ) 

58 

59 

60class ObservationDimensionPacker(DimensionPacker): 

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

62 

63 Parameters 

64 ---------- 

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

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

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

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

69 config : `ObservationDimensionPackerConfig` 

70 Configuration for this dimension packer. 

71 is_exposure : `bool`, optional 

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

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

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

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

76 both are present. 

77 

78 Notes 

79 ----- 

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

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

82 

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

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

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

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

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

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

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

90 of its IDs to compress them into fewer bits. 

91 """ 

92 

93 ConfigClass = ObservationDimensionPackerConfig 

94 

95 def __init__( 

96 self, 

97 data_id: DataCoordinate, 

98 config: ObservationDimensionPackerConfig, 

99 is_exposure: bool | None = None, 

100 ): 

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

102 if is_exposure is None: 

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

104 is_exposure = False 

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

106 is_exposure = True 

107 else: 

108 raise ValueError( 

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

110 ) 

111 if is_exposure: 

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

113 else: 

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

115 super().__init__(fixed, dimensions) 

116 self.is_exposure = is_exposure 

117 if config.n_detectors is not None: 

118 self._n_detectors = config.n_detectors 

119 else: 

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

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

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

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

124 if config.n_observations is not None: 

125 self._n_observations = config.n_observations 

126 elif self.is_exposure: 

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

128 else: 

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

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

131 

132 @property 

133 def maxBits(self) -> int: 

134 # Docstring inherited from DimensionPacker.maxBits 

135 return self._max_bits 

136 

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

138 # Docstring inherited from DimensionPacker._pack 

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

140 if detector_id >= self._n_detectors: 

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

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

143 if observation_id >= self._n_observations: 

144 raise ValueError( 

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

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

147 ) 

148 return detector_id + self._n_detectors * observation_id 

149 

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

151 # Docstring inherited from DimensionPacker.unpack 

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

153 return DataCoordinate.standardize( 

154 { 

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

156 "detector": detector, 

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

158 }, 

159 graph=self.dimensions, 

160 ) 

161 

162 

163observation_packer_registry = makeRegistry( 

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

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

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

167) 

168observation_packer_registry.register("observation", ObservationDimensionPacker)