28 from .patchInfo
import PatchInfo, makeSkyPolygonFromBBox
30 __all__ = [
"TractInfo"]
34 """Information about a tract in a SkyMap sky pixelization 36 The tract is subdivided into rectangular patches. Each patch has the following properties: 37 - An inner region defined by an inner bounding. The inner regions of the patches exactly tile the tract, 38 and all inner regions have the same dimensions. The tract is made larger as required to make this work. 39 - An outer region defined by an outer bounding box. The outer region extends beyond the inner region 40 by patchBorder pixels in all directions, except there is no border at the edges of the tract. 41 Thus patches overlap each other but never extend off the tract. If you do not want any overlap 42 between adjacent patches then set patchBorder to 0. 43 - An index that consists of a pair of integers: 44 0 <= x index < numPatches[0] 45 0 <= y index < numPatches[1] 46 Patch 0,0 is at the minimum corner of the tract bounding box. 49 def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs):
50 """Construct a TractInfo 52 @param[in] id: tract ID 53 @param[in] patchInnerDimensions: dimensions of inner region of patches (x,y pixels) 54 @param[in] patchBorder: overlap between adjacent patches (in pixels, one int) 55 @param[in] ctrCoord: ICRS sky coordinate of center of inner region of tract 56 as an lsst.afw.geom.SpherePoint; also used as the CRVAL for the WCS. 57 @param[in] vertexCoordList: list of ICRS sky coordinates (lsst.afw.geom.SpherePoint) 58 of vertices that define the boundaries of the inner region 59 @param[in] tractOverlap: minimum overlap between adjacent sky tracts; an afwGeom.Angle; 60 this defines the minimum distance the tract extends beyond the inner region in all directions 61 @param[in,out] wcs: an afwImage.Wcs; the reference pixel will be shifted as required 62 so that the lower left-hand pixel (index 0,0) has pixel position 0.0, 0.0 65 - It is not enforced that ctrCoord is the center of vertexCoordList, but SkyMap relies on it 69 assert len(patchInnerDimensions) == 2
72 raise TypeError(
"patchInnerDimensions=%s; must be two ints" % (patchInnerDimensions,))
82 def _minimumBoundingBox(self, wcs):
83 """Calculate the minimum bounding box for the tract, given the WCS 85 The bounding box is created in the frame of the supplied WCS, 86 so that it's OK if the coordinates are negative. 88 We compute the bounding box that holds all the vertices and the 91 minBBoxD = afwGeom.Box2D()
95 minBBoxD.include(wcs.skyToPixel(vertexCoord))
98 angleIncr = afwGeom.Angle(360.0, afwGeom.degrees) / float(numAngles)
99 for i
in range(numAngles):
100 offAngle = angleIncr * i
101 offCoord = vertexCoord.offset(offAngle, halfOverlap)
102 pixPos = wcs.skyToPixel(offCoord)
103 minBBoxD.include(pixPos)
106 def _setupPatches(self, minBBox, wcs):
107 """Setup for patches of a particular size. 109 We grow the bounding box to hold an exact multiple of 110 the desired size (patchInnerDimensions), while keeping 111 the center roughly the same. We return the final 112 bounding box, and the number of patches in each dimension 115 @param minBBox Minimum bounding box for tract 116 @param wcs Wcs object 117 @return final bounding box, number of patches 119 bbox = afwGeom.Box2I(minBBox)
120 bboxMin = bbox.getMin()
121 bboxDim = bbox.getDimensions()
122 numPatches = afwGeom.Extent2I(0, 0)
124 num = (bboxDim[i] + innerDim - 1) // innerDim
125 deltaDim = (innerDim * num) - bboxDim[i]
127 bboxDim[i] = innerDim * num
128 bboxMin[i] -= deltaDim // 2
130 bbox = afwGeom.Box2I(bboxMin, bboxDim)
131 return bbox, numPatches
133 def _finalOrientation(self, bbox, wcs):
134 """Determine the final orientation 136 We offset everything so the lower-left corner is at 0,0 137 and compute the final Wcs. 139 @param bbox Current bounding box 140 @param wcs Current Wcs 141 @return revised bounding box, revised Wcs 143 finalBBox = afwGeom.Box2I(afwGeom.Point2I(0, 0), bbox.getDimensions())
146 pixPosOffset = afwGeom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
147 finalBBox.getMinY() - bbox.getMinY())
148 wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset)
149 return finalBBox, wcs
152 """Return a single integer that uniquely identifies the given patch 155 x, y = patchInfo.getIndex()
161 x = sequentialIndex % nx
162 y = (sequentialIndex - x) / nx
166 """Find the patch containing the specified coord 168 @param[in] coord: ICRS sky coordinate (lsst.afw.geom.SpherePoint) 169 @return PatchInfo of patch whose inner bbox contains the specified coord 171 @raise LookupError if coord is not in tract or we cannot determine the 172 pixel coordinate (which likely means the coord is off the tract). 174 @note This routine will be more efficient if coord is ICRS. 177 pixel = self.
getWcs().skyToPixel(coord)
180 raise LookupError(
"Unable to determine pixel position for coordinate %s" % (coord,))
181 pixelInd = afwGeom.Point2I(pixel)
183 raise LookupError(
"coord %s is not in tract %s" % (coord, self.
getId()))
188 """Find patches containing the specified list of coords 190 @param[in] coordList: list of sky coordinates (lsst.afw.geom.SpherePoint) 191 @return list of PatchInfo for patches that contain, or may contain, the specified region. 192 The list will be empty if there is no overlap. 195 * This may give incorrect answers on regions that are larger than a tract 196 * This uses a naive algorithm that may find some patches that do not overlap the region 197 (especially if the region is not a rectangle aligned along patch x,y). 199 box2D = afwGeom.Box2D()
200 for coord
in coordList:
202 pixelPos = self.
getWcs().skyToPixel(coord)
206 box2D.include(pixelPos)
207 bbox = afwGeom.Box2I(box2D)
216 for xInd
in range(llPatchInd[0], urPatchInd[0]+1)
217 for yInd
in range(llPatchInd[1], urPatchInd[1]+1))
220 """Get bounding box of tract (as an afwGeom.Box2I) 222 return afwGeom.Box2I(self._bbox)
225 """Get ICRS sky coordinate of center of tract (as an lsst.afw.geom.SpherePoint) 235 """Get the number of patches in x, y 237 @return the number of patches in x, y 244 @return patch border (pixels) 249 """Return information for the specified patch 251 @param[in] index: index of patch, as a pair of ints, 252 or a sequential index as returned by getSequentialPatchIndex; 253 negative values are not supported. 254 @return patch info, an instance of PatchInfo 256 @raise IndexError if index is out of range 258 if isinstance(index, numbers.Number):
262 raise IndexError(
"Patch index %s is not in range [0-%d, 0-%d]" %
266 if not self._bbox.
contains(innerBBox):
268 "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
269 (index, innerBBox, self._bbox))
270 outerBBox = afwGeom.Box2I(innerBBox)
272 outerBBox.clip(self._bbox)
280 """Get dimensions of inner region of the patches (all are the same) 282 @return dimensions of inner region of the patches (as an afwGeom Extent2I) 287 """Get minimum overlap of adjacent sky tracts 289 @return minimum overlap between adjacent sky tracts, as an afwGeom Angle 294 """Get list of sky coordinates of vertices that define the boundary of the inner region 296 @warning: this is not a deep copy 301 """Get inner on-sky region as a sphgeom.ConvexPolygon. 303 skyUnitVectors = [sp.getVector()
for sp
in self.
getVertexList()]
304 return ConvexPolygon.convexHull(skyUnitVectors)
307 """Get outer on-sky region as a sphgeom.ConvexPolygon 314 @warning: this is not a deep copy 319 return "TractInfo(id=%s)" % (self.
_id,)
322 return "TractInfo(id=%s, ctrCoord=%s)" % (self.
_id, self.
_ctrCoord.getVector())
326 for y
in range(yNum):
327 for x
in range(xNum):
338 """Does this tract contain the coordinate?""" 340 pixels = self.
getWcs().skyToPixel(coord)
348 """Information for a tract specified explicitly 350 A tract is placed at the explicitly defined coordinates, with the nominated 351 radius. The tracts are square (i.e., the radius is really a half-size). 354 def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
358 super(ExplicitTractInfo, self).
__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
359 vertexList, tractOverlap, wcs)
361 bboxD = afwGeom.BoxD(self.
getBBox())
366 def _minimumBoundingBox(self, wcs):
367 """The minimum bounding box is calculated using the nominated radius""" 368 bbox = afwGeom.Box2D()
371 pixPos = wcs.skyToPixel(cornerCoord)
def getTractOverlap(self)
def _finalOrientation(self, bbox, wcs)
def _setupPatches(self, minBBox, wcs)
def makeSkyPolygonFromBBox(bbox, wcs)
def getInnerSkyPolygon(self)
def getPatchIndexPair(self, sequentialIndex)
def getPatchInfo(self, index)
def contains(self, coord)
def getPatchInnerDimensions(self)
def __getitem__(self, index)
def _minimumBoundingBox(self, wcs)
def getSequentialPatchIndex(self, patchInfo)
def findPatch(self, coord)
def findPatchList(self, coordList)
def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs)
def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs)
def getOuterSkyPolygon(self)