23todo: Consider tweaking pixel scale so the average scale is as specified,
24rather than the scale at the center.
27__all__ = [
"BaseSkyMapConfig",
"BaseSkyMap"]
34from lsst.geom import SpherePoint, Angle, arcseconds, degrees
36from .tractBuilder
import tractBuilderRegistry
40 tractBuilder = tractBuilderRegistry.makeField(
41 doc=
"Algorithm for creating patches within the tract.",
45 tractOverlap = pexConfig.Field(
46 doc=
"minimum overlap between adjacent sky tracts, on the sky (deg)",
50 pixelScale = pexConfig.Field(
51 doc=
"nominal pixel scale (arcsec/pixel)",
55 projection = pexConfig.Field(
56 doc=
"one of the FITS WCS projection codes, such as:"
57 "- STG: stereographic projection"
58 "- MOL: Molleweide's projection"
59 "- TAN: tangent-plane projection",
63 rotation = pexConfig.Field(
64 doc=
"Rotation for WCS (deg)",
72 """Get the patch inner dimensions, for backwards compatibility.
74 This value is only used
with the ``legacy`` tract builder,
75 and is ignored otherwise. In general, the config should be
76 accessed directly
with config.tractBuilder[
"legacy"].patchInnerDimensions.
80 innerDimensions : `list` [`int`, `int`]
82 return self.
tractBuildertractBuilder[
"legacy"].patchInnerDimensions
85 """Set the patch inner dimensions, for backwards compatibility.
87 This value is only used
with the ``legacy`` tract builder,
88 and is ignored otherwise. In general, the config should be
89 accessed directly
with config.tractBuilder[
"legacy"].patchInnerDimensions.
93 value : `list` [`int`, `int`]
95 self.tractBuildertractBuilder["legacy"].patchInnerDimensions = value
97 patchInnerDimensions = property(getPatchInnerDimensions, setPatchInnerDimensions)
100 """Get the patch border, for backwards compatibility.
102 This value is only used
with the ``legacy`` tract builder,
103 and is ignored otherwise. In general, the config should be
104 accessed directly
with config.tractBuilder[
"legacy"].patchBorder.
110 return self.
tractBuildertractBuilder[
"legacy"].patchBorder
113 """Set the patch border, for backwards compatibility.
115 This value is only used
with the ``legacy`` tract builder,
116 and is ignored otherwise. In general, the config should be
117 accessed directly
with config.tractBuilder[
"legacy"].patchBorder.
123 self.tractBuildertractBuilder["legacy"].patchBorder = value
125 patchBorder = property(getPatchBorder, setPatchBorder)
129 """A collection of overlapping Tracts that map part or all of the sky.
131 See TractInfo for more information.
135 config : `BaseSkyMapConfig`
or None (optional)
136 The configuration
for this SkyMap;
if None use the default config.
140 BaseSkyMap
is an abstract base
class. Subclasses must do the following:
141 define ``__init__``
and have it construct the TractInfo objects
and put
142 them
in ``__tractInfoList__`` define ``__getstate__``
and ``__setstate__``
143 to allow pickling (the butler saves sky maps using pickle);
144 see DodecaSkyMap
for an example of how to do this. (Most of that code could
145 be moved into this base
class, but that would make it harder to handle
146 older versions of pickle data.) define updateSha1 to add any
147 subclass-specific state to the hash.
149 All SkyMap subclasses must be conceptually immutable; they must always
150 refer to the same set of mathematical tracts
and patches even
if the
in-
151 memory representation of those objects changes.
154 ConfigClass = BaseSkyMapConfig
163 pixelScale=
Angle(self.
configconfig.pixelScale, arcseconds),
164 projection=self.
configconfig.projection,
165 rotation=
Angle(self.
configconfig.rotation, degrees),
167 self.
_sha1_sha1 =
None
168 self.
_tractBuilder_tractBuilder = config.tractBuilder.apply()
171 """Find the tract whose center is nearest the specified coord.
176 ICRS sky coordinate to search for.
181 TractInfo of tract whose center
is nearest the specified coord.
185 - If coord
is equidistant between multiple sky tract centers then one
186 is arbitrarily chosen.
188 - The default implementation
is not very efficient; subclasses may wish
192 If tracts do
not cover the whole sky then the returned tract may
not
195 distTractInfoList = []
196 for i, tractInfo
in enumerate(self):
197 angSep = coord.separation(tractInfo.getCtrCoord()).asDegrees()
199 distTractInfoList.append((angSep, i, tractInfo))
200 distTractInfoList.sort()
201 return distTractInfoList[0][2]
204 """Find array of tract IDs with vectorized operations (where supported).
206 If a given sky map does not support vectorized operations, then a loop
207 over findTract will be called.
212 Array of Right Ascension. Units are radians unless
215 Array of Declination. Units are radians unless
217 degrees : `bool`, optional
218 Input ra, dec arrays are degrees
if True.
222 tractId : `np.ndarray`
227 - If coord
is equidistant between multiple sky tract centers then one
228 is arbitrarily chosen.
231 If tracts do
not cover the whole sky then the returned tract may
not
232 include the given ra/dec.
234 units = geom.degrees if degrees
else geom.radians
235 coords = [
geom.SpherePoint(r*units, d*units)
for r, d
in zip(np.atleast_1d(ra),
238 tractId = np.array([self.
findTractfindTract(coord).getId()
for coord
in coords])
243 """Find tracts and patches that overlap a region.
248 List of ICRS sky coordinates to search for.
252 reList : `list` of (`TractInfo`, `list` of `PatchInfo`)
253 For tracts
and patches that contain,
or may contain, the specified
254 region. The list will be empty
if there
is no overlap.
259 This uses a naive algorithm that may find some tracts
and patches
260 that do
not overlap the region (especially
if the region
is not a
261 rectangle aligned along patch x, y).
264 for tractInfo
in self:
265 patchList = tractInfo.findPatchList(coordList)
267 retList.append((tractInfo, patchList))
271 """Find closest tract and patches that overlap coordinates.
276 List of ICRS sky coordinates to search for.
281 list of (TractInfo, list of PatchInfo)
for tracts
and patches
282 that contain,
or may contain, the specified region.
283 The list will be empty
if there
is no overlap.
286 for coord
in coordList:
287 tractInfo = self.
findTractfindTract(coord)
288 patchList = tractInfo.findPatchList(coordList)
289 if patchList
and not (tractInfo, patchList)
in retList:
290 retList.append((tractInfo, patchList))
303 return hash(self.
getSha1getSha1())
307 return self.
getSha1getSha1() == other.getSha1()
308 except AttributeError:
309 return NotImplemented
312 return not (self == other)
315 """Write information about a sky map to supplied log
320 Log object that information about skymap will be written
322 log.info("sky map has %s tracts" % (len(self),))
323 for tractInfo
in self:
324 wcs = tractInfo.getWcs()
332 skyPosList = [wcs.pixelToSky(pos).getPosition(geom.degrees)
for pos
in pixelPosList]
333 posStrList = [
"(%0.3f, %0.3f)" % tuple(skyPos)
for skyPos
in skyPosList]
334 log.info(
"tract %s has corners %s (RA, Dec deg) and %s x %s patches" %
335 (tractInfo.getId(),
", ".join(posStrList),
336 tractInfo.getNumPatches()[0], tractInfo.getNumPatches()[1]))
339 """Return a SHA1 hash that uniquely identifies this SkyMap instance.
344 A 20-byte hash that uniquely identifies this SkyMap instance.
348 Subclasses should almost always override ``updateSha1`` instead of
349 this function to add subclass-specific state to the hash.
351 if self.
_sha1_sha1
is None:
352 sha1 = hashlib.sha1()
353 sha1.update(
type(self).__name__.encode(
'utf-8'))
355 sha1.update(configPacked)
357 self.
_sha1_sha1 = sha1.digest()
358 return self.
_sha1_sha1
361 """Add subclass-specific state or configuration options to the SHA1.
365 sha1 : `hashlib.sha1`
366 A hashlib object on which `update` can be called to add
367 additional state to the hash.
371 This method is conceptually
"protected" : it should be reimplemented by
372 all subclasses, but called only by the base
class implementation of
375 raise NotImplementedError()
377 SKYMAP_RUN_COLLECTION_NAME =
"skymaps"
379 SKYMAP_DATASET_TYPE_NAME =
"skyMap"
382 """Add skymap, tract, and patch Dimension entries to the given Gen3
388 The name of the skymap.
389 butler : `lsst.daf.butler.Butler`
390 The butler to add to.
394 lsst.daf.butler.registry.ConflictingDefinitionError
395 Raised if a different skymap exists
with the same name.
399 Registering the same skymap multiple times (
with the exact same
400 definition)
is safe, but inefficient; most of the work of computing
401 the rows to be inserted must be done first
in order to check
for
402 consistency between the new skymap
and any existing one.
404 Re-registering a skymap
with different tract
and/
or patch definitions
405 but the same summary information may
not be detected
as a conflict but
406 will never result
in updating the skymap; there
is intentionally no
407 way to modify a registered skymap (aside
from manual administrative
408 operations on the database),
as it
is hard to guarantee that this can
409 be done without affecting reproducibility.
411 numPatches = [tractInfo.getNumPatches() for tractInfo
in self]
412 nxMax =
max(nn[0]
for nn
in numPatches)
413 nyMax =
max(nn[1]
for nn
in numPatches)
418 "tract_max": len(self),
419 "patch_nx_max": nxMax,
420 "patch_ny_max": nyMax,
426 from lsst.daf.butler
import DatasetType
427 from lsst.daf.butler.registry
import ConflictingDefinitionError
428 datasetType = DatasetType(
430 dimensions=[
"skymap"],
431 storageClass=
"SkyMap",
432 universe=butler.registry.dimensions
434 butler.registry.registerDatasetType(datasetType)
435 with butler.transaction():
437 inserted = butler.registry.syncDimensionData(
"skymap", skyMapRecord)
438 except ConflictingDefinitionError
as err:
439 raise ConflictingDefinitionError(
440 f
"SkyMap with hash {self.getSha1().hex()} is already registered with a different name."
443 for tractInfo
in self:
444 tractId = tractInfo.getId()
445 tractRegion = tractInfo.getOuterSkyPolygon()
447 tractWcs = tractInfo.getWcs()
452 ra=centroid.getRa().asDegrees(),
453 dec=centroid.getDec().asDegrees(),
455 butler.registry.insertDimensionData(
"tract", tractRecord)
458 for patchInfo
in tractInfo:
459 xx, yy = patchInfo.getIndex()
464 patch=tractInfo.getSequentialPatchIndex(patchInfo),
467 region=patchInfo.getOuterSkyPolygon(tractWcs),
470 butler.registry.insertDimensionData(
"patch", *patchRecords)
475 """Pack a skymap-based data ID into an integer.
480 Integer ID for the tract.
481 patch : `tuple` (`int`)
or `int`
482 Either a 2-element (x, y) tuple (Gen2 patch ID)
or a single integer
483 (Gen3 patch ID, corresponding to the
"sequential" patch index
484 methods
in this package).
485 band : `str`, optional
486 If provided, a filter name present
in
487 `SkyMapDimensionPacker.SUPPORTED_FILTERS` (which
is aspirationally
488 a list of all Gen3
'bands', but
in practice may be missing some;
489 see RFC-785). If
not provided, the packing algorithm that does
490 not include the filter will be used.
495 Integer that corresponds to the data ID.
497 Maximum number of bits that ``packed`` could have, assuming this
498 skymap
and presence
or absence of ``band``.
502 This method uses a Gen3 `lsst.daf.butler.DimensionPacker` object under
503 the hood to guarantee consistency
with pure Gen3 code, but it does
not
504 require the caller to actually have a Gen3 butler available. It does,
505 however, require a filter value compatible
with the Gen3
"band"
508 This
is a temporary interface intended to aid
with the migration
from
509 Gen2 to Gen3 middleware. It will be removed
with the Gen2 middleware
510 or when DM-31924 provides a longer-term replacement, whichever comes
511 first. Pure Gen3 code should use `lsst.daf.butler.DataCoordinate.pack`
512 or other `lsst.daf.butler.DimensionPacker` interfaces.
514 from lsst.daf.butler
import DataCoordinate, DimensionUniverse
515 universe = DimensionUniverse()
516 dummy_skymap_name =
"unimportant"
517 tract_info = self[tract]
518 patch_info = tract_info[patch]
519 nx, ny = tract_info.getNumPatches()
520 skymap_record = universe[
"skymap"].RecordClass(
521 name=dummy_skymap_name,
527 skymap_data_id = DataCoordinate.standardize(
528 skymap=dummy_skymap_name,
531 records={
"skymap": skymap_record},
533 full_data_id = DataCoordinate.standardize(
534 skymap=dummy_skymap_name,
535 tract=tract_info.getId(),
536 patch=tract_info.getSequentialPatchIndex(patch_info),
540 packer = universe.makePacker(
"tract_patch", skymap_data_id)
542 packer = universe.makePacker(
"tract_patch_band", skymap_data_id)
543 full_data_id = DataCoordinate.standardize(full_data_id, band=band)
544 return packer.pack(full_data_id, returnMaxBits=
True)
A class representing an angle.
A floating-point coordinate rectangle geometry.
Point in an unspecified spherical coordinate system.
This static class includes a variety of methods for interacting with the the logging module.
def getPatchInnerDimensions(self)
def setPatchBorder(self, value)
def setPatchInnerDimensions(self, value)
def findTract(self, coord)
def register(self, name, butler)
def updateSha1(self, sha1)
def pack_data_id(self, tract, patch, band=None)
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)