Coverage for python/lsst/pipe/base/_observation_dimension_packer.py: 27%
49 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-06 02:51 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-06 02:51 -0700
1# This file is part of pipe_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://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__ = ("ObservationDimensionPacker", "ObservationDimensionPackerConfig", "observation_packer_registry")
26from typing import Any, cast
28from lsst.daf.butler import DataCoordinate, DimensionPacker
29from lsst.pex.config import Config, Field, makeRegistry
31observation_packer_registry = makeRegistry(
32 "Configurables that can pack visit+detector or exposure+detector data IDs into integers."
33)
36class ObservationDimensionPackerConfig(Config):
37 # Config fields are annotated as Any because support for better
38 # annotations is broken on Fields with optional=True.
39 n_detectors: Any = Field(
40 "Number of detectors, or, more precisely, one greater than the "
41 "maximum detector ID, for this instrument. "
42 "Default (None) obtains this value from the instrument dimension record. "
43 "This should rarely need to be overridden outside of tests.",
44 dtype=int,
45 default=None,
46 optional=True,
47 )
48 n_observations: Any = Field(
49 "Number of observations (visits or exposures, as per 'is_exposure`) "
50 "expected, or, more precisely, one greater than the maximum "
51 "visit/exposure ID. "
52 "Default (None) obtains this value from the instrument dimension record. "
53 "This should rarely need to be overridden outside of tests.",
54 dtype=int,
55 default=None,
56 optional=True,
57 )
60class ObservationDimensionPacker(DimensionPacker):
61 """A `DimensionPacker` for visit+detector or exposure+detector.
63 Parameters
64 ----------
65 data_id : `lsst.daf.butler.DataCoordinate`
66 Data ID that identifies at least the ``instrument`` dimension. Must
67 have dimension records attached unless ``config.n_detectors`` and
68 ``config.n_visits`` are both not `None`.
69 config : `ObservationDimensionPackerConfig`
70 Configuration for this dimension packer.
71 is_exposure : `bool`, optional
72 If `False`, construct a packer for visit+detector data IDs. If `True`,
73 construct a packer for exposure+detector data IDs. If `None`,
74 this is determined based on whether ``visit`` or ``exposure`` is
75 present in ``data_id``, with ``visit`` checked first and hence used if
76 both are present.
78 Notes
79 -----
80 The standard pattern for constructing instances of the class is to use
81 `Instrument.make_dimension_packer`; see that method for details.
83 This packer assumes all visit/exposure and detector IDs are sequential or
84 otherwise densely packed between zero and their upper bound, such that
85 ``n_detectors`` * ``n_observations`` leaves plenty of bits remaining for
86 any other IDs that need to be included in the same integer (such as a
87 counter for Sources detected on an image with this data ID). Instruments
88 whose data ID values are not densely packed, should provide their own
89 `~lsst.daf.butler.DimensionPacker` that takes advantage of the structure
90 of its IDs to compress them into fewer bits.
91 """
93 ConfigClass = ObservationDimensionPackerConfig
95 def __init__(
96 self,
97 data_id: DataCoordinate,
98 config: ObservationDimensionPackerConfig,
99 is_exposure: bool | None = None,
100 ):
101 fixed = data_id.subset(data_id.universe.extract(["instrument"]))
102 if is_exposure is None:
103 if "visit" in data_id.graph.names:
104 is_exposure = False
105 elif "exposure" in data_id.graph.names:
106 is_exposure = True
107 else:
108 raise ValueError(
109 "'is_exposure' was not provided and 'data_id' has no visit or exposure value."
110 )
111 if is_exposure:
112 dimensions = fixed.universe.extract(["instrument", "exposure", "detector"])
113 else:
114 dimensions = fixed.universe.extract(["instrument", "visit", "detector"])
115 super().__init__(fixed, dimensions)
116 self.is_exposure = is_exposure
117 if config.n_detectors is not None:
118 self._n_detectors = config.n_detectors
119 else:
120 # Records accessed here should never be None; that possibility is
121 # only for non-dimension elements like join tables that are
122 # are sometimes not present in an expanded data ID.
123 self._n_detectors = fixed.records["instrument"].detector_max # type: ignore[union-attr]
124 if config.n_observations is not None:
125 self._n_observations = config.n_observations
126 elif self.is_exposure:
127 self._n_observations = fixed.records["instrument"].exposure_max # type: ignore[union-attr]
128 else:
129 self._n_observations = fixed.records["instrument"].visit_max # type: ignore[union-attr]
130 self._max_bits = (self._n_observations * self._n_detectors - 1).bit_length()
132 @property
133 def maxBits(self) -> int:
134 # Docstring inherited from DimensionPacker.maxBits
135 return self._max_bits
137 def _pack(self, dataId: DataCoordinate) -> int:
138 # Docstring inherited from DimensionPacker._pack
139 detector_id = cast(int, dataId["detector"])
140 if detector_id >= self._n_detectors:
141 raise ValueError(f"Detector ID {detector_id} is out of bounds; expected <{self._n_detectors}.")
142 observation_id = cast(int, dataId["exposure" if self.is_exposure else "visit"])
143 if observation_id >= self._n_observations:
144 raise ValueError(
145 f"{'Exposure' if self.is_exposure else 'Visit'} ID {observation_id} is out of bounds; "
146 f"expected <{self._n_observations}."
147 )
148 return detector_id + self._n_detectors * observation_id
150 def unpack(self, packedId: int) -> DataCoordinate:
151 # Docstring inherited from DimensionPacker.unpack
152 observation, detector = divmod(packedId, self._n_detectors)
153 return DataCoordinate.standardize(
154 {
155 "instrument": self.fixed["instrument"],
156 "detector": detector,
157 ("exposure" if self.is_exposure else "visit"): observation,
158 },
159 graph=self.dimensions,
160 )
163observation_packer_registry = makeRegistry(
164 "Configurables that can pack visit+detector or exposure+detector data IDs into integers. "
165 "Members of this registry should be callable with the same signature as "
166 "`lsst.pipe.base.ObservationDimensionPacker` construction."
167)
168observation_packer_registry.register("observation", ObservationDimensionPacker)