lsst.skymap  13.0-4-gd4595c5+7
 All Classes Namespaces Files Functions Variables Pages
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 #
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
30 
31 __all__ = ["TractInfo"]
32 
33 
34 class TractInfo(object):
35  """Information about a tract in a SkyMap sky pixelization
36 
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.
48  """
49 
50  def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs):
51  """Construct a TractInfo
52 
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
64 
65  @warning
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.
68  """
69  self._id = id
70  try:
71  assert len(patchInnerDimensions) == 2
72  self._patchInnerDimensions = afwGeom.Extent2I(*(int(val) for val in patchInnerDimensions))
73  except:
74  raise TypeError("patchInnerDimensions=%s; must be two ints" % (patchInnerDimensions,))
75  self._patchBorder = int(patchBorder)
76  self._ctrCoord = ctrCoord
77  self._vertexCoordList = tuple(coord.clone() for coord in vertexCoordList)
78  self._tractOverlap = tractOverlap
79 
80  minBBox = self._minimumBoundingBox(wcs)
81  initialBBox, self._numPatches = self._setupPatches(minBBox, wcs)
82  self._bbox, self._wcs = self._finalOrientation(initialBBox, wcs)
83 
84  def _minimumBoundingBox(self, wcs):
85  """Calculate the minimum bounding box for the tract, given the WCS
86 
87  The bounding box is created in the frame of the supplied WCS,
88  so that it's OK if the coordinates are negative.
89 
90  We compute the bounding box that holds all the vertices and the
91  desired overlap.
92  """
93  minBBoxD = afwGeom.Box2D()
94  halfOverlap = self._tractOverlap / 2.0
95  for vertexCoord in self._vertexCoordList:
96  vertexDeg = vertexCoord.getPosition(afwGeom.degrees)
97  if self._tractOverlap == 0:
98  minBBoxD.include(wcs.skyToPixel(vertexCoord))
99  else:
100  numAngles = 24
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)
108  return minBBoxD
109 
110  def _setupPatches(self, minBBox, wcs):
111  """Setup for patches of a particular size.
112 
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
117  (as an Extent2I).
118 
119  @param minBBox Minimum bounding box for tract
120  @param wcs Wcs object
121  @return final bounding box, number of patches
122  """
123  bbox = afwGeom.Box2I(minBBox)
124  bboxMin = bbox.getMin()
125  bboxDim = bbox.getDimensions()
126  numPatches = afwGeom.Extent2I(0, 0)
127  for i, innerDim in enumerate(self._patchInnerDimensions):
128  num = (bboxDim[i] + innerDim - 1) // innerDim # round up
129  deltaDim = (innerDim * num) - bboxDim[i]
130  if deltaDim > 0:
131  bboxDim[i] = innerDim * num
132  bboxMin[i] -= deltaDim // 2
133  numPatches[i] = num
134  bbox = afwGeom.Box2I(bboxMin, bboxDim)
135  return bbox, numPatches
136 
137  def _finalOrientation(self, bbox, wcs):
138  """Determine the final orientation
139 
140  We offset everything so the lower-left corner is at 0,0
141  and compute the final Wcs.
142 
143  @param bbox Current bounding box
144  @param wcs Current Wcs
145  @return revised bounding box, revised Wcs
146  """
147  finalBBox = afwGeom.Box2I(afwGeom.Point2I(0, 0), bbox.getDimensions())
148  # shift the WCS by the same amount as the bbox; extra code is required
149  # because simply subtracting makes an Extent2I
150  pixPosOffset = afwGeom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
151  finalBBox.getMinY() - bbox.getMinY())
152  wcs.shiftReferencePixel(pixPosOffset)
153  return finalBBox, wcs
154 
155  def findPatch(self, coord):
156  """Find the patch containing the specified coord
157 
158  @param[in] coord: sky coordinate (afwCoord.Coord)
159  @return PatchInfo of patch whose inner bbox contains the specified coord
160 
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).
163 
164  @note This routine will be more efficient if coord is ICRS.
165  """
166  icrsCoord = coord.toIcrs()
167  try:
168  pixel = self.getWcs().skyToPixel(icrsCoord)
169  except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
170  # Point must be way off the tract
171  raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
172  pixelInd = afwGeom.Point2I(pixel)
173  if not self.getBBox().contains(pixelInd):
174  raise LookupError("coord %s is not in tract %s" % (coord, self.getId()))
175  patchInd = tuple(int(pixelInd[i]/self._patchInnerDimensions[i]) for i in range(2))
176  return self.getPatchInfo(patchInd)
177 
178  def findPatchList(self, coordList):
179  """Find patches containing the specified list of coords
180 
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.
184 
185  @warning:
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).
189  """
190  box2D = afwGeom.Box2D()
191  for coord in coordList:
192  icrsCoord = coord.toIcrs()
193  try:
194  pixelPos = self.getWcs().skyToPixel(icrsCoord)
195  except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
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 sky coordinate of center of tract (as an afwCoord.Coord)
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  @return patch info, an instance of PatchInfo
245 
246  @raise IndexError if index is out of range
247  """
248  if (not 0 <= index[0] < self._numPatches[0]) \
249  or (not 0 <= index[1] < self._numPatches[1]):
250  raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
251  (index, self._numPatches[0]-1, self._numPatches[1]-1))
252  innerMin = afwGeom.Point2I(*[index[i] * self._patchInnerDimensions[i] for i in range(2)])
253  innerBBox = afwGeom.Box2I(innerMin, self._patchInnerDimensions)
254  if not self._bbox.contains(innerBBox):
255  raise RuntimeError(
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)
259  outerBBox.grow(self.getPatchBorder())
260  outerBBox.clip(self._bbox)
261  return PatchInfo(
262  index=index,
263  innerBBox=innerBBox,
264  outerBBox=outerBBox,
265  )
266 
268  """Get dimensions of inner region of the patches (all are the same)
269 
270  @return dimensions of inner region of the patches (as an afwGeom Extent2I)
271  """
272  return self._patchInnerDimensions
273 
274  def getTractOverlap(self):
275  """Get minimum overlap of adjacent sky tracts
276 
277  @return minimum overlap between adjacent sky tracts, as an afwGeom Angle
278  """
279  return self._tractOverlap
280 
281  def getVertexList(self):
282  """Get list of sky coordinates of vertices that define the boundary of the inner region
283 
284  @warning: this is not a deep copy
285  @warning vertexCoordList will likely become a geom SphericalConvexPolygon someday.
286  """
287  return self._vertexCoordList
288 
289  def getWcs(self):
290  """Get WCS of tract
291 
292  @warning: this is not a deep copy
293  """
294  return self._wcs
295 
296  def __str__(self):
297  return "TractInfo(id=%s)" % (self._id,)
298 
299  def __repr__(self):
300  return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
301 
302  def __iter__(self):
303  xNum, yNum = self.getNumPatches()
304  for y in range(yNum):
305  for x in range(xNum):
306  yield self.getPatchInfo((x, y))
307 
308  def __len__(self):
309  xNum, yNum = self.getNumPatches()
310  return xNum*yNum
311 
312  def __getitem__(self, index):
313  return self.getPatchInfo(index)
314 
315  def contains(self, coord):
316  """Does this tract contain the coordinate?"""
317  icrsCoord = coord.toIcrs()
318  try:
319  pixels = self.getWcs().skyToPixel(icrsCoord)
320  except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
321  # Point must be way off the tract
322  return False
323  return self.getBBox().contains(afwGeom.Point2I(pixels))
324 
325 
327  """Information for a tract specified explicitly
328 
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).
331  """
332 
333  def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
334  # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
335  vertexList = []
336  self._radius = radius
337  super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
338  vertexList, tractOverlap, wcs)
339  # Now we know what the vertices are
340  self._vertexCoordList = [wcs.pixelToSky(afwGeom.Point2D(p)) for p in self.getBBox().getCorners()]
341 
342  def _minimumBoundingBox(self, wcs):
343  """The minimum bounding box is calculated using the nominated radius"""
344  bbox = afwGeom.Box2D()
345  for i in range(4):
346  coord = self._ctrCoord.clone()
347  coord.offset(i * 90 * afwGeom.degrees, self._radius + self._tractOverlap)
348  pixPos = wcs.skyToPixel(coord)
349  bbox.include(pixPos)
350  return bbox