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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

137 statements  

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", "BaseTractBuilder", 

22 "LegacyTractBuilder", "CellTractBuilder"] 

23 

24import abc 

25import numbers 

26import struct 

27from collections.abc import Iterable 

28 

29import lsst.pex.config as pexConfig 

30import lsst.geom as geom 

31from .patchInfo import PatchInfo 

32from .detail import Index2D 

33 

34 

35class BaseTractBuilderConfig(pexConfig.Config): 

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

37 pass 

38 

39 

40class BaseTractBuilder(metaclass=abc.ABCMeta): 

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

42 

43 Parameters 

44 ---------- 

45 config : `lsst.pexConfig.Config` 

46 Input for configuring the algorithm 

47 """ 

48 def __init__(self, config): 

49 self.config = config 

50 

51 def setupPatches(self, minBBox, wcs): 

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

53 

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

55 the desired size (patchInnerDimensions or 

56 numCellsPerPatchInner*cellInnerDimensions), while keeping 

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

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

59 (as an Index2D). 

60 

61 Parameters 

62 ---------- 

63 minBBox : `lsst.geom.Box2I` 

64 Minimum bounding box for tract 

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

66 Wcs object 

67 

68 Returns 

69 ------- 

70 bbox : `lsst.geom.Box2I 

71 final bounding box, number of patches 

72 numPatches : `lsst.skymap.Index2D` 

73 """ 

74 bbox = geom.Box2I(minBBox) 

75 bboxMin = bbox.getMin() 

76 bboxDim = bbox.getDimensions() 

77 numPatchesList = [0, 0] 

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

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

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

81 if deltaDim > 0: 

82 bboxDim[i] = innerDim * num 

83 bboxMin[i] -= deltaDim // 2 

84 numPatchesList[i] = num 

85 numPatches = Index2D(*numPatchesList) 

86 bbox = geom.Box2I(bboxMin, bboxDim) 

87 self._numPatches = numPatches 

88 # The final tract BBox starts at zero. 

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

90 self._initialized = True 

91 

92 return bbox, numPatches 

93 

94 def getPatchBorder(self): 

95 return self._patchBorder 

96 

97 @abc.abstractmethod 

98 def getPatchInfo(self, index, tractWcs): 

99 """Return information for the specified patch. 

100 

101 Parameters 

102 ---------- 

103 index : `lsst.skymap.Index2D` or `Iterable` [`int`, `int`] 

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

105 or a sequential index as returned by getSequentialPatchIndex; 

106 negative values are not supported. 

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

108 WCS associated with the tract. 

109 

110 Returns 

111 ------- 

112 result : `lsst.skymap.PatchInfo` 

113 The patch info for that index. 

114 

115 Raises 

116 ------ 

117 IndexError 

118 If index is out of range. 

119 """ 

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

121 

122 def getPatchInnerDimensions(self): 

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

124 """ 

125 return self._patchInnerDimensions 

126 

127 def getSequentialPatchIndex(self, patchInfo): 

128 """Return a single integer that uniquely identifies 

129 the given patch within this tract. 

130 

131 Parameters 

132 ---------- 

133 patchInfo : `lsst.skymap.PatchInfo` 

134 

135 Returns 

136 ------- 

137 sequentialIndex : `int` 

138 """ 

139 index = patchInfo.getIndex() 

140 return self.getSequentialPatchIndexFromPair(index) 

141 

142 def getSequentialPatchIndexFromPair(self, index): 

143 """Return a single integer that uniquely identifies 

144 the patch index within the tract. 

145 

146 Parameters 

147 ---------- 

148 index : `lsst.skymap.Index2D` or `Iterable` [`int`, `int`] 

149 

150 Returns 

151 ------- 

152 sequentialIndex : `int` 

153 """ 

154 if isinstance(index, Index2D): 

155 _index = index 

156 else: 

157 if not isinstance(index, Iterable): 

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

159 if len(index) != 2: 

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

161 _index = Index2D(*index) 

162 nx, ny = self._numPatches 

163 return nx*_index.y + _index.x 

164 

165 def getPatchIndexPair(self, sequentialIndex): 

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

167 

168 Parameters 

169 ---------- 

170 sequentialIndex : `int` 

171 

172 Returns 

173 ------- 

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

175 """ 

176 nx, ny = self._numPatches 

177 x = sequentialIndex % nx 

178 y = sequentialIndex // nx 

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

180 

181 @abc.abstractmethod 

182 def getPackedConfig(self, config): 

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

184 

185 Parameters 

186 ---------- 

187 config : `lsst.skymap.BaseTractBuilderConfig` 

188 

189 Returns 

190 ------- 

191 configPacked : `bytes` 

192 """ 

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

194 

195 

196class LegacyTractBuilderConfig(BaseTractBuilderConfig): 

197 patchInnerDimensions = pexConfig.ListField( 

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

199 dtype=int, 

200 length=2, 

201 default=(4000, 4000), 

202 ) 

203 patchBorder = pexConfig.Field( 

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

205 dtype=int, 

206 default=100, 

207 ) 

208 

209 

210class LegacyTractBuilder(BaseTractBuilder): 

211 ConfigClass = LegacyTractBuilderConfig 

212 

213 def __init__(self, config): 

214 super().__init__(config) 

215 

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

217 for val in config.patchInnerDimensions)) 

218 self._patchBorder = config.patchBorder 

219 self._initialized = False 

220 

221 def getPatchInfo(self, index, tractWcs): 

222 # This should always be initialized 

223 if not self._initialized: 

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

225 if isinstance(index, Index2D): 

226 _index = index 

227 else: 

228 if isinstance(index, numbers.Number): 

229 _index = self.getPatchIndexPair(index) 

230 else: 

231 _index = Index2D(*index) 

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

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

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

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

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

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

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

239 raise RuntimeError( 

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

241 (_index, innerBBox, self._tractBBox)) 

242 outerBBox = geom.Box2I(innerBBox) 

243 outerBBox.grow(self.getPatchBorder()) 

244 outerBBox.clip(self._tractBBox) 

245 return PatchInfo( 

246 index=_index, 

247 innerBBox=innerBBox, 

248 outerBBox=outerBBox, 

249 sequentialIndex=self.getSequentialPatchIndexFromPair(_index), 

250 tractWcs=tractWcs 

251 ) 

252 

253 def getPackedConfig(self, config): 

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

255 configPacked = struct.pack( 

256 "<iiidd3sd", 

257 subConfig.patchInnerDimensions[0], 

258 subConfig.patchInnerDimensions[1], 

259 subConfig.patchBorder, 

260 config.tractOverlap, 

261 config.pixelScale, 

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

263 config.rotation 

264 ) 

265 

266 return configPacked 

267 

268 

269class CellTractBuilderConfig(BaseTractBuilderConfig): 

270 cellInnerDimensions = pexConfig.ListField( 

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

272 dtype=int, 

273 length=2, 

274 default=(150, 150), 

275 ) 

276 cellBorder = pexConfig.Field( 

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

278 dtype=int, 

279 default=50, 

280 ) 

281 numCellsPerPatchInner = pexConfig.Field( 

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

283 dtype=int, 

284 default=20, 

285 ) 

286 numCellsInPatchBorder = pexConfig.Field( 

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

288 dtype=int, 

289 default=1, 

290 ) 

291 

292 def validate(self): 

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

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

295 

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

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

298 

299 

300class CellTractBuilder(BaseTractBuilder): 

301 ConfigClass = CellTractBuilderConfig 

302 

303 def __init__(self, config): 

304 super().__init__(config) 

305 

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

307 for val in config.cellInnerDimensions)) 

308 self._cellBorder = config.cellBorder 

309 self._numCellsPerPatchInner = config.numCellsPerPatchInner 

310 self._numCellsInPatchBorder = config.numCellsInPatchBorder 

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

312 for val in config.cellInnerDimensions)) 

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

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

315 self._initialized = False 

316 

317 def getPatchInfo(self, index, tractWcs): 

318 # This should always be initialized 

319 if not self._initialized: 

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

321 if isinstance(index, Index2D): 

322 _index = index 

323 else: 

324 if isinstance(index, numbers.Number): 

325 _index = self.getPatchIndexPair(index) 

326 else: 

327 _index = Index2D(*index) 

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

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

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

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

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

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

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

335 raise RuntimeError( 

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

337 (_index, innerBBox, self._tractBBox)) 

338 outerBBox = geom.Box2I(innerBBox) 

339 outerBBox.grow(self.getPatchBorder()) 

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

341 return PatchInfo( 

342 index=_index, 

343 innerBBox=innerBBox, 

344 outerBBox=outerBBox, 

345 sequentialIndex=self.getSequentialPatchIndexFromPair(_index), 

346 tractWcs=tractWcs, 

347 cellInnerDimensions=self._cellInnerDimensions, 

348 cellBorder=self._cellBorder, 

349 numCellsPerPatchInner=self._numCellsPerPatchInner, 

350 numCellsInPatchBorder=self._numCellsInPatchBorder 

351 ) 

352 

353 def getPackedConfig(self, config): 

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

355 configPacked = struct.pack( 

356 "<iiiiidd3sd", 

357 subConfig.cellInnerDimensions[0], 

358 subConfig.cellInnerDimensions[1], 

359 subConfig.cellBorder, 

360 subConfig.numCellsPerPatchInner, 

361 subConfig.numCellsInPatchBorder, 

362 config.tractOverlap, 

363 config.pixelScale, 

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

365 config.rotation 

366 ) 

367 

368 return configPacked 

369 

370 

371tractBuilderRegistry = pexConfig.makeRegistry( 

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

373) 

374 

375tractBuilderRegistry.register("legacy", LegacyTractBuilder) 

376tractBuilderRegistry.register("cells", CellTractBuilder)