23 todo: Consider tweaking pixel scale so the average scale is as specified, 
   24 rather than the scale at the center. 
   27 __all__ = [
"BaseSkyMapConfig", 
"BaseSkyMap"]
 
   33 import lsst.pex.config 
as pexConfig
 
   34 from lsst.geom import SpherePoint, Angle, arcseconds, degrees
 
   39     patchInnerDimensions = pexConfig.ListField(
 
   40         doc=
"dimensions of inner region of patches (x,y pixels)",
 
   45     patchBorder = pexConfig.Field(
 
   46         doc=
"border between patch inner and outer bbox (pixels)",
 
   50     tractOverlap = pexConfig.Field(
 
   51         doc=
"minimum overlap between adjacent sky tracts, on the sky (deg)",
 
   55     pixelScale = pexConfig.Field(
 
   56         doc=
"nominal pixel scale (arcsec/pixel)",
 
   60     projection = pexConfig.Field(
 
   61         doc=
"one of the FITS WCS projection codes, such as:" 
   62         "- STG: stereographic projection" 
   63         "- MOL: Molleweide's projection" 
   64         "- TAN: tangent-plane projection",
 
   68     rotation = pexConfig.Field(
 
   69         doc=
"Rotation for WCS (deg)",
 
   76     """A collection of overlapping Tracts that map part or all of the sky. 
   78     See TractInfo for more information. 
   82     config : `BaseSkyMapConfig` or None (optional) 
   83         The configuration for this SkyMap; if None use the default config. 
   87     BaseSkyMap is an abstract base class. Subclasses must do the following: 
   88     define ``__init__`` and have it construct the TractInfo objects and put 
   89     them in ``__tractInfoList__`` define ``__getstate__`` and ``__setstate__`` 
   90     to allow pickling (the butler saves sky maps using pickle); 
   91     see DodecaSkyMap for an example of how to do this. (Most of that code could 
   92     be moved into this base class, but that would make it harder to handle 
   93     older versions of pickle data.) define updateSha1 to add any 
   94     subclass-specific state to the hash. 
   96     All SkyMap subclasses must be conceptually immutable; they must always 
   97     refer to the same set of mathematical tracts and patches even if the in- 
   98     memory representation of those objects changes. 
  101     ConfigClass = BaseSkyMapConfig
 
  110             pixelScale=
Angle(self.
config.pixelScale, arcseconds),
 
  111             projection=self.
config.projection,
 
  117         """Find the tract whose center is nearest the specified coord. 
  121         coord : `lsst.geom.SpherePoint` 
  122             ICRS sky coordinate to search for. 
  127             TractInfo of tract whose center is nearest the specified coord. 
  131         - If coord is equidistant between multiple sky tract centers then one 
  132           is arbitrarily chosen. 
  134         - The default implementation is not very efficient; subclasses may wish 
  138         If tracts do not cover the whole sky then the returned tract may not 
  141         distTractInfoList = []
 
  142         for i, tractInfo 
in enumerate(self):
 
  143             angSep = coord.separation(tractInfo.getCtrCoord()).asDegrees()
 
  145             distTractInfoList.append((angSep, i, tractInfo))
 
  146         distTractInfoList.sort()
 
  147         return distTractInfoList[0][2]
 
  150         """Find tracts and patches that overlap a region. 
  154         coordList : `list` of `lsst.geom.SpherePoint` 
  155             List of ICRS sky coordinates to search for. 
  159         reList : `list` of (`TractInfo`, `list` of `PatchInfo`) 
  160             For tracts and patches that contain, or may contain, the specified 
  161             region. The list will be empty if there is no overlap. 
  166             This uses a naive algorithm that may find some tracts and patches 
  167             that do not overlap the region (especially if the region is not a 
  168             rectangle aligned along patch x, y). 
  171         for tractInfo 
in self:
 
  172             patchList = tractInfo.findPatchList(coordList)
 
  174                 retList.append((tractInfo, patchList))
 
  178         """Find closest tract and patches that overlap coordinates. 
  182         coordList : `lsst.geom.SpherePoint` 
  183             List of ICRS sky coordinates to search for. 
  188             list of (TractInfo, list of PatchInfo) for tracts and patches 
  189             that contain, or may contain, the specified region. 
  190             The list will be empty if there is no overlap. 
  193         for coord 
in coordList:
 
  195             patchList = tractInfo.findPatchList(coordList)
 
  196             if patchList 
and not (tractInfo, patchList) 
in retList:
 
  197                 retList.append((tractInfo, patchList))
 
  214             return self.
getSha1() == other.getSha1()
 
  215         except AttributeError:
 
  216             return NotImplemented
 
  219         return not (self == other)
 
  222         """Write information about a sky map to supplied log 
  227             Log object that information about skymap will be written 
  229         log.info(
"sky map has %s tracts" % (len(self),))
 
  230         for tractInfo 
in self:
 
  231             wcs = tractInfo.getWcs()
 
  239             skyPosList = [wcs.pixelToSky(pos).getPosition(geom.degrees) 
for pos 
in pixelPosList]
 
  240             posStrList = [
"(%0.3f, %0.3f)" % tuple(skyPos) 
for skyPos 
in skyPosList]
 
  241             log.info(
"tract %s has corners %s (RA, Dec deg) and %s x %s patches" %
 
  242                      (tractInfo.getId(), 
", ".join(posStrList),
 
  243                       tractInfo.getNumPatches()[0], tractInfo.getNumPatches()[1]))
 
  246         """Return a SHA1 hash that uniquely identifies this SkyMap instance. 
  251             A 20-byte hash that uniquely identifies this SkyMap instance. 
  255         Subclasses should almost always override ``updateSha1`` instead of 
  256         this function to add subclass-specific state to the hash. 
  258         if self.
_sha1 is None:
 
  259             sha1 = hashlib.sha1()
 
  260             sha1.update(
type(self).__name__.encode(
'utf-8'))
 
  261             configPacked = struct.pack(
 
  263                 self.
config.patchInnerDimensions[0],
 
  264                 self.
config.patchInnerDimensions[1],
 
  268                 self.
config.projection.encode(
'ascii'),
 
  271             sha1.update(configPacked)
 
  273             self.
_sha1 = sha1.digest()
 
  277         """Add subclass-specific state or configuration options to the SHA1. 
  281         sha1 : `hashlib.sha1` 
  282             A hashlib object on which `update` can be called to add 
  283             additional state to the hash. 
  287         This method is conceptually "protected" : it should be reimplemented by 
  288         all subclasses, but called only by the base class implementation of 
  291         raise NotImplementedError()
 
  294         """Add SkyMap, Tract, and Patch Dimension entries to the given Gen3 
  300             The name of the skymap. 
  301         registry : `lsst.daf.butler.Registry` 
  302             The registry to add to. 
  311         for tractInfo 
in self:
 
  312             nx, ny = tractInfo.getNumPatches()
 
  313             nxMax = 
max(nxMax, nx)
 
  314             nyMax = 
max(nyMax, ny)
 
  315             region = tractInfo.getOuterSkyPolygon()
 
  319                 "tract": tractInfo.getId(),
 
  321                 "ra": centroid.getRa().asDegrees(),
 
  322                 "dec": centroid.getDec().asDegrees(),
 
  324             for patchInfo 
in tractInfo:
 
  325                 cellX, cellY = patchInfo.getIndex()
 
  328                     "tract": tractInfo.getId(),
 
  329                     "patch": tractInfo.getSequentialPatchIndex(patchInfo),
 
  332                     "region": patchInfo.getOuterSkyPolygon(tractInfo.getWcs()),
 
  334         records[
"skymap"].
append({
 
  337             "tract_max": len(self),
 
  338             "patch_nx_max": nxMax,
 
  339             "patch_ny_max": nyMax,
 
  341         with registry.transaction():
 
  342             for dimension, recordsForDimension 
in records.items():
 
  343                 registry.insertDimensionData(dimension, *recordsForDimension)