lsst.skymap  16.0-3-g6923fb6+1
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
25 
26 from .patchInfo import PatchInfo, makeSkyPolygonFromBBox
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  """
65  self._id = id
66  try:
67  assert len(patchInnerDimensions) == 2
68  self._patchInnerDimensions = afwGeom.Extent2I(*(int(val) for val in patchInnerDimensions))
69  except Exception:
70  raise TypeError("patchInnerDimensions=%s; must be two ints" % (patchInnerDimensions,))
71  self._patchBorder = int(patchBorder)
72  self._ctrCoord = ctrCoord
73  self._vertexCoordList = tuple(vertexCoordList)
74  self._tractOverlap = tractOverlap
75 
76  minBBox = self._minimumBoundingBox(wcs)
77  initialBBox, self._numPatches = self._setupPatches(minBBox, wcs)
78  self._bbox, self._wcs = self._finalOrientation(initialBBox, wcs)
79 
80  def _minimumBoundingBox(self, wcs):
81  """Calculate the minimum bounding box for the tract, given the WCS
82 
83  The bounding box is created in the frame of the supplied WCS,
84  so that it's OK if the coordinates are negative.
85 
86  We compute the bounding box that holds all the vertices and the
87  desired overlap.
88  """
89  minBBoxD = afwGeom.Box2D()
90  halfOverlap = self._tractOverlap / 2.0
91  for vertexCoord in self._vertexCoordList:
92  if self._tractOverlap == 0:
93  minBBoxD.include(wcs.skyToPixel(vertexCoord))
94  else:
95  numAngles = 24
96  angleIncr = afwGeom.Angle(360.0, afwGeom.degrees) / float(numAngles)
97  for i in range(numAngles):
98  offAngle = angleIncr * i
99  offCoord = vertexCoord.offset(offAngle, halfOverlap)
100  pixPos = wcs.skyToPixel(offCoord)
101  minBBoxD.include(pixPos)
102  return minBBoxD
103 
104  def _setupPatches(self, minBBox, wcs):
105  """Setup for patches of a particular size.
106 
107  We grow the bounding box to hold an exact multiple of
108  the desired size (patchInnerDimensions), while keeping
109  the center roughly the same. We return the final
110  bounding box, and the number of patches in each dimension
111  (as an Extent2I).
112 
113  @param minBBox Minimum bounding box for tract
114  @param wcs Wcs object
115  @return final bounding box, number of patches
116  """
117  bbox = afwGeom.Box2I(minBBox)
118  bboxMin = bbox.getMin()
119  bboxDim = bbox.getDimensions()
120  numPatches = afwGeom.Extent2I(0, 0)
121  for i, innerDim in enumerate(self._patchInnerDimensions):
122  num = (bboxDim[i] + innerDim - 1) // innerDim # round up
123  deltaDim = (innerDim * num) - bboxDim[i]
124  if deltaDim > 0:
125  bboxDim[i] = innerDim * num
126  bboxMin[i] -= deltaDim // 2
127  numPatches[i] = num
128  bbox = afwGeom.Box2I(bboxMin, bboxDim)
129  return bbox, numPatches
130 
131  def _finalOrientation(self, bbox, wcs):
132  """Determine the final orientation
133 
134  We offset everything so the lower-left corner is at 0,0
135  and compute the final Wcs.
136 
137  @param bbox Current bounding box
138  @param wcs Current Wcs
139  @return revised bounding box, revised Wcs
140  """
141  finalBBox = afwGeom.Box2I(afwGeom.Point2I(0, 0), bbox.getDimensions())
142  # shift the WCS by the same amount as the bbox; extra code is required
143  # because simply subtracting makes an Extent2I
144  pixPosOffset = afwGeom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
145  finalBBox.getMinY() - bbox.getMinY())
146  wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset)
147  return finalBBox, wcs
148 
149  def getSequentialPatchIndex(self, patchInfo):
150  """Return a single integer that uniquely identifies the given patch
151  within this tract.
152  """
153  x, y = patchInfo.getIndex()
154  nx, ny = self.getNumPatches()
155  return nx*y + x
156 
157  def findPatch(self, coord):
158  """Find the patch containing the specified coord
159 
160  @param[in] coord: ICRS sky coordinate (lsst.afw.geom.SpherePoint)
161  @return PatchInfo of patch whose inner bbox contains the specified coord
162 
163  @raise LookupError if coord is not in tract or we cannot determine the
164  pixel coordinate (which likely means the coord is off the tract).
165 
166  @note This routine will be more efficient if coord is ICRS.
167  """
168  try:
169  pixel = self.getWcs().skyToPixel(coord)
171  # Point must be way off the tract
172  raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
173  pixelInd = afwGeom.Point2I(pixel)
174  if not self.getBBox().contains(pixelInd):
175  raise LookupError("coord %s is not in tract %s" % (coord, self.getId()))
176  patchInd = tuple(int(pixelInd[i]/self._patchInnerDimensions[i]) for i in range(2))
177  return self.getPatchInfo(patchInd)
178 
179  def findPatchList(self, coordList):
180  """Find patches containing the specified list of coords
181 
182  @param[in] coordList: list of sky coordinates (lsst.afw.geom.SpherePoint)
183  @return list of PatchInfo for patches that contain, or may contain, the specified region.
184  The list will be empty if there is no overlap.
185 
186  @warning:
187  * This may give incorrect answers on regions that are larger than a tract
188  * This uses a naive algorithm that may find some patches that do not overlap the region
189  (especially if the region is not a rectangle aligned along patch x,y).
190  """
191  box2D = afwGeom.Box2D()
192  for coord in coordList:
193  try:
194  pixelPos = self.getWcs().skyToPixel(coord)
196  # the point is so far off the tract that its pixel position cannot be computed
197  continue
198  box2D.include(pixelPos)
199  bbox = afwGeom.Box2I(box2D)
200  bbox.grow(self.getPatchBorder())
201  bbox.clip(self.getBBox())
202  if bbox.isEmpty():
203  return ()
204 
205  llPatchInd = tuple(int(bbox.getMin()[i]/self._patchInnerDimensions[i]) for i in range(2))
206  urPatchInd = tuple(int(bbox.getMax()[i]/self._patchInnerDimensions[i]) for i in range(2))
207  return tuple(self.getPatchInfo((xInd, yInd))
208  for xInd in range(llPatchInd[0], urPatchInd[0]+1)
209  for yInd in range(llPatchInd[1], urPatchInd[1]+1))
210 
211  def getBBox(self):
212  """Get bounding box of tract (as an afwGeom.Box2I)
213  """
214  return afwGeom.Box2I(self._bbox)
215 
216  def getCtrCoord(self):
217  """Get ICRS sky coordinate of center of tract (as an lsst.afw.geom.SpherePoint)
218  """
219  return self._ctrCoord
220 
221  def getId(self):
222  """Get ID of tract
223  """
224  return self._id
225 
226  def getNumPatches(self):
227  """Get the number of patches in x, y
228 
229  @return the number of patches in x, y
230  """
231  return self._numPatches
232 
233  def getPatchBorder(self):
234  """Get batch border
235 
236  @return patch border (pixels)
237  """
238  return self._patchBorder
239 
240  def getPatchInfo(self, index):
241  """Return information for the specified patch
242 
243  @param[in] index: index of patch, as a pair of ints;
244  negative values are not supported
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  """
287  return self._vertexCoordList
288 
290  """Get inner on-sky region as a sphgeom.ConvexPolygon.
291  """
292  skyUnitVectors = [sp.getVector() for sp in self.getVertexList()]
293  return ConvexPolygon.convexHull(skyUnitVectors)
294 
296  """Get outer on-sky region as a sphgeom.ConvexPolygon
297  """
298  return makeSkyPolygonFromBBox(bbox=self.getBBox(), wcs=self.getWcs())
299 
300  def getWcs(self):
301  """Get WCS of tract
302 
303  @warning: this is not a deep copy
304  """
305  return self._wcs
306 
307  def __str__(self):
308  return "TractInfo(id=%s)" % (self._id,)
309 
310  def __repr__(self):
311  return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
312 
313  def __iter__(self):
314  xNum, yNum = self.getNumPatches()
315  for y in range(yNum):
316  for x in range(xNum):
317  yield self.getPatchInfo((x, y))
318 
319  def __len__(self):
320  xNum, yNum = self.getNumPatches()
321  return xNum*yNum
322 
323  def __getitem__(self, index):
324  return self.getPatchInfo(index)
325 
326  def contains(self, coord):
327  """Does this tract contain the coordinate?"""
328  try:
329  pixels = self.getWcs().skyToPixel(coord)
331  # Point must be way off the tract
332  return False
333  return self.getBBox().contains(afwGeom.Point2I(pixels))
334 
335 
337  """Information for a tract specified explicitly
338 
339  A tract is placed at the explicitly defined coordinates, with the nominated
340  radius. The tracts are square (i.e., the radius is really a half-size).
341  """
342 
343  def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
344  # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
345  vertexList = []
346  self._radius = radius
347  super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
348  vertexList, tractOverlap, wcs)
349  # Shrink the box slightly to make sure the vertices are in the tract
350  bboxD = afwGeom.BoxD(self.getBBox())
351  bboxD.grow(-0.001)
352  finalWcs = self.getWcs()
353  self._vertexCoordList = finalWcs.pixelToSky(bboxD.getCorners())
354 
355  def _minimumBoundingBox(self, wcs):
356  """The minimum bounding box is calculated using the nominated radius"""
357  bbox = afwGeom.Box2D()
358  for i in range(4):
359  cornerCoord = self._ctrCoord.offset(i*90*afwGeom.degrees, self._radius + self._tractOverlap)
360  pixPos = wcs.skyToPixel(cornerCoord)
361  bbox.include(pixPos)
362  return bbox
def _finalOrientation(self, bbox, wcs)
Definition: tractInfo.py:131
def _setupPatches(self, minBBox, wcs)
Definition: tractInfo.py:104
def makeSkyPolygonFromBBox(bbox, wcs)
Definition: patchInfo.py:29
def getPatchInfo(self, index)
Definition: tractInfo.py:240
def __getitem__(self, index)
Definition: tractInfo.py:323
def _minimumBoundingBox(self, wcs)
Definition: tractInfo.py:80
def getSequentialPatchIndex(self, patchInfo)
Definition: tractInfo.py:149
def findPatch(self, coord)
Definition: tractInfo.py:157
def findPatchList(self, coordList)
Definition: tractInfo.py:179
def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs)
Definition: tractInfo.py:343
def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs)
Definition: tractInfo.py:47