Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 """An abstract base class for bidirectional mappings between a 

51 `DataCoordinate` and a packed integer ID. 

52 

53 Parameters 

54 ---------- 

55 fixed : `DataCoordinate` 

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

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

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

59 dimensions : `DimensionGraph` 

60 The dimensions of data IDs packed by this instance. 

61 """ 

62 

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

64 self.fixed = fixed 

65 self.dimensions = dimensions 

66 

67 @property 

68 def universe(self) -> DimensionUniverse: 

69 """A graph containing all known dimensions (`DimensionUniverse`). 

70 """ 

71 return self.fixed.universe 

72 

73 @property 

74 @abstractmethod 

75 def maxBits(self) -> int: 

76 """The maximum number of nonzero bits in the packed ID returned by 

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

78 

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

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

81 """ 

82 raise NotImplementedError() 

83 

84 @abstractmethod 

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

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

87 

88 Must be implemented by all concrete derived classes. 

89 

90 Parameters 

91 ---------- 

92 dataId : `DataCoordinate` 

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

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

95 `DataCoordinate`, not an informal data ID 

96 

97 Returns 

98 ------- 

99 packed : `int` 

100 Packed integer ID. 

101 """ 

102 raise NotImplementedError() 

103 

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

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

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

107 

108 Parameters 

109 ---------- 

110 dataId : `DataId` 

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

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

113 passed at construction. 

114 returnMaxBits : `bool` 

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

116 **kwargs 

117 Additional keyword arguments forwarded to 

118 `DataCoordinate.standardize`. 

119 

120 Returns 

121 ------- 

122 packed : `int` 

123 Packed integer ID. 

124 maxBits : `int`, optional 

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

126 ``returnMaxBits`` is `True`. 

127 

128 Notes 

129 ----- 

130 Should not be overridden by derived class 

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

132 """ 

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

134 packed = self._pack(dataId) 

135 if returnMaxBits: 

136 return packed, self.maxBits 

137 else: 

138 return packed 

139 

140 @abstractmethod 

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

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

143 

144 Must be implemented by all concrete derived classes. 

145 

146 Parameters 

147 ---------- 

148 packedId : `int` 

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

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

151 

152 Returns 

153 ------- 

154 dataId : `DataCoordinate` 

155 Dictionary-like ID that uniquely identifies all covered 

156 dimensions. 

157 """ 

158 raise NotImplementedError() 

159 

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

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

162 

163 fixed: DataCoordinate 

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

165 (`DataCoordinate`) 

166 

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

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

169 """ 

170 

171 dimensions: DimensionGraph 

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

173 """ 

174 

175 

176class DimensionPackerFactory: 

177 """A factory class for `DimensionPacker` instances that can be constructed 

178 from configuration. 

179 

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

181 

182 Parameters 

183 ---------- 

184 clsName : `str` 

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

186 fixed : `AbstractSet` [ `str` ] 

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

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

189 prior to `DimensionPacker` construction. 

190 dimensions : `AbstractSet` [ `str` ] 

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

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

193 `DimensionPacker` construction. 

194 """ 

195 def __init__( 

196 self, 

197 clsName: str, 

198 fixed: AbstractSet[str], 

199 dimensions: AbstractSet[str], 

200 ): 

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

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

203 # DimensionGraph instances can only be constructed afterwards. 

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

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

206 self._clsName = clsName 

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

208 

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

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

211 

212 Parameters 

213 ---------- 

214 fixed : `DataCoordinate` 

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

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

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

218 """ 

219 # Construct DimensionGraph instances if necessary on first use. 

220 # See related comment in __init__. 

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

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

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

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

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

226 if self._cls is None: 

227 self._cls = doImport(self._clsName) 

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 def __init__(self, name: str, clsName: str, fixed: Iterable[str], dimensions: Iterable[str]): 

255 super().__init__(name) 

256 self._fixed = set(fixed) 

257 self._dimensions = set(dimensions) 

258 self._clsName = clsName 

259 

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

261 # Docstring inherited from DimensionConstructionVisitor. 

262 return False 

263 

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

265 # Docstring inherited from DimensionConstructionVisitor. 

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

267 clsName=self._clsName, 

268 fixed=self._fixed, 

269 dimensions=self._dimensions, 

270 )