Coverage for python/lsst/skymap/packers.py: 27%
92 statements
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-27 09:20 +0000
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-27 09:20 +0000
1# This file is part of skymap.
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__ = ("SkyMapDimensionPacker",)
26from collections.abc import Mapping
28from lsst.pex.config import Config, Field, DictField, ConfigurableField
29from lsst.daf.butler import DimensionPacker, DimensionGraph, DataCoordinate
30from deprecated.sphinx import deprecated
33class SkyMapDimensionPackerConfig(Config):
34 bands = DictField(
35 "Mapping from band name to integer to use in the packed ID. "
36 "The default (None) is to use a hard-coded list of common bands; "
37 "pipelines that can enumerate the set of bands they are likely to see "
38 "should override this.",
39 keytype=str,
40 itemtype=int,
41 default=None,
42 optional=True,
43 )
44 n_bands = Field(
45 "Number of bands to reserve space for. "
46 "If zero, bands are not included in the packed integer at all. "
47 "If `None`, the size of 'bands' is used.",
48 dtype=int,
49 optional=True,
50 default=0,
51 )
52 n_tracts = Field(
53 "Number of tracts, or, more precisely, one greater than the maximum tract ID."
54 "Default (None) obtains this value from the skymap dimension record.",
55 dtype=int,
56 optional=True,
57 default=None,
58 )
59 n_patches = Field(
60 "Number of patches per tract, or, more precisely, one greater than the maximum patch ID."
61 "Default (None) obtains this value from the skymap dimension record.",
62 dtype=int,
63 optional=True,
64 default=None,
65 )
68class SkyMapDimensionPacker(DimensionPacker):
69 """A `DimensionPacker` for tract, patch and optionally band,
70 given a SkyMap.
72 Parameters
73 ----------
74 fixed : `lsst.daf.butler.DataCoordinate`
75 Data ID that identifies just the ``skymap`` dimension. Must have
76 dimension records attached unless ``n_tracts`` and ``n_patches`` are
77 not `None`.
78 dimensions : `lsst.daf.butler.DimensionGraph`, optional
79 The dimensions of data IDs packed by this instance. Must include
80 ``{skymap, tract, patch}``, and may include ``band``. If not provided,
81 this will be set to include ``band`` if ``n_bands != 0``.
82 bands : `~collections.abc.Mapping` [ `str`, `int` ] or `None`, optional
83 Mapping from band name to integer to use in the packed ID. `None` uses
84 a fixed set of bands defined in this class. When calling code can
85 enumerate the bands it is likely to see, providing an explicit mapping
86 is preferable.
87 n_bands : `int` or `None`, optional
88 The number of bands to leave room for in the packed ID. If `None`,
89 this will be set to ``len(bands)``. If ``0``, the band will not be
90 included in the dimensions at all. If ``1``, the band will be included
91 in the dimensions but will not occupy any extra bits in the packed ID.
92 This may be larger or smaller than ``len(bands)``, to reserve extra
93 space for the future or align to byte boundaries, or support a subset
94 of a larger mapping, respectively.
95 n_tracts : `int` or `None`, optional
96 The number of tracts to leave room for in the packed ID. If `None`,
97 this will be set via the ``skymap`` dimension record in ``fixed``.
98 n_patches : `int` or `None`, optional
99 The number of patches (per tract) to leave room for in the packed ID.
100 If `None`, this will be set via the ``skymap`` dimension record in
101 ``fixed``.
103 Notes
104 -----
105 The standard pattern for constructing instances of this class is to use
106 `make_config_field`::
108 class SomeConfig(lsst.pex.config.Config):
109 packer = ObservationDimensionPacker.make_config_field()
111 class SomeTask(lsst.pipe.base.Task):
112 ConfigClass = SomeConfig
114 def run(self, ..., data_id: DataCoordinate):
115 packer = self.config.packer.apply(data_id)
116 packed_id = packer.pack(data_id)
117 ...
119 """
121 SUPPORTED_FILTERS = (
122 [None]
123 + list("ugrizyUBGVRIZYJHK") # split string into single chars
124 + [f"N{d}" for d in (387, 515, 656, 816, 921, 1010)] # HSC narrow-bands
125 + [f"N{d}" for d in (419, 540, 708, 964)] # DECam narrow-bands
126 )
127 """Sequence of supported bands used to construct a mapping from band name
128 to integer when the 'bands' config option is `None` or no config is
129 provided.
131 This variable should no longer be modified to add new filters; pass
132 ``bands`` at construction or use `from_config` instead.
133 """
135 ConfigClass = SkyMapDimensionPackerConfig
137 @classmethod
138 @deprecated(
139 reason="This classmethod cannot reflect all __init__ args and will be removed after v27.",
140 version="v26.0",
141 category=FutureWarning,
142 )
143 def getIntFromFilter(cls, name):
144 """Return an integer that represents the band with the given
145 name.
146 """
147 try:
148 return cls.SUPPORTED_FILTERS.index(name)
149 except ValueError:
150 raise NotImplementedError(f"band '{name}' not supported by this ID packer.")
152 @classmethod
153 @deprecated(
154 reason="This classmethod cannot reflect all __init__ args and will be removed after v27.",
155 version="v26.0",
156 category=FutureWarning,
157 )
158 def getFilterNameFromInt(cls, num):
159 """Return an band name from its integer representation."""
160 return cls.SUPPORTED_FILTERS[num]
162 @classmethod
163 @deprecated(
164 reason="This classmethod cannot reflect all __init__ args and will be removed after v27.",
165 version="v26.0",
166 category=FutureWarning,
167 )
168 def getMaxIntForFilters(cls):
169 return len(cls.SUPPORTED_FILTERS)
171 def __init__(
172 self,
173 fixed: DataCoordinate,
174 dimensions: DimensionGraph | None = None,
175 bands: Mapping[str, int] | None = None,
176 n_bands: int | None = None,
177 n_tracts: int | None = None,
178 n_patches: int | None = None,
179 ):
180 if bands is None:
181 bands = {b: i for i, b in enumerate(self.SUPPORTED_FILTERS)}
182 if dimensions is None:
183 if n_bands is None:
184 n_bands = len(bands)
185 dimension_names = ["tract", "patch"]
186 if n_bands != 0:
187 dimension_names.append("band")
188 dimensions = fixed.universe.extract(dimension_names)
189 else:
190 if "band" not in dimensions.names:
191 n_bands = 0
192 if dimensions.names != {"tract", "patch", "skymap"}:
193 raise ValueError(
194 f"Invalid dimensions for skymap dimension packer with n_bands=0: {dimensions}."
195 )
196 else:
197 if dimensions.names != {"tract", "patch", "skymap", "band"}:
198 raise ValueError(
199 f"Invalid dimensions for skymap dimension packer with n_bands>0: {dimensions}."
200 )
201 if n_bands is None:
202 n_bands = len(bands)
203 if n_tracts is None:
204 n_tracts = fixed.records["skymap"].tract_max
205 if n_patches is None:
206 n_patches = (
207 fixed.records["skymap"].patch_nx_max
208 * fixed.records["skymap"].patch_ny_max
209 )
210 super().__init__(fixed, dimensions)
211 self._bands = bands
212 self._n_bands = n_bands
213 self._n_tracts = n_tracts
214 self._n_patches = n_patches
215 self._bands_list = None
217 @classmethod
218 def make_config_field(
219 cls,
220 doc: str = "How to pack tract, patch, and possibly band into an integer."
221 ) -> ConfigurableField:
222 """Make a config field to control how skymap data IDs are packed.
224 Parameters
225 ----------
226 doc : `str`, optional
227 Documentation for the config field.
229 Returns
230 -------
231 field : `lsst.pex.config.ConfigurableField`
232 A config field whose instance values are [wrapper proxies to]
233 `SkyMapDimensionPackerConfig` instances.
234 """
235 return ConfigurableField(doc, target=cls.from_config, ConfigClass=cls.ConfigClass)
237 @classmethod
238 def from_config(
239 cls, data_id: DataCoordinate, config: SkyMapDimensionPackerConfig
240 ) -> SkyMapDimensionPacker:
241 """Construct a dimension packer from a config object and a data ID.
243 Parameters
244 ----------
245 data_id : `lsst.daf.butler.DataCoordinate`
246 Data ID that identifies at least the ``skymap`` dimension. Must
247 have dimension records attached unless ``config.n_tracts`` and
248 ``config.n_patches`` are both not `None`.
249 config : `SkyMapDimensionPackerConfig`
250 Configuration object.
252 Returns
253 -------
254 packer : `SkyMapDimensionPackerConfig`
255 New dimension packer.
257 Notes
258 -----
259 This interface is provided for consistency with the `lsst.pex.config`
260 "Configurable" concept, and is invoked when ``apply(data_id)`` is
261 called on a config instance attribute that corresponds to a field
262 created by `make_config_field`. The constructor signature cannot play
263 this role easily for backwards compatibility reasons.
264 """
265 return cls(
266 data_id.subset(data_id.universe.extract(["skymap"])),
267 n_bands=config.n_bands,
268 bands=config.bands,
269 n_tracts=config.n_tracts,
270 n_patches=config.n_patches,
271 )
273 @property
274 def maxBits(self) -> int:
275 # Docstring inherited from DataIdPacker.maxBits
276 packedMax = self._n_tracts * self._n_patches
277 if self._n_bands:
278 packedMax *= self._n_bands
279 return (packedMax - 1).bit_length()
281 def _pack(self, dataId: DataCoordinate) -> int:
282 # Docstring inherited from DataIdPacker.pack
283 if dataId["patch"] >= self._n_patches:
284 raise ValueError(f"Patch ID {dataId['patch']} is out of bounds; expected <{self._n_patches}.")
285 if dataId["tract"] >= self._n_tracts:
286 raise ValueError(f"Tract ID {dataId['tract']} is out of bounds; expected <{self._n_tracts}.")
287 packed = dataId["patch"] + self._n_patches * dataId["tract"]
288 if self._n_bands:
289 if (band_index := self._bands.get(dataId["band"])) is None:
290 raise ValueError(
291 f"Band {dataId['band']!r} is not supported by SkyMapDimensionPacker "
292 f"configuration; expected one of {list(self._bands)}."
293 )
294 if band_index >= self._n_bands:
295 raise ValueError(
296 f"Band index {band_index} for {dataId['band']!r} is out of bounds; "
297 f"expected <{self._n_bands}."
298 )
299 packed += self._bands[dataId["band"]] * self._n_patches * self._n_tracts
300 return packed
302 def unpack(self, packedId: int) -> DataCoordinate:
303 # Docstring inherited from DataIdPacker.unpack
304 d = {"skymap": self.fixed["skymap"]}
305 if self._n_bands:
306 index, packedId = divmod(packedId, (self._n_tracts * self._n_patches))
307 if self._bands_list is None:
308 self._bands_list = list(self._bands)
309 d["band"] = self._bands_list[index]
310 d["tract"], d["patch"] = divmod(packedId, self._n_patches)
311 return DataCoordinate.standardize(d, graph=self.dimensions)