Coverage for python/lsst/skymap/tractInfo.py : 18%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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__all__ = ["TractInfo"]
25import numbers
27import lsst.pex.exceptions
28import lsst.geom as geom
29from lsst.sphgeom import ConvexPolygon
31from .patchInfo import PatchInfo, makeSkyPolygonFromBBox
34class TractInfo:
35 """Information about a tract in a SkyMap sky pixelization
37 Parameters
38 ----------
39 id : `int`
40 tract ID
41 patchInnerDimensions : `tuple` of `int`
42 Dimensions of inner region of patches (x,y pixels).
43 patchBorder : `int`
44 Overlap between adjacent patches (in pixels)
45 ctrCoord : `lsst.geom.SpherePoint`
46 ICRS sky coordinate of center of inner region of tract; also used as
47 the CRVAL for the WCS.
48 vertexCoordList : `list` of `lsst.geom.SpherePoint`
49 Vertices that define the boundaries of the inner region.
50 tractOverlap : `lsst.geom.Angle`
51 Minimum overlap between adjacent sky tracts; this defines the minimum
52 distance the tract extends beyond the inner region in all directions.
53 wcs : `lsst.afw.image.SkyWcs`
54 WCS for tract. The reference pixel will be shifted as required so that
55 the lower left-hand pixel (index 0,0) has pixel position 0.0, 0.0.
57 Notes
58 -----
59 The tract is subdivided into rectangular patches. Each patch has the
60 following properties:
62 - An inner region defined by an inner bounding box. The inner regions of
63 the patches exactly tile the tract, and all inner regions have the same
64 dimensions. The tract is made larger as required to make this work.
66 - An outer region defined by an outer bounding box. The outer region
67 extends beyond the inner region by patchBorder pixels in all directions,
68 except there is no border at the edges of the tract.
69 Thus patches overlap each other but never extend off the tract.
70 If you do not want any overlap between adjacent patches then set
71 patchBorder to 0.
73 - An index that consists of a pair of integers:
75 * 0 <= x index < numPatches[0]
77 * 0 <= y index < numPatches[1]
79 Patch 0,0 is at the minimum corner of the tract bounding box.
81 - It is not enforced that ctrCoord is the center of vertexCoordList, but
82 SkyMap relies on it.
83 """
85 def __init__(self, id, patchInnerDimensions, patchBorder, ctrCoord, vertexCoordList, tractOverlap, wcs):
86 self._id = id
87 try:
88 assert len(patchInnerDimensions) == 2
89 self._patchInnerDimensions = geom.Extent2I(*(int(val) for val in patchInnerDimensions))
90 except Exception:
91 raise TypeError("patchInnerDimensions=%s; must be two ints" % (patchInnerDimensions,))
92 self._patchBorder = int(patchBorder)
93 self._ctrCoord = ctrCoord
94 self._vertexCoordList = tuple(vertexCoordList)
95 self._tractOverlap = tractOverlap
97 minBBox = self._minimumBoundingBox(wcs)
98 initialBBox, self._numPatches = self._setupPatches(minBBox, wcs)
99 self._bbox, self._wcs = self._finalOrientation(initialBBox, wcs)
101 def _minimumBoundingBox(self, wcs):
102 """Calculate the minimum bounding box for the tract, given the WCS.
104 The bounding box is created in the frame of the supplied WCS,
105 so that it's OK if the coordinates are negative.
107 We compute the bounding box that holds all the vertices and the
108 desired overlap.
109 """
110 minBBoxD = geom.Box2D()
111 halfOverlap = self._tractOverlap / 2.0
112 for vertexCoord in self._vertexCoordList:
113 if self._tractOverlap == 0:
114 minBBoxD.include(wcs.skyToPixel(vertexCoord))
115 else:
116 numAngles = 24
117 angleIncr = geom.Angle(360.0, geom.degrees) / float(numAngles)
118 for i in range(numAngles):
119 offAngle = angleIncr * i
120 offCoord = vertexCoord.offset(offAngle, halfOverlap)
121 pixPos = wcs.skyToPixel(offCoord)
122 minBBoxD.include(pixPos)
123 return minBBoxD
125 def _setupPatches(self, minBBox, wcs):
126 """Setup for patches of a particular size.
128 We grow the bounding box to hold an exact multiple of
129 the desired size (patchInnerDimensions), while keeping
130 the center roughly the same. We return the final
131 bounding box, and the number of patches in each dimension
132 (as an Extent2I).
134 Parameters
135 ----------
136 minBBox : `lsst.geom.Box2I`
137 Minimum bounding box for tract
138 wcs : `lsst.afw.geom.SkyWcs`
139 Wcs object
141 Returns
142 -------
143 bbox : `lsst.geom.Box2I
144 final bounding box, number of patches
145 numPatches : `int`
146 """
147 bbox = geom.Box2I(minBBox)
148 bboxMin = bbox.getMin()
149 bboxDim = bbox.getDimensions()
150 numPatches = geom.Extent2I(0, 0)
151 for i, innerDim in enumerate(self._patchInnerDimensions):
152 num = (bboxDim[i] + innerDim - 1) // innerDim # round up
153 deltaDim = (innerDim * num) - bboxDim[i]
154 if deltaDim > 0:
155 bboxDim[i] = innerDim * num
156 bboxMin[i] -= deltaDim // 2
157 numPatches[i] = num
158 bbox = geom.Box2I(bboxMin, bboxDim)
159 return bbox, numPatches
161 def _finalOrientation(self, bbox, wcs):
162 """Determine the final orientation
164 We offset everything so the lower-left corner is at 0,0
165 and compute the final Wcs.
167 Parameters
168 ----------
169 bbox : `lsst.geom.Box2I`
170 Current bounding box.
171 wcs : `lsst.afw.geom.SkyWcs
172 Current Wcs.
174 Returns
175 -------
176 finalBBox : `lsst.geom.Box2I`
177 Revised bounding box.
178 wcs : `lsst.afw.geom.SkyWcs`
179 Revised Wcs.
180 """
181 finalBBox = geom.Box2I(geom.Point2I(0, 0), bbox.getDimensions())
182 # shift the WCS by the same amount as the bbox; extra code is required
183 # because simply subtracting makes an Extent2I
184 pixPosOffset = geom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
185 finalBBox.getMinY() - bbox.getMinY())
186 wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset)
187 return finalBBox, wcs
189 def getSequentialPatchIndex(self, patchInfo):
190 """Return a single integer that uniquely identifies the given patch
191 within this tract.
192 """
193 x, y = patchInfo.getIndex()
194 nx, ny = self.getNumPatches()
195 return nx*y + x
197 def getPatchIndexPair(self, sequentialIndex):
198 nx, ny = self.getNumPatches()
199 x = sequentialIndex % nx
200 y = (sequentialIndex - x) / nx
201 return (x, y)
203 def findPatch(self, coord):
204 """Find the patch containing the specified coord.
206 Parameters
207 ----------
208 coord : `lsst.geom.SpherePoint`
209 ICRS sky coordinate to search for.
211 Returns
212 -------
213 result : `lsst.skymap.PatchInfo`
214 PatchInfo of patch whose inner bbox contains the specified coord
216 Raises
217 ------
218 LookupError
219 If coord is not in tract or we cannot determine the
220 pixel coordinate (which likely means the coord is off the tract).
221 """
222 try:
223 pixel = self.getWcs().skyToPixel(coord)
224 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
225 # Point must be way off the tract
226 raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
227 pixelInd = geom.Point2I(pixel)
228 if not self.getBBox().contains(pixelInd):
229 raise LookupError("coord %s is not in tract %s" % (coord, self.getId()))
230 patchInd = tuple(int(pixelInd[i]/self._patchInnerDimensions[i]) for i in range(2))
231 return self.getPatchInfo(patchInd)
233 def findPatchList(self, coordList):
234 """Find patches containing the specified list of coords.
236 Parameters
237 ----------
238 coordList : `list` of `lsst.geom.SpherePoint`
239 ICRS sky coordinates to search for.
241 Returns
242 -------
243 result : `list` of `lsst.skymap.PatchInfo`
244 List of PatchInfo for patches that contain, or may contain, the
245 specified region. The list will be empty if there is no overlap.
247 Notes
248 -----
249 **Warning:**
251 - This may give incorrect answers on regions that are larger than a
252 tract.
254 - This uses a naive algorithm that may find some patches that do not
255 overlap the region (especially if the region is not a rectangle
256 aligned along patch x,y).
257 """
258 box2D = geom.Box2D()
259 for coord in coordList:
260 try:
261 pixelPos = self.getWcs().skyToPixel(coord)
262 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
263 # the point is so far off the tract that its pixel position cannot be computed
264 continue
265 box2D.include(pixelPos)
266 bbox = geom.Box2I(box2D)
267 bbox.grow(self.getPatchBorder())
268 bbox.clip(self.getBBox())
269 if bbox.isEmpty():
270 return ()
272 llPatchInd = tuple(int(bbox.getMin()[i]/self._patchInnerDimensions[i]) for i in range(2))
273 urPatchInd = tuple(int(bbox.getMax()[i]/self._patchInnerDimensions[i]) for i in range(2))
274 return tuple(self.getPatchInfo((xInd, yInd))
275 for xInd in range(llPatchInd[0], urPatchInd[0]+1)
276 for yInd in range(llPatchInd[1], urPatchInd[1]+1))
278 def getBBox(self):
279 """Get bounding box of tract (as an geom.Box2I)
280 """
281 return geom.Box2I(self._bbox)
283 def getCtrCoord(self):
284 """Get ICRS sky coordinate of center of tract
285 (as an lsst.geom.SpherePoint)
286 """
287 return self._ctrCoord
289 def getId(self):
290 """Get ID of tract
291 """
292 return self._id
294 def getNumPatches(self):
295 """Get the number of patches in x, y.
297 Returns
298 -------
299 result : `tuple` of `int`
300 The number of patches in x, y
301 """
302 return self._numPatches
304 def getPatchBorder(self):
305 return self._patchBorder
307 def getPatchInfo(self, index):
308 """Return information for the specified patch.
310 Parameters
311 ----------
312 index : `tuple` of `int`
313 Index of patch, as a pair of ints;
314 or a sequential index as returned by getSequentialPatchIndex;
315 negative values are not supported.
317 Returns
318 -------
319 result : `lsst.skymap.PatchInfo`
320 The patch info for that index.
322 Raises
323 ------
324 IndexError
325 If index is out of range.
326 """
327 if isinstance(index, numbers.Number):
328 index = self.getPatchIndexPair(index)
329 if (not 0 <= index[0] < self._numPatches[0]) \
330 or (not 0 <= index[1] < self._numPatches[1]):
331 raise IndexError("Patch index %s is not in range [0-%d, 0-%d]" %
332 (index, self._numPatches[0]-1, self._numPatches[1]-1))
333 innerMin = geom.Point2I(*[index[i] * self._patchInnerDimensions[i] for i in range(2)])
334 innerBBox = geom.Box2I(innerMin, self._patchInnerDimensions)
335 if not self._bbox.contains(innerBBox):
336 raise RuntimeError(
337 "Bug: patch index %s valid but inner bbox=%s not contained in tract bbox=%s" %
338 (index, innerBBox, self._bbox))
339 outerBBox = geom.Box2I(innerBBox)
340 outerBBox.grow(self.getPatchBorder())
341 outerBBox.clip(self._bbox)
342 return PatchInfo(
343 index=index,
344 innerBBox=innerBBox,
345 outerBBox=outerBBox,
346 )
348 def getPatchInnerDimensions(self):
349 """Get dimensions of inner region of the patches (all are the same)
350 """
351 return self._patchInnerDimensions
353 def getTractOverlap(self):
354 """Get minimum overlap of adjacent sky tracts.
355 """
356 return self._tractOverlap
358 def getVertexList(self):
359 """Get list of ICRS sky coordinates of vertices that define the
360 boundary of the inner region.
362 Notes
363 -----
364 **warning:** this is not a deep copy.
365 """
366 return self._vertexCoordList
368 def getInnerSkyPolygon(self):
369 """Get inner on-sky region as a sphgeom.ConvexPolygon.
370 """
371 skyUnitVectors = [sp.getVector() for sp in self.getVertexList()]
372 return ConvexPolygon.convexHull(skyUnitVectors)
374 def getOuterSkyPolygon(self):
375 """Get outer on-sky region as a sphgeom.ConvexPolygon
376 """
377 return makeSkyPolygonFromBBox(bbox=self.getBBox(), wcs=self.getWcs())
379 def getWcs(self):
380 """Get WCS of tract.
382 Returns
383 -------
384 wcs : `lsst.afw.geom.SkyWcs`
385 The WCS of this tract
386 """
387 return self._wcs
389 def __str__(self):
390 return "TractInfo(id=%s)" % (self._id,)
392 def __repr__(self):
393 return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
395 def __iter__(self):
396 xNum, yNum = self.getNumPatches()
397 for y in range(yNum):
398 for x in range(xNum):
399 yield self.getPatchInfo((x, y))
401 def __len__(self):
402 xNum, yNum = self.getNumPatches()
403 return xNum*yNum
405 def __getitem__(self, index):
406 return self.getPatchInfo(index)
408 def contains(self, coord):
409 """Does this tract contain the coordinate?"""
410 try:
411 pixels = self.getWcs().skyToPixel(coord)
412 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
413 # Point must be way off the tract
414 return False
415 return self.getBBox().contains(geom.Point2I(pixels))
418class ExplicitTractInfo(TractInfo):
419 """Information for a tract specified explicitly.
421 A tract is placed at the explicitly defined coordinates, with the nominated
422 radius. The tracts are square (i.e., the radius is really a half-size).
423 """
425 def __init__(self, ident, patchInnerDimensions, patchBorder, ctrCoord, radius, tractOverlap, wcs):
426 # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
427 vertexList = []
428 self._radius = radius
429 super(ExplicitTractInfo, self).__init__(ident, patchInnerDimensions, patchBorder, ctrCoord,
430 vertexList, tractOverlap, wcs)
431 # Shrink the box slightly to make sure the vertices are in the tract
432 bboxD = geom.BoxD(self.getBBox())
433 bboxD.grow(-0.001)
434 finalWcs = self.getWcs()
435 self._vertexCoordList = finalWcs.pixelToSky(bboxD.getCorners())
437 def _minimumBoundingBox(self, wcs):
438 """Calculate the minimum bounding box for the tract, given the WCS, and
439 the nominated radius.
440 """
441 bbox = geom.Box2D()
442 for i in range(4):
443 cornerCoord = self._ctrCoord.offset(i*90*geom.degrees, self._radius + self._tractOverlap)
444 pixPos = wcs.skyToPixel(cornerCoord)
445 bbox.include(pixPos)
446 return bbox