1 from __future__
import division
2 from builtins
import range
3 from builtins
import object
25 import lsst.pex.exceptions
26 import lsst.afw.coord
as afwCoord
27 import lsst.afw.geom
as afwGeom
28 import lsst.afw.image
as afwImage
29 from .patchInfo
import PatchInfo
31 __all__ = [
"TractInfo"]
35 """Information about a tract in a SkyMap sky pixelization
37 The tract is subdivided into rectangular patches. Each patch has the following properties:
38 - An inner region defined by an inner bounding. The inner regions of the patches exactly tile the tract,
39 and all inner regions have the same dimensions. The tract is made larger as required to make this work.
40 - An outer region defined by an outer bounding box. The outer region extends beyond the inner region
41 by patchBorder pixels in all directions, except there is no border at the edges of the tract.
42 Thus patches overlap each other but never extend off the tract. If you do not want any overlap
43 between adjacent patches then set patchBorder to 0.
44 - An index that consists of a pair of integers:
45 0 <= x index < numPatches[0]
46 0 <= y index < numPatches[1]
47 Patch 0,0 is at the minimum corner of the tract bounding box.
50 def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs):
51 """Construct a TractInfo
53 @param[in] id: tract ID
54 @param[in] patchInnerDimensions: dimensions of inner region of patches (x,y pixels)
55 @param[in] patchBorder: overlap between adjacent patches (in pixels, one int)
56 @param[in] ctrCoord: sky coordinate of center of inner region of tract, as an afwCoord.Coord;
57 also used as the CRVAL for the WCS.
58 @param[in] vertexCoordList: list of sky coordinates (afwCoord.Coord)
59 of vertices that define the boundaries of the inner region
60 @param[in] tractOverlap: minimum overlap between adjacent sky tracts; an afwGeom.Angle;
61 this defines the minimum distance the tract extends beyond the inner region in all directions
62 @param[in,out] wcs: an afwImage.Wcs; the reference pixel will be shifted as required
63 so that the lower left-hand pixel (index 0,0) has pixel position 0.0, 0.0
66 - It is not enforced that ctrCoord is the center of vertexCoordList, but SkyMap relies on it
67 - vertexCoordList will likely become a geom SphericalConvexPolygon someday.
71 assert len(patchInnerDimensions) == 2
74 raise TypeError(
"patchInnerDimensions=%s; must be two ints" % (patchInnerDimensions,))
84 def _minimumBoundingBox(self, wcs):
85 """Calculate the minimum bounding box for the tract, given the WCS
87 The bounding box is created in the frame of the supplied WCS,
88 so that it's OK if the coordinates are negative.
90 We compute the bounding box that holds all the vertices and the
93 minBBoxD = afwGeom.Box2D()
96 vertexDeg = vertexCoord.getPosition(afwGeom.degrees)
98 minBBoxD.include(wcs.skyToPixel(vertexCoord))
101 angleIncr = afwGeom.Angle(360.0, afwGeom.degrees) / float(numAngles)
102 for i
in range(numAngles):
103 offAngle = angleIncr * i
104 offCoord = vertexCoord.clone()
105 offCoord.offset(offAngle, halfOverlap)
106 pixPos = wcs.skyToPixel(offCoord)
107 minBBoxD.include(pixPos)
110 def _setupPatches(self, minBBox, wcs):
111 """Setup for patches of a particular size.
113 We grow the bounding box to hold an exact multiple of
114 the desired size (patchInnerDimensions), while keeping
115 the center roughly the same. We return the final
116 bounding box, and the number of patches in each dimension
119 @param minBBox Minimum bounding box for tract
120 @param wcs Wcs object
121 @return final bounding box, number of patches
123 bbox = afwGeom.Box2I(minBBox)
124 bboxMin = bbox.getMin()
125 bboxDim = bbox.getDimensions()
126 numPatches = afwGeom.Extent2I(0, 0)
128 num = (bboxDim[i] + innerDim - 1) // innerDim
129 deltaDim = (innerDim * num) - bboxDim[i]
131 bboxDim[i] = innerDim * num
132 bboxMin[i] -= deltaDim // 2
134 bbox = afwGeom.Box2I(bboxMin, bboxDim)
135 return bbox, numPatches
137 def _finalOrientation(self, bbox, wcs):
138 """Determine the final orientation
140 We offset everything so the lower-left corner is at 0,0
141 and compute the final Wcs.
143 @param bbox Current bounding box
144 @param wcs Current Wcs
145 @return revised bounding box, revised Wcs
147 finalBBox = afwGeom.Box2I(afwGeom.Point2I(0, 0), bbox.getDimensions())
150 pixPosOffset = afwGeom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
151 finalBBox.getMinY() - bbox.getMinY())
152 wcs.shiftReferencePixel(pixPosOffset)
153 return finalBBox, wcs
156 """Find the patch containing the specified coord
158 @param[in] coord: sky coordinate (afwCoord.Coord)
159 @return PatchInfo of patch whose inner bbox contains the specified coord
161 @raise LookupError if coord is not in tract or we cannot determine the
162 pixel coordinate (which likely means the coord is off the tract).
164 @note This routine will be more efficient if coord is ICRS.
166 icrsCoord = coord.toIcrs()
168 pixel = self.
getWcs().skyToPixel(icrsCoord)
169 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
171 raise LookupError(
"Unable to determine pixel position for coordinate %s" % (coord,))
172 pixelInd = afwGeom.Point2I(pixel)
174 raise LookupError(
"coord %s is not in tract %s" % (coord, self.
getId()))
179 """Find patches containing the specified list of coords
181 @param[in] coordList: list of sky coordinates (afwCoord.Coord)
182 @return list of PatchInfo for patches that contain, or may contain, the specified region.
183 The list will be empty if there is no overlap.
186 * This may give incorrect answers on regions that are larger than a tract
187 * This uses a naive algorithm that may find some patches that do not overlap the region
188 (especially if the region is not a rectangle aligned along patch x,y).
190 box2D = afwGeom.Box2D()
191 for coord
in coordList:
192 icrsCoord = coord.toIcrs()
194 pixelPos = self.
getWcs().skyToPixel(icrsCoord)
195 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
198 box2D.include(pixelPos)
199 bbox = afwGeom.Box2I(box2D)
208 for xInd
in range(llPatchInd[0], urPatchInd[0]+1)
209 for yInd
in range(llPatchInd[1], urPatchInd[1]+1))
212 """Get bounding box of tract (as an afwGeom.Box2I)
214 return afwGeom.Box2I(self._bbox)
217 """Get sky coordinate of center of tract (as an afwCoord.Coord)
227 """Get the number of patches in x, y
229 @return the number of patches in x, y
236 @return patch border (pixels)
241 """Return information for the specified patch
243 @param[in] index: index of patch, as a pair of ints
244 @return patch info, an instance of PatchInfo
246 @raise IndexError if index is out of range
250 raise IndexError(
"Patch index %s is not in range [0-%d, 0-%d]" %
254 if not self._bbox.contains(innerBBox):
256 "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
257 (index, innerBBox, self._bbox))
258 outerBBox = afwGeom.Box2I(innerBBox)
260 outerBBox.clip(self._bbox)
268 """Get dimensions of inner region of the patches (all are the same)
270 @return dimensions of inner region of the patches (as an afwGeom Extent2I)
275 """Get minimum overlap of adjacent sky tracts
277 @return minimum overlap between adjacent sky tracts, as an afwGeom Angle
282 """Get list of sky coordinates of vertices that define the boundary of the inner region
284 @warning: this is not a deep copy
285 @warning vertexCoordList will likely become a geom SphericalConvexPolygon someday.
292 @warning: this is not a deep copy
297 return "TractInfo(id=%s)" % (self.
_id,)
300 return "TractInfo(id=%s, ctrCoord=%s)" % (self.
_id, self._ctrCoord.getVector())
304 for y
in range(yNum):
305 for x
in range(xNum):
316 """Does this tract contain the coordinate?"""
317 icrsCoord = coord.toIcrs()
319 pixels = self.
getWcs().skyToPixel(icrsCoord)
320 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
327 """Information for a tract specified explicitly
329 A tract is placed at the explicitly defined coordinates, with the nominated
330 radius. The tracts are square (i.e., the radius is really a half-size).
333 def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
337 super(ExplicitTractInfo, self).
__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
338 vertexList, tractOverlap, wcs)
342 def _minimumBoundingBox(self, wcs):
343 """The minimum bounding box is calculated using the nominated radius"""
344 bbox = afwGeom.Box2D()
346 coord = self._ctrCoord.clone()
348 pixPos = wcs.skyToPixel(coord)
def getPatchInnerDimensions