lsst.skymap ge3564fa9f0+44fc6c952b
Loading...
Searching...
No Matches
packers.py
Go to the documentation of this file.
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/>.
21
22from __future__ import annotations
23
24__all__ = ("SkyMapDimensionPacker",)
25
26from collections.abc import Mapping
27
28from lsst.pex.config import Config, Field, DictField, ConfigurableField
29from lsst.daf.butler import DimensionPacker, DimensionGraph, DimensionGroup, DataCoordinate
30from deprecated.sphinx import deprecated
31
32
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 )
66
67
68class SkyMapDimensionPacker(DimensionPacker):
69 """A `DimensionPacker` for tract, patch and optionally band,
70 given a SkyMap.
71
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`, or \
79 `lsst.daf.butler.DimensionGroup`, optional
80 The dimensions of data IDs packed by this instance. Must include
81 ``{skymap, tract, patch}``, and may include ``band``. If not provided,
82 this will be set to include ``band`` if ``n_bands != 0``.
83 bands : `~collections.abc.Mapping` [ `str`, `int` ] or `None`, optional
84 Mapping from band name to integer to use in the packed ID. `None` uses
85 a fixed set of bands defined in this class. When calling code can
86 enumerate the bands it is likely to see, providing an explicit mapping
87 is preferable.
88 n_bands : `int` or `None`, optional
89 The number of bands to leave room for in the packed ID. If `None`,
90 this will be set to ``len(bands)``. If ``0``, the band will not be
91 included in the dimensions at all. If ``1``, the band will be included
92 in the dimensions but will not occupy any extra bits in the packed ID.
93 This may be larger or smaller than ``len(bands)``, to reserve extra
94 space for the future or align to byte boundaries, or support a subset
95 of a larger mapping, respectively.
96 n_tracts : `int` or `None`, optional
97 The number of tracts to leave room for in the packed ID. If `None`,
98 this will be set via the ``skymap`` dimension record in ``fixed``.
99 n_patches : `int` or `None`, optional
100 The number of patches (per tract) to leave room for in the packed ID.
101 If `None`, this will be set via the ``skymap`` dimension record in
102 ``fixed``.
103
104 Notes
105 -----
106 The standard pattern for constructing instances of this class is to use
107 `make_config_field`::
108
109 class SomeConfig(lsst.pex.config.Config):
110 packer = ObservationDimensionPacker.make_config_field()
111
112 class SomeTask(lsst.pipe.base.Task):
113 ConfigClass = SomeConfig
114
115 def run(self, ..., data_id: DataCoordinate):
116 packer = self.config.packer.apply(data_id)
117 packed_id = packer.pack(data_id)
118 ...
119
120 """
121
122 SUPPORTED_FILTERS = (
123 [None]
124 + list("ugrizyUBGVRIZYJHK") # split string into single chars
125 + [f"N{d}" for d in (387, 515, 656, 816, 921, 1010)] # HSC narrow-bands
126 + [f"N{d}" for d in (419, 540, 708, 964)] # DECam narrow-bands
127 )
128 """Sequence of supported bands used to construct a mapping from band name
129 to integer when the 'bands' config option is `None` or no config is
130 provided.
131
132 This variable should no longer be modified to add new filters; pass
133 ``bands`` at construction or use `from_config` instead.
134 """
135
136 ConfigClass = SkyMapDimensionPackerConfig
137
138 # TODO: remove on DM-38687.
139 @classmethod
140 @deprecated(
141 reason="This classmethod cannot reflect all __init__ args and will be removed after v26.",
142 version="v26.0",
143 category=FutureWarning,
144 )
145 def getIntFromFilter(cls, name):
146 """Return an integer that represents the band with the given
147 name.
148 """
149 try:
150 return cls.SUPPORTED_FILTERSSUPPORTED_FILTERS.index(name)
151 except ValueError:
152 raise NotImplementedError(f"band '{name}' not supported by this ID packer.")
153
154 # TODO: remove on DM-38687.
155 @classmethod
156 @deprecated(
157 reason="This classmethod cannot reflect all __init__ args and will be removed after v26.",
158 version="v26.0",
159 category=FutureWarning,
160 )
161 def getFilterNameFromInt(cls, num):
162 """Return an band name from its integer representation."""
164
165 # TODO: remove on DM-38687.
166 @classmethod
167 @deprecated(
168 reason="This classmethod cannot reflect all __init__ args and will be removed after v26.",
169 version="v26.0",
170 category=FutureWarning,
171 )
176 self,
177 fixed: DataCoordinate,
178 dimensions: DimensionGroup | DimensionGraph | None = None,
179 bands: Mapping[str, int] | None = None,
180 n_bands: int | None = None,
181 n_tracts: int | None = None,
182 n_patches: int | None = None,
183 ):
184 if bands is None:
185 bands = {b: i for i, b in enumerate(self.SUPPORTED_FILTERSSUPPORTED_FILTERS)}
186 if dimensions is None:
187 if n_bands is None:
188 n_bands = len(bands)
189 dimension_names = ["tract", "patch"]
190 if n_bands != 0:
191 dimension_names.append("band")
192 dimensions = fixed.universe.conform(dimension_names)
193 else:
194 if "band" not in dimensions.names:
195 n_bands = 0
196 if dimensions.names != {"tract", "patch", "skymap"}:
197 raise ValueError(
198 f"Invalid dimensions for skymap dimension packer with n_bands=0: {dimensions}."
199 )
200 else:
201 if dimensions.names != {"tract", "patch", "skymap", "band"}:
202 raise ValueError(
203 f"Invalid dimensions for skymap dimension packer with n_bands>0: {dimensions}."
204 )
205 if n_bands is None:
206 n_bands = len(bands)
207 if n_tracts is None:
208 n_tracts = fixed.records["skymap"].tract_max
209 if n_patches is None:
210 n_patches = (
211 fixed.records["skymap"].patch_nx_max
212 * fixed.records["skymap"].patch_ny_max
213 )
214 super().__init__(fixed, dimensions)
215 self._bands = bands
216 self._n_bands = n_bands
217 self._n_tracts = n_tracts
218 self._n_patches = n_patches
219 self._bands_list = None
220
221 @classmethod
223 cls,
224 doc: str = "How to pack tract, patch, and possibly band into an integer."
225 ) -> ConfigurableField:
226 """Make a config field to control how skymap data IDs are packed.
227
228 Parameters
229 ----------
230 doc : `str`, optional
231 Documentation for the config field.
232
233 Returns
234 -------
235 field : `lsst.pex.config.ConfigurableField`
236 A config field whose instance values are [wrapper proxies to]
237 `SkyMapDimensionPackerConfig` instances.
238 """
239 return ConfigurableField(doc, target=cls.from_config, ConfigClass=cls.ConfigClass)
240
241 @classmethod
243 cls, data_id: DataCoordinate, config: SkyMapDimensionPackerConfig
244 ) -> SkyMapDimensionPacker:
245 """Construct a dimension packer from a config object and a data ID.
246
247 Parameters
248 ----------
249 data_id : `lsst.daf.butler.DataCoordinate`
250 Data ID that identifies at least the ``skymap`` dimension. Must
251 have dimension records attached unless ``config.n_tracts`` and
252 ``config.n_patches`` are both not `None`.
253 config : `SkyMapDimensionPackerConfig`
254 Configuration object.
255
256 Returns
257 -------
258 packer : `SkyMapDimensionPackerConfig`
259 New dimension packer.
260
261 Notes
262 -----
263 This interface is provided for consistency with the `lsst.pex.config`
264 "Configurable" concept, and is invoked when ``apply(data_id)`` is
265 called on a config instance attribute that corresponds to a field
266 created by `make_config_field`. The constructor signature cannot play
267 this role easily for backwards compatibility reasons.
268 """
269 return cls(
270 data_id.subset(data_id.universe.conform(["skymap"])),
271 n_bands=config.n_bands,
272 bands=config.bands,
273 n_tracts=config.n_tracts,
274 n_patches=config.n_patches,
275 )
276
277 @property
278 def maxBits(self) -> int:
279 # Docstring inherited from DataIdPacker.maxBits
280 packedMax = self._n_tracts * self._n_patches
281 if self._n_bands:
282 packedMax *= self._n_bands
283 return (packedMax - 1).bit_length()
284
285 def _pack(self, dataId: DataCoordinate) -> int:
286 # Docstring inherited from DataIdPacker.pack
287 if dataId["patch"] >= self._n_patches:
288 raise ValueError(f"Patch ID {dataId['patch']} is out of bounds; expected <{self._n_patches}.")
289 if dataId["tract"] >= self._n_tracts:
290 raise ValueError(f"Tract ID {dataId['tract']} is out of bounds; expected <{self._n_tracts}.")
291 packed = dataId["patch"] + self._n_patches * dataId["tract"]
292 if self._n_bands:
293 if (band_index := self._bands.get(dataId["band"])) is None:
294 raise ValueError(
295 f"Band {dataId['band']!r} is not supported by SkyMapDimensionPacker "
296 f"configuration; expected one of {list(self._bands)}."
297 )
298 if band_index >= self._n_bands:
299 raise ValueError(
300 f"Band index {band_index} for {dataId['band']!r} is out of bounds; "
301 f"expected <{self._n_bands}."
302 )
303 packed += self._bands[dataId["band"]] * self._n_patches * self._n_tracts
304 return packed
305
306 def unpack(self, packedId: int) -> DataCoordinate:
307 # Docstring inherited from DataIdPacker.unpack
308 d = {"skymap": self.fixed["skymap"]}
309 if self._n_bands:
310 index, packedId = divmod(packedId, (self._n_tracts * self._n_patches))
311 if self._bands_list is None:
312 self._bands_list = list(self._bands)
313 d["band"] = self._bands_list[index]
314 d["tract"], d["patch"] = divmod(packedId, self._n_patches)
315 return DataCoordinate.standardize(d, dimensions=self.dimensions)
ConfigurableField make_config_field(cls, str doc="How to pack tract, patch, and possibly band into an integer.")
Definition packers.py:225
__init__(self, DataCoordinate fixed, DimensionGroup|DimensionGraph|None dimensions=None, Mapping[str, int]|None bands=None, int|None n_bands=None, int|None n_tracts=None, int|None n_patches=None)
Definition packers.py:183
SkyMapDimensionPacker from_config(cls, DataCoordinate data_id, SkyMapDimensionPackerConfig config)
Definition packers.py:244
int _pack(self, DataCoordinate dataId)
Definition packers.py:285
DataCoordinate unpack(self, int packedId)
Definition packers.py:306