Coverage for python/lsst/skymap/ringsSkyMap.py : 14%

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, 2012 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__ = ["RingsSkyMapConfig", "RingsSkyMap"]
25import struct
26import math
28from lsst.pex.config import Field
29import lsst.geom as geom
30from .cachingSkyMap import CachingSkyMap
31from .tractInfo import ExplicitTractInfo
34class RingsSkyMapConfig(CachingSkyMap.ConfigClass):
35 """Configuration for the RingsSkyMap"""
36 numRings = Field(dtype=int, doc="Number of rings", check=lambda x: x > 0) 36 ↛ exitline 36 didn't run the lambda on line 36
37 raStart = Field(dtype=float, default=0.0, doc="Starting center RA for each ring (degrees)", 37 ↛ exitline 37 didn't jump to the function exit
38 check=lambda x: x >= 0.0 and x < 360.0)
41class RingsSkyMap(CachingSkyMap):
42 """Rings sky map pixelization.
44 We divide the sphere into N rings of Declination, plus the two polar
45 caps, which sets the size of the individual tracts. The rings are
46 divided in RA into an integral number of tracts of this size; this
47 division is made at the Declination closest to zero so as to ensure
48 full overlap.
50 Rings are numbered in the rings from south to north. The south pole cap is
51 ``tract=0``, then the tract at ``raStart`` in the southernmost ring is
52 ``tract=1``. Numbering continues (in the positive RA direction) around that
53 ring and then continues in the same fashion with the next ring north, and
54 so on until all reaching the north pole cap, which is
55 ``tract=len(skymap) - 1``.
57 However, ``version=0`` had a bug in the numbering of the tracts: the first
58 and last tracts in the first (southernmost) ring were identical, and the
59 first tract in the last (northernmost) ring was missing. When using
60 ``version=0``, these tracts remain missing in order to preserve the
61 numbering scheme.
63 Parameters
64 ----------
65 config : `lsst.skymap.RingsSkyMapConfig`
66 The configuration for this SkyMap.
67 version : `int`, optional
68 Software version of this class, to retain compatibility with old
69 verisons. ``version=0`` covers the period from first implementation
70 until DM-14809, at which point bugs were identified in the numbering
71 of tracts (affecting only tracts at RA=0). ``version=1`` uses the
72 post-DM-14809 tract numbering.
73 """
74 ConfigClass = RingsSkyMapConfig
75 _version = (1, 0) # for pickle
77 def __init__(self, config, version=1):
78 assert version in (0, 1), "Unrecognised version: %s" % (version,)
79 # We count rings from south to north
80 # Note: pole caps together count for one additional ring when calculating the ring size
81 self._ringSize = math.pi / (config.numRings + 1) # Size of a ring in Declination (radians)
82 self._ringNums = [] # Number of tracts for each ring
83 for i in range(config.numRings):
84 startDec = self._ringSize*(i + 0.5) - 0.5*math.pi
85 stopDec = startDec + self._ringSize
86 dec = min(math.fabs(startDec), math.fabs(stopDec)) # Declination for determining division in RA
87 self._ringNums.append(int(2*math.pi*math.cos(dec)/self._ringSize) + 1)
88 numTracts = sum(self._ringNums) + 2
89 super(RingsSkyMap, self).__init__(numTracts, config, version)
90 self._raStart = self.config.raStart*geom.degrees
92 def getRingIndices(self, index):
93 """Calculate ring indices given a numerical index of a tract.
95 The ring indices are the ring number and the tract number within
96 the ring.
98 The ring number is -1 for the south polar cap and increases to the
99 north. The north polar cap has ring number = numRings. The tract
100 number is zero for either of the polar caps.
101 """
102 if index == 0: # South polar cap
103 return -1, 0
104 if index == self._numTracts - 1: # North polar cap
105 return self.config.numRings, 0
106 if index < 0 or index >= self._numTracts:
107 raise IndexError("Tract index %d is out of range [0, %d]" % (index, len(self) - 1))
108 ring = 0 # Ring number
109 tractNum = index - 1 # Tract number within ring
110 if self._version == 0:
111 # Maintain the off-by-one bug in version=0 (DM-14809).
112 # This means that the first tract in the first ring is duplicated
113 # and the first tract in the last ring is missing.
114 while ring < self.config.numRings and tractNum > self._ringNums[ring]:
115 tractNum -= self._ringNums[ring]
116 ring += 1
117 else:
118 while ring < self.config.numRings and tractNum >= self._ringNums[ring]:
119 tractNum -= self._ringNums[ring]
120 ring += 1
121 return ring, tractNum
123 def generateTract(self, index):
124 """Generate TractInfo for the specified tract index."""
125 ringNum, tractNum = self.getRingIndices(index)
126 if ringNum == -1: # South polar cap
127 ra, dec = 0, -0.5*math.pi
128 elif ringNum == self.config.numRings: # North polar cap
129 ra, dec = 0, 0.5*math.pi
130 else:
131 dec = self._ringSize*(ringNum + 1) - 0.5*math.pi
132 ra = ((2*math.pi*tractNum/self._ringNums[ringNum])*geom.radians
133 + self._raStart).wrap().asRadians()
135 center = geom.SpherePoint(ra, dec, geom.radians)
136 wcs = self._wcsFactory.makeWcs(crPixPos=geom.Point2D(0, 0), crValCoord=center)
137 return ExplicitTractInfo(index, self.config.patchInnerDimensions, self.config.patchBorder, center,
138 0.5*self._ringSize*geom.radians, self.config.tractOverlap*geom.degrees,
139 wcs)
141 def _decToRingNum(self, dec):
142 """Calculate ring number from Declination.
144 Parameters
145 ----------
146 dec : `lsst.geom.Angle`
147 Declination.
149 Returns
150 -------
151 ringNum : `int`
152 Ring number: -1 for the south polar cap, and increasing to the
153 north, ending with ``numRings`` for the north polar cap.
154 """
155 firstRingStart = self._ringSize*0.5 - 0.5*math.pi
156 if dec < firstRingStart:
157 # Southern cap
158 return -1
159 elif dec > firstRingStart*-1:
160 # Northern cap
161 return self.config.numRings
162 return int((dec.asRadians() - firstRingStart)/self._ringSize)
164 def _raToTractNum(self, ra, ringNum):
165 """Calculate tract number from the Right Ascension.
167 Parameters
168 ----------
169 ra : `lsst.geom.Angle`
170 Right Ascension.
171 ringNum : `int`
172 Ring number (from ``_decToRingNum``).
174 Returns
175 -------
176 tractNum : `int`
177 Tract number within the ring (starts at 0 for the tract at raStart).
178 """
179 if ringNum in (-1, self.config.numRings):
180 return 0
181 assert ringNum in range(self.config.numRings)
182 tractNum = int((ra - self._raStart).wrap().asRadians()
183 / (2*math.pi/self._ringNums[ringNum]) + 0.5)
184 return 0 if tractNum == self._ringNums[ringNum] else tractNum # Allow wraparound
186 def findTract(self, coord):
187 ringNum = self._decToRingNum(coord.getLatitude())
188 if ringNum == -1:
189 # Southern cap
190 return self[0]
191 if ringNum == self.config.numRings:
192 # Northern cap
193 return self[self._numTracts - 1]
194 tractNum = self._raToTractNum(coord.getLongitude(), ringNum)
196 if self._version == 0 and tractNum == 0 and ringNum != 0:
197 # Account for off-by-one error in getRingIndices
198 # Note that this means that tract 1 gets duplicated.
199 ringNum += 1
201 index = sum(self._ringNums[:ringNum], tractNum + 1) # Allow 1 for south pole
202 return self[index]
204 def findAllTracts(self, coord):
205 """Find all tracts which include the specified coord.
207 Parameters
208 ----------
209 coord : `lsst.geom.SpherePoint`
210 ICRS sky coordinate to search for.
212 Returns
213 -------
214 tractList : `list` of `TractInfo`
215 The tracts which include the specified coord.
216 """
217 ringNum = self._decToRingNum(coord.getLatitude())
219 tractList = list()
220 # ringNum denotes the closest ring to the specified coord
221 # I will check adjacent rings which may include the specified coord
222 for r in [ringNum - 1, ringNum, ringNum + 1]:
223 if r < 0 or r >= self.config.numRings:
224 # Poles will be checked explicitly outside this loop
225 continue
226 tractNum = self._raToTractNum(coord.getLongitude(), r)
227 # Adjacent tracts will also be checked.
228 for t in [tractNum - 1, tractNum, tractNum + 1]:
229 # Wrap over raStart
230 if t < 0:
231 t = t + self._ringNums[r]
232 elif t > self._ringNums[r] - 1:
233 t = t - self._ringNums[r]
235 extra = 0
236 if self._version == 0 and t == 0 and r != 0:
237 # Account for off-by-one error in getRingIndices
238 # Note that this means that tract 1 gets duplicated.
239 extra = 1
241 index = sum(self._ringNums[:r + extra], t + 1) # Allow 1 for south pole
242 tract = self[index]
243 if tract.contains(coord):
244 tractList.append(tract)
246 # Always check tracts at poles
247 # Southern cap is 0, Northern cap is the last entry in self
248 for entry in [0, len(self)-1]:
249 tract = self[entry]
250 if tract.contains(coord):
251 tractList.append(tract)
253 return tractList
255 def findTractPatchList(self, coordList):
256 retList = []
257 for coord in coordList:
258 for tractInfo in self.findAllTracts(coord):
259 patchList = tractInfo.findPatchList(coordList)
260 if patchList and not (tractInfo, patchList) in retList:
261 retList.append((tractInfo, patchList))
262 return retList
264 def updateSha1(self, sha1):
265 """Add subclass-specific state or configuration options to the SHA1."""
266 sha1.update(struct.pack("<id", self.config.numRings, self.config.raStart))