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

141 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-06 04:15 -0700

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010 LSST Corporation. 

4# 

5# This product includes software developed by the 

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

7# 

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

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

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

11# (at your option) any later version. 

12# 

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

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

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

16# GNU General Public License for more details. 

17# 

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

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

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

21# 

22 

23__all__ = ["TractInfo"] 

24 

25import numpy as np 

26 

27import lsst.pex.exceptions 

28import lsst.geom as geom 

29from lsst.sphgeom import ConvexPolygon 

30 

31from .detail import makeSkyPolygonFromBBox, Index2D 

32 

33 

34class TractInfo: 

35 """Information about a tract in a SkyMap sky pixelization 

36 

37 Parameters 

38 ---------- 

39 id : `int` 

40 tract ID 

41 tractBuilder : Subclass of `lsst.skymap.BaseTractBuilder` 

42 Object used to compute patch geometry. 

43 ctrCoord : `lsst.geom.SpherePoint` 

44 ICRS sky coordinate of center of inner region of tract; also used as 

45 the CRVAL for the WCS. 

46 vertexCoordList : `list` of `lsst.geom.SpherePoint` 

47 Vertices that define the boundaries of the inner region. 

48 tractOverlap : `lsst.geom.Angle` 

49 Minimum overlap between adjacent sky tracts; this defines the minimum 

50 distance the tract extends beyond the inner region in all directions. 

51 wcs : `lsst.afw.image.SkyWcs` 

52 WCS for tract. The reference pixel will be shifted as required so that 

53 the lower left-hand pixel (index 0,0) has pixel position 0.0, 0.0. 

54 

55 Notes 

56 ----- 

57 The tract is subdivided into rectangular patches. Each patch has the 

58 following properties: 

59 

60 - An inner region defined by an inner bounding box. The inner regions of 

61 the patches exactly tile the tract, and all inner regions have the same 

62 dimensions. The tract is made larger as required to make this work. 

63 

64 - An outer region defined by an outer bounding box. The outer region 

65 extends beyond the inner region by patchBorder pixels in all directions, 

66 except there is no border at the edges of the tract. 

67 Thus patches overlap each other but never extend off the tract. 

68 If you do not want any overlap between adjacent patches then set 

69 patchBorder to 0. 

70 

71 - An index that consists of a pair of integers: 

72 

73 * 0 <= x index < numPatches[0] 

74 

75 * 0 <= y index < numPatches[1] 

76 

77 Patch 0,0 is at the minimum corner of the tract bounding box. 

78 

79 - It is not enforced that ctrCoord is the center of vertexCoordList, but 

80 SkyMap relies on it. 

81 """ 

82 def __init__(self, id, tractBuilder, ctrCoord, vertexCoordList, tractOverlap, wcs): 

83 self._id = id 

84 self._ctrCoord = ctrCoord 

85 self._vertexCoordList = tuple(vertexCoordList) 

86 self._tractOverlap = tractOverlap 

87 self._tractBuilder = tractBuilder 

88 

89 minBBox = self._minimumBoundingBox(wcs) 

90 initialBBox, self._numPatches = self._tractBuilder.setupPatches(minBBox, wcs) 

91 self._bbox, self._wcs = self._finalOrientation(initialBBox, wcs) 

92 

93 def _minimumBoundingBox(self, wcs): 

94 """Calculate the minimum bounding box for the tract, given the WCS. 

95 

96 The bounding box is created in the frame of the supplied WCS, 

97 so that it's OK if the coordinates are negative. 

98 

99 We compute the bounding box that holds all the vertices and the 

100 desired overlap. 

101 """ 

102 minBBoxD = geom.Box2D() 

103 halfOverlap = self._tractOverlap / 2.0 

104 for vertexCoord in self._vertexCoordList: 

105 if self._tractOverlap == 0: 

106 minBBoxD.include(wcs.skyToPixel(vertexCoord)) 

107 else: 

108 numAngles = 24 

109 angleIncr = geom.Angle(360.0, geom.degrees) / float(numAngles) 

110 for i in range(numAngles): 

111 offAngle = angleIncr * i 

112 offCoord = vertexCoord.offset(offAngle, halfOverlap) 

113 pixPos = wcs.skyToPixel(offCoord) 

114 minBBoxD.include(pixPos) 

115 return minBBoxD 

116 

117 def _finalOrientation(self, bbox, wcs): 

118 """Determine the final orientation 

119 

120 We offset everything so the lower-left corner is at 0,0 

121 and compute the final Wcs. 

122 

123 Parameters 

124 ---------- 

125 bbox : `lsst.geom.Box2I` 

126 Current bounding box. 

127 wcs : `lsst.afw.geom.SkyWcs 

128 Current Wcs. 

129 

130 Returns 

131 ------- 

132 finalBBox : `lsst.geom.Box2I` 

133 Revised bounding box. 

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

135 Revised Wcs. 

136 """ 

137 finalBBox = geom.Box2I(geom.Point2I(0, 0), bbox.getDimensions()) 

138 # shift the WCS by the same amount as the bbox; extra code is required 

139 # because simply subtracting makes an Extent2I 

140 pixPosOffset = geom.Extent2D(finalBBox.getMinX() - bbox.getMinX(), 

141 finalBBox.getMinY() - bbox.getMinY()) 

142 wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset) 

143 return finalBBox, wcs 

144 

145 def getSequentialPatchIndex(self, patchInfo): 

146 """Return a single integer that uniquely identifies 

147 the given patch within this tract. 

148 

149 Parameters 

150 ---------- 

151 patchInfo : `lsst.skymap.PatchInfo` 

152 

153 Returns 

154 ------- 

155 sequentialIndex : `int` 

156 """ 

157 return self._tractBuilder.getSequentialPatchIndex(patchInfo) 

158 

159 def getSequentialPatchIndexFromPair(self, index): 

160 """Return a single integer that uniquely identifies 

161 the patch index within the tract. 

162 

163 Parameters 

164 ---------- 

165 index : `lsst.skymap.Index2D` 

166 

167 Returns 

168 ------- 

169 sequentialIndex : `int` 

170 """ 

171 return self._tractBuilder.getSequentialPatchIndexFromPair(index) 

172 

173 def getPatchIndexPair(self, sequentialIndex): 

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

175 

176 Parameters 

177 ---------- 

178 sequentialIndex : `int` 

179 

180 Returns 

181 ------- 

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

183 """ 

184 return self._tractBuilder.getPatchIndexPair(sequentialIndex) 

185 

186 def findPatch(self, coord): 

187 """Find the patch containing the specified coord. 

188 

189 Parameters 

190 ---------- 

191 coord : `lsst.geom.SpherePoint` 

192 ICRS sky coordinate to search for. 

193 

194 Returns 

195 ------- 

196 result : `lsst.skymap.PatchInfo` 

197 PatchInfo of patch whose inner bbox contains the specified coord 

198 

199 Raises 

200 ------ 

201 LookupError 

202 Raised if coord is not in tract or we cannot determine the 

203 pixel coordinate (which likely means the coord is off the tract). 

204 """ 

205 try: 

206 pixel = self.wcs.skyToPixel(coord) 

207 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError): 

208 # Point must be way off the tract 

209 raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,)) 

210 pixelInd = geom.Point2I(pixel) 

211 if not self._bbox.contains(pixelInd): 

212 raise LookupError("coord %s is not in tract %s" % (coord, self.tract_id)) 

213 # This should probably be the same as above because we only 

214 # care about the INNER dimensions. 

215 patchInd = tuple(int(pixelInd[i]/self.patch_inner_dimensions[i]) for i in range(2)) 

216 return self.getPatchInfo(patchInd) 

217 

218 def findPatchList(self, coordList): 

219 """Find patches containing the specified list of coords. 

220 

221 Parameters 

222 ---------- 

223 coordList : `list` of `lsst.geom.SpherePoint` 

224 ICRS sky coordinates to search for. 

225 

226 Returns 

227 ------- 

228 result : `list` of `lsst.skymap.PatchInfo` 

229 List of PatchInfo for patches that contain, or may contain, the 

230 specified region. The list will be empty if there is no overlap. 

231 

232 Notes 

233 ----- 

234 **Warning:** 

235 

236 - This may give incorrect answers on regions that are larger than a 

237 tract. 

238 

239 - This uses a naive algorithm that may find some patches that do not 

240 overlap the region (especially if the region is not a rectangle 

241 aligned along patch x,y). 

242 """ 

243 box2D = geom.Box2D() 

244 for coord in coordList: 

245 try: 

246 pixelPos = self.wcs.skyToPixel(coord) 

247 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError): 

248 # The point is so far off the tract that its pixel position 

249 # cannot be computed. 

250 continue 

251 box2D.include(pixelPos) 

252 bbox = geom.Box2I(box2D) 

253 bbox.grow(self.getPatchBorder()) 

254 bbox.clip(self._bbox) 

255 if bbox.isEmpty(): 

256 return () 

257 

258 llPatchInd = tuple(int(bbox.getMin()[i]/self.patch_inner_dimensions[i]) for i in range(2)) 

259 urPatchInd = tuple(int(bbox.getMax()[i]/self.patch_inner_dimensions[i]) for i in range(2)) 

260 return tuple(self.getPatchInfo((xInd, yInd)) 

261 for xInd in range(llPatchInd[0], urPatchInd[0]+1) 

262 for yInd in range(llPatchInd[1], urPatchInd[1]+1)) 

263 

264 def getBBox(self): 

265 """Get bounding box of tract (as an geom.Box2I) 

266 """ 

267 return geom.Box2I(self._bbox) 

268 

269 bbox = property(getBBox) 

270 

271 def getCtrCoord(self): 

272 """Get ICRS sky coordinate of center of tract 

273 (as an lsst.geom.SpherePoint) 

274 """ 

275 return self._ctrCoord 

276 

277 ctr_coord = property(getCtrCoord) 

278 

279 def getId(self): 

280 """Get ID of tract 

281 """ 

282 return self._id 

283 

284 tract_id = property(getId) 

285 

286 def getNumPatches(self): 

287 """Get the number of patches in x, y. 

288 

289 Returns 

290 ------- 

291 result : `lsst.skymap.Index2D` 

292 The number of patches in x, y 

293 """ 

294 return self._numPatches 

295 

296 num_patches = property(getNumPatches) 

297 

298 def getPatchBorder(self): 

299 return self._tractBuilder.getPatchBorder() 

300 

301 patch_border = property(getPatchBorder) 

302 

303 def getPatchInfo(self, index): 

304 """Return information for the specified patch. 

305 

306 Parameters 

307 ---------- 

308 index : `typing.NamedTuple` ['x': `int`, 'y': `int`] 

309 Index of patch, as a pair of ints; 

310 or a sequential index as returned by getSequentialPatchIndex; 

311 negative values are not supported. 

312 

313 Returns 

314 ------- 

315 result : `lsst.skymap.PatchInfo` 

316 The patch info for that index. 

317 

318 Raises 

319 ------ 

320 IndexError 

321 Raised if index is out of range. 

322 """ 

323 return self._tractBuilder.getPatchInfo(index, self._wcs) 

324 

325 def getPatchInnerDimensions(self): 

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

327 """ 

328 return self._tractBuilder.getPatchInnerDimensions() 

329 

330 patch_inner_dimensions = property(getPatchInnerDimensions) 

331 

332 def getTractOverlap(self): 

333 """Get minimum overlap of adjacent sky tracts. 

334 """ 

335 return self._tractOverlap 

336 

337 tract_overlap = property(getTractOverlap) 

338 

339 def getVertexList(self): 

340 """Get list of ICRS sky coordinates of vertices that define the 

341 boundary of the inner region. 

342 

343 Notes 

344 ----- 

345 **warning:** this is not a deep copy. 

346 """ 

347 return self._vertexCoordList 

348 

349 vertex_list = property(getVertexList) 

350 

351 def getInnerSkyPolygon(self): 

352 """Get inner on-sky region as a sphgeom.ConvexPolygon. 

353 """ 

354 skyUnitVectors = [sp.getVector() for sp in self.getVertexList()] 

355 return ConvexPolygon.convexHull(skyUnitVectors) 

356 

357 inner_sky_polygon = property(getInnerSkyPolygon) 

358 

359 def getOuterSkyPolygon(self): 

360 """Get outer on-sky region as a sphgeom.ConvexPolygon 

361 """ 

362 return makeSkyPolygonFromBBox(bbox=self.getBBox(), wcs=self.getWcs()) 

363 

364 outer_sky_polygon = property(getOuterSkyPolygon) 

365 

366 def getWcs(self): 

367 """Get WCS of tract. 

368 

369 Returns 

370 ------- 

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

372 The WCS of this tract 

373 """ 

374 return self._wcs 

375 

376 wcs = property(getWcs) 

377 

378 def __str__(self): 

379 return "TractInfo(id=%s)" % (self._id,) 

380 

381 def __repr__(self): 

382 return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector()) 

383 

384 def __iter__(self): 

385 xNum, yNum = self.getNumPatches() 

386 for y in range(yNum): 

387 for x in range(xNum): 

388 yield self.getPatchInfo(Index2D(x=x, y=y)) 

389 

390 def __len__(self): 

391 xNum, yNum = self.getNumPatches() 

392 return xNum*yNum 

393 

394 def __getitem__(self, index): 

395 return self.getPatchInfo(index) 

396 

397 def contains(self, coord): 

398 """Does this tract contain the coordinate?""" 

399 try: 

400 pixel = self.getWcs().skyToPixel(coord) 

401 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError): 

402 # Point must be way off the tract 

403 return False 

404 if not np.isfinite(pixel.getX()) or not np.isfinite(pixel.getY()): 

405 # Point is definitely off the tract 

406 return False 

407 return self.getBBox().contains(geom.Point2I(pixel)) 

408 

409 

410class ExplicitTractInfo(TractInfo): 

411 """Information for a tract specified explicitly. 

412 

413 A tract is placed at the explicitly defined coordinates, with the nominated 

414 radius. The tracts are square (i.e., the radius is really a half-size). 

415 """ 

416 def __init__(self, id, tractBuilder, ctrCoord, radius, tractOverlap, wcs): 

417 # We don't want TractInfo setting the bbox on the basis of vertices, 

418 # but on the radius. 

419 vertexList = [] 

420 self._radius = radius 

421 super(ExplicitTractInfo, self).__init__(id, tractBuilder, ctrCoord, 

422 vertexList, tractOverlap, wcs) 

423 # Shrink the box slightly to make sure the vertices are in the tract 

424 bboxD = geom.BoxD(self.getBBox()) 

425 bboxD.grow(-0.001) 

426 finalWcs = self.getWcs() 

427 self._vertexCoordList = finalWcs.pixelToSky(bboxD.getCorners()) 

428 

429 def _minimumBoundingBox(self, wcs): 

430 """Calculate the minimum bounding box for the tract, given the WCS, and 

431 the nominated radius. 

432 """ 

433 bbox = geom.Box2D() 

434 for i in range(4): 

435 cornerCoord = self._ctrCoord.offset(i*90*geom.degrees, self._radius + self._tractOverlap) 

436 pixPos = wcs.skyToPixel(cornerCoord) 

437 bbox.include(pixPos) 

438 return bbox