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

76 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-24 11:00 +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 

30 

31 

32class SkyMapDimensionPackerConfig(Config): 

33 bands = DictField( 

34 "Mapping from band name to integer to use in the packed ID. " 

35 "The default (None) is to use a hard-coded list of common bands; " 

36 "pipelines that can enumerate the set of bands they are likely to see " 

37 "should override this.", 

38 keytype=str, 

39 itemtype=int, 

40 default=None, 

41 optional=True, 

42 ) 

43 n_bands = Field( 

44 "Number of bands to reserve space for. " 

45 "If zero, bands are not included in the packed integer at all. " 

46 "If `None`, the size of 'bands' is used.", 

47 dtype=int, 

48 optional=True, 

49 default=0, 

50 ) 

51 n_tracts = Field( 

52 "Number of tracts, or, more precisely, one greater than the maximum tract ID." 

53 "Default (None) obtains this value from the skymap dimension record.", 

54 dtype=int, 

55 optional=True, 

56 default=None, 

57 ) 

58 n_patches = Field( 

59 "Number of patches per tract, or, more precisely, one greater than the maximum patch ID." 

60 "Default (None) obtains this value from the skymap dimension record.", 

61 dtype=int, 

62 optional=True, 

63 default=None, 

64 ) 

65 

66 

67class SkyMapDimensionPacker(DimensionPacker): 

68 """A `DimensionPacker` for tract, patch and optionally band, 

69 given a SkyMap. 

70 

71 Parameters 

72 ---------- 

73 fixed : `lsst.daf.butler.DataCoordinate` 

74 Data ID that identifies just the ``skymap`` dimension. Must have 

75 dimension records attached unless ``n_tracts`` and ``n_patches`` are 

76 not `None`. 

77 dimensions : `lsst.daf.butler.DimensionGraph`, or \ 

78 `lsst.daf.butler.DimensionGroup`, 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 def __init__( 

138 self, 

139 fixed: DataCoordinate, 

140 dimensions: DimensionGroup | DimensionGraph | None = None, 

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

142 n_bands: int | None = None, 

143 n_tracts: int | None = None, 

144 n_patches: int | None = None, 

145 ): 

146 if bands is None: 

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

148 if dimensions is None: 

149 if n_bands is None: 

150 n_bands = len(bands) 

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

152 if n_bands != 0: 

153 dimension_names.append("band") 

154 dimensions = fixed.universe.conform(dimension_names) 

155 else: 

156 if "band" not in dimensions.names: 

157 n_bands = 0 

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

159 raise ValueError( 

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

161 ) 

162 else: 

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

164 raise ValueError( 

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

166 ) 

167 if n_bands is None: 

168 n_bands = len(bands) 

169 if n_tracts is None: 

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

171 if n_patches is None: 

172 n_patches = ( 

173 fixed.records["skymap"].patch_nx_max 

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

175 ) 

176 super().__init__(fixed, dimensions) 

177 self._bands = bands 

178 self._n_bands = n_bands 

179 self._n_tracts = n_tracts 

180 self._n_patches = n_patches 

181 self._bands_list = None 

182 

183 @classmethod 

184 def make_config_field( 

185 cls, 

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

187 ) -> ConfigurableField: 

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

189 

190 Parameters 

191 ---------- 

192 doc : `str`, optional 

193 Documentation for the config field. 

194 

195 Returns 

196 ------- 

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

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

199 `SkyMapDimensionPackerConfig` instances. 

200 """ 

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

202 

203 @classmethod 

204 def from_config( 

205 cls, data_id: DataCoordinate, config: SkyMapDimensionPackerConfig 

206 ) -> SkyMapDimensionPacker: 

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

208 

209 Parameters 

210 ---------- 

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

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

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

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

215 config : `SkyMapDimensionPackerConfig` 

216 Configuration object. 

217 

218 Returns 

219 ------- 

220 packer : `SkyMapDimensionPackerConfig` 

221 New dimension packer. 

222 

223 Notes 

224 ----- 

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

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

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

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

229 this role easily for backwards compatibility reasons. 

230 """ 

231 return cls( 

232 data_id.subset(data_id.universe.conform(["skymap"])), 

233 n_bands=config.n_bands, 

234 bands=config.bands, 

235 n_tracts=config.n_tracts, 

236 n_patches=config.n_patches, 

237 ) 

238 

239 @property 

240 def maxBits(self) -> int: 

241 # Docstring inherited from DataIdPacker.maxBits 

242 packedMax = self._n_tracts * self._n_patches 

243 if self._n_bands: 

244 packedMax *= self._n_bands 

245 return (packedMax - 1).bit_length() 

246 

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

248 # Docstring inherited from DataIdPacker.pack 

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

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

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

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

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

254 if self._n_bands: 

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

256 raise ValueError( 

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

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

259 ) 

260 if band_index >= self._n_bands: 

261 raise ValueError( 

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

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

264 ) 

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

266 return packed 

267 

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

269 # Docstring inherited from DataIdPacker.unpack 

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

271 if self._n_bands: 

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

273 if self._bands_list is None: 

274 self._bands_list = list(self._bands) 

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

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

277 return DataCoordinate.standardize(d, dimensions=self.dimensions)