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