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

92 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-26 10:33 +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, 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`, 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``. 

102 

103 Notes 

104 ----- 

105 The standard pattern for constructing instances of this class is to use 

106 `make_config_field`:: 

107 

108 class SomeConfig(lsst.pex.config.Config): 

109 packer = ObservationDimensionPacker.make_config_field() 

110 

111 class SomeTask(lsst.pipe.base.Task): 

112 ConfigClass = SomeConfig 

113 

114 def run(self, ..., data_id: DataCoordinate): 

115 packer = self.config.packer.apply(data_id) 

116 packed_id = packer.pack(data_id) 

117 ... 

118 

119 """ 

120 

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. 

130 

131 This variable should no longer be modified to add new filters; pass 

132 ``bands`` at construction or use `from_config` instead. 

133 """ 

134 

135 ConfigClass = SkyMapDimensionPackerConfig 

136 

137 # TODO: remove on DM-38687. 

138 @classmethod 

139 @deprecated( 

140 reason="This classmethod cannot reflect all __init__ args and will be removed after v26.", 

141 version="v26.0", 

142 category=FutureWarning, 

143 ) 

144 def getIntFromFilter(cls, name): 

145 """Return an integer that represents the band with the given 

146 name. 

147 """ 

148 try: 

149 return cls.SUPPORTED_FILTERS.index(name) 

150 except ValueError: 

151 raise NotImplementedError(f"band '{name}' not supported by this ID packer.") 

152 

153 # TODO: remove on DM-38687. 

154 @classmethod 

155 @deprecated( 

156 reason="This classmethod cannot reflect all __init__ args and will be removed after v26.", 

157 version="v26.0", 

158 category=FutureWarning, 

159 ) 

160 def getFilterNameFromInt(cls, num): 

161 """Return an band name from its integer representation.""" 

162 return cls.SUPPORTED_FILTERS[num] 

163 

164 # TODO: remove on DM-38687. 

165 @classmethod 

166 @deprecated( 

167 reason="This classmethod cannot reflect all __init__ args and will be removed after v26.", 

168 version="v26.0", 

169 category=FutureWarning, 

170 ) 

171 def getMaxIntForFilters(cls): 

172 return len(cls.SUPPORTED_FILTERS) 

173 

174 def __init__( 

175 self, 

176 fixed: DataCoordinate, 

177 dimensions: DimensionGraph | None = None, 

178 bands: Mapping[str, int] | None = None, 

179 n_bands: int | None = None, 

180 n_tracts: int | None = None, 

181 n_patches: int | None = None, 

182 ): 

183 if bands is None: 

184 bands = {b: i for i, b in enumerate(self.SUPPORTED_FILTERS)} 

185 if dimensions is None: 

186 if n_bands is None: 

187 n_bands = len(bands) 

188 dimension_names = ["tract", "patch"] 

189 if n_bands != 0: 

190 dimension_names.append("band") 

191 dimensions = fixed.universe.extract(dimension_names) 

192 else: 

193 if "band" not in dimensions.names: 

194 n_bands = 0 

195 if dimensions.names != {"tract", "patch", "skymap"}: 

196 raise ValueError( 

197 f"Invalid dimensions for skymap dimension packer with n_bands=0: {dimensions}." 

198 ) 

199 else: 

200 if dimensions.names != {"tract", "patch", "skymap", "band"}: 

201 raise ValueError( 

202 f"Invalid dimensions for skymap dimension packer with n_bands>0: {dimensions}." 

203 ) 

204 if n_bands is None: 

205 n_bands = len(bands) 

206 if n_tracts is None: 

207 n_tracts = fixed.records["skymap"].tract_max 

208 if n_patches is None: 

209 n_patches = ( 

210 fixed.records["skymap"].patch_nx_max 

211 * fixed.records["skymap"].patch_ny_max 

212 ) 

213 super().__init__(fixed, dimensions) 

214 self._bands = bands 

215 self._n_bands = n_bands 

216 self._n_tracts = n_tracts 

217 self._n_patches = n_patches 

218 self._bands_list = None 

219 

220 @classmethod 

221 def make_config_field( 

222 cls, 

223 doc: str = "How to pack tract, patch, and possibly band into an integer." 

224 ) -> ConfigurableField: 

225 """Make a config field to control how skymap data IDs are packed. 

226 

227 Parameters 

228 ---------- 

229 doc : `str`, optional 

230 Documentation for the config field. 

231 

232 Returns 

233 ------- 

234 field : `lsst.pex.config.ConfigurableField` 

235 A config field whose instance values are [wrapper proxies to] 

236 `SkyMapDimensionPackerConfig` instances. 

237 """ 

238 return ConfigurableField(doc, target=cls.from_config, ConfigClass=cls.ConfigClass) 

239 

240 @classmethod 

241 def from_config( 

242 cls, data_id: DataCoordinate, config: SkyMapDimensionPackerConfig 

243 ) -> SkyMapDimensionPacker: 

244 """Construct a dimension packer from a config object and a data ID. 

245 

246 Parameters 

247 ---------- 

248 data_id : `lsst.daf.butler.DataCoordinate` 

249 Data ID that identifies at least the ``skymap`` dimension. Must 

250 have dimension records attached unless ``config.n_tracts`` and 

251 ``config.n_patches`` are both not `None`. 

252 config : `SkyMapDimensionPackerConfig` 

253 Configuration object. 

254 

255 Returns 

256 ------- 

257 packer : `SkyMapDimensionPackerConfig` 

258 New dimension packer. 

259 

260 Notes 

261 ----- 

262 This interface is provided for consistency with the `lsst.pex.config` 

263 "Configurable" concept, and is invoked when ``apply(data_id)`` is 

264 called on a config instance attribute that corresponds to a field 

265 created by `make_config_field`. The constructor signature cannot play 

266 this role easily for backwards compatibility reasons. 

267 """ 

268 return cls( 

269 data_id.subset(data_id.universe.extract(["skymap"])), 

270 n_bands=config.n_bands, 

271 bands=config.bands, 

272 n_tracts=config.n_tracts, 

273 n_patches=config.n_patches, 

274 ) 

275 

276 @property 

277 def maxBits(self) -> int: 

278 # Docstring inherited from DataIdPacker.maxBits 

279 packedMax = self._n_tracts * self._n_patches 

280 if self._n_bands: 

281 packedMax *= self._n_bands 

282 return (packedMax - 1).bit_length() 

283 

284 def _pack(self, dataId: DataCoordinate) -> int: 

285 # Docstring inherited from DataIdPacker.pack 

286 if dataId["patch"] >= self._n_patches: 

287 raise ValueError(f"Patch ID {dataId['patch']} is out of bounds; expected <{self._n_patches}.") 

288 if dataId["tract"] >= self._n_tracts: 

289 raise ValueError(f"Tract ID {dataId['tract']} is out of bounds; expected <{self._n_tracts}.") 

290 packed = dataId["patch"] + self._n_patches * dataId["tract"] 

291 if self._n_bands: 

292 if (band_index := self._bands.get(dataId["band"])) is None: 

293 raise ValueError( 

294 f"Band {dataId['band']!r} is not supported by SkyMapDimensionPacker " 

295 f"configuration; expected one of {list(self._bands)}." 

296 ) 

297 if band_index >= self._n_bands: 

298 raise ValueError( 

299 f"Band index {band_index} for {dataId['band']!r} is out of bounds; " 

300 f"expected <{self._n_bands}." 

301 ) 

302 packed += self._bands[dataId["band"]] * self._n_patches * self._n_tracts 

303 return packed 

304 

305 def unpack(self, packedId: int) -> DataCoordinate: 

306 # Docstring inherited from DataIdPacker.unpack 

307 d = {"skymap": self.fixed["skymap"]} 

308 if self._n_bands: 

309 index, packedId = divmod(packedId, (self._n_tracts * self._n_patches)) 

310 if self._bands_list is None: 

311 self._bands_list = list(self._bands) 

312 d["band"] = self._bands_list[index] 

313 d["tract"], d["patch"] = divmod(packedId, self._n_patches) 

314 return DataCoordinate.standardize(d, graph=self.dimensions)