Coverage for python/lsst/skymap/tractBuilder.py: 26%

137 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-27 03:07 -0700

1# LSST Data Management System 

2# Copyright 2008, 2009, 2010 LSST Corporation. 

3# 

4# This product includes software developed by the 

5# LSST Project (http://www.lsst.org/). 

6# 

7# This program is free software: you can redistribute it and/or modify 

8# it under the terms of the GNU General Public License as published by 

9# the Free Software Foundation, either version 3 of the License, or 

10# (at your option) any later version. 

11# 

12# This program is distributed in the hope that it will be useful, 

13# but WITHOUT ANY WARRANTY; without even the implied warranty of 

14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

15# GNU General Public License for more details. 

16# 

17# You should have received a copy of the LSST License Statement and 

18# the GNU General Public License along with this program. If not, 

19# see <http://www.lsstcorp.org/LegalNotices/>. 

20# 

21__all__ = ["tractBuilderRegistry", 

22 "BaseTractBuilderConfig", "BaseTractBuilder", 

23 "LegacyTractBuilderConfig", "LegacyTractBuilder", 

24 "CellTractBuilderConfig", "CellTractBuilder"] 

25 

26import abc 

27import numbers 

28import struct 

29from collections.abc import Iterable 

30 

31import lsst.pex.config as pexConfig 

32import lsst.geom as geom 

33from .patchInfo import PatchInfo 

34from .detail import Index2D 

35 

36 

37class BaseTractBuilderConfig(pexConfig.Config): 

38 """Configuration that is to be shared amongst all tract builders.""" 

39 pass 

40 

41 

42class BaseTractBuilder(metaclass=abc.ABCMeta): 

43 """Base class for algorithms that define patches within the tract. 

44 

45 Parameters 

46 ---------- 

47 config : `lsst.pexConfig.Config` 

48 Input for configuring the algorithm 

49 """ 

50 def __init__(self, config): 

51 self.config = config 

52 

53 def setupPatches(self, minBBox, wcs): 

54 """Set up the patches of a particular size in a tract. 

55 

56 We grow the tract bounding box to hold an exact multiple of 

57 the desired size (patchInnerDimensions or 

58 numCellsPerPatchInner*cellInnerDimensions), while keeping 

59 the center roughly the same. We return the final tract 

60 bounding box, and the number of patches in each dimension 

61 (as an Index2D). 

62 

63 Parameters 

64 ---------- 

65 minBBox : `lsst.geom.Box2I` 

66 Minimum bounding box for tract. 

67 wcs : `lsst.afw.geom.SkyWcs` 

68 Wcs object. 

69 

70 Returns 

71 ------- 

72 bbox : `lsst.geom.Box2I` 

73 final bounding box, number of patches. 

74 numPatches : `lsst.skymap.Index2D` 

75 """ 

76 bbox = geom.Box2I(minBBox) 

77 bboxMin = bbox.getMin() 

78 bboxDim = bbox.getDimensions() 

79 numPatchesList = [0, 0] 

80 for i, innerDim in enumerate(self._patchInnerDimensions): 

81 num = (bboxDim[i] + innerDim - 1) // innerDim # round up 

82 deltaDim = (innerDim*num) - bboxDim[i] 

83 if deltaDim > 0: 

84 bboxDim[i] = innerDim * num 

85 bboxMin[i] -= deltaDim // 2 

86 numPatchesList[i] = num 

87 numPatches = Index2D(*numPatchesList) 

88 bbox = geom.Box2I(bboxMin, bboxDim) 

89 self._numPatches = numPatches 

90 # The final tract BBox starts at zero. 

91 self._tractBBox = geom.Box2I(geom.Point2I(0, 0), bbox.getDimensions()) 

92 self._initialized = True 

93 

94 return bbox, numPatches 

95 

96 def getPatchBorder(self): 

97 return self._patchBorder 

98 

99 @abc.abstractmethod 

100 def getPatchInfo(self, index, tractWcs): 

101 """Return information for the specified patch. 

102 

103 Parameters 

104 ---------- 

105 index : `lsst.skymap.Index2D` or `~collections.abc.Iterable` of 2 `int` 

106 Index of patch, as Index2D or pair of ints; 

107 or a sequential index as returned by getSequentialPatchIndex; 

108 negative values are not supported. 

109 tractWcs : `lsst.afw.geom.SkyWcs` 

110 WCS associated with the tract. 

111 

112 Returns 

113 ------- 

114 result : `lsst.skymap.PatchInfo` 

115 The patch info for that index. 

116 

117 Raises 

118 ------ 

119 IndexError 

120 Raised if index is out of range. 

121 """ 

122 raise NotImplementedError("Must be implemented by a subclass") 

123 

124 def getPatchInnerDimensions(self): 

125 """Get dimensions of inner region of the patches (all are the same) 

126 """ 

127 return self._patchInnerDimensions 

128 

129 def getSequentialPatchIndex(self, patchInfo): 

130 """Return a single integer that uniquely identifies 

131 the given patch within this tract. 

132 

133 Parameters 

134 ---------- 

135 patchInfo : `lsst.skymap.PatchInfo` 

136 

137 Returns 

138 ------- 

139 sequentialIndex : `int` 

140 """ 

141 index = patchInfo.getIndex() 

142 return self.getSequentialPatchIndexFromPair(index) 

143 

144 def getSequentialPatchIndexFromPair(self, index): 

145 """Return a single integer that uniquely identifies 

146 the patch index within the tract. 

147 

148 Parameters 

149 ---------- 

150 index : `lsst.skymap.Index2D` or `~collections.abc.Iterable` of 2 `int` 

151 

152 Returns 

153 ------- 

154 sequentialIndex : `int` 

155 """ 

156 if isinstance(index, Index2D): 

157 _index = index 

158 else: 

159 if not isinstance(index, Iterable): 

160 raise ValueError("Input index is not an iterable.") 

161 if len(index) != 2: 

162 raise ValueError("Input index does not have two values.") 

163 _index = Index2D(*index) 

164 nx, ny = self._numPatches 

165 return nx*_index.y + _index.x 

166 

167 def getPatchIndexPair(self, sequentialIndex): 

168 """Convert sequential index into patch index (x,y) pair. 

169 

170 Parameters 

171 ---------- 

172 sequentialIndex : `int` 

173 

174 Returns 

175 ------- 

176 x, y : `lsst.skymap.Index2D` 

177 """ 

178 nx, ny = self._numPatches 

179 x = sequentialIndex % nx 

180 y = sequentialIndex // nx 

181 return Index2D(x=x, y=y) 

182 

183 @abc.abstractmethod 

184 def getPackedConfig(self, config): 

185 """Get a packed config suitable for using in a sha1. 

186 

187 Parameters 

188 ---------- 

189 config : `lsst.skymap.BaseTractBuilderConfig` 

190 

191 Returns 

192 ------- 

193 configPacked : `bytes` 

194 """ 

195 raise NotImplementedError("Must be implemented by a subclass") 

196 

197 

198class LegacyTractBuilderConfig(BaseTractBuilderConfig): 

199 patchInnerDimensions = pexConfig.ListField( 

200 doc="dimensions of inner region of patches (x,y pixels)", 

201 dtype=int, 

202 length=2, 

203 default=(4000, 4000), 

204 ) 

205 patchBorder = pexConfig.Field( 

206 doc="border between patch inner and outer bbox (pixels)", 

207 dtype=int, 

208 default=100, 

209 ) 

210 

211 

212class LegacyTractBuilder(BaseTractBuilder): 

213 ConfigClass = LegacyTractBuilderConfig 

214 

215 def __init__(self, config): 

216 super().__init__(config) 

217 

218 self._patchInnerDimensions = geom.Extent2I(*(val 

219 for val in config.patchInnerDimensions)) 

220 self._patchBorder = config.patchBorder 

221 self._initialized = False 

222 

223 def getPatchInfo(self, index, tractWcs): 

224 # This should always be initialized 

225 if not self._initialized: 

226 raise RuntimeError("Programmer error; this should always be initialized.") 

227 if isinstance(index, Index2D): 

228 _index = index 

229 else: 

230 if isinstance(index, numbers.Number): 

231 _index = self.getPatchIndexPair(index) 

232 else: 

233 _index = Index2D(*index) 

234 if (not 0 <= _index.x < self._numPatches.x) \ 

235 or (not 0 <= _index.y < self._numPatches.y): 

236 raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" % 

237 (_index, self._numPatches.x - 1, self._numPatches.y - 1)) 

238 innerMin = geom.Point2I(*[_index[i] * self._patchInnerDimensions[i] for i in range(2)]) 

239 innerBBox = geom.Box2I(innerMin, self._patchInnerDimensions) 

240 if not self._tractBBox.contains(innerBBox): 

241 raise RuntimeError( 

242 "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" % 

243 (_index, innerBBox, self._tractBBox)) 

244 outerBBox = geom.Box2I(innerBBox) 

245 outerBBox.grow(self.getPatchBorder()) 

246 outerBBox.clip(self._tractBBox) 

247 return PatchInfo( 

248 index=_index, 

249 innerBBox=innerBBox, 

250 outerBBox=outerBBox, 

251 sequentialIndex=self.getSequentialPatchIndexFromPair(_index), 

252 tractWcs=tractWcs 

253 ) 

254 

255 def getPackedConfig(self, config): 

256 subConfig = config.tractBuilder[config.tractBuilder.name] 

257 configPacked = struct.pack( 

258 "<iiidd3sd", 

259 subConfig.patchInnerDimensions[0], 

260 subConfig.patchInnerDimensions[1], 

261 subConfig.patchBorder, 

262 config.tractOverlap, 

263 config.pixelScale, 

264 config.projection.encode('ascii'), 

265 config.rotation 

266 ) 

267 

268 return configPacked 

269 

270 

271class CellTractBuilderConfig(BaseTractBuilderConfig): 

272 cellInnerDimensions = pexConfig.ListField( 

273 doc="dimensions of inner region of cells (x,y pixels)", 

274 dtype=int, 

275 length=2, 

276 default=(150, 150), 

277 ) 

278 cellBorder = pexConfig.Field( 

279 doc="Border between cell inner and outer bbox (pixels)", 

280 dtype=int, 

281 default=50, 

282 ) 

283 numCellsPerPatchInner = pexConfig.Field( 

284 doc="Number of cells per inner patch.", 

285 dtype=int, 

286 default=20, 

287 ) 

288 numCellsInPatchBorder = pexConfig.Field( 

289 doc="Number of cells in the patch border (outside the inner patch region).", 

290 dtype=int, 

291 default=1, 

292 ) 

293 

294 def validate(self): 

295 if len(self.cellInnerDimensions) != 2: 

296 raise ValueError("cellInnerDimensions must be 2 ints.") 

297 

298 if self.cellInnerDimensions[0] != self.cellInnerDimensions[1]: 

299 raise ValueError("cellInnerDimensions must be equal (for square cells).") 

300 

301 

302class CellTractBuilder(BaseTractBuilder): 

303 ConfigClass = CellTractBuilderConfig 

304 

305 def __init__(self, config): 

306 super().__init__(config) 

307 

308 self._cellInnerDimensions = geom.Extent2I(*(val 

309 for val in config.cellInnerDimensions)) 

310 self._cellBorder = config.cellBorder 

311 self._numCellsPerPatchInner = config.numCellsPerPatchInner 

312 self._numCellsInPatchBorder = config.numCellsInPatchBorder 

313 self._patchInnerDimensions = geom.Extent2I(*(val*self._numCellsPerPatchInner 

314 for val in config.cellInnerDimensions)) 

315 # The patch border is the number of cells in the border + the cell 

316 # border. 

317 self._patchBorder = config.numCellsInPatchBorder*config.cellInnerDimensions[0] + self._cellBorder 

318 self._initialized = False 

319 

320 def getPatchInfo(self, index, tractWcs): 

321 # This should always be initialized 

322 if not self._initialized: 

323 raise RuntimeError("Programmer error; this should always be initialized.") 

324 if isinstance(index, Index2D): 

325 _index = index 

326 else: 

327 if isinstance(index, numbers.Number): 

328 _index = self.getPatchIndexPair(index) 

329 else: 

330 _index = Index2D(*index) 

331 if (not 0 <= _index.x < self._numPatches.x) \ 

332 or (not 0 <= _index.y < self._numPatches.y): 

333 raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" % 

334 (_index, self._numPatches.x - 1, self._numPatches.y - 1)) 

335 innerMin = geom.Point2I(*[_index[i]*self._patchInnerDimensions[i] for i in range(2)]) 

336 innerBBox = geom.Box2I(innerMin, self._patchInnerDimensions) 

337 if not self._tractBBox.contains(innerBBox): 

338 raise RuntimeError( 

339 "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" % 

340 (_index, innerBBox, self._tractBBox)) 

341 outerBBox = geom.Box2I(innerBBox) 

342 outerBBox.grow(self.getPatchBorder()) 

343 # We do not clip the patch for cell-based tracts. 

344 return PatchInfo( 

345 index=_index, 

346 innerBBox=innerBBox, 

347 outerBBox=outerBBox, 

348 sequentialIndex=self.getSequentialPatchIndexFromPair(_index), 

349 tractWcs=tractWcs, 

350 cellInnerDimensions=self._cellInnerDimensions, 

351 cellBorder=self._cellBorder, 

352 numCellsPerPatchInner=self._numCellsPerPatchInner, 

353 numCellsInPatchBorder=self._numCellsInPatchBorder 

354 ) 

355 

356 def getPackedConfig(self, config): 

357 subConfig = config.tractBuilder[config.tractBuilder.name] 

358 configPacked = struct.pack( 

359 "<iiiiidd3sd", 

360 subConfig.cellInnerDimensions[0], 

361 subConfig.cellInnerDimensions[1], 

362 subConfig.cellBorder, 

363 subConfig.numCellsPerPatchInner, 

364 subConfig.numCellsInPatchBorder, 

365 config.tractOverlap, 

366 config.pixelScale, 

367 config.projection.encode('ascii'), 

368 config.rotation 

369 ) 

370 

371 return configPacked 

372 

373 

374tractBuilderRegistry = pexConfig.makeRegistry( 

375 doc="A registry of Tract Builders (subclasses of BaseTractBuilder)", 

376) 

377 

378tractBuilderRegistry.register("legacy", LegacyTractBuilder) 

379tractBuilderRegistry.register("cells", CellTractBuilder)