Coverage for python/lsst/pipe/base/_observation_dimension_packer.py: 27%
51 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 10:02 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-05 10:02 +0000
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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28from __future__ import annotations
30__all__ = ("ObservationDimensionPacker", "ObservationDimensionPackerConfig", "observation_packer_registry")
32from typing import Any, cast
34from lsst.daf.butler import DataCoordinate, DimensionPacker
35from lsst.pex.config import Config, Field, makeRegistry
37observation_packer_registry = makeRegistry(
38 "Configurables that can pack visit+detector or exposure+detector data IDs into integers."
39)
42class ObservationDimensionPackerConfig(Config):
43 """Config associated with a `ObservationDimensionPacker`."""
45 # Config fields are annotated as Any because support for better
46 # annotations is broken on Fields with optional=True.
47 n_detectors: Any = Field(
48 "Number of detectors, or, more precisely, one greater than the "
49 "maximum detector ID, for this instrument. "
50 "Default (None) obtains this value from the instrument dimension record. "
51 "This should rarely need to be overridden outside of tests.",
52 dtype=int,
53 default=None,
54 optional=True,
55 )
56 n_observations: Any = Field(
57 "Number of observations (visits or exposures, as per 'is_exposure`) "
58 "expected, or, more precisely, one greater than the maximum "
59 "visit/exposure ID. "
60 "Default (None) obtains this value from the instrument dimension record. "
61 "This should rarely need to be overridden outside of tests.",
62 dtype=int,
63 default=None,
64 optional=True,
65 )
68class ObservationDimensionPacker(DimensionPacker):
69 """A `DimensionPacker` for visit+detector or exposure+detector.
71 Parameters
72 ----------
73 data_id : `lsst.daf.butler.DataCoordinate`
74 Data ID that identifies at least the ``instrument`` dimension. Must
75 have dimension records attached unless ``config.n_detectors`` and
76 ``config.n_visits`` are both not `None`.
77 config : `ObservationDimensionPackerConfig`, optional
78 Configuration for this dimension packer.
79 is_exposure : `bool`, optional
80 If `False`, construct a packer for visit+detector data IDs. If `True`,
81 construct a packer for exposure+detector data IDs. If `None`,
82 this is determined based on whether ``visit`` or ``exposure`` is
83 present in ``data_id``, with ``visit`` checked first and hence used if
84 both are present.
86 Notes
87 -----
88 The standard pattern for constructing instances of the class is to use
89 `Instrument.make_dimension_packer`; see that method for details.
91 This packer assumes all visit/exposure and detector IDs are sequential or
92 otherwise densely packed between zero and their upper bound, such that
93 ``n_detectors`` * ``n_observations`` leaves plenty of bits remaining for
94 any other IDs that need to be included in the same integer (such as a
95 counter for Sources detected on an image with this data ID). Instruments
96 whose data ID values are not densely packed, should provide their own
97 `~lsst.daf.butler.DimensionPacker` that takes advantage of the structure
98 of its IDs to compress them into fewer bits.
99 """
101 ConfigClass = ObservationDimensionPackerConfig
103 def __init__(
104 self,
105 data_id: DataCoordinate,
106 config: ObservationDimensionPackerConfig | None = None,
107 is_exposure: bool | None = None,
108 ):
109 if config is None:
110 config = ObservationDimensionPackerConfig()
111 fixed = data_id.subset(data_id.universe.conform(["instrument"]))
112 if is_exposure is None:
113 if "visit" in data_id.dimensions.names:
114 is_exposure = False
115 elif "exposure" in data_id.dimensions.names:
116 is_exposure = True
117 else:
118 raise ValueError(
119 "'is_exposure' was not provided and 'data_id' has no visit or exposure value."
120 )
121 if is_exposure:
122 dimensions = fixed.universe.conform(["instrument", "exposure", "detector"])
123 else:
124 dimensions = fixed.universe.conform(["instrument", "visit", "detector"])
125 super().__init__(fixed, dimensions)
126 self.is_exposure = is_exposure
127 if config.n_detectors is not None:
128 self._n_detectors = config.n_detectors
129 else:
130 # Records accessed here should never be None; that possibility is
131 # only for non-dimension elements like join tables that are
132 # are sometimes not present in an expanded data ID.
133 self._n_detectors = fixed.records["instrument"].detector_max # type: ignore[union-attr]
134 if config.n_observations is not None:
135 self._n_observations = config.n_observations
136 elif self.is_exposure:
137 self._n_observations = fixed.records["instrument"].exposure_max # type: ignore[union-attr]
138 else:
139 self._n_observations = fixed.records["instrument"].visit_max # type: ignore[union-attr]
140 self._max_bits = (self._n_observations * self._n_detectors - 1).bit_length()
142 @property
143 def maxBits(self) -> int:
144 # Docstring inherited from DimensionPacker.maxBits
145 return self._max_bits
147 def _pack(self, dataId: DataCoordinate) -> int:
148 # Docstring inherited from DimensionPacker._pack
149 detector_id = cast(int, dataId["detector"])
150 if detector_id >= self._n_detectors:
151 raise ValueError(f"Detector ID {detector_id} is out of bounds; expected <{self._n_detectors}.")
152 observation_id = cast(int, dataId["exposure" if self.is_exposure else "visit"])
153 if observation_id >= self._n_observations:
154 raise ValueError(
155 f"{'Exposure' if self.is_exposure else 'Visit'} ID {observation_id} is out of bounds; "
156 f"expected <{self._n_observations}."
157 )
158 return detector_id + self._n_detectors * observation_id
160 def unpack(self, packedId: int) -> DataCoordinate:
161 # Docstring inherited from DimensionPacker.unpack
162 observation, detector = divmod(packedId, self._n_detectors)
163 return DataCoordinate.standardize(
164 {
165 "instrument": self.fixed["instrument"],
166 "detector": detector,
167 ("exposure" if self.is_exposure else "visit"): observation,
168 },
169 dimensions=self._dimensions,
170 )
173observation_packer_registry = makeRegistry(
174 "Configurables that can pack visit+detector or exposure+detector data IDs into integers. "
175 "Members of this registry should be callable with the same signature as "
176 "`lsst.pipe.base.ObservationDimensionPacker` construction."
177)
178observation_packer_registry.register("observation", ObservationDimensionPacker)