lsst.skymap  15.0-5-gb31927c
tractInfo.py
Go to the documentation of this file.
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 #
23 import lsst.afw.geom as afwGeom
24 from lsst.sphgeom import ConvexPolygon, UnitVector3d
25 
26 from .patchInfo import PatchInfo
27 
28 __all__ = ["TractInfo"]
29 
30 
31 class TractInfo:
32  """Information about a tract in a SkyMap sky pixelization
33 
34  The tract is subdivided into rectangular patches. Each patch has the following properties:
35  - An inner region defined by an inner bounding. The inner regions of the patches exactly tile the tract,
36  and all inner regions have the same dimensions. The tract is made larger as required to make this work.
37  - An outer region defined by an outer bounding box. The outer region extends beyond the inner region
38  by patchBorder pixels in all directions, except there is no border at the edges of the tract.
39  Thus patches overlap each other but never extend off the tract. If you do not want any overlap
40  between adjacent patches then set patchBorder to 0.
41  - An index that consists of a pair of integers:
42  0 <= x index < numPatches[0]
43  0 <= y index < numPatches[1]
44  Patch 0,0 is at the minimum corner of the tract bounding box.
45  """
46 
47  def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs):
48  """Construct a TractInfo
49 
50  @param[in] id: tract ID
51  @param[in] patchInnerDimensions: dimensions of inner region of patches (x,y pixels)
52  @param[in] patchBorder: overlap between adjacent patches (in pixels, one int)
53  @param[in] ctrCoord: ICRS sky coordinate of center of inner region of tract
54  as an lsst.afw.geom.SpherePoint; also used as the CRVAL for the WCS.
55  @param[in] vertexCoordList: list of ICRS sky coordinates (lsst.afw.geom.SpherePoint)
56  of vertices that define the boundaries of the inner region
57  @param[in] tractOverlap: minimum overlap between adjacent sky tracts; an afwGeom.Angle;
58  this defines the minimum distance the tract extends beyond the inner region in all directions
59  @param[in,out] wcs: an afwImage.Wcs; the reference pixel will be shifted as required
60  so that the lower left-hand pixel (index 0,0) has pixel position 0.0, 0.0
61 
62  @warning
63  - It is not enforced that ctrCoord is the center of vertexCoordList, but SkyMap relies on it
64  - vertexCoordList will likely become a geom SphericalConvexPolygon someday.
65  """
66  self._id = id
67  try:
68  assert len(patchInnerDimensions) == 2
69  self._patchInnerDimensions = afwGeom.Extent2I(*(int(val) for val in patchInnerDimensions))
70  except Exception:
71  raise TypeError("patchInnerDimensions=%s; must be two ints" % (patchInnerDimensions,))
72  self._patchBorder = int(patchBorder)
73  self._ctrCoord = ctrCoord
74  self._vertexCoordList = tuple(vertexCoordList)
75  self._tractOverlap = tractOverlap
76 
77  minBBox = self._minimumBoundingBox(wcs)
78  initialBBox, self._numPatches = self._setupPatches(minBBox, wcs)
79  self._bbox, self._wcs = self._finalOrientation(initialBBox, wcs)
80 
81  def _minimumBoundingBox(self, wcs):
82  """Calculate the minimum bounding box for the tract, given the WCS
83 
84  The bounding box is created in the frame of the supplied WCS,
85  so that it's OK if the coordinates are negative.
86 
87  We compute the bounding box that holds all the vertices and the
88  desired overlap.
89  """
90  minBBoxD = afwGeom.Box2D()
91  halfOverlap = self._tractOverlap / 2.0
92  for vertexCoord in self._vertexCoordList:
93  if self._tractOverlap == 0:
94  minBBoxD.include(wcs.skyToPixel(vertexCoord))
95  else:
96  numAngles = 24
97  angleIncr = afwGeom.Angle(360.0, afwGeom.degrees) / float(numAngles)
98  for i in range(numAngles):
99  offAngle = angleIncr * i
100  offCoord = vertexCoord.offset(offAngle, halfOverlap)
101  pixPos = wcs.skyToPixel(offCoord)
102  minBBoxD.include(pixPos)
103  return minBBoxD
104 
105  def _setupPatches(self, minBBox, wcs):
106  """Setup for patches of a particular size.
107 
108  We grow the bounding box to hold an exact multiple of
109  the desired size (patchInnerDimensions), while keeping
110  the center roughly the same. We return the final
111  bounding box, and the number of patches in each dimension
112  (as an Extent2I).
113 
114  @param minBBox Minimum bounding box for tract
115  @param wcs Wcs object
116  @return final bounding box, number of patches
117  """
118  bbox = afwGeom.Box2I(minBBox)
119  bboxMin = bbox.getMin()
120  bboxDim = bbox.getDimensions()
121  numPatches = afwGeom.Extent2I(0, 0)
122  for i, innerDim in enumerate(self._patchInnerDimensions):
123  num = (bboxDim[i] + innerDim - 1) // innerDim # round up
124  deltaDim = (innerDim * num) - bboxDim[i]
125  if deltaDim > 0:
126  bboxDim[i] = innerDim * num
127  bboxMin[i] -= deltaDim // 2
128  numPatches[i] = num
129  bbox = afwGeom.Box2I(bboxMin, bboxDim)
130  return bbox, numPatches
131 
132  def _finalOrientation(self, bbox, wcs):
133  """Determine the final orientation
134 
135  We offset everything so the lower-left corner is at 0,0
136  and compute the final Wcs.
137 
138  @param bbox Current bounding box
139  @param wcs Current Wcs
140  @return revised bounding box, revised Wcs
141  """
142  finalBBox = afwGeom.Box2I(afwGeom.Point2I(0, 0), bbox.getDimensions())
143  # shift the WCS by the same amount as the bbox; extra code is required
144  # because simply subtracting makes an Extent2I
145  pixPosOffset = afwGeom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
146  finalBBox.getMinY() - bbox.getMinY())
147  wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset)
148  return finalBBox, wcs
149 
150  def getSequentialPatchIndex(self, patchInfo):
151  """Return a single integer that uniquely identifies the given patch
152  within this tract.
153  """
154  x, y = patchInfo.getIndex()
155  nx, ny = self.getNumPatches()
156  return nx*y + x
157 
158  def findPatch(self, coord):
159  """Find the patch containing the specified coord
160 
161  @param[in] coord: ICRS sky coordinate (lsst.afw.geom.SpherePoint)
162  @return PatchInfo of patch whose inner bbox contains the specified coord
163 
164  @raise LookupError if coord is not in tract or we cannot determine the
165  pixel coordinate (which likely means the coord is off the tract).
166 
167  @note This routine will be more efficient if coord is ICRS.
168  """
169  try:
170  pixel = self.getWcs().skyToPixel(coord)
172  # Point must be way off the tract
173  raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
174  pixelInd = afwGeom.Point2I(pixel)
175  if not self.getBBox().contains(pixelInd):
176  raise LookupError("coord %s is not in tract %s" % (coord, self.getId()))
177  patchInd = tuple(int(pixelInd[i]/self._patchInnerDimensions[i]) for i in range(2))
178  return self.getPatchInfo(patchInd)
179 
180  def findPatchList(self, coordList):
181  """Find patches containing the specified list of coords
182 
183  @param[in] coordList: list of sky coordinates (lsst.afw.geom.SpherePoint)
184  @return list of PatchInfo for patches that contain, or may contain, the specified region.
185  The list will be empty if there is no overlap.
186 
187  @warning:
188  * This may give incorrect answers on regions that are larger than a tract
189  * This uses a naive algorithm that may find some patches that do not overlap the region
190  (especially if the region is not a rectangle aligned along patch x,y).
191  """
192  box2D = afwGeom.Box2D()
193  for coord in coordList:
194  try:
195  pixelPos = self.getWcs().skyToPixel(coord)
197  # the point is so far off the tract that its pixel position cannot be computed
198  continue
199  box2D.include(pixelPos)
200  bbox = afwGeom.Box2I(box2D)
201  bbox.grow(self.getPatchBorder())
202  bbox.clip(self.getBBox())
203  if bbox.isEmpty():
204  return ()
205 
206  llPatchInd = tuple(int(bbox.getMin()[i]/self._patchInnerDimensions[i]) for i in range(2))
207  urPatchInd = tuple(int(bbox.getMax()[i]/self._patchInnerDimensions[i]) for i in range(2))
208  return tuple(self.getPatchInfo((xInd, yInd))
209  for xInd in range(llPatchInd[0], urPatchInd[0]+1)
210  for yInd in range(llPatchInd[1], urPatchInd[1]+1))
211 
212  def getBBox(self):
213  """Get bounding box of tract (as an afwGeom.Box2I)
214  """
215  return afwGeom.Box2I(self._bbox)
216 
217  def getCtrCoord(self):
218  """Get ICRS sky coordinate of center of tract (as an lsst.afw.geom.SpherePoint)
219  """
220  return self._ctrCoord
221 
222  def getId(self):
223  """Get ID of tract
224  """
225  return self._id
226 
227  def getNumPatches(self):
228  """Get the number of patches in x, y
229 
230  @return the number of patches in x, y
231  """
232  return self._numPatches
233 
234  def getPatchBorder(self):
235  """Get batch border
236 
237  @return patch border (pixels)
238  """
239  return self._patchBorder
240 
241  def getPatchInfo(self, index):
242  """Return information for the specified patch
243 
244  @param[in] index: index of patch, as a pair of ints
245  @return patch info, an instance of PatchInfo
246 
247  @raise IndexError if index is out of range
248  """
249  if (not 0 <= index[0] < self._numPatches[0]) \
250  or (not 0 <= index[1] < self._numPatches[1]):
251  raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
252  (index, self._numPatches[0]-1, self._numPatches[1]-1))
253  innerMin = afwGeom.Point2I(*[index[i] * self._patchInnerDimensions[i] for i in range(2)])
254  innerBBox = afwGeom.Box2I(innerMin, self._patchInnerDimensions)
255  if not self._bbox.contains(innerBBox):
256  raise RuntimeError(
257  "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
258  (index, innerBBox, self._bbox))
259  outerBBox = afwGeom.Box2I(innerBBox)
260  outerBBox.grow(self.getPatchBorder())
261  outerBBox.clip(self._bbox)
262  return PatchInfo(
263  index=index,
264  innerBBox=innerBBox,
265  outerBBox=outerBBox,
266  )
267 
269  """Get dimensions of inner region of the patches (all are the same)
270 
271  @return dimensions of inner region of the patches (as an afwGeom Extent2I)
272  """
273  return self._patchInnerDimensions
274 
275  def getTractOverlap(self):
276  """Get minimum overlap of adjacent sky tracts
277 
278  @return minimum overlap between adjacent sky tracts, as an afwGeom Angle
279  """
280  return self._tractOverlap
281 
282  def getVertexList(self):
283  """Get list of sky coordinates of vertices that define the boundary of the inner region
284 
285  @warning: this is not a deep copy
286  @warning vertexCoordList will likely become a geom SphericalConvexPolygon someday.
287  """
288  return self._vertexCoordList
289 
290  def getPolygon(self):
291  """Return the tract region as a sphgeom.ConvexPolygon.
292  """
293  points = [UnitVector3d(*sp.getVector()) for sp in self.getVertexList()]
294  return ConvexPolygon(points)
295 
296  def getWcs(self):
297  """Get WCS of tract
298 
299  @warning: this is not a deep copy
300  """
301  return self._wcs
302 
303  def __str__(self):
304  return "TractInfo(id=%s)" % (self._id,)
305 
306  def __repr__(self):
307  return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
308 
309  def __iter__(self):
310  xNum, yNum = self.getNumPatches()
311  for y in range(yNum):
312  for x in range(xNum):
313  yield self.getPatchInfo((x, y))
314 
315  def __len__(self):
316  xNum, yNum = self.getNumPatches()
317  return xNum*yNum
318 
319  def __getitem__(self, index):
320  return self.getPatchInfo(index)
321 
322  def contains(self, coord):
323  """Does this tract contain the coordinate?"""
324  try:
325  pixels = self.getWcs().skyToPixel(coord)
327  # Point must be way off the tract
328  return False
329  return self.getBBox().contains(afwGeom.Point2I(pixels))
330 
331 
333  """Information for a tract specified explicitly
334 
335  A tract is placed at the explicitly defined coordinates, with the nominated
336  radius. The tracts are square (i.e., the radius is really a half-size).
337  """
338 
339  def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
340  # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
341  vertexList = []
342  self._radius = radius
343  super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
344  vertexList, tractOverlap, wcs)
345  # Shrink the box slightly to make sure the vertices are in the tract
346  bboxD = afwGeom.BoxD(self.getBBox())
347  bboxD.grow(-0.001)
348  finalWcs = self.getWcs()
349  self._vertexCoordList = finalWcs.pixelToSky(bboxD.getCorners())
350 
351  def _minimumBoundingBox(self, wcs):
352  """The minimum bounding box is calculated using the nominated radius"""
353  bbox = afwGeom.Box2D()
354  for i in range(4):
355  cornerCoord = self._ctrCoord.offset(i*90*afwGeom.degrees, self._radius + self._tractOverlap)
356  pixPos = wcs.skyToPixel(cornerCoord)
357  bbox.include(pixPos)
358  return bbox
def _finalOrientation(self, bbox, wcs)
Definition: tractInfo.py:132
def _setupPatches(self, minBBox, wcs)
Definition: tractInfo.py:105
def getPatchInfo(self, index)
Definition: tractInfo.py:241
def __getitem__(self, index)
Definition: tractInfo.py:319
def _minimumBoundingBox(self, wcs)
Definition: tractInfo.py:81
def getSequentialPatchIndex(self, patchInfo)
Definition: tractInfo.py:150
def findPatch(self, coord)
Definition: tractInfo.py:158
def findPatchList(self, coordList)
Definition: tractInfo.py:180
def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs)
Definition: tractInfo.py:339
def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs)
Definition: tractInfo.py:47