Coverage for python/lsst/daf/butler/core/dimensions/_packer.py: 48%
Shortcuts 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
Shortcuts 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/>.
22from __future__ import annotations
25__all__ = ("DimensionPacker",)
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)
39from lsst.utils import doImportType
41from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor
42from ._coordinate import DataCoordinate, DataId
43from ._graph import DimensionGraph
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
49class DimensionPacker(metaclass=ABCMeta):
50 """Class for going from `DataCoordinate` to packed integer ID and back.
52 An abstract base class for bidirectional mappings between a
53 `DataCoordinate` and a packed integer ID.
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 """
65 def __init__(self, fixed: DataCoordinate, dimensions: DimensionGraph):
66 self.fixed = fixed
67 self.dimensions = dimensions
69 @property
70 def universe(self) -> DimensionUniverse:
71 """Graph containing all known dimensions (`DimensionUniverse`)."""
72 return self.fixed.universe
74 @property
75 @abstractmethod
76 def maxBits(self) -> int:
77 """Return The maximum number of nonzero bits in the packed ID.
79 This packed ID will be returned by
80 `~DimensionPacker.pack` (`int`).
82 Must be implemented by all concrete derived classes. May return
83 `None` to indicate that there is no maximum.
84 """
85 raise NotImplementedError()
87 @abstractmethod
88 def _pack(self, dataId: DataCoordinate) -> int:
89 """Abstract implementation for `~DimensionPacker.pack`.
91 Must be implemented by all concrete derived classes.
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
100 Returns
101 -------
102 packed : `int`
103 Packed integer ID.
104 """
105 raise NotImplementedError()
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.
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`.
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`.
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
143 @abstractmethod
144 def unpack(self, packedId: int) -> DataCoordinate:
145 """Unpack an ID produced by `pack` into a full `DataCoordinate`.
147 Must be implemented by all concrete derived classes.
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.
155 Returns
156 -------
157 dataId : `DataCoordinate`
158 Dictionary-like ID that uniquely identifies all covered
159 dimensions.
160 """
161 raise NotImplementedError()
163 # Class attributes below are shadowed by instance attributes, and are
164 # present just to hold the docstrings for those instance attributes.
166 fixed: DataCoordinate
167 """The dimensions provided to the packer at construction
168 (`DataCoordinate`)
170 The packed ID values are only unique and reversible with these
171 dimensions held fixed. ``fixed.hasRecords() is True`` is guaranteed.
172 """
174 dimensions: DimensionGraph
175 """The dimensions of data IDs packed by this instance (`DimensionGraph`).
176 """
179class DimensionPackerFactory:
180 """A factory class for `DimensionPacker` instances.
182 Can be constructed from configuration.
184 This class is primarily intended for internal use by `DimensionUniverse`.
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 """
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
214 def __call__(self, universe: DimensionUniverse, fixed: DataCoordinate) -> DimensionPacker:
215 """Construct a `DimensionPacker` instance for the given fixed data ID.
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 packer_class = doImportType(self._clsName)
233 assert not isinstance(packer_class, DimensionPacker), \
234 f"Packer class {self._clsName} must be a DimensionPacker."
235 self._cls = packer_class
236 return self._cls(fixed, self._dimensions)
239class DimensionPackerConstructionVisitor(DimensionConstructionVisitor):
240 """Builder visitor for a single `DimensionPacker`.
242 A single `DimensionPackerConstructionVisitor` should be added to a
243 `DimensionConstructionBuilder` for each `DimensionPackerFactory` that
244 should be added to a universe.
246 Parameters
247 ----------
248 name : `str`
249 Name used to identify this configuration of the packer in a
250 `DimensionUniverse`.
251 clsName : `str`
252 Fully-qualified name of a `DimensionPacker` subclass.
253 fixed : `Iterable` [ `str` ]
254 Names of dimensions whose values must be provided to the packer when it
255 is constructed. This will be expanded lazily into a `DimensionGraph`
256 prior to `DimensionPacker` construction.
257 dimensions : `Iterable` [ `str` ]
258 Names of dimensions whose values are passed to `DimensionPacker.pack`.
259 This will be expanded lazily into a `DimensionGraph` prior to
260 `DimensionPacker` construction.
261 """
263 def __init__(self, name: str, clsName: str, fixed: Iterable[str], dimensions: Iterable[str]):
264 super().__init__(name)
265 self._fixed = set(fixed)
266 self._dimensions = set(dimensions)
267 self._clsName = clsName
269 def hasDependenciesIn(self, others: AbstractSet[str]) -> bool:
270 # Docstring inherited from DimensionConstructionVisitor.
271 return False
273 def visit(self, builder: DimensionConstructionBuilder) -> None:
274 # Docstring inherited from DimensionConstructionVisitor.
275 builder.packers[self.name] = DimensionPackerFactory(
276 clsName=self._clsName,
277 fixed=self._fixed,
278 dimensions=self._dimensions,
279 )