lsst.skymap  16.0-5-gef99c9f+7
baseSkyMap.py
Go to the documentation of this file.
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 #
22 """
23 @todo
24 - Consider tweaking pixel scale so the average scale is as specified, rather than the scale at the center
25 """
26 import hashlib
27 import struct
28 
29 import lsst.pex.config as pexConfig
30 from lsst.geom import SpherePoint, Angle, arcseconds, degrees
31 from . import detail
32 
33 __all__ = ["BaseSkyMap"]
34 
35 
36 class BaseSkyMapConfig(pexConfig.Config):
37  patchInnerDimensions = pexConfig.ListField(
38  doc="dimensions of inner region of patches (x,y pixels)",
39  dtype=int,
40  length=2,
41  default=(4000, 4000),
42  )
43  patchBorder = pexConfig.Field(
44  doc="border between patch inner and outer bbox (pixels)",
45  dtype=int,
46  default=100,
47  )
48  tractOverlap = pexConfig.Field(
49  doc="minimum overlap between adjacent sky tracts, on the sky (deg)",
50  dtype=float,
51  default=1.0,
52  )
53  pixelScale = pexConfig.Field(
54  doc="nominal pixel scale (arcsec/pixel)",
55  dtype=float,
56  default=0.333
57  )
58  projection = pexConfig.Field(
59  doc="""one of the FITS WCS projection codes, such as:
60  - STG: stereographic projection
61  - MOL: Molleweide's projection
62  - TAN: tangent-plane projection
63  """,
64  dtype=str,
65  default="STG",
66  )
67  rotation = pexConfig.Field(
68  doc="Rotation for WCS (deg)",
69  dtype=float,
70  default=0,
71  )
72 
73 
74 class BaseSkyMap:
75  """A collection of overlapping Tracts that map part or all of the sky.
76 
77  See TractInfo for more information.
78 
79  BaseSkyMap is an abstract base class. Subclasses must do the following:
80  @li define __init__ and have it construct the TractInfo objects and put them in _tractInfoList
81  @li define __getstate__ and __setstate__ to allow pickling (the butler saves sky maps using pickle);
82  see DodecaSkyMap for an example of how to do this. (Most of that code could be moved
83  into this base class, but that would make it harder to handle older versions of pickle data.)
84  @li define updateSha1 to add any subclass-specific state to the hash.
85 
86  All SkyMap subclasses must be conceptually immutable; they must always
87  refer to the same set of mathematical tracts and patches even if the in-
88  memory representation of those objects changes.
89  """
90  ConfigClass = BaseSkyMapConfig
91 
92  def __init__(self, config=None):
93  """Construct a BaseSkyMap
94 
95  @param[in] config: an instance of self.ConfigClass; if None the default config is used
96  """
97  if config is None:
98  config = self.ConfigClass()
99  config.freeze() # just to be sure, e.g. for pickling
100  self.config = config
101  self._tractInfoList = []
103  pixelScale=Angle(self.config.pixelScale, arcseconds),
104  projection=self.config.projection,
105  rotation=Angle(self.config.rotation, degrees),
106  )
107  self._sha1 = None
108 
109  def findTract(self, coord):
110  """Find the tract whose center is nearest the specified coord.
111 
112  @param[in] coord: ICRS sky coordinate (lsst.afw.geom.SpherePoint)
113  @return TractInfo of tract whose center is nearest the specified coord
114 
115  @warning:
116  - if tracts do not cover the whole sky then the returned tract may not include the coord
117 
118  @note
119  - This routine will be more efficient if coord is ICRS.
120  - If coord is equidistant between multiple sky tract centers then one is arbitrarily chosen.
121  - The default implementation is not very efficient; subclasses may wish to override.
122  """
123  distTractInfoList = []
124  for i, tractInfo in enumerate(self):
125  angSep = coord.separation(tractInfo.getCtrCoord()).asDegrees()
126  # include index in order to disambiguate identical angSep values
127  distTractInfoList.append((angSep, i, tractInfo))
128  distTractInfoList.sort()
129  return distTractInfoList[0][2]
130 
131  def findTractPatchList(self, coordList):
132  """Find tracts and patches that overlap a region
133 
134  @param[in] coordList: list of ICRS sky coordinates (lsst.afw.geom.SpherePoint)
135  @return list of (TractInfo, list of PatchInfo) for tracts and patches that contain,
136  or may contain, the specified region. The list will be empty if there is no overlap.
137 
138  @warning this uses a naive algorithm that may find some tracts and patches that do not overlap
139  the region (especially if the region is not a rectangle aligned along patch x,y).
140  """
141  retList = []
142  for tractInfo in self:
143  patchList = tractInfo.findPatchList(coordList)
144  if patchList:
145  retList.append((tractInfo, patchList))
146  return retList
147 
148  def findClosestTractPatchList(self, coordList):
149  """Find closest tract and patches that overlap coordinates
150 
151  @param[in] coordList: list of ICRS sky coordinates (lsst.afw.geom.SpherePoint)
152  @return list of (TractInfo, list of PatchInfo) for tracts and patches that contain,
153  or may contain, the specified region. The list will be empty if there is no overlap.
154  """
155  retList = []
156  for coord in coordList:
157  tractInfo = self.findTract(coord)
158  patchList = tractInfo.findPatchList(coordList)
159  if patchList and not (tractInfo, patchList) in retList:
160  retList.append((tractInfo, patchList))
161  return retList
162 
163  def __getitem__(self, ind):
164  return self._tractInfoList[ind]
165 
166  def __iter__(self):
167  return iter(self._tractInfoList)
168 
169  def __len__(self):
170  return len(self._tractInfoList)
171 
172  def __hash__(self):
173  return hash(self.getSha1())
174 
175  def __eq__(self, other):
176  try:
177  return self.getSha1() == other.getSha1()
178  except AttributeError:
179  return NotImplemented
180 
181  def __ne__(self, other):
182  return not (self == other)
183 
184  def getSha1(self):
185  """Return a SHA1 hash that uniquely identifies this SkyMap instance.
186 
187  Returns
188  -------
189  sha1 : bytes
190  A 20-byte hash that uniquely identifies this SkyMap instance.
191 
192  Subclasses should almost always override `updateSha1()` instead of
193  this function to add subclass-specific state to the hash.
194  """
195  if self._sha1 is None:
196  sha1 = hashlib.sha1()
197  sha1.update(type(self).__name__.encode('utf-8'))
198  configPacked = struct.pack(
199  "<iiidd3sd",
200  self.config.patchInnerDimensions[0],
201  self.config.patchInnerDimensions[1],
202  self.config.patchBorder,
203  self.config.tractOverlap,
204  self.config.pixelScale,
205  self.config.projection.encode('ascii'),
206  self.config.rotation
207  )
208  sha1.update(configPacked)
209  self.updateSha1(sha1)
210  self._sha1 = sha1.digest()
211  return self._sha1
212 
213  def updateSha1(self, sha1):
214  """Add subclass-specific state or configuration options to the SHA1.
215 
216  Parameters
217  ----------
218  sha1 : hashlib.sha1
219  A hashlib object on which `update()` can be called to add
220  additional state to the hash.
221 
222  This method is conceptually "protected": it should be reimplemented by
223  all subclasses, but called only by the base class implementation of
224  `getSha1()`.
225  """
226  raise NotImplementedError()
227 
228  def register(self, name, registry):
229  """Add SkyMap, Tract, and Patch DataUnits to the given Gen3 Butler Registry.
230  """
231  registry.addDataUnitEntry("SkyMap", {"skymap": name, "hash": self.getSha1()})
232  for tractInfo in self:
233  region = tractInfo.getOuterSkyPolygon()
234  centroid = SpherePoint(region.getCentroid())
235  registry.addDataUnitEntry(
236  "Tract",
237  {"skymap": name, "tract": tractInfo.getId(),
238  "region": region,
239  "ra": centroid.getRa().asDegrees(),
240  "dec": centroid.getDec().asDegrees()}
241  )
242  for patchInfo in tractInfo:
243  cellX, cellY = patchInfo.getIndex()
244  registry.addDataUnitEntry(
245  "Patch",
246  {"skymap": name, "tract": tractInfo.getId(),
247  "patch": tractInfo.getSequentialPatchIndex(patchInfo),
248  "cell_x": cellX, "cell_y": cellY,
249  "region": patchInfo.getOuterSkyPolygon(tractInfo.getWcs())}
250  )
def findTractPatchList(self, coordList)
Definition: baseSkyMap.py:131
def findClosestTractPatchList(self, coordList)
Definition: baseSkyMap.py:148
def register(self, name, registry)
Definition: baseSkyMap.py:228
def __init__(self, config=None)
Definition: baseSkyMap.py:92