Coverage for python/lsst/daf/butler/core/dimensions/_packer.py: 49%
64 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-03 02:30 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-03 02:30 -0700
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
24__all__ = ("DimensionPacker",)
26from abc import ABCMeta, abstractmethod
27from typing import TYPE_CHECKING, AbstractSet, Any, Iterable, Optional, Tuple, Type, Union
29from lsst.utils import doImportType
31from ._coordinate import DataCoordinate, DataId
32from ._graph import DimensionGraph
33from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor
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
39class DimensionPacker(metaclass=ABCMeta):
40 """Class for going from `DataCoordinate` to packed integer ID and back.
42 An abstract base class for bidirectional mappings between a
43 `DataCoordinate` and a packed integer ID.
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 """
55 def __init__(self, fixed: DataCoordinate, dimensions: DimensionGraph):
56 self.fixed = fixed
57 self.dimensions = dimensions
59 @property
60 def universe(self) -> DimensionUniverse:
61 """Graph containing all known dimensions (`DimensionUniverse`)."""
62 return self.fixed.universe
64 @property
65 @abstractmethod
66 def maxBits(self) -> int:
67 """Return The maximum number of nonzero bits in the packed ID.
69 This packed ID will be returned by
70 `~DimensionPacker.pack` (`int`).
72 Must be implemented by all concrete derived classes. May return
73 `None` to indicate that there is no maximum.
74 """
75 raise NotImplementedError()
77 @abstractmethod
78 def _pack(self, dataId: DataCoordinate) -> int:
79 """Abstract implementation for `~DimensionPacker.pack`.
81 Must be implemented by all concrete derived classes.
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
90 Returns
91 -------
92 packed : `int`
93 Packed integer ID.
94 """
95 raise NotImplementedError()
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.
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`.
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`.
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
134 @abstractmethod
135 def unpack(self, packedId: int) -> DataCoordinate:
136 """Unpack an ID produced by `pack` into a full `DataCoordinate`.
138 Must be implemented by all concrete derived classes.
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.
146 Returns
147 -------
148 dataId : `DataCoordinate`
149 Dictionary-like ID that uniquely identifies all covered
150 dimensions.
151 """
152 raise NotImplementedError()
154 # Class attributes below are shadowed by instance attributes, and are
155 # present just to hold the docstrings for those instance attributes.
157 fixed: DataCoordinate
158 """The dimensions provided to the packer at construction
159 (`DataCoordinate`)
161 The packed ID values are only unique and reversible with these
162 dimensions held fixed. ``fixed.hasRecords() is True`` is guaranteed.
163 """
165 dimensions: DimensionGraph
166 """The dimensions of data IDs packed by this instance (`DimensionGraph`).
167 """
170class DimensionPackerFactory:
171 """A factory class for `DimensionPacker` instances.
173 Can be constructed from configuration.
175 This class is primarily intended for internal use by `DimensionUniverse`.
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 """
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
205 def __call__(self, universe: DimensionUniverse, fixed: DataCoordinate) -> DimensionPacker:
206 """Construct a `DimensionPacker` instance for the given fixed data ID.
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)
231class DimensionPackerConstructionVisitor(DimensionConstructionVisitor):
232 """Builder visitor for a single `DimensionPacker`.
234 A single `DimensionPackerConstructionVisitor` should be added to a
235 `DimensionConstructionBuilder` for each `DimensionPackerFactory` that
236 should be added to a universe.
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 """
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
261 def hasDependenciesIn(self, others: AbstractSet[str]) -> bool:
262 # Docstring inherited from DimensionConstructionVisitor.
263 return False
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 )