Coverage for python/lsst/skymap/packers.py: 38%

92 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-29 09:25 +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/>. 

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 

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 ) 

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_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.""" 

163 return cls.SUPPORTED_FILTERS[num] 

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 ) 

172 def getMaxIntForFilters(cls): 

173 return len(cls.SUPPORTED_FILTERS) 

174 

175 def __init__( 

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_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 

222 def make_config_field( 

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 

242 def from_config( 

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)