LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
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
161 
162  @note This routine will be more efficient if coord is ICRS.
163  """
164  pixelInd = afwGeom.Point2I(self.getWcs().skyToPixel(coord.toIcrs()))
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 (afwCoord.Coord)
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.toIcrs())
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 sky coordinate of center of tract (as an afwCoord.Coord)
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 
308  """Information for a tract specified explicitly
309 
310  A tract is placed at the explicitly defined coordinates, with the nominated
311  radius. The tracts are square (i.e., the radius is really a half-size).
312  """
313 
314  def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
315  # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
316  vertexList = []
317  self._radius = radius
318  super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
319  vertexList, tractOverlap, wcs)
320  # Now we know what the vertices are
321  self._vertexCoordList = [wcs.pixelToSky(afwGeom.Point2D(p)) for p in self.getBBox().getCorners()]
322 
323  def _minimumBoundingBox(self, wcs):
324  """The minimum bounding box is calculated using the nominated radius"""
325  bbox = afwGeom.Box2D()
326  for i in range(4):
327  coord = self._ctrCoord.clone()
328  coord.offset(i * 90 * afwGeom.degrees, self._radius + self._tractOverlap)
329  pixPos = wcs.skyToPixel(coord)
330  bbox.include(pixPos)
331  return bbox
An integer coordinate rectangle.
Definition: Box.h:53
A floating-point coordinate rectangle geometry.
Definition: Box.h:271