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

68 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-14 09:11 +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__all__ = ("DimensionPacker",) 

25 

26import warnings 

27from abc import ABCMeta, abstractmethod 

28from collections.abc import Iterable, Set 

29from typing import TYPE_CHECKING, Any 

30 

31from deprecated.sphinx import deprecated 

32from lsst.utils import doImportType 

33 

34from ._coordinate import DataCoordinate, DataId 

35from ._graph import DimensionGraph 

36from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor 

37 

38if TYPE_CHECKING: # Imports needed only for type annotations; may be circular. 

39 from ._universe import DimensionUniverse 

40 

41 

42class DimensionPacker(metaclass=ABCMeta): 

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

44 

45 An abstract base class for bidirectional mappings between a 

46 `DataCoordinate` and a packed integer ID. 

47 

48 Parameters 

49 ---------- 

50 fixed : `DataCoordinate` 

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

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

53 of calls to `unpack`. Subclasses are permitted to require that 

54 ``fixed.hasRecords()`` return `True`. 

55 dimensions : `DimensionGraph` 

56 The dimensions of data IDs packed by this instance. 

57 """ 

58 

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

60 self.fixed = fixed 

61 self.dimensions = dimensions 

62 

63 @property 

64 def universe(self) -> DimensionUniverse: 

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

66 return self.fixed.universe 

67 

68 @property 

69 @abstractmethod 

70 def maxBits(self) -> int: 

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

72 

73 This packed ID will be returned by 

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

75 

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

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

78 """ 

79 raise NotImplementedError() 

80 

81 @abstractmethod 

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

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

84 

85 Must be implemented by all concrete derived classes. 

86 

87 Parameters 

88 ---------- 

89 dataId : `DataCoordinate` 

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

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

92 `DataCoordinate`, not an informal data ID 

93 

94 Returns 

95 ------- 

96 packed : `int` 

97 Packed integer ID. 

98 """ 

99 raise NotImplementedError() 

100 

101 def pack( 

102 self, dataId: DataId | None = None, *, returnMaxBits: bool = False, **kwargs: Any 

103 ) -> tuple[int, int] | int: 

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

105 

106 Parameters 

107 ---------- 

108 dataId : `DataId` 

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

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

111 passed at construction. 

112 returnMaxBits : `bool` 

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

114 **kwargs 

115 Additional keyword arguments forwarded to 

116 `DataCoordinate.standardize`. 

117 

118 Returns 

119 ------- 

120 packed : `int` 

121 Packed integer ID. 

122 maxBits : `int`, optional 

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

124 ``returnMaxBits`` is `True`. 

125 

126 Notes 

127 ----- 

128 Should not be overridden by derived class 

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

130 """ 

131 dataId = DataCoordinate.standardize( 

132 dataId, **kwargs, universe=self.fixed.universe, defaults=self.fixed 

133 ) 

134 if dataId.subset(self.fixed.graph) != self.fixed: 

135 raise ValueError(f"Data ID packer expected a data ID consistent with {self.fixed}, got {dataId}.") 

136 packed = self._pack(dataId) 

137 if returnMaxBits: 

138 return packed, self.maxBits 

139 else: 

140 return packed 

141 

142 @abstractmethod 

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

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

145 

146 Must be implemented by all concrete derived classes. 

147 

148 Parameters 

149 ---------- 

150 packedId : `int` 

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

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

153 

154 Returns 

155 ------- 

156 dataId : `DataCoordinate` 

157 Dictionary-like ID that uniquely identifies all covered 

158 dimensions. 

159 """ 

160 raise NotImplementedError() 

161 

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

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

164 

165 fixed: DataCoordinate 

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

167 (`DataCoordinate`) 

168 

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

170 dimensions held fixed. 

171 """ 

172 

173 dimensions: DimensionGraph 

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

175 """ 

176 

177 

178# TODO: Remove this class on DM-38687. 

179@deprecated( 

180 "Deprecated in favor of configurable dimension packers. Will be removed after v27.", 

181 version="v26", 

182 category=FutureWarning, 

183) 

184class DimensionPackerFactory: 

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

186 

187 Can be constructed from configuration. 

188 

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

190 

191 Parameters 

192 ---------- 

193 clsName : `str` 

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

195 fixed : `~collections.abc.Set` [ `str` ] 

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

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

198 prior to `DimensionPacker` construction. 

199 dimensions : `~collections.abc.Set` [ `str` ] 

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

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

202 `DimensionPacker` construction. 

203 """ 

204 

205 def __init__( 

206 self, 

207 clsName: str, 

208 fixed: Set[str], 

209 dimensions: Set[str], 

210 ): 

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

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

213 # DimensionGraph instances can only be constructed afterwards. 

214 self._fixed: Set[str] | DimensionGraph = fixed 

215 self._dimensions: Set[str] | DimensionGraph = dimensions 

216 self._clsName = clsName 

217 self._cls: type[DimensionPacker] | None = None 

218 

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

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

221 

222 Parameters 

223 ---------- 

224 fixed : `DataCoordinate` 

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

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

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

228 """ 

229 # Construct DimensionGraph instances if necessary on first use. 

230 # See related comment in __init__. 

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

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

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

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

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

236 if self._cls is None: 

237 packer_class = doImportType(self._clsName) 

238 assert not isinstance( 

239 packer_class, DimensionPacker 

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

241 self._cls = packer_class 

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

243 

244 

245# TODO: Remove this class on DM-38687. 

246@deprecated( 

247 "Deprecated in favor of configurable dimension packers. Will be removed after v27.", 

248 version="v26", 

249 category=FutureWarning, 

250) 

251class DimensionPackerConstructionVisitor(DimensionConstructionVisitor): 

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

253 

254 A single `DimensionPackerConstructionVisitor` should be added to a 

255 `DimensionConstructionBuilder` for each `DimensionPackerFactory` that 

256 should be added to a universe. 

257 

258 Parameters 

259 ---------- 

260 name : `str` 

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

262 `DimensionUniverse`. 

263 clsName : `str` 

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

265 fixed : `~collections.abc.Iterable` [ `str` ] 

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

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

268 prior to `DimensionPacker` construction. 

269 dimensions : `~collections.abc.Iterable` [ `str` ] 

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

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

272 `DimensionPacker` construction. 

273 """ 

274 

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

276 super().__init__(name) 

277 self._fixed = set(fixed) 

278 self._dimensions = set(dimensions) 

279 self._clsName = clsName 

280 

281 def hasDependenciesIn(self, others: Set[str]) -> bool: 

282 # Docstring inherited from DimensionConstructionVisitor. 

283 return False 

284 

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

286 # Docstring inherited from DimensionConstructionVisitor. 

287 with warnings.catch_warnings(): 

288 # Don't warn when deprecated code calls other deprecated code. 

289 warnings.simplefilter("ignore", FutureWarning) 

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

291 clsName=self._clsName, 

292 fixed=self._fixed, 

293 dimensions=self._dimensions, 

294 )