Coverage for python/lsst/pipe/base/_observation_dimension_packer.py: 29%
49 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-19 10:39 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-19 10:39 +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`
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,
107 is_exposure: bool | None = None,
108 ):
109 fixed = data_id.subset(data_id.universe.extract(["instrument"]))
110 if is_exposure is None:
111 if "visit" in data_id.graph.names:
112 is_exposure = False
113 elif "exposure" in data_id.graph.names:
114 is_exposure = True
115 else:
116 raise ValueError(
117 "'is_exposure' was not provided and 'data_id' has no visit or exposure value."
118 )
119 if is_exposure:
120 dimensions = fixed.universe.extract(["instrument", "exposure", "detector"])
121 else:
122 dimensions = fixed.universe.extract(["instrument", "visit", "detector"])
123 super().__init__(fixed, dimensions)
124 self.is_exposure = is_exposure
125 if config.n_detectors is not None:
126 self._n_detectors = config.n_detectors
127 else:
128 # Records accessed here should never be None; that possibility is
129 # only for non-dimension elements like join tables that are
130 # are sometimes not present in an expanded data ID.
131 self._n_detectors = fixed.records["instrument"].detector_max # type: ignore[union-attr]
132 if config.n_observations is not None:
133 self._n_observations = config.n_observations
134 elif self.is_exposure:
135 self._n_observations = fixed.records["instrument"].exposure_max # type: ignore[union-attr]
136 else:
137 self._n_observations = fixed.records["instrument"].visit_max # type: ignore[union-attr]
138 self._max_bits = (self._n_observations * self._n_detectors - 1).bit_length()
140 @property
141 def maxBits(self) -> int:
142 # Docstring inherited from DimensionPacker.maxBits
143 return self._max_bits
145 def _pack(self, dataId: DataCoordinate) -> int:
146 # Docstring inherited from DimensionPacker._pack
147 detector_id = cast(int, dataId["detector"])
148 if detector_id >= self._n_detectors:
149 raise ValueError(f"Detector ID {detector_id} is out of bounds; expected <{self._n_detectors}.")
150 observation_id = cast(int, dataId["exposure" if self.is_exposure else "visit"])
151 if observation_id >= self._n_observations:
152 raise ValueError(
153 f"{'Exposure' if self.is_exposure else 'Visit'} ID {observation_id} is out of bounds; "
154 f"expected <{self._n_observations}."
155 )
156 return detector_id + self._n_detectors * observation_id
158 def unpack(self, packedId: int) -> DataCoordinate:
159 # Docstring inherited from DimensionPacker.unpack
160 observation, detector = divmod(packedId, self._n_detectors)
161 return DataCoordinate.standardize(
162 {
163 "instrument": self.fixed["instrument"],
164 "detector": detector,
165 ("exposure" if self.is_exposure else "visit"): observation,
166 },
167 graph=self.dimensions,
168 )
171observation_packer_registry = makeRegistry(
172 "Configurables that can pack visit+detector or exposure+detector data IDs into integers. "
173 "Members of this registry should be callable with the same signature as "
174 "`lsst.pipe.base.ObservationDimensionPacker` construction."
175)
176observation_packer_registry.register("observation", ObservationDimensionPacker)