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"]
35 from lsst.geom import SpherePoint, Angle, arcseconds, degrees
40 patchInnerDimensions = pexConfig.ListField(
41 doc=
"dimensions of inner region of patches (x,y pixels)",
46 patchBorder = pexConfig.Field(
47 doc=
"border between patch inner and outer bbox (pixels)",
51 tractOverlap = pexConfig.Field(
52 doc=
"minimum overlap between adjacent sky tracts, on the sky (deg)",
56 pixelScale = pexConfig.Field(
57 doc=
"nominal pixel scale (arcsec/pixel)",
61 projection = pexConfig.Field(
62 doc=
"one of the FITS WCS projection codes, such as:"
63 "- STG: stereographic projection"
64 "- MOL: Molleweide's projection"
65 "- TAN: tangent-plane projection",
69 rotation = pexConfig.Field(
70 doc=
"Rotation for WCS (deg)",
77 """A collection of overlapping Tracts that map part or all of the sky.
79 See TractInfo for more information.
83 config : `BaseSkyMapConfig` or None (optional)
84 The configuration for this SkyMap; if None use the default config.
88 BaseSkyMap is an abstract base class. Subclasses must do the following:
89 define ``__init__`` and have it construct the TractInfo objects and put
90 them in ``__tractInfoList__`` define ``__getstate__`` and ``__setstate__``
91 to allow pickling (the butler saves sky maps using pickle);
92 see DodecaSkyMap for an example of how to do this. (Most of that code could
93 be moved into this base class, but that would make it harder to handle
94 older versions of pickle data.) define updateSha1 to add any
95 subclass-specific state to the hash.
97 All SkyMap subclasses must be conceptually immutable; they must always
98 refer to the same set of mathematical tracts and patches even if the in-
99 memory representation of those objects changes.
102 ConfigClass = BaseSkyMapConfig
111 pixelScale=
Angle(self.
configconfig.pixelScale, arcseconds),
112 projection=self.
configconfig.projection,
113 rotation=
Angle(self.
configconfig.rotation, degrees),
115 self.
_sha1_sha1 =
None
118 """Find the tract whose center is nearest the specified coord.
122 coord : `lsst.geom.SpherePoint`
123 ICRS sky coordinate to search for.
128 TractInfo of tract whose center is nearest the specified coord.
132 - If coord is equidistant between multiple sky tract centers then one
133 is arbitrarily chosen.
135 - The default implementation is not very efficient; subclasses may wish
139 If tracts do not cover the whole sky then the returned tract may not
142 distTractInfoList = []
143 for i, tractInfo
in enumerate(self):
144 angSep = coord.separation(tractInfo.getCtrCoord()).asDegrees()
146 distTractInfoList.append((angSep, i, tractInfo))
147 distTractInfoList.sort()
148 return distTractInfoList[0][2]
151 """Find array of tract IDs with vectorized operations (where supported).
153 If a given sky map does not support vectorized operations, then a loop
154 over findTract will be called.
159 Array of Right Ascension. Units are radians unless
162 Array of Declination. Units are radians unless
164 degrees : `bool`, optional
165 Input ra, dec arrays are degrees if True.
169 tractId : `np.ndarray`
174 - If coord is equidistant between multiple sky tract centers then one
175 is arbitrarily chosen.
178 If tracts do not cover the whole sky then the returned tract may not
179 include the given ra/dec.
181 units = geom.degrees
if degrees
else geom.radians
182 coords = [
geom.SpherePoint(r*units, d*units)
for r, d
in zip(np.atleast_1d(ra),
185 tractId = np.array([self.
findTractfindTract(coord).getId()
for coord
in coords])
190 """Find tracts and patches that overlap a region.
194 coordList : `list` of `lsst.geom.SpherePoint`
195 List of ICRS sky coordinates to search for.
199 reList : `list` of (`TractInfo`, `list` of `PatchInfo`)
200 For tracts and patches that contain, or may contain, the specified
201 region. The list will be empty if there is no overlap.
206 This uses a naive algorithm that may find some tracts and patches
207 that do not overlap the region (especially if the region is not a
208 rectangle aligned along patch x, y).
211 for tractInfo
in self:
212 patchList = tractInfo.findPatchList(coordList)
214 retList.append((tractInfo, patchList))
218 """Find closest tract and patches that overlap coordinates.
222 coordList : `lsst.geom.SpherePoint`
223 List of ICRS sky coordinates to search for.
228 list of (TractInfo, list of PatchInfo) for tracts and patches
229 that contain, or may contain, the specified region.
230 The list will be empty if there is no overlap.
233 for coord
in coordList:
234 tractInfo = self.
findTractfindTract(coord)
235 patchList = tractInfo.findPatchList(coordList)
236 if patchList
and not (tractInfo, patchList)
in retList:
237 retList.append((tractInfo, patchList))
250 return hash(self.
getSha1getSha1())
254 return self.
getSha1getSha1() == other.getSha1()
255 except AttributeError:
256 return NotImplemented
259 return not (self == other)
262 """Write information about a sky map to supplied log
267 Log object that information about skymap will be written
269 log.info(
"sky map has %s tracts" % (len(self),))
270 for tractInfo
in self:
271 wcs = tractInfo.getWcs()
279 skyPosList = [wcs.pixelToSky(pos).getPosition(geom.degrees)
for pos
in pixelPosList]
280 posStrList = [
"(%0.3f, %0.3f)" % tuple(skyPos)
for skyPos
in skyPosList]
281 log.info(
"tract %s has corners %s (RA, Dec deg) and %s x %s patches" %
282 (tractInfo.getId(),
", ".join(posStrList),
283 tractInfo.getNumPatches()[0], tractInfo.getNumPatches()[1]))
286 """Return a SHA1 hash that uniquely identifies this SkyMap instance.
291 A 20-byte hash that uniquely identifies this SkyMap instance.
295 Subclasses should almost always override ``updateSha1`` instead of
296 this function to add subclass-specific state to the hash.
298 if self.
_sha1_sha1
is None:
299 sha1 = hashlib.sha1()
300 sha1.update(
type(self).__name__.encode(
'utf-8'))
301 configPacked = struct.pack(
303 self.
configconfig.patchInnerDimensions[0],
304 self.
configconfig.patchInnerDimensions[1],
305 self.
configconfig.patchBorder,
306 self.
configconfig.tractOverlap,
307 self.
configconfig.pixelScale,
308 self.
configconfig.projection.encode(
'ascii'),
309 self.
configconfig.rotation
311 sha1.update(configPacked)
313 self.
_sha1_sha1 = sha1.digest()
314 return self.
_sha1_sha1
317 """Add subclass-specific state or configuration options to the SHA1.
321 sha1 : `hashlib.sha1`
322 A hashlib object on which `update` can be called to add
323 additional state to the hash.
327 This method is conceptually "protected" : it should be reimplemented by
328 all subclasses, but called only by the base class implementation of
331 raise NotImplementedError()
333 SKYMAP_RUN_COLLECTION_NAME =
"skymaps"
335 SKYMAP_DATASET_TYPE_NAME =
"skyMap"
338 """Add skymap, tract, and patch Dimension entries to the given Gen3
344 The name of the skymap.
345 butler : `lsst.daf.butler.Butler`
346 The butler to add to.
350 lsst.daf.butler.registry.ConflictingDefinitionError
351 Raised if a different skymap exists with the same name.
355 Registering the same skymap multiple times (with the exact same
356 definition) is safe, but inefficient; most of the work of computing
357 the rows to be inserted must be done first in order to check for
358 consistency between the new skymap and any existing one.
360 Re-registering a skymap with different tract and/or patch definitions
361 but the same summary information may not be detected as a conflict but
362 will never result in updating the skymap; there is intentionally no
363 way to modify a registered skymap (aside from manual administrative
364 operations on the database), as it is hard to guarantee that this can
365 be done without affecting reproducibility.
371 for tractInfo
in self:
372 nx, ny = tractInfo.getNumPatches()
373 nxMax =
max(nxMax, nx)
374 nyMax =
max(nyMax, ny)
375 region = tractInfo.getOuterSkyPolygon()
377 tractRecords.append({
379 "tract": tractInfo.getId(),
381 "ra": centroid.getRa().asDegrees(),
382 "dec": centroid.getDec().asDegrees(),
384 for patchInfo
in tractInfo:
385 cellX, cellY = patchInfo.getIndex()
386 patchRecords.append({
388 "tract": tractInfo.getId(),
389 "patch": tractInfo.getSequentialPatchIndex(patchInfo),
392 "region": patchInfo.getOuterSkyPolygon(tractInfo.getWcs()),
397 "tract_max": len(self),
398 "patch_nx_max": nxMax,
399 "patch_ny_max": nyMax,
405 from lsst.daf.butler
import DatasetType
406 from lsst.daf.butler.registry
import ConflictingDefinitionError
407 datasetType = DatasetType(
409 dimensions=[
"skymap"],
410 storageClass=
"SkyMap",
411 universe=butler.registry.dimensions
413 butler.registry.registerDatasetType(datasetType)
414 with butler.transaction():
416 inserted = butler.registry.syncDimensionData(
"skymap", skyMapRecord)
417 except ConflictingDefinitionError
as err:
418 raise ConflictingDefinitionError(
419 f
"SkyMap with hash {self.getSha1().hex()} is already registered with a different name."
422 butler.registry.insertDimensionData(
"tract", *tractRecords)
423 butler.registry.insertDimensionData(
"patch", *patchRecords)
A class representing an angle.
A floating-point coordinate rectangle geometry.
Point in an unspecified spherical coordinate system.
def findTract(self, coord)
def register(self, name, butler)
def updateSha1(self, sha1)
def findClosestTractPatchList(self, coordList)
string SKYMAP_RUN_COLLECTION_NAME
string SKYMAP_DATASET_TYPE_NAME
def logSkyMapInfo(self, log)
def findTractPatchList(self, coordList)
def findTractIdArray(self, ra, dec, degrees=False)
def __init__(self, config=None)
def __getitem__(self, ind)