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