lsst.skymap  16.0-3-g6923fb6+5
ringsSkyMap.py
Go to the documentation of this file.
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 #
22 
23 import struct
24 import math
25 
26 from lsst.pex.config import Field
27 import lsst.afw.geom as afwGeom
28 from .cachingSkyMap import CachingSkyMap
29 from .tractInfo import ExplicitTractInfo
30 
31 __all__ = ["RingsSkyMapConfig", "RingsSkyMap"]
32 
33 
34 class RingsSkyMapConfig(CachingSkyMap.ConfigClass):
35  """Configuration for the RingsSkyMap"""
36  numRings = Field(dtype=int, doc="Number of rings", check=lambda x: x > 0)
37  raStart = Field(dtype=float, default=0.0, doc="Starting center RA for each ring (degrees)",
38  check=lambda x: x >= 0.0 and x < 360.0)
39 
40 
42  """Rings sky map pixelization.
43 
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.
49 
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``.
56 
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.
62 
63  Parameters
64  ----------
65  config : `lsst.skymap.RingsSkyMapConfig`
66  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
76 
77  def __init__(self, config, version=1):
78  """Constructor"""
79  assert version in (0, 1), "Unrecognised version: %s" % (version,)
80  # We count rings from south to north
81  # Note: pole caps together count for one additional ring when calculating the ring size
82  self._ringSize = math.pi / (config.numRings + 1) # Size of a ring in Declination (radians)
83  self._ringNums = [] # Number of tracts for each ring
84  for i in range(config.numRings):
85  startDec = self._ringSize*(i + 0.5) - 0.5*math.pi
86  stopDec = startDec + self._ringSize
87  dec = min(math.fabs(startDec), math.fabs(stopDec)) # Declination for determining division in RA
88  self._ringNums.append(int(2*math.pi*math.cos(dec)/self._ringSize) + 1)
89  numTracts = sum(self._ringNums) + 2
90  super(RingsSkyMap, self).__init__(numTracts, config, version)
91  self._raStart = self.config.raStart*afwGeom.degrees
92 
93  def getRingIndices(self, index):
94  """Calculate ring indices given a numerical index of a tract
95 
96  The ring indices are the ring number and the tract number within
97  the ring.
98 
99  The ring number is -1 for the south polar cap and increases to the
100  north. The north polar cap has ring number = numRings. The tract
101  number is zero for either of the polar caps.
102  """
103  if index == 0: # South polar cap
104  return -1, 0
105  if index == self._numTracts - 1: # North polar cap
106  return self.config.numRings, 0
107  if index < 0 or index >= self._numTracts:
108  raise IndexError("Tract index %d is out of range [0, %d]" % (index, len(self) - 1))
109  ring = 0 # Ring number
110  tractNum = index - 1 # Tract number within ring
111  if self._version == 0:
112  # Maintain the off-by-one bug in version=0 (DM-14809).
113  # This means that the first tract in the first ring is duplicated
114  # and the first tract in the last ring is missing.
115  while ring < self.config.numRings and tractNum > self._ringNums[ring]:
116  tractNum -= self._ringNums[ring]
117  ring += 1
118  else:
119  while ring < self.config.numRings and tractNum >= self._ringNums[ring]:
120  tractNum -= self._ringNums[ring]
121  ring += 1
122  return ring, tractNum
123 
124  def generateTract(self, index):
125  """Generate the TractInfo for this index"""
126  ringNum, tractNum = self.getRingIndices(index)
127  if ringNum == -1: # South polar cap
128  ra, dec = 0, -0.5*math.pi
129  elif ringNum == self.config.numRings: # North polar cap
130  ra, dec = 0, 0.5*math.pi
131  else:
132  dec = self._ringSize*(ringNum + 1) - 0.5*math.pi
133  ra = ((2*math.pi*tractNum/self._ringNums[ringNum])*afwGeom.radians +
134  self._raStart).wrap().asRadians()
135 
136  center = afwGeom.SpherePoint(ra, dec, afwGeom.radians)
137  wcs = self._wcsFactory.makeWcs(crPixPos=afwGeom.Point2D(0, 0), crValCoord=center)
138  return ExplicitTractInfo(index, self.config.patchInnerDimensions, self.config.patchBorder, center,
139  0.5*self._ringSize*afwGeom.radians, self.config.tractOverlap*afwGeom.degrees,
140  wcs)
141 
142  def _decToRingNum(self, dec):
143  """Calculate ring number from Declination
144 
145  Parameters
146  ----------
147  dec : `lsst.afw.geom.Angle`
148  Declination.
149 
150  Returns
151  -------
152  ringNum : `int`
153  Ring number: -1 for the south polar cap, and increasing to the
154  north, ending with ``numRings`` for the north polar cap.
155  """
156  firstRingStart = self._ringSize*0.5 - 0.5*math.pi
157  if dec < firstRingStart:
158  # Southern cap
159  return -1
160  elif dec > firstRingStart*-1:
161  # Northern cap
162  return self.config.numRings
163  return int((dec.asRadians() - firstRingStart)/self._ringSize)
164 
165  def _raToTractNum(self, ra, ringNum):
166  """Calculate tract number from the Right Ascension
167 
168  Parameters
169  ----------
170  ra : `lsst.afw.geom.Angle`
171  Right Ascension.
172  ringNum : `int`
173  Ring number (from ``_decToRingNum``).
174 
175  Returns
176  -------
177  tractNum : `int`
178  Tract number within the ring (starts at 0 for the tract at raStart).
179  """
180  if ringNum in (-1, self.config.numRings):
181  return 0
182  assert ringNum in range(self.config.numRings)
183  tractNum = int((ra - self._raStart).wrap().asRadians() /
184  (2*math.pi/self._ringNums[ringNum]) + 0.5)
185  return 0 if tractNum == self._ringNums[ringNum] else tractNum # Allow wraparound
186 
187  def findTract(self, coord):
188  """Find the tract whose center is nearest the specified coord.
189 
190  @param[in] coord: sky coordinate (afwCoord.Coord)
191  @return TractInfo of tract whose center is nearest the specified coord
192 
193  @warning:
194  - if tracts do not cover the whole sky then the returned tract may not include the coord
195 
196  @note
197  - If coord is equidistant between multiple sky tract centers then one is arbitrarily chosen.
198  """
199  ringNum = self._decToRingNum(coord.getLatitude())
200  if ringNum == -1:
201  # Southern cap
202  return self[0]
203  if ringNum == self.config.numRings:
204  # Northern cap
205  return self[self._numTracts - 1]
206  tractNum = self._raToTractNum(coord.getLongitude(), ringNum)
207 
208  if self._version == 0 and tractNum == 0 and ringNum != 0:
209  # Account for off-by-one error in getRingIndices
210  # Note that this means that tract 1 gets duplicated.
211  ringNum += 1
212 
213  index = sum(self._ringNums[:ringNum], tractNum + 1) # Allow 1 for south pole
214  return self[index]
215 
216  def findAllTracts(self, coord):
217  """Find all tracts which include the specified coord.
218 
219  @param[in] coord: sky coordinate (afwCoord.Coord)
220  @return List of TractInfo of tracts which include the specified coord
221 
222  @note
223  - This routine will be more efficient if coord is ICRS.
224  """
225  ringNum = self._decToRingNum(coord.getLatitude())
226 
227  tractList = list()
228  # ringNum denotes the closest ring to the specified coord
229  # I will check adjacent rings which may include the specified coord
230  for r in [ringNum - 1, ringNum, ringNum + 1]:
231  if r < 0 or r >= self.config.numRings:
232  # Poles will be checked explicitly outside this loop
233  continue
234  tractNum = self._raToTractNum(coord.getLongitude(), r)
235  # Adjacent tracts will also be checked.
236  for t in [tractNum - 1, tractNum, tractNum + 1]:
237  # Wrap over raStart
238  if t < 0:
239  t = t + self._ringNums[r]
240  elif t > self._ringNums[r] - 1:
241  t = t - self._ringNums[r]
242 
243  extra = 0
244  if self._version == 0 and t == 0 and r != 0:
245  # Account for off-by-one error in getRingIndices
246  # Note that this means that tract 1 gets duplicated.
247  extra = 1
248 
249  index = sum(self._ringNums[:r + extra], t + 1) # Allow 1 for south pole
250  tract = self[index]
251  if tract.contains(coord):
252  tractList.append(tract)
253 
254  # Always check tracts at poles
255  # Southern cap is 0, Northern cap is the last entry in self
256  for entry in [0, len(self)-1]:
257  tract = self[entry]
258  if tract.contains(coord):
259  tractList.append(tract)
260 
261  return tractList
262 
263  def findTractPatchList(self, coordList):
264  """Find tracts and patches that overlap a region
265 
266  @param[in] coordList: list of sky coordinates (afwCoord.Coord)
267  @return list of (TractInfo, list of PatchInfo) for tracts and patches that contain,
268  or may contain, the specified region. The list will be empty if there is no overlap.
269 
270  @warning this uses a naive algorithm that may find some tracts and patches that do not overlap
271  the region (especially if the region is not a rectangle aligned along patch x,y).
272  """
273  retList = []
274  for coord in coordList:
275  for tractInfo in self.findAllTracts(coord):
276  patchList = tractInfo.findPatchList(coordList)
277  if patchList and not (tractInfo, patchList) in retList:
278  retList.append((tractInfo, patchList))
279  return retList
280 
281  def updateSha1(self, sha1):
282  """Add subclass-specific state or configuration options to the SHA1."""
283  sha1.update(struct.pack("<id", self.config.numRings, self.config.raStart))
def _raToTractNum(self, ra, ringNum)
Definition: ringsSkyMap.py:165
def findTractPatchList(self, coordList)
Definition: ringsSkyMap.py:263
def __init__(self, config, version=1)
Definition: ringsSkyMap.py:77