Coverage for python/lsst/obs/base/exposureIdInfo.py: 39%

35 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-17 07:55 +0000

1# This file is part of obs_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 <https://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["ExposureIdInfo"] 

25 

26 

27from deprecated.sphinx import deprecated 

28from lsst.afw.table import IdFactory 

29from lsst.daf.butler import DataCoordinate 

30 

31 

32# TODO: remove on DM-38687. 

33@deprecated( 

34 "Deprecated in favor of `lsst.meas.base.IdGenerator`; will be removed after v26.", 

35 version="v26", 

36 category=FutureWarning, 

37) 

38class ExposureIdInfo: 

39 """Struct representing an exposure ID and the number of bits it uses. 

40 

41 Parameters 

42 ---------- 

43 expId : `int` 

44 Exposure ID. Note that this is typically the ID of an 

45 `afw.image.Exposure`, not the ID of an actual observation, and hence it 

46 usually either includes a detector component or is derived from SkyMap 

47 IDs, and the observation ID component usually represents a ``visit`` 

48 rather than ``exposure``. For code using the Gen3 butler, this will 

49 usually be obtained via a `~lsst.daf.butler.DimensionPacker` (see 

50 example below). 

51 expBits : `int` 

52 Maximum number of bits allowed for exposure IDs of this type. 

53 maxBits : `int`, optional 

54 Maximum number of bits available for values that combine exposure ID 

55 with other information, such as source ID. If not provided 

56 (recommended when possible), `unusedBits` will be computed by assuming 

57 the full ID must fit an an `lsst.afw.table` RecordId field. 

58 

59 Examples 

60 -------- 

61 One common use is creating an ID factory for making a source table. 

62 For example, given a `ExposureIdInfo` instance ``info``, 

63 

64 .. code-block:: python 

65 

66 from lsst.afw.table import SourceTable 

67 

68 schema = SourceTable.makeMinimalSchema() 

69 # ...add fields to schema as desired, then... 

70 sourceTable = SourceTable.make(self.schema, info.makeSourceIdFactory()) 

71 

72 An `ExposureIdInfo` instance can be obtained from a 

73 `~lsst.daf.butler.DataCoordinate` with: 

74 

75 .. code-block:: python 

76 

77 expandedDataId = butler.registry.expandDataId(dataId) 

78 info = ExposureIdInfo.fromDataId(expandedDataId, "visit_detector") 

79 

80 The first line should be unnecessary for the data IDs passed to 

81 `~lsst.pipe.base.PipelineTask` methods, as those are already expanded, and 

82 ``"visit_detector"`` can be replaced by other strings to pack data IDs with 

83 different dimensions (e.g. ``"tract_patch"`` or ``"tract_patch_band"``); 

84 see the data repository's dimensions configuration for other options. 

85 

86 At least one bit must be reserved for the exposure ID, even if there is no 

87 exposure ID, for reasons that are not entirely clear (this is DM-6664). 

88 """ 

89 

90 def __init__(self, expId: int = 0, expBits: int = 1, maxBits: int | None = None): 

91 """Construct an ExposureIdInfo. 

92 

93 See the class doc string for an explanation of the arguments. 

94 """ 

95 expId = int(expId) 

96 expBits = int(expBits) 

97 

98 if expId.bit_length() > expBits: 

99 raise RuntimeError(f"expId={expId} uses {expId.bit_length()} bits > expBits={expBits}") 

100 

101 self.expId = expId 

102 self.expBits = expBits 

103 

104 if maxBits is not None: 

105 maxBits = int(maxBits) 

106 if maxBits < expBits: 

107 raise RuntimeError(f"expBits={expBits} > maxBits={maxBits}") 

108 self.maxBits = maxBits 

109 

110 def __repr__(self) -> str: 

111 return ( 

112 f"{self.__class__.__name__}(expId={self.expId}, expBits={self.expBits}, maxBits={self.maxBits})" 

113 ) 

114 

115 @classmethod 

116 def fromDataId( 

117 cls, dataId: DataCoordinate, name: str = "visit_detector", maxBits: int | None = None 

118 ) -> ExposureIdInfo: 

119 """Construct an instance from a fully-expanded data ID. 

120 

121 Parameters 

122 ---------- 

123 dataId : `lsst.daf.butler.DataCoordinate` 

124 An expanded data ID that identifies the dimensions to be packed and 

125 contains extra information about the maximum values for those 

126 dimensions. An expanded data ID can be obtained from 

127 `Registry.expandDataId`, but all data IDs passed to `PipelineTask` 

128 methods should already be expanded. 

129 name : `str`, optional 

130 Name of the packer to use. The set of available packers can be 

131 found in the data repository's dimension configuration (see the 

132 "packers" section of ``dimensions.yaml`` in ``daf_butler`` for the 

133 defaults). 

134 maxBits : `int`, optional 

135 Forwarded as the ``__init__`` parameter of the same name. Should 

136 usually be unnecessary. 

137 

138 Returns 

139 ------- 

140 info : `ExposureIdInfo` 

141 An `ExposureIdInfo` instance. 

142 """ 

143 if not isinstance(dataId, DataCoordinate) or not dataId.hasRecords(): 

144 raise RuntimeError( 

145 "A fully-expanded data ID is required; use Registry.expandDataId to obtain one." 

146 ) 

147 expId, expBits = dataId.pack(name, returnMaxBits=True) 

148 return cls(expId=expId, expBits=expBits, maxBits=maxBits) 

149 

150 @property 

151 def unusedBits(self) -> int: 

152 """Maximum number of bits available for non-exposure info `(int)`.""" 

153 if self.maxBits is None: 

154 from lsst.afw.table import IdFactory 

155 

156 return IdFactory.computeReservedFromMaxBits(self.expBits) 

157 else: 

158 return self.maxBits - self.expBits 

159 

160 def makeSourceIdFactory(self) -> IdFactory: 

161 """Make a `lsst.afw.table.SourceTable.IdFactory` instance from this 

162 exposure information. 

163 

164 Returns 

165 ------- 

166 idFactory : `lsst.afw.table.SourceTable.IdFactory` 

167 An ID factory that generates new IDs that fold in the image IDs 

168 managed by this object. 

169 """ 

170 return IdFactory.makeSource(self.expId, self.unusedBits)