Coverage for python/lsst/skymap/tractInfo.py: 27%
141 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-28 10:32 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-28 10:32 +0000
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 numpy as np
27import lsst.pex.exceptions
28import lsst.geom as geom
29from lsst.sphgeom import ConvexPolygon
31from .detail import makeSkyPolygonFromBBox, Index2D
34class TractInfo:
35 """Information about a tract in a SkyMap sky pixelization
37 Parameters
38 ----------
39 id : `int`
40 tract ID
41 tractBuilder : Subclass of `lsst.skymap.BaseTractBuilder`
42 Object used to compute patch geometry.
43 ctrCoord : `lsst.geom.SpherePoint`
44 ICRS sky coordinate of center of inner region of tract; also used as
45 the CRVAL for the WCS.
46 vertexCoordList : `list` of `lsst.geom.SpherePoint`
47 Vertices that define the boundaries of the inner region.
48 tractOverlap : `lsst.geom.Angle`
49 Minimum overlap between adjacent sky tracts; this defines the minimum
50 distance the tract extends beyond the inner region in all directions.
51 wcs : `lsst.afw.image.SkyWcs`
52 WCS for tract. The reference pixel will be shifted as required so that
53 the lower left-hand pixel (index 0,0) has pixel position 0.0, 0.0.
55 Notes
56 -----
57 The tract is subdivided into rectangular patches. Each patch has the
58 following properties:
60 - An inner region defined by an inner bounding box. The inner regions of
61 the patches exactly tile the tract, and all inner regions have the same
62 dimensions. The tract is made larger as required to make this work.
64 - An outer region defined by an outer bounding box. The outer region
65 extends beyond the inner region by patchBorder pixels in all directions,
66 except there is no border at the edges of the tract.
67 Thus patches overlap each other but never extend off the tract.
68 If you do not want any overlap between adjacent patches then set
69 patchBorder to 0.
71 - An index that consists of a pair of integers:
73 * 0 <= x index < numPatches[0]
75 * 0 <= y index < numPatches[1]
77 Patch 0,0 is at the minimum corner of the tract bounding box.
79 - It is not enforced that ctrCoord is the center of vertexCoordList, but
80 SkyMap relies on it.
81 """
82 def __init__(self, id, tractBuilder, ctrCoord, vertexCoordList, tractOverlap, wcs):
83 self._id = id
84 self._ctrCoord = ctrCoord
85 self._vertexCoordList = tuple(vertexCoordList)
86 self._tractOverlap = tractOverlap
87 self._tractBuilder = tractBuilder
89 minBBox = self._minimumBoundingBox(wcs)
90 initialBBox, self._numPatches = self._tractBuilder.setupPatches(minBBox, wcs)
91 self._bbox, self._wcs = self._finalOrientation(initialBBox, wcs)
93 def _minimumBoundingBox(self, wcs):
94 """Calculate the minimum bounding box for the tract, given the WCS.
96 The bounding box is created in the frame of the supplied WCS,
97 so that it's OK if the coordinates are negative.
99 We compute the bounding box that holds all the vertices and the
100 desired overlap.
101 """
102 minBBoxD = geom.Box2D()
103 halfOverlap = self._tractOverlap / 2.0
104 for vertexCoord in self._vertexCoordList:
105 if self._tractOverlap == 0:
106 minBBoxD.include(wcs.skyToPixel(vertexCoord))
107 else:
108 numAngles = 24
109 angleIncr = geom.Angle(360.0, geom.degrees) / float(numAngles)
110 for i in range(numAngles):
111 offAngle = angleIncr * i
112 offCoord = vertexCoord.offset(offAngle, halfOverlap)
113 pixPos = wcs.skyToPixel(offCoord)
114 minBBoxD.include(pixPos)
115 return minBBoxD
117 def _finalOrientation(self, bbox, wcs):
118 """Determine the final orientation
120 We offset everything so the lower-left corner is at 0,0
121 and compute the final Wcs.
123 Parameters
124 ----------
125 bbox : `lsst.geom.Box2I`
126 Current bounding box.
127 wcs : `lsst.afw.geom.SkyWcs
128 Current Wcs.
130 Returns
131 -------
132 finalBBox : `lsst.geom.Box2I`
133 Revised bounding box.
134 wcs : `lsst.afw.geom.SkyWcs`
135 Revised Wcs.
136 """
137 finalBBox = geom.Box2I(geom.Point2I(0, 0), bbox.getDimensions())
138 # shift the WCS by the same amount as the bbox; extra code is required
139 # because simply subtracting makes an Extent2I
140 pixPosOffset = geom.Extent2D(finalBBox.getMinX() - bbox.getMinX(),
141 finalBBox.getMinY() - bbox.getMinY())
142 wcs = wcs.copyAtShiftedPixelOrigin(pixPosOffset)
143 return finalBBox, wcs
145 def getSequentialPatchIndex(self, patchInfo):
146 """Return a single integer that uniquely identifies
147 the given patch within this tract.
149 Parameters
150 ----------
151 patchInfo : `lsst.skymap.PatchInfo`
153 Returns
154 -------
155 sequentialIndex : `int`
156 """
157 return self._tractBuilder.getSequentialPatchIndex(patchInfo)
159 def getSequentialPatchIndexFromPair(self, index):
160 """Return a single integer that uniquely identifies
161 the patch index within the tract.
163 Parameters
164 ----------
165 index : `lsst.skymap.Index2D`
167 Returns
168 -------
169 sequentialIndex : `int`
170 """
171 return self._tractBuilder.getSequentialPatchIndexFromPair(index)
173 def getPatchIndexPair(self, sequentialIndex):
174 """Convert sequential index into patch index (x,y) pair.
176 Parameters
177 ----------
178 sequentialIndex : `int`
180 Returns
181 -------
182 x, y : `lsst.skymap.Index2D`
183 """
184 return self._tractBuilder.getPatchIndexPair(sequentialIndex)
186 def findPatch(self, coord):
187 """Find the patch containing the specified coord.
189 Parameters
190 ----------
191 coord : `lsst.geom.SpherePoint`
192 ICRS sky coordinate to search for.
194 Returns
195 -------
196 result : `lsst.skymap.PatchInfo`
197 PatchInfo of patch whose inner bbox contains the specified coord
199 Raises
200 ------
201 LookupError
202 If coord is not in tract or we cannot determine the
203 pixel coordinate (which likely means the coord is off the tract).
204 """
205 try:
206 pixel = self.wcs.skyToPixel(coord)
207 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
208 # Point must be way off the tract
209 raise LookupError("Unable to determine pixel position for coordinate %s" % (coord,))
210 pixelInd = geom.Point2I(pixel)
211 if not self._bbox.contains(pixelInd):
212 raise LookupError("coord %s is not in tract %s" % (coord, self.tract_id))
213 # This should probably be the same as above because we only
214 # care about the INNER dimensions.
215 patchInd = tuple(int(pixelInd[i]/self.patch_inner_dimensions[i]) for i in range(2))
216 return self.getPatchInfo(patchInd)
218 def findPatchList(self, coordList):
219 """Find patches containing the specified list of coords.
221 Parameters
222 ----------
223 coordList : `list` of `lsst.geom.SpherePoint`
224 ICRS sky coordinates to search for.
226 Returns
227 -------
228 result : `list` of `lsst.skymap.PatchInfo`
229 List of PatchInfo for patches that contain, or may contain, the
230 specified region. The list will be empty if there is no overlap.
232 Notes
233 -----
234 **Warning:**
236 - This may give incorrect answers on regions that are larger than a
237 tract.
239 - This uses a naive algorithm that may find some patches that do not
240 overlap the region (especially if the region is not a rectangle
241 aligned along patch x,y).
242 """
243 box2D = geom.Box2D()
244 for coord in coordList:
245 try:
246 pixelPos = self.wcs.skyToPixel(coord)
247 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
248 # the point is so far off the tract that its pixel position cannot be computed
249 continue
250 box2D.include(pixelPos)
251 bbox = geom.Box2I(box2D)
252 bbox.grow(self.getPatchBorder())
253 bbox.clip(self._bbox)
254 if bbox.isEmpty():
255 return ()
257 llPatchInd = tuple(int(bbox.getMin()[i]/self.patch_inner_dimensions[i]) for i in range(2))
258 urPatchInd = tuple(int(bbox.getMax()[i]/self.patch_inner_dimensions[i]) for i in range(2))
259 return tuple(self.getPatchInfo((xInd, yInd))
260 for xInd in range(llPatchInd[0], urPatchInd[0]+1)
261 for yInd in range(llPatchInd[1], urPatchInd[1]+1))
263 def getBBox(self):
264 """Get bounding box of tract (as an geom.Box2I)
265 """
266 return geom.Box2I(self._bbox)
268 bbox = property(getBBox)
270 def getCtrCoord(self):
271 """Get ICRS sky coordinate of center of tract
272 (as an lsst.geom.SpherePoint)
273 """
274 return self._ctrCoord
276 ctr_coord = property(getCtrCoord)
278 def getId(self):
279 """Get ID of tract
280 """
281 return self._id
283 tract_id = property(getId)
285 def getNumPatches(self):
286 """Get the number of patches in x, y.
288 Returns
289 -------
290 result : `lsst.skymap.Index2D`
291 The number of patches in x, y
292 """
293 return self._numPatches
295 num_patches = property(getNumPatches)
297 def getPatchBorder(self):
298 return self._tractBuilder.getPatchBorder()
300 patch_border = property(getPatchBorder)
302 def getPatchInfo(self, index):
303 """Return information for the specified patch.
305 Parameters
306 ----------
307 index : `typing.NamedTuple` ['x': `int`, 'y': `int`]
308 Index of patch, as a pair of ints;
309 or a sequential index as returned by getSequentialPatchIndex;
310 negative values are not supported.
312 Returns
313 -------
314 result : `lsst.skymap.PatchInfo`
315 The patch info for that index.
317 Raises
318 ------
319 IndexError
320 If index is out of range.
321 """
322 return self._tractBuilder.getPatchInfo(index, self._wcs)
324 def getPatchInnerDimensions(self):
325 """Get dimensions of inner region of the patches (all are the same)
326 """
327 return self._tractBuilder.getPatchInnerDimensions()
329 patch_inner_dimensions = property(getPatchInnerDimensions)
331 def getTractOverlap(self):
332 """Get minimum overlap of adjacent sky tracts.
333 """
334 return self._tractOverlap
336 tract_overlap = property(getTractOverlap)
338 def getVertexList(self):
339 """Get list of ICRS sky coordinates of vertices that define the
340 boundary of the inner region.
342 Notes
343 -----
344 **warning:** this is not a deep copy.
345 """
346 return self._vertexCoordList
348 vertex_list = property(getVertexList)
350 def getInnerSkyPolygon(self):
351 """Get inner on-sky region as a sphgeom.ConvexPolygon.
352 """
353 skyUnitVectors = [sp.getVector() for sp in self.getVertexList()]
354 return ConvexPolygon.convexHull(skyUnitVectors)
356 inner_sky_polygon = property(getInnerSkyPolygon)
358 def getOuterSkyPolygon(self):
359 """Get outer on-sky region as a sphgeom.ConvexPolygon
360 """
361 return makeSkyPolygonFromBBox(bbox=self.getBBox(), wcs=self.getWcs())
363 outer_sky_polygon = property(getOuterSkyPolygon)
365 def getWcs(self):
366 """Get WCS of tract.
368 Returns
369 -------
370 wcs : `lsst.afw.geom.SkyWcs`
371 The WCS of this tract
372 """
373 return self._wcs
375 wcs = property(getWcs)
377 def __str__(self):
378 return "TractInfo(id=%s)" % (self._id,)
380 def __repr__(self):
381 return "TractInfo(id=%s, ctrCoord=%s)" % (self._id, self._ctrCoord.getVector())
383 def __iter__(self):
384 xNum, yNum = self.getNumPatches()
385 for y in range(yNum):
386 for x in range(xNum):
387 yield self.getPatchInfo(Index2D(x=x, y=y))
389 def __len__(self):
390 xNum, yNum = self.getNumPatches()
391 return xNum*yNum
393 def __getitem__(self, index):
394 return self.getPatchInfo(index)
396 def contains(self, coord):
397 """Does this tract contain the coordinate?"""
398 try:
399 pixel = self.getWcs().skyToPixel(coord)
400 except (lsst.pex.exceptions.DomainError, lsst.pex.exceptions.RuntimeError):
401 # Point must be way off the tract
402 return False
403 if not np.isfinite(pixel.getX()) or not np.isfinite(pixel.getY()):
404 # Point is definitely off the tract
405 return False
406 return self.getBBox().contains(geom.Point2I(pixel))
409class ExplicitTractInfo(TractInfo):
410 """Information for a tract specified explicitly.
412 A tract is placed at the explicitly defined coordinates, with the nominated
413 radius. The tracts are square (i.e., the radius is really a half-size).
414 """
415 def __init__(self, id, tractBuilder, ctrCoord, radius, tractOverlap, wcs):
416 # We don't want TractInfo setting the bbox on the basis of vertices, but on the radius.
417 vertexList = []
418 self._radius = radius
419 super(ExplicitTractInfo, self).__init__(id, tractBuilder, ctrCoord,
420 vertexList, tractOverlap, wcs)
421 # Shrink the box slightly to make sure the vertices are in the tract
422 bboxD = geom.BoxD(self.getBBox())
423 bboxD.grow(-0.001)
424 finalWcs = self.getWcs()
425 self._vertexCoordList = finalWcs.pixelToSky(bboxD.getCorners())
427 def _minimumBoundingBox(self, wcs):
428 """Calculate the minimum bounding box for the tract, given the WCS, and
429 the nominated radius.
430 """
431 bbox = geom.Box2D()
432 for i in range(4):
433 cornerCoord = self._ctrCoord.offset(i*90*geom.degrees, self._radius + self._tractOverlap)
434 pixPos = wcs.skyToPixel(cornerCoord)
435 bbox.include(pixPos)
436 return bbox