lsst.skymap  14.0-4-g3609236+1
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: sky coordinate of center of inner region of tract, as an afwCoord.Coord;
55  also used as the CRVAL for the WCS.
56  @param[in] vertexCoordList: list of sky coordinates (afwCoord.Coord)
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(coord.toIcrs() for coord in 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.clone()
102  offCoord.offset(offAngle, halfOverlap)
103  pixPos = wcs.skyToPixel(offCoord)
104  minBBoxD.include(pixPos)
105  return minBBoxD
106 
107  def _setupPatches(self, minBBox, wcs):
108  """Setup for patches of a particular size.
109 
110  We grow the bounding box to hold an exact multiple of
111  the desired size (patchInnerDimensions), while keeping
112  the center roughly the same. We return the final
113  bounding box, and the number of patches in each dimension
114  (as an Extent2I).
115 
116  @param minBBox Minimum bounding box for tract
117  @param wcs Wcs object
118  @return final bounding box, number of patches
119  """
120  bbox = afwGeom.Box2I(minBBox)
121  bboxMin = bbox.getMin()
122  bboxDim = bbox.getDimensions()
123  numPatches = afwGeom.Extent2I(0, 0)
124  for i, innerDim in enumerate(self._patchInnerDimensions):
125  num = (bboxDim[i] + innerDim - 1) // innerDim # round up
126  deltaDim = (innerDim * num) - bboxDim[i]
127  if deltaDim > 0:
128  bboxDim[i] = innerDim * num
129  bboxMin[i] -= deltaDim // 2
130  numPatches[i] = num
131  bbox = afwGeom.Box2I(bboxMin, bboxDim)
132  return bbox, numPatches
133 
134  def _finalOrientation(self, bbox, wcs):
135  """Determine the final orientation
136 
137  We offset everything so the lower-left corner is at 0,0
138  and compute the final Wcs.
139 
140  @param bbox Current bounding box
141  @param wcs Current Wcs
142  @return revised bounding box, revised Wcs
143  """
144  finalBBox = afwGeom.Box2I(afwGeom.Point2I(0, 0), bbox.getDimensions())
145  # shift the WCS by the same amount as the bbox; extra code is required
146  # because simply subtracting makes an Extent2I
147  pixPosOffset = afwGeom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
148  finalBBox.getMinY() - bbox.getMinY())
149  wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset)
150  return finalBBox, wcs
151 
152  def findPatch(self, coord):
153  """Find the patch containing the specified coord
154 
155  @param[in] coord: sky coordinate (afwCoord.Coord)
156  @return PatchInfo of patch whose inner bbox contains the specified coord
157 
158  @raise LookupError if coord is not in tract or we cannot determine the
159  pixel coordinate (which likely means the coord is off the tract).
160 
161  @note This routine will be more efficient if coord is ICRS.
162  """
163  icrsCoord = coord.toIcrs()
164  try:
165  pixel = self.getWcs().skyToPixel(icrsCoord)
167  # Point must be way off the tract
168  raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
169  pixelInd = afwGeom.Point2I(pixel)
170  if not self.getBBox().contains(pixelInd):
171  raise LookupError("coord %s is not in tract %s" % (coord, self.getId()))
172  patchInd = tuple(int(pixelInd[i]/self._patchInnerDimensions[i]) for i in range(2))
173  return self.getPatchInfo(patchInd)
174 
175  def findPatchList(self, coordList):
176  """Find patches containing the specified list of coords
177 
178  @param[in] coordList: list of sky coordinates (afwCoord.Coord)
179  @return list of PatchInfo for patches that contain, or may contain, the specified region.
180  The list will be empty if there is no overlap.
181 
182  @warning:
183  * This may give incorrect answers on regions that are larger than a tract
184  * This uses a naive algorithm that may find some patches that do not overlap the region
185  (especially if the region is not a rectangle aligned along patch x,y).
186  """
187  box2D = afwGeom.Box2D()
188  for coord in coordList:
189  icrsCoord = coord.toIcrs()
190  try:
191  pixelPos = self.getWcs().skyToPixel(icrsCoord)
193  # the point is so far off the tract that its pixel position cannot be computed
194  continue
195  box2D.include(pixelPos)
196  bbox = afwGeom.Box2I(box2D)
197  bbox.grow(self.getPatchBorder())
198  bbox.clip(self.getBBox())
199  if bbox.isEmpty():
200  return ()
201 
202  llPatchInd = tuple(int(bbox.getMin()[i]/self._patchInnerDimensions[i]) for i in range(2))
203  urPatchInd = tuple(int(bbox.getMax()[i]/self._patchInnerDimensions[i]) for i in range(2))
204  return tuple(self.getPatchInfo((xInd, yInd))
205  for xInd in range(llPatchInd[0], urPatchInd[0]+1)
206  for yInd in range(llPatchInd[1], urPatchInd[1]+1))
207 
208  def getBBox(self):
209  """Get bounding box of tract (as an afwGeom.Box2I)
210  """
211  return afwGeom.Box2I(self._bbox)
212 
213  def getCtrCoord(self):
214  """Get sky coordinate of center of tract (as an afwCoord.Coord)
215  """
216  return self._ctrCoord
217 
218  def getId(self):
219  """Get ID of tract
220  """
221  return self._id
222 
223  def getNumPatches(self):
224  """Get the number of patches in x, y
225 
226  @return the number of patches in x, y
227  """
228  return self._numPatches
229 
230  def getPatchBorder(self):
231  """Get batch border
232 
233  @return patch border (pixels)
234  """
235  return self._patchBorder
236 
237  def getPatchInfo(self, index):
238  """Return information for the specified patch
239 
240  @param[in] index: index of patch, as a pair of ints
241  @return patch info, an instance of PatchInfo
242 
243  @raise IndexError if index is out of range
244  """
245  if (not 0 <= index[0] < self._numPatches[0]) \
246  or (not 0 <= index[1] < self._numPatches[1]):
247  raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
248  (index, self._numPatches[0]-1, self._numPatches[1]-1))
249  innerMin = afwGeom.Point2I(*[index[i] * self._patchInnerDimensions[i] for i in range(2)])
250  innerBBox = afwGeom.Box2I(innerMin, self._patchInnerDimensions)
251  if not self._bbox.contains(innerBBox):
252  raise RuntimeError(
253  "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
254  (index, innerBBox, self._bbox))
255  outerBBox = afwGeom.Box2I(innerBBox)
256  outerBBox.grow(self.getPatchBorder())
257  outerBBox.clip(self._bbox)
258  return PatchInfo(
259  index=index,
260  innerBBox=innerBBox,
261  outerBBox=outerBBox,
262  )
263 
265  """Get dimensions of inner region of the patches (all are the same)
266 
267  @return dimensions of inner region of the patches (as an afwGeom Extent2I)
268  """
269  return self._patchInnerDimensions
270 
271  def getTractOverlap(self):
272  """Get minimum overlap of adjacent sky tracts
273 
274  @return minimum overlap between adjacent sky tracts, as an afwGeom Angle
275  """
276  return self._tractOverlap
277 
278  def getVertexList(self):
279  """Get list of sky coordinates of vertices that define the boundary of the inner region
280 
281  @warning: this is not a deep copy
282  @warning vertexCoordList will likely become a geom SphericalConvexPolygon someday.
283  """
284  return self._vertexCoordList
285 
286  def getWcs(self):
287  """Get WCS of tract
288 
289  @warning: this is not a deep copy
290  """
291  return self._wcs
292 
293  def __str__(self):
294  return "TractInfo(id=%s)" % (self._id,)
295 
296  def __repr__(self):
297  return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
298 
299  def __iter__(self):
300  xNum, yNum = self.getNumPatches()
301  for y in range(yNum):
302  for x in range(xNum):
303  yield self.getPatchInfo((x, y))
304 
305  def __len__(self):
306  xNum, yNum = self.getNumPatches()
307  return xNum*yNum
308 
309  def __getitem__(self, index):
310  return self.getPatchInfo(index)
311 
312  def contains(self, coord):
313  """Does this tract contain the coordinate?"""
314  icrsCoord = coord.toIcrs()
315  try:
316  pixels = self.getWcs().skyToPixel(icrsCoord)
318  # Point must be way off the tract
319  return False
320  return self.getBBox().contains(afwGeom.Point2I(pixels))
321 
322 
324  """Information for a tract specified explicitly
325 
326  A tract is placed at the explicitly defined coordinates, with the nominated
327  radius. The tracts are square (i.e., the radius is really a half-size).
328  """
329 
330  def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
331  # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
332  vertexList = []
333  self._radius = radius
334  super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
335  vertexList, tractOverlap, wcs)
336  # Shrink the box slightly to make sure the vertices are in the tract
337  bboxD = afwGeom.BoxD(self.getBBox())
338  bboxD.grow(-0.001)
339  finalWcs = self.getWcs()
340  self._vertexCoordList = finalWcs.pixelToSky(bboxD.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
def _finalOrientation(self, bbox, wcs)
Definition: tractInfo.py:134
def _setupPatches(self, minBBox, wcs)
Definition: tractInfo.py:107
def getPatchInfo(self, index)
Definition: tractInfo.py:237
def __getitem__(self, index)
Definition: tractInfo.py:309
def _minimumBoundingBox(self, wcs)
Definition: tractInfo.py:82
def findPatch(self, coord)
Definition: tractInfo.py:152
def findPatchList(self, coordList)
Definition: tractInfo.py:175
def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs)
Definition: tractInfo.py:330
def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs)
Definition: tractInfo.py:48