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

34 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-15 02:04 -0800

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 

26from typing import Optional 

27 

28from lsst.afw.table import IdFactory 

29from lsst.daf.butler import DataCoordinate 

30 

31 

32class ExposureIdInfo: 

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

34 

35 Parameters 

36 ---------- 

37 expId : `int` 

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

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

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

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

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

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

44 example below). 

45 expBits : `int` 

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

47 maxBits : `int`, optional 

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

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

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

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

52 

53 Examples 

54 -------- 

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

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

57 

58 .. code-block:: python 

59 

60 from lsst.afw.table import SourceTable 

61 schema = SourceTable.makeMinimalSchema() 

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

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

64 

65 An `ExposureIdInfo` instance can be obtained from a 

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

67 

68 .. code-block:: python 

69 

70 expandedDataId = butler.registry.expandDataId(dataId) 

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

72 

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

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

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

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

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

78 

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

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

81 """ 

82 

83 def __init__(self, expId: int = 0, expBits: int = 1, maxBits: Optional[int] = None): 

84 """Construct an ExposureIdInfo 

85 

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

87 """ 

88 expId = int(expId) 

89 expBits = int(expBits) 

90 

91 if expId.bit_length() > expBits: 

92 raise RuntimeError("expId=%s uses %s bits > expBits=%s" % (expId, expId.bit_length(), expBits)) 

93 

94 self.expId = expId 

95 self.expBits = expBits 

96 

97 if maxBits is not None: 

98 maxBits = int(maxBits) 

99 if maxBits < expBits: 

100 raise RuntimeError("expBits=%s > maxBits=%s" % (expBits, maxBits)) 

101 self.maxBits = maxBits 

102 

103 def __repr__(self) -> str: 

104 return ( 

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

106 ) 

107 

108 @classmethod 

109 def fromDataId( 

110 cls, dataId: DataCoordinate, name: str = "visit_detector", maxBits: Optional[int] = None 

111 ) -> ExposureIdInfo: 

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

113 

114 Parameters 

115 ---------- 

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

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

118 contains extra information about the maximum values for those 

119 dimensions. An expanded data ID can be obtained from 

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

121 methods should already be expanded. 

122 name : `str`, optional 

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

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

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

126 defaults). 

127 maxBits : `int`, optional 

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

129 usually be unnecessary. 

130 

131 Returns 

132 ------- 

133 info : `ExposureIdInfo` 

134 An `ExposureIdInfo` instance. 

135 """ 

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

137 raise RuntimeError( 

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

139 ) 

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

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

142 

143 @property 

144 def unusedBits(self) -> int: 

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

146 if self.maxBits is None: 

147 from lsst.afw.table import IdFactory 

148 

149 return IdFactory.computeReservedFromMaxBits(self.expBits) 

150 else: 

151 return self.maxBits - self.expBits 

152 

153 def makeSourceIdFactory(self) -> IdFactory: 

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

155 exposure information. 

156 

157 Returns 

158 ------- 

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

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

161 managed by this object. 

162 """ 

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