lsst.skymap  14.0-5-g3f35923
tractInfo.py
Go to the documentation of this file.
1 from __future__ import division
2 from builtins import range
3 from builtins import object
4 #
5 # LSST Data Management System
6 # Copyright 2008, 2009, 2010 LSST Corporation.
7 #
8 # This product includes software developed by the
9 # LSST Project (http://www.lsst.org/).
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the LSST License Statement and
22 # the GNU General Public License along with this program. If not,
23 # see <http://www.lsstcorp.org/LegalNotices/>.
24 #
26 import lsst.afw.geom as afwGeom
27 from .patchInfo import PatchInfo
28 
29 __all__ = ["TractInfo"]
30 
31 
32 class TractInfo(object):
33  """Information about a tract in a SkyMap sky pixelization
34 
35  The tract is subdivided into rectangular patches. Each patch has the following properties:
36  - An inner region defined by an inner bounding. The inner regions of the patches exactly tile the tract,
37  and all inner regions have the same dimensions. The tract is made larger as required to make this work.
38  - An outer region defined by an outer bounding box. The outer region extends beyond the inner region
39  by patchBorder pixels in all directions, except there is no border at the edges of the tract.
40  Thus patches overlap each other but never extend off the tract. If you do not want any overlap
41  between adjacent patches then set patchBorder to 0.
42  - An index that consists of a pair of integers:
43  0 <= x index < numPatches[0]
44  0 <= y index < numPatches[1]
45  Patch 0,0 is at the minimum corner of the tract bounding box.
46  """
47 
48  def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs):
49  """Construct a TractInfo
50 
51  @param[in] id: tract ID
52  @param[in] patchInnerDimensions: dimensions of inner region of patches (x,y pixels)
53  @param[in] patchBorder: overlap between adjacent patches (in pixels, one int)
54  @param[in] ctrCoord: ICRS sky coordinate of center of inner region of tract
55  as an lsst.afw.geom.SpherePoint; also used as the CRVAL for the WCS.
56  @param[in] vertexCoordList: list of ICRS sky coordinates (lsst.afw.geom.SpherePoint)
57  of vertices that define the boundaries of the inner region
58  @param[in] tractOverlap: minimum overlap between adjacent sky tracts; an afwGeom.Angle;
59  this defines the minimum distance the tract extends beyond the inner region in all directions
60  @param[in,out] wcs: an afwImage.Wcs; the reference pixel will be shifted as required
61  so that the lower left-hand pixel (index 0,0) has pixel position 0.0, 0.0
62 
63  @warning
64  - It is not enforced that ctrCoord is the center of vertexCoordList, but SkyMap relies on it
65  - vertexCoordList will likely become a geom SphericalConvexPolygon someday.
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 findPatch(self, coord):
152  """Find the patch containing the specified coord
153 
154  @param[in] coord: ICRS sky coordinate (lsst.afw.geom.SpherePoint)
155  @return PatchInfo of patch whose inner bbox contains the specified coord
156 
157  @raise LookupError if coord is not in tract or we cannot determine the
158  pixel coordinate (which likely means the coord is off the tract).
159 
160  @note This routine will be more efficient if coord is ICRS.
161  """
162  try:
163  pixel = self.getWcs().skyToPixel(coord)
165  # Point must be way off the tract
166  raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
167  pixelInd = afwGeom.Point2I(pixel)
168  if not self.getBBox().contains(pixelInd):
169  raise LookupError("coord %s is not in tract %s" % (coord, self.getId()))
170  patchInd = tuple(int(pixelInd[i]/self._patchInnerDimensions[i]) for i in range(2))
171  return self.getPatchInfo(patchInd)
172 
173  def findPatchList(self, coordList):
174  """Find patches containing the specified list of coords
175 
176  @param[in] coordList: list of sky coordinates (lsst.afw.geom.SpherePoint)
177  @return list of PatchInfo for patches that contain, or may contain, the specified region.
178  The list will be empty if there is no overlap.
179 
180  @warning:
181  * This may give incorrect answers on regions that are larger than a tract
182  * This uses a naive algorithm that may find some patches that do not overlap the region
183  (especially if the region is not a rectangle aligned along patch x,y).
184  """
185  box2D = afwGeom.Box2D()
186  for coord in coordList:
187  try:
188  pixelPos = self.getWcs().skyToPixel(coord)
190  # the point is so far off the tract that its pixel position cannot be computed
191  continue
192  box2D.include(pixelPos)
193  bbox = afwGeom.Box2I(box2D)
194  bbox.grow(self.getPatchBorder())
195  bbox.clip(self.getBBox())
196  if bbox.isEmpty():
197  return ()
198 
199  llPatchInd = tuple(int(bbox.getMin()[i]/self._patchInnerDimensions[i]) for i in range(2))
200  urPatchInd = tuple(int(bbox.getMax()[i]/self._patchInnerDimensions[i]) for i in range(2))
201  return tuple(self.getPatchInfo((xInd, yInd))
202  for xInd in range(llPatchInd[0], urPatchInd[0]+1)
203  for yInd in range(llPatchInd[1], urPatchInd[1]+1))
204 
205  def getBBox(self):
206  """Get bounding box of tract (as an afwGeom.Box2I)
207  """
208  return afwGeom.Box2I(self._bbox)
209 
210  def getCtrCoord(self):
211  """Get ICRS sky coordinate of center of tract (as an lsst.afw.geom.SpherePoint)
212  """
213  return self._ctrCoord
214 
215  def getId(self):
216  """Get ID of tract
217  """
218  return self._id
219 
220  def getNumPatches(self):
221  """Get the number of patches in x, y
222 
223  @return the number of patches in x, y
224  """
225  return self._numPatches
226 
227  def getPatchBorder(self):
228  """Get batch border
229 
230  @return patch border (pixels)
231  """
232  return self._patchBorder
233 
234  def getPatchInfo(self, index):
235  """Return information for the specified patch
236 
237  @param[in] index: index of patch, as a pair of ints
238  @return patch info, an instance of PatchInfo
239 
240  @raise IndexError if index is out of range
241  """
242  if (not 0 <= index[0] < self._numPatches[0]) \
243  or (not 0 <= index[1] < self._numPatches[1]):
244  raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
245  (index, self._numPatches[0]-1, self._numPatches[1]-1))
246  innerMin = afwGeom.Point2I(*[index[i] * self._patchInnerDimensions[i] for i in range(2)])
247  innerBBox = afwGeom.Box2I(innerMin, self._patchInnerDimensions)
248  if not self._bbox.contains(innerBBox):
249  raise RuntimeError(
250  "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
251  (index, innerBBox, self._bbox))
252  outerBBox = afwGeom.Box2I(innerBBox)
253  outerBBox.grow(self.getPatchBorder())
254  outerBBox.clip(self._bbox)
255  return PatchInfo(
256  index=index,
257  innerBBox=innerBBox,
258  outerBBox=outerBBox,
259  )
260 
262  """Get dimensions of inner region of the patches (all are the same)
263 
264  @return dimensions of inner region of the patches (as an afwGeom Extent2I)
265  """
266  return self._patchInnerDimensions
267 
268  def getTractOverlap(self):
269  """Get minimum overlap of adjacent sky tracts
270 
271  @return minimum overlap between adjacent sky tracts, as an afwGeom Angle
272  """
273  return self._tractOverlap
274 
275  def getVertexList(self):
276  """Get list of sky coordinates of vertices that define the boundary of the inner region
277 
278  @warning: this is not a deep copy
279  @warning vertexCoordList will likely become a geom SphericalConvexPolygon someday.
280  """
281  return self._vertexCoordList
282 
283  def getWcs(self):
284  """Get WCS of tract
285 
286  @warning: this is not a deep copy
287  """
288  return self._wcs
289 
290  def __str__(self):
291  return "TractInfo(id=%s)" % (self._id,)
292 
293  def __repr__(self):
294  return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
295 
296  def __iter__(self):
297  xNum, yNum = self.getNumPatches()
298  for y in range(yNum):
299  for x in range(xNum):
300  yield self.getPatchInfo((x, y))
301 
302  def __len__(self):
303  xNum, yNum = self.getNumPatches()
304  return xNum*yNum
305 
306  def __getitem__(self, index):
307  return self.getPatchInfo(index)
308 
309  def contains(self, coord):
310  """Does this tract contain the coordinate?"""
311  try:
312  pixels = self.getWcs().skyToPixel(coord)
314  # Point must be way off the tract
315  return False
316  return self.getBBox().contains(afwGeom.Point2I(pixels))
317 
318 
320  """Information for a tract specified explicitly
321 
322  A tract is placed at the explicitly defined coordinates, with the nominated
323  radius. The tracts are square (i.e., the radius is really a half-size).
324  """
325 
326  def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
327  # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
328  vertexList = []
329  self._radius = radius
330  super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
331  vertexList, tractOverlap, wcs)
332  # Shrink the box slightly to make sure the vertices are in the tract
333  bboxD = afwGeom.BoxD(self.getBBox())
334  bboxD.grow(-0.001)
335  finalWcs = self.getWcs()
336  self._vertexCoordList = finalWcs.pixelToSky(bboxD.getCorners())
337 
338  def _minimumBoundingBox(self, wcs):
339  """The minimum bounding box is calculated using the nominated radius"""
340  bbox = afwGeom.Box2D()
341  for i in range(4):
342  cornerCoord = self._ctrCoord.offset(i*90*afwGeom.degrees, self._radius + self._tractOverlap)
343  pixPos = wcs.skyToPixel(cornerCoord)
344  bbox.include(pixPos)
345  return bbox
def _finalOrientation(self, bbox, wcs)
Definition: tractInfo.py:133
def _setupPatches(self, minBBox, wcs)
Definition: tractInfo.py:106
def getPatchInfo(self, index)
Definition: tractInfo.py:234
def __getitem__(self, index)
Definition: tractInfo.py:306
def _minimumBoundingBox(self, wcs)
Definition: tractInfo.py:82
def findPatch(self, coord)
Definition: tractInfo.py:151
def findPatchList(self, coordList)
Definition: tractInfo.py:173
def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs)
Definition: tractInfo.py:326
def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs)
Definition: tractInfo.py:48