lsst.skymap  16.0-7-g00e1f5f
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 #
22 import numbers
23 
25 import lsst.afw.geom as afwGeom
26 from lsst.sphgeom import ConvexPolygon
27 
28 from .patchInfo import PatchInfo, makeSkyPolygonFromBBox
29 
30 __all__ = ["TractInfo"]
31 
32 
33 class TractInfo:
34  """Information about a tract in a SkyMap sky pixelization
35 
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.
47  """
48 
49  def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs):
50  """Construct a TractInfo
51 
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
63 
64  @warning
65  - It is not enforced that ctrCoord is the center of vertexCoordList, but SkyMap relies on it
66  """
67  self._id = id
68  try:
69  assert len(patchInnerDimensions) == 2
70  self._patchInnerDimensions = afwGeom.Extent2I(*(int(val) for val in patchInnerDimensions))
71  except Exception:
72  raise TypeError("patchInnerDimensions=%s; must be two ints" % (patchInnerDimensions,))
73  self._patchBorder = int(patchBorder)
74  self._ctrCoord = ctrCoord
75  self._vertexCoordList = tuple(vertexCoordList)
76  self._tractOverlap = tractOverlap
77 
78  minBBox = self._minimumBoundingBox(wcs)
79  initialBBox, self._numPatches = self._setupPatches(minBBox, wcs)
80  self._bbox, self._wcs = self._finalOrientation(initialBBox, wcs)
81 
82  def _minimumBoundingBox(self, wcs):
83  """Calculate the minimum bounding box for the tract, given the WCS
84 
85  The bounding box is created in the frame of the supplied WCS,
86  so that it's OK if the coordinates are negative.
87 
88  We compute the bounding box that holds all the vertices and the
89  desired overlap.
90  """
91  minBBoxD = afwGeom.Box2D()
92  halfOverlap = self._tractOverlap / 2.0
93  for vertexCoord in self._vertexCoordList:
94  if self._tractOverlap == 0:
95  minBBoxD.include(wcs.skyToPixel(vertexCoord))
96  else:
97  numAngles = 24
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)
104  return minBBoxD
105 
106  def _setupPatches(self, minBBox, wcs):
107  """Setup for patches of a particular size.
108 
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
113  (as an Extent2I).
114 
115  @param minBBox Minimum bounding box for tract
116  @param wcs Wcs object
117  @return final bounding box, number of patches
118  """
119  bbox = afwGeom.Box2I(minBBox)
120  bboxMin = bbox.getMin()
121  bboxDim = bbox.getDimensions()
122  numPatches = afwGeom.Extent2I(0, 0)
123  for i, innerDim in enumerate(self._patchInnerDimensions):
124  num = (bboxDim[i] + innerDim - 1) // innerDim # round up
125  deltaDim = (innerDim * num) - bboxDim[i]
126  if deltaDim > 0:
127  bboxDim[i] = innerDim * num
128  bboxMin[i] -= deltaDim // 2
129  numPatches[i] = num
130  bbox = afwGeom.Box2I(bboxMin, bboxDim)
131  return bbox, numPatches
132 
133  def _finalOrientation(self, bbox, wcs):
134  """Determine the final orientation
135 
136  We offset everything so the lower-left corner is at 0,0
137  and compute the final Wcs.
138 
139  @param bbox Current bounding box
140  @param wcs Current Wcs
141  @return revised bounding box, revised Wcs
142  """
143  finalBBox = afwGeom.Box2I(afwGeom.Point2I(0, 0), bbox.getDimensions())
144  # shift the WCS by the same amount as the bbox; extra code is required
145  # because simply subtracting makes an Extent2I
146  pixPosOffset = afwGeom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
147  finalBBox.getMinY() - bbox.getMinY())
148  wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset)
149  return finalBBox, wcs
150 
151  def getSequentialPatchIndex(self, patchInfo):
152  """Return a single integer that uniquely identifies the given patch
153  within this tract.
154  """
155  x, y = patchInfo.getIndex()
156  nx, ny = self.getNumPatches()
157  return nx*y + x
158 
159  def getPatchIndexPair(self, sequentialIndex):
160  nx, ny = self.getNumPatches()
161  x = sequentialIndex % nx
162  y = (sequentialIndex - x) / nx
163  return (x, y)
164 
165  def findPatch(self, coord):
166  """Find the patch containing the specified coord
167 
168  @param[in] coord: ICRS sky coordinate (lsst.afw.geom.SpherePoint)
169  @return PatchInfo of patch whose inner bbox contains the specified coord
170 
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).
173 
174  @note This routine will be more efficient if coord is ICRS.
175  """
176  try:
177  pixel = self.getWcs().skyToPixel(coord)
179  # Point must be way off the tract
180  raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
181  pixelInd = afwGeom.Point2I(pixel)
182  if not self.getBBox().contains(pixelInd):
183  raise LookupError("coord %s is not in tract %s" % (coord, self.getId()))
184  patchInd = tuple(int(pixelInd[i]/self._patchInnerDimensions[i]) for i in range(2))
185  return self.getPatchInfo(patchInd)
186 
187  def findPatchList(self, coordList):
188  """Find patches containing the specified list of coords
189 
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.
193 
194  @warning:
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).
198  """
199  box2D = afwGeom.Box2D()
200  for coord in coordList:
201  try:
202  pixelPos = self.getWcs().skyToPixel(coord)
204  # the point is so far off the tract that its pixel position cannot be computed
205  continue
206  box2D.include(pixelPos)
207  bbox = afwGeom.Box2I(box2D)
208  bbox.grow(self.getPatchBorder())
209  bbox.clip(self.getBBox())
210  if bbox.isEmpty():
211  return ()
212 
213  llPatchInd = tuple(int(bbox.getMin()[i]/self._patchInnerDimensions[i]) for i in range(2))
214  urPatchInd = tuple(int(bbox.getMax()[i]/self._patchInnerDimensions[i]) for i in range(2))
215  return tuple(self.getPatchInfo((xInd, yInd))
216  for xInd in range(llPatchInd[0], urPatchInd[0]+1)
217  for yInd in range(llPatchInd[1], urPatchInd[1]+1))
218 
219  def getBBox(self):
220  """Get bounding box of tract (as an afwGeom.Box2I)
221  """
222  return afwGeom.Box2I(self._bbox)
223 
224  def getCtrCoord(self):
225  """Get ICRS sky coordinate of center of tract (as an lsst.afw.geom.SpherePoint)
226  """
227  return self._ctrCoord
228 
229  def getId(self):
230  """Get ID of tract
231  """
232  return self._id
233 
234  def getNumPatches(self):
235  """Get the number of patches in x, y
236 
237  @return the number of patches in x, y
238  """
239  return self._numPatches
240 
241  def getPatchBorder(self):
242  """Get batch border
243 
244  @return patch border (pixels)
245  """
246  return self._patchBorder
247 
248  def getPatchInfo(self, index):
249  """Return information for the specified patch
250 
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
255 
256  @raise IndexError if index is out of range
257  """
258  if isinstance(index, numbers.Number):
259  index = self.getPatchIndexPair(index)
260  if (not 0 <= index[0] < self._numPatches[0]) \
261  or (not 0 <= index[1] < self._numPatches[1]):
262  raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
263  (index, self._numPatches[0]-1, self._numPatches[1]-1))
264  innerMin = afwGeom.Point2I(*[index[i] * self._patchInnerDimensions[i] for i in range(2)])
265  innerBBox = afwGeom.Box2I(innerMin, self._patchInnerDimensions)
266  if not self._bbox.contains(innerBBox):
267  raise RuntimeError(
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)
271  outerBBox.grow(self.getPatchBorder())
272  outerBBox.clip(self._bbox)
273  return PatchInfo(
274  index=index,
275  innerBBox=innerBBox,
276  outerBBox=outerBBox,
277  )
278 
280  """Get dimensions of inner region of the patches (all are the same)
281 
282  @return dimensions of inner region of the patches (as an afwGeom Extent2I)
283  """
284  return self._patchInnerDimensions
285 
286  def getTractOverlap(self):
287  """Get minimum overlap of adjacent sky tracts
288 
289  @return minimum overlap between adjacent sky tracts, as an afwGeom Angle
290  """
291  return self._tractOverlap
292 
293  def getVertexList(self):
294  """Get list of sky coordinates of vertices that define the boundary of the inner region
295 
296  @warning: this is not a deep copy
297  """
298  return self._vertexCoordList
299 
301  """Get inner on-sky region as a sphgeom.ConvexPolygon.
302  """
303  skyUnitVectors = [sp.getVector() for sp in self.getVertexList()]
304  return ConvexPolygon.convexHull(skyUnitVectors)
305 
307  """Get outer on-sky region as a sphgeom.ConvexPolygon
308  """
309  return makeSkyPolygonFromBBox(bbox=self.getBBox(), wcs=self.getWcs())
310 
311  def getWcs(self):
312  """Get WCS of tract
313 
314  @warning: this is not a deep copy
315  """
316  return self._wcs
317 
318  def __str__(self):
319  return "TractInfo(id=%s)" % (self._id,)
320 
321  def __repr__(self):
322  return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
323 
324  def __iter__(self):
325  xNum, yNum = self.getNumPatches()
326  for y in range(yNum):
327  for x in range(xNum):
328  yield self.getPatchInfo((x, y))
329 
330  def __len__(self):
331  xNum, yNum = self.getNumPatches()
332  return xNum*yNum
333 
334  def __getitem__(self, index):
335  return self.getPatchInfo(index)
336 
337  def contains(self, coord):
338  """Does this tract contain the coordinate?"""
339  try:
340  pixels = self.getWcs().skyToPixel(coord)
342  # Point must be way off the tract
343  return False
344  return self.getBBox().contains(afwGeom.Point2I(pixels))
345 
346 
348  """Information for a tract specified explicitly
349 
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).
352  """
353 
354  def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
355  # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
356  vertexList = []
357  self._radius = radius
358  super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
359  vertexList, tractOverlap, wcs)
360  # Shrink the box slightly to make sure the vertices are in the tract
361  bboxD = afwGeom.BoxD(self.getBBox())
362  bboxD.grow(-0.001)
363  finalWcs = self.getWcs()
364  self._vertexCoordList = finalWcs.pixelToSky(bboxD.getCorners())
365 
366  def _minimumBoundingBox(self, wcs):
367  """The minimum bounding box is calculated using the nominated radius"""
368  bbox = afwGeom.Box2D()
369  for i in range(4):
370  cornerCoord = self._ctrCoord.offset(i*90*afwGeom.degrees, self._radius + self._tractOverlap)
371  pixPos = wcs.skyToPixel(cornerCoord)
372  bbox.include(pixPos)
373  return bbox
def _finalOrientation(self, bbox, wcs)
Definition: tractInfo.py:133
def _setupPatches(self, minBBox, wcs)
Definition: tractInfo.py:106
def makeSkyPolygonFromBBox(bbox, wcs)
Definition: patchInfo.py:29
def getPatchIndexPair(self, sequentialIndex)
Definition: tractInfo.py:159
def getPatchInfo(self, index)
Definition: tractInfo.py:248
def __getitem__(self, index)
Definition: tractInfo.py:334
def _minimumBoundingBox(self, wcs)
Definition: tractInfo.py:82
def getSequentialPatchIndex(self, patchInfo)
Definition: tractInfo.py:151
def findPatch(self, coord)
Definition: tractInfo.py:165
def findPatchList(self, coordList)
Definition: tractInfo.py:187
def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs)
Definition: tractInfo.py:354
def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs)
Definition: tractInfo.py:49