Coverage for python/lsst/daf/butler/core/dimensions/_packer.py: 45%

61 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 19:55 +0000

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 

25__all__ = ("DimensionPacker",) 

26 

27from abc import ABCMeta, abstractmethod 

28from typing import ( 

29 AbstractSet, 

30 Any, 

31 Iterable, 

32 Optional, 

33 Tuple, 

34 Type, 

35 TYPE_CHECKING, 

36 Union, 

37) 

38 

39from lsst.utils import doImport 

40 

41from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor 

42from ._coordinate import DataCoordinate, DataId 

43from ._graph import DimensionGraph 

44 

45if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 45 ↛ 46line 45 didn't jump to line 46, because the condition on line 45 was never true

46 from ._universe import DimensionUniverse 

47 

48 

49class DimensionPacker(metaclass=ABCMeta): 

50 """Class for going from `DataCoordinate` to packed integer ID and back. 

51 

52 An abstract base class for bidirectional mappings between a 

53 `DataCoordinate` and a packed integer ID. 

54 

55 Parameters 

56 ---------- 

57 fixed : `DataCoordinate` 

58 Expanded data ID for the dimensions whose values must remain fixed 

59 (to these values) in all calls to `pack`, and are used in the results 

60 of calls to `unpack`. ``fixed.hasRecords()`` must return `True`. 

61 dimensions : `DimensionGraph` 

62 The dimensions of data IDs packed by this instance. 

63 """ 

64 

65 def __init__(self, fixed: DataCoordinate, dimensions: DimensionGraph): 

66 self.fixed = fixed 

67 self.dimensions = dimensions 

68 

69 @property 

70 def universe(self) -> DimensionUniverse: 

71 """Graph containing all known dimensions (`DimensionUniverse`).""" 

72 return self.fixed.universe 

73 

74 @property 

75 @abstractmethod 

76 def maxBits(self) -> int: 

77 """Return The maximum number of nonzero bits in the packed ID. 

78 

79 This packed ID will be returned by 

80 `~DimensionPacker.pack` (`int`). 

81 

82 Must be implemented by all concrete derived classes. May return 

83 `None` to indicate that there is no maximum. 

84 """ 

85 raise NotImplementedError() 

86 

87 @abstractmethod 

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

89 """Abstract implementation for `~DimensionPacker.pack`. 

90 

91 Must be implemented by all concrete derived classes. 

92 

93 Parameters 

94 ---------- 

95 dataId : `DataCoordinate` 

96 Dictionary-like object identifying (at least) all packed 

97 dimensions associated with this packer. Guaranteed to be a true 

98 `DataCoordinate`, not an informal data ID 

99 

100 Returns 

101 ------- 

102 packed : `int` 

103 Packed integer ID. 

104 """ 

105 raise NotImplementedError() 

106 

107 def pack(self, dataId: DataId, *, returnMaxBits: bool = False, 

108 **kwargs: Any) -> Union[Tuple[int, int], int]: 

109 """Pack the given data ID into a single integer. 

110 

111 Parameters 

112 ---------- 

113 dataId : `DataId` 

114 Data ID to pack. Values for any keys also present in the "fixed" 

115 data ID passed at construction must be the same as the values 

116 passed at construction. 

117 returnMaxBits : `bool` 

118 If `True`, return a tuple of ``(packed, self.maxBits)``. 

119 **kwargs 

120 Additional keyword arguments forwarded to 

121 `DataCoordinate.standardize`. 

122 

123 Returns 

124 ------- 

125 packed : `int` 

126 Packed integer ID. 

127 maxBits : `int`, optional 

128 Maximum number of nonzero bits in ``packed``. Not returned unless 

129 ``returnMaxBits`` is `True`. 

130 

131 Notes 

132 ----- 

133 Should not be overridden by derived class 

134 (`~DimensionPacker._pack` should be overridden instead). 

135 """ 

136 dataId = DataCoordinate.standardize(dataId, **kwargs) 

137 packed = self._pack(dataId) 

138 if returnMaxBits: 

139 return packed, self.maxBits 

140 else: 

141 return packed 

142 

143 @abstractmethod 

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

145 """Unpack an ID produced by `pack` into a full `DataCoordinate`. 

146 

147 Must be implemented by all concrete derived classes. 

148 

149 Parameters 

150 ---------- 

151 packedId : `int` 

152 The result of a call to `~DimensionPacker.pack` on either 

153 ``self`` or an identically-constructed packer instance. 

154 

155 Returns 

156 ------- 

157 dataId : `DataCoordinate` 

158 Dictionary-like ID that uniquely identifies all covered 

159 dimensions. 

160 """ 

161 raise NotImplementedError() 

162 

163 # Class attributes below are shadowed by instance attributes, and are 

164 # present just to hold the docstrings for those instance attributes. 

165 

166 fixed: DataCoordinate 

167 """The dimensions provided to the packer at construction 

168 (`DataCoordinate`) 

169 

170 The packed ID values are only unique and reversible with these 

171 dimensions held fixed. ``fixed.hasRecords() is True`` is guaranteed. 

172 """ 

173 

174 dimensions: DimensionGraph 

175 """The dimensions of data IDs packed by this instance (`DimensionGraph`). 

176 """ 

177 

178 

179class DimensionPackerFactory: 

180 """A factory class for `DimensionPacker` instances. 

181 

182 Can be constructed from configuration. 

183 

184 This class is primarily intended for internal use by `DimensionUniverse`. 

185 

186 Parameters 

187 ---------- 

188 clsName : `str` 

189 Fully-qualified name of the packer class this factory constructs. 

190 fixed : `AbstractSet` [ `str` ] 

191 Names of dimensions whose values must be provided to the packer when it 

192 is constructed. This will be expanded lazily into a `DimensionGraph` 

193 prior to `DimensionPacker` construction. 

194 dimensions : `AbstractSet` [ `str` ] 

195 Names of dimensions whose values are passed to `DimensionPacker.pack`. 

196 This will be expanded lazily into a `DimensionGraph` prior to 

197 `DimensionPacker` construction. 

198 """ 

199 

200 def __init__( 

201 self, 

202 clsName: str, 

203 fixed: AbstractSet[str], 

204 dimensions: AbstractSet[str], 

205 ): 

206 # We defer turning these into DimensionGraph objects until first use 

207 # because __init__ is called before a DimensionUniverse exists, and 

208 # DimensionGraph instances can only be constructed afterwards. 

209 self._fixed: Union[AbstractSet[str], DimensionGraph] = fixed 

210 self._dimensions: Union[AbstractSet[str], DimensionGraph] = dimensions 

211 self._clsName = clsName 

212 self._cls: Optional[Type[DimensionPacker]] = None 

213 

214 def __call__(self, universe: DimensionUniverse, fixed: DataCoordinate) -> DimensionPacker: 

215 """Construct a `DimensionPacker` instance for the given fixed data ID. 

216 

217 Parameters 

218 ---------- 

219 fixed : `DataCoordinate` 

220 Data ID that provides values for the "fixed" dimensions of the 

221 packer. Must be expanded with all metadata known to the 

222 `Registry`. ``fixed.hasRecords()`` must return `True`. 

223 """ 

224 # Construct DimensionGraph instances if necessary on first use. 

225 # See related comment in __init__. 

226 if not isinstance(self._fixed, DimensionGraph): 

227 self._fixed = universe.extract(self._fixed) 

228 if not isinstance(self._dimensions, DimensionGraph): 

229 self._dimensions = universe.extract(self._dimensions) 

230 assert fixed.graph.issuperset(self._fixed) 

231 if self._cls is None: 

232 self._cls = doImport(self._clsName) 

233 return self._cls(fixed, self._dimensions) 

234 

235 

236class DimensionPackerConstructionVisitor(DimensionConstructionVisitor): 

237 """Builder visitor for a single `DimensionPacker`. 

238 

239 A single `DimensionPackerConstructionVisitor` should be added to a 

240 `DimensionConstructionBuilder` for each `DimensionPackerFactory` that 

241 should be added to a universe. 

242 

243 Parameters 

244 ---------- 

245 name : `str` 

246 Name used to identify this configuration of the packer in a 

247 `DimensionUniverse`. 

248 clsName : `str` 

249 Fully-qualified name of a `DimensionPacker` subclass. 

250 fixed : `Iterable` [ `str` ] 

251 Names of dimensions whose values must be provided to the packer when it 

252 is constructed. This will be expanded lazily into a `DimensionGraph` 

253 prior to `DimensionPacker` construction. 

254 dimensions : `Iterable` [ `str` ] 

255 Names of dimensions whose values are passed to `DimensionPacker.pack`. 

256 This will be expanded lazily into a `DimensionGraph` prior to 

257 `DimensionPacker` construction. 

258 """ 

259 

260 def __init__(self, name: str, clsName: str, fixed: Iterable[str], dimensions: Iterable[str]): 

261 super().__init__(name) 

262 self._fixed = set(fixed) 

263 self._dimensions = set(dimensions) 

264 self._clsName = clsName 

265 

266 def hasDependenciesIn(self, others: AbstractSet[str]) -> bool: 

267 # Docstring inherited from DimensionConstructionVisitor. 

268 return False 

269 

270 def visit(self, builder: DimensionConstructionBuilder) -> None: 

271 # Docstring inherited from DimensionConstructionVisitor. 

272 builder.packers[self.name] = DimensionPackerFactory( 

273 clsName=self._clsName, 

274 fixed=self._fixed, 

275 dimensions=self._dimensions, 

276 )