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

92 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 03:23 -0700

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 @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.") 

151 

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] 

161 

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) 

170 

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 

216 

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. 

223 

224 Parameters 

225 ---------- 

226 doc : `str`, optional 

227 Documentation for the config field. 

228 

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) 

236 

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. 

242 

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. 

251 

252 Returns 

253 ------- 

254 packer : `SkyMapDimensionPackerConfig` 

255 New dimension packer. 

256 

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 ) 

272 

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() 

280 

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 

301 

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)