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

64 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-08 14:18 -0800

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__all__ = ("DimensionPacker",) 

25 

26from abc import ABCMeta, abstractmethod 

27from typing import TYPE_CHECKING, AbstractSet, Any, Iterable, Optional, Tuple, Type, Union 

28 

29from lsst.utils import doImportType 

30 

31from ._coordinate import DataCoordinate, DataId 

32from ._graph import DimensionGraph 

33from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor 

34 

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

36 from ._universe import DimensionUniverse 

37 

38 

39class DimensionPacker(metaclass=ABCMeta): 

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

41 

42 An abstract base class for bidirectional mappings between a 

43 `DataCoordinate` and a packed integer ID. 

44 

45 Parameters 

46 ---------- 

47 fixed : `DataCoordinate` 

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

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

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

51 dimensions : `DimensionGraph` 

52 The dimensions of data IDs packed by this instance. 

53 """ 

54 

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

56 self.fixed = fixed 

57 self.dimensions = dimensions 

58 

59 @property 

60 def universe(self) -> DimensionUniverse: 

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

62 return self.fixed.universe 

63 

64 @property 

65 @abstractmethod 

66 def maxBits(self) -> int: 

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

68 

69 This packed ID will be returned by 

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

71 

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

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

74 """ 

75 raise NotImplementedError() 

76 

77 @abstractmethod 

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

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

80 

81 Must be implemented by all concrete derived classes. 

82 

83 Parameters 

84 ---------- 

85 dataId : `DataCoordinate` 

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

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

88 `DataCoordinate`, not an informal data ID 

89 

90 Returns 

91 ------- 

92 packed : `int` 

93 Packed integer ID. 

94 """ 

95 raise NotImplementedError() 

96 

97 def pack( 

98 self, dataId: DataId, *, returnMaxBits: bool = False, **kwargs: Any 

99 ) -> Union[Tuple[int, int], int]: 

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

101 

102 Parameters 

103 ---------- 

104 dataId : `DataId` 

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

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

107 passed at construction. 

108 returnMaxBits : `bool` 

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

110 **kwargs 

111 Additional keyword arguments forwarded to 

112 `DataCoordinate.standardize`. 

113 

114 Returns 

115 ------- 

116 packed : `int` 

117 Packed integer ID. 

118 maxBits : `int`, optional 

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

120 ``returnMaxBits`` is `True`. 

121 

122 Notes 

123 ----- 

124 Should not be overridden by derived class 

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

126 """ 

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

128 packed = self._pack(dataId) 

129 if returnMaxBits: 

130 return packed, self.maxBits 

131 else: 

132 return packed 

133 

134 @abstractmethod 

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

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

137 

138 Must be implemented by all concrete derived classes. 

139 

140 Parameters 

141 ---------- 

142 packedId : `int` 

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

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

145 

146 Returns 

147 ------- 

148 dataId : `DataCoordinate` 

149 Dictionary-like ID that uniquely identifies all covered 

150 dimensions. 

151 """ 

152 raise NotImplementedError() 

153 

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

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

156 

157 fixed: DataCoordinate 

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

159 (`DataCoordinate`) 

160 

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

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

163 """ 

164 

165 dimensions: DimensionGraph 

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

167 """ 

168 

169 

170class DimensionPackerFactory: 

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

172 

173 Can be constructed from configuration. 

174 

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

176 

177 Parameters 

178 ---------- 

179 clsName : `str` 

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

181 fixed : `AbstractSet` [ `str` ] 

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

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

184 prior to `DimensionPacker` construction. 

185 dimensions : `AbstractSet` [ `str` ] 

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

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

188 `DimensionPacker` construction. 

189 """ 

190 

191 def __init__( 

192 self, 

193 clsName: str, 

194 fixed: AbstractSet[str], 

195 dimensions: AbstractSet[str], 

196 ): 

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

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

199 # DimensionGraph instances can only be constructed afterwards. 

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

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

202 self._clsName = clsName 

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

204 

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

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

207 

208 Parameters 

209 ---------- 

210 fixed : `DataCoordinate` 

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

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

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

214 """ 

215 # Construct DimensionGraph instances if necessary on first use. 

216 # See related comment in __init__. 

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

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

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

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

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

222 if self._cls is None: 

223 packer_class = doImportType(self._clsName) 

224 assert not isinstance( 

225 packer_class, DimensionPacker 

226 ), f"Packer class {self._clsName} must be a DimensionPacker." 

227 self._cls = packer_class 

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

229 

230 

231class DimensionPackerConstructionVisitor(DimensionConstructionVisitor): 

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

233 

234 A single `DimensionPackerConstructionVisitor` should be added to a 

235 `DimensionConstructionBuilder` for each `DimensionPackerFactory` that 

236 should be added to a universe. 

237 

238 Parameters 

239 ---------- 

240 name : `str` 

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

242 `DimensionUniverse`. 

243 clsName : `str` 

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

245 fixed : `Iterable` [ `str` ] 

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

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

248 prior to `DimensionPacker` construction. 

249 dimensions : `Iterable` [ `str` ] 

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

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

252 `DimensionPacker` construction. 

253 """ 

254 

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

256 super().__init__(name) 

257 self._fixed = set(fixed) 

258 self._dimensions = set(dimensions) 

259 self._clsName = clsName 

260 

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

262 # Docstring inherited from DimensionConstructionVisitor. 

263 return False 

264 

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

266 # Docstring inherited from DimensionConstructionVisitor. 

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

268 clsName=self._clsName, 

269 fixed=self._fixed, 

270 dimensions=self._dimensions, 

271 )