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