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-23 09:30 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-23 09:30 +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/>.
22from __future__ import annotations
24__all__ = ("DimensionPacker",)
26import warnings
27from abc import ABCMeta, abstractmethod
28from collections.abc import Iterable, Set
29from typing import TYPE_CHECKING, Any
31from deprecated.sphinx import deprecated
32from lsst.utils import doImportType
34from ._coordinate import DataCoordinate, DataId
35from ._graph import DimensionGraph
36from .construction import DimensionConstructionBuilder, DimensionConstructionVisitor
38if TYPE_CHECKING: # Imports needed only for type annotations; may be circular.
39 from ._universe import DimensionUniverse
42class DimensionPacker(metaclass=ABCMeta):
43 """Class for going from `DataCoordinate` to packed integer ID and back.
45 An abstract base class for bidirectional mappings between a
46 `DataCoordinate` and a packed integer ID.
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 """
59 def __init__(self, fixed: DataCoordinate, dimensions: DimensionGraph):
60 self.fixed = fixed
61 self.dimensions = dimensions
63 @property
64 def universe(self) -> DimensionUniverse:
65 """Graph containing all known dimensions (`DimensionUniverse`)."""
66 return self.fixed.universe
68 @property
69 @abstractmethod
70 def maxBits(self) -> int:
71 """Return The maximum number of nonzero bits in the packed ID.
73 This packed ID will be returned by
74 `~DimensionPacker.pack` (`int`).
76 Must be implemented by all concrete derived classes. May return
77 `None` to indicate that there is no maximum.
78 """
79 raise NotImplementedError()
81 @abstractmethod
82 def _pack(self, dataId: DataCoordinate) -> int:
83 """Abstract implementation for `~DimensionPacker.pack`.
85 Must be implemented by all concrete derived classes.
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
94 Returns
95 -------
96 packed : `int`
97 Packed integer ID.
98 """
99 raise NotImplementedError()
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.
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`.
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`.
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
142 @abstractmethod
143 def unpack(self, packedId: int) -> DataCoordinate:
144 """Unpack an ID produced by `pack` into a full `DataCoordinate`.
146 Must be implemented by all concrete derived classes.
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.
154 Returns
155 -------
156 dataId : `DataCoordinate`
157 Dictionary-like ID that uniquely identifies all covered
158 dimensions.
159 """
160 raise NotImplementedError()
162 # Class attributes below are shadowed by instance attributes, and are
163 # present just to hold the docstrings for those instance attributes.
165 fixed: DataCoordinate
166 """The dimensions provided to the packer at construction
167 (`DataCoordinate`)
169 The packed ID values are only unique and reversible with these
170 dimensions held fixed.
171 """
173 dimensions: DimensionGraph
174 """The dimensions of data IDs packed by this instance (`DimensionGraph`).
175 """
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.
187 Can be constructed from configuration.
189 This class is primarily intended for internal use by `DimensionUniverse`.
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 """
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
219 def __call__(self, universe: DimensionUniverse, fixed: DataCoordinate) -> DimensionPacker:
220 """Construct a `DimensionPacker` instance for the given fixed data ID.
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)
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`.
254 A single `DimensionPackerConstructionVisitor` should be added to a
255 `DimensionConstructionBuilder` for each `DimensionPackerFactory` that
256 should be added to a universe.
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 """
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
281 def hasDependenciesIn(self, others: Set[str]) -> bool:
282 # Docstring inherited from DimensionConstructionVisitor.
283 return False
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 )