23todo: Consider tweaking pixel scale so the average scale is as specified,
24rather than the scale at the center.
27__all__ = [
"BaseSkyMapConfig",
"BaseSkyMap"]
35from 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.
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.
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.
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)
def findTract(self, coord)
def register(self, name, butler)
def updateSha1(self, sha1)
def findClosestTractPatchList(self, coordList)
def logSkyMapInfo(self, log)
string SKYMAP_DATASET_TYPE_NAME
def findTractPatchList(self, coordList)
string SKYMAP_RUN_COLLECTION_NAME
def findTractIdArray(self, ra, dec, degrees=False)
def __init__(self, config=None)
def __getitem__(self, ind)