LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
baseSkyMap.py
Go to the documentation of this file.
1# LSST Data Management System
2# Copyright 2008, 2009, 2010 LSST Corporation.
3#
4# This product includes software developed by the
5# LSST Project (http://www.lsst.org/).
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the LSST License Statement and
18# the GNU General Public License along with this program. If not,
19# see <http://www.lsstcorp.org/LegalNotices/>.
20#
21
22"""
23todo: Consider tweaking pixel scale so the average scale is as specified,
24rather than the scale at the center.
25"""
26
27__all__ = ["BaseSkyMapConfig", "BaseSkyMap"]
28
29import hashlib
30import numpy as np
31
32import lsst.geom as geom
33import lsst.pex.config as pexConfig
34from lsst.geom import SpherePoint, Angle, arcseconds, degrees
35from . import detail
36from .tractBuilder import tractBuilderRegistry
37
38
39class BaseSkyMapConfig(pexConfig.Config):
40 tractBuilder = tractBuilderRegistry.makeField(
41 doc="Algorithm for creating patches within the tract.",
42 default="legacy"
43 )
44
45 tractOverlap = pexConfig.Field(
46 doc="minimum overlap between adjacent sky tracts, on the sky (deg)",
47 dtype=float,
48 default=1.0,
49 )
50 pixelScale = pexConfig.Field(
51 doc="nominal pixel scale (arcsec/pixel)",
52 dtype=float,
53 default=0.333
54 )
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",
60 dtype=str,
61 default="STG",
62 )
63 rotation = pexConfig.Field(
64 doc="Rotation for WCS (deg)",
65 dtype=float,
66 default=0,
67 )
68
69 # Backwards compatibility
70 # We can't use the @property decorator because it makes pexConfig sad.
72 """Get the patch inner dimensions, for backwards compatibility.
73
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.
77
78 Returns
79 -------
80 innerDimensions : `list` [`int`, `int`]
81 """
82 return self.tractBuildertractBuilder["legacy"].patchInnerDimensions
83
84 def setPatchInnerDimensions(self, value):
85 """Set the patch inner dimensions, for backwards compatibility.
86
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.
90
91 Parameters
92 ----------
93 value : `list` [`int`, `int`]
94 """
95 self.tractBuildertractBuilder["legacy"].patchInnerDimensions = value
96
97 patchInnerDimensions = property(getPatchInnerDimensions, setPatchInnerDimensions)
98
99 def getPatchBorder(self):
100 """Get the patch border, for backwards compatibility.
101
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.
105
106 Returns
107 -------
108 border: `int`
109 """
110 return self.tractBuildertractBuilder["legacy"].patchBorder
111
112 def setPatchBorder(self, value):
113 """Set the patch border, for backwards compatibility.
114
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.
118
119 Parameters
120 -------
121 border: `int`
122 """
123 self.tractBuildertractBuilder["legacy"].patchBorder = value
124
125 patchBorder = property(getPatchBorder, setPatchBorder)
126
127
129 """A collection of overlapping Tracts that map part or all of the sky.
130
131 See TractInfo for more information.
132
133 Parameters
134 ----------
135 config : `BaseSkyMapConfig` or None (optional)
136 The configuration for this SkyMap; if None use the default config.
137
138 Notes
139 -----
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.
148
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.
152 """
153
154 ConfigClass = BaseSkyMapConfig
155
156 def __init__(self, config=None):
157 if config is None:
158 config = self.ConfigClassConfigClass()
159 config.freeze() # just to be sure, e.g. for pickling
160 self.configconfig = config
161 self._tractInfoList_tractInfoList = []
162 self._wcsFactory_wcsFactory = detail.WcsFactory(
163 pixelScale=Angle(self.configconfig.pixelScale, arcseconds),
164 projection=self.configconfig.projection,
165 rotation=Angle(self.configconfig.rotation, degrees),
166 )
167 self._sha1_sha1 = None
168 self._tractBuilder_tractBuilder = config.tractBuilder.apply()
169
170 def findTract(self, coord):
171 """Find the tract whose center is nearest the specified coord.
172
173 Parameters
174 ----------
175 coord : `lsst.geom.SpherePoint`
176 ICRS sky coordinate to search for.
177
178 Returns
179 -------
180 result : `TractInfo`
181 TractInfo of tract whose center is nearest the specified coord.
182
183 Notes
184 -----
185 - If coord is equidistant between multiple sky tract centers then one
186 is arbitrarily chosen.
187
188 - The default implementation is not very efficient; subclasses may wish
189 to override.
190
191 **Warning:**
192 If tracts do not cover the whole sky then the returned tract may not
193 include the coord.
194 """
195 distTractInfoList = []
196 for i, tractInfo in enumerate(self):
197 angSep = coord.separation(tractInfo.getCtrCoord()).asDegrees()
198 # include index in order to disambiguate identical angSep values
199 distTractInfoList.append((angSep, i, tractInfo))
200 distTractInfoList.sort()
201 return distTractInfoList[0][2]
202
203 def findTractIdArray(self, ra, dec, degrees=False):
204 """Find array of tract IDs with vectorized operations (where supported).
205
206 If a given sky map does not support vectorized operations, then a loop
207 over findTract will be called.
208
209 Parameters
210 ----------
211 ra : `np.ndarray`
212 Array of Right Ascension. Units are radians unless
213 degrees=True.
214 dec : `np.ndarray`
215 Array of Declination. Units are radians unless
216 degrees=True.
217 degrees : `bool`, optional
218 Input ra, dec arrays are degrees if True.
219
220 Returns
221 -------
222 tractId : `np.ndarray`
223 Array of tract IDs
224
225 Notes
226 -----
227 - If coord is equidistant between multiple sky tract centers then one
228 is arbitrarily chosen.
229
230 **Warning:**
231 If tracts do not cover the whole sky then the returned tract may not
232 include the given ra/dec.
233 """
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),
236 np.atleast_1d(dec))]
237
238 tractId = np.array([self.findTractfindTract(coord).getId() for coord in coords])
239
240 return tractId
241
242 def findTractPatchList(self, coordList):
243 """Find tracts and patches that overlap a region.
244
245 Parameters
246 ----------
247 coordList : `list` of `lsst.geom.SpherePoint`
248 List of ICRS sky coordinates to search for.
249
250 Returns
251 -------
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.
255
256 Notes
257 -----
258 **warning:**
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).
262 """
263 retList = []
264 for tractInfo in self:
265 patchList = tractInfo.findPatchList(coordList)
266 if patchList:
267 retList.append((tractInfo, patchList))
268 return retList
269
270 def findClosestTractPatchList(self, coordList):
271 """Find closest tract and patches that overlap coordinates.
272
273 Parameters
274 ----------
275 coordList : `lsst.geom.SpherePoint`
276 List of ICRS sky coordinates to search for.
277
278 Returns
279 -------
280 retList : `list`
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.
284 """
285 retList = []
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))
291 return retList
292
293 def __getitem__(self, ind):
294 return self._tractInfoList_tractInfoList[ind]
295
296 def __iter__(self):
297 return iter(self._tractInfoList_tractInfoList)
298
299 def __len__(self):
300 return len(self._tractInfoList_tractInfoList)
301
302 def __hash__(self):
303 return hash(self.getSha1getSha1())
304
305 def __eq__(self, other):
306 try:
307 return self.getSha1getSha1() == other.getSha1()
308 except AttributeError:
309 return NotImplemented
310
311 def __ne__(self, other):
312 return not (self == other)
313
314 def logSkyMapInfo(self, log):
315 """Write information about a sky map to supplied log
316
317 Parameters
318 ----------
319 log : `lsst.log.Log`
320 Log object that information about skymap will be written
321 """
322 log.info("sky map has %s tracts" % (len(self),))
323 for tractInfo in self:
324 wcs = tractInfo.getWcs()
325 posBox = geom.Box2D(tractInfo.getBBox())
326 pixelPosList = (
327 posBox.getMin(),
328 geom.Point2D(posBox.getMaxX(), posBox.getMinY()),
329 posBox.getMax(),
330 geom.Point2D(posBox.getMinX(), posBox.getMaxY()),
331 )
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]))
337
338 def getSha1(self):
339 """Return a SHA1 hash that uniquely identifies this SkyMap instance.
340
341 Returns
342 -------
343 sha1 : `bytes`
344 A 20-byte hash that uniquely identifies this SkyMap instance.
345
346 Notes
347 -----
348 Subclasses should almost always override ``updateSha1`` instead of
349 this function to add subclass-specific state to the hash.
350 """
351 if self._sha1_sha1 is None:
352 sha1 = hashlib.sha1()
353 sha1.update(type(self).__name__.encode('utf-8'))
354 configPacked = self._tractBuilder_tractBuilder.getPackedConfig(self.configconfig)
355 sha1.update(configPacked)
356 self.updateSha1updateSha1(sha1)
357 self._sha1_sha1 = sha1.digest()
358 return self._sha1_sha1
359
360 def updateSha1(self, sha1):
361 """Add subclass-specific state or configuration options to the SHA1.
362
363 Parameters
364 ----------
365 sha1 : `hashlib.sha1`
366 A hashlib object on which `update` can be called to add
367 additional state to the hash.
368
369 Notes
370 -----
371 This method is conceptually "protected" : it should be reimplemented by
372 all subclasses, but called only by the base class implementation of
373 `getSha1` .
374 """
375 raise NotImplementedError()
376
377 SKYMAP_RUN_COLLECTION_NAME = "skymaps"
378
379 SKYMAP_DATASET_TYPE_NAME = "skyMap"
380
381 def register(self, name, butler):
382 """Add skymap, tract, and patch Dimension entries to the given Gen3
383 Butler.
384
385 Parameters
386 ----------
387 name : `str`
388 The name of the skymap.
389 butler : `lsst.daf.butler.Butler`
390 The butler to add to.
391
392 Raises
393 ------
394 lsst.daf.butler.registry.ConflictingDefinitionError
395 Raised if a different skymap exists with the same name.
396
397 Notes
398 -----
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.
403
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.
410 """
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)
414
415 skyMapRecord = {
416 "skymap": name,
417 "hash": self.getSha1getSha1(),
418 "tract_max": len(self),
419 "patch_nx_max": nxMax,
420 "patch_ny_max": nyMax,
421 }
422 butler.registry.registerRun(self.SKYMAP_RUN_COLLECTION_NAMESKYMAP_RUN_COLLECTION_NAME)
423 # Kind of crazy that we've got three different capitalizations of
424 # "skymap" here, but that's what the various conventions (or at least
425 # precedents) dictate.
426 from lsst.daf.butler import DatasetType
427 from lsst.daf.butler.registry import ConflictingDefinitionError
428 datasetType = DatasetType(
429 name=self.SKYMAP_DATASET_TYPE_NAMESKYMAP_DATASET_TYPE_NAME,
430 dimensions=["skymap"],
431 storageClass="SkyMap",
432 universe=butler.registry.dimensions
433 )
434 butler.registry.registerDatasetType(datasetType)
435 with butler.transaction():
436 try:
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."
441 ) from err
442 if inserted:
443 for tractInfo in self:
444 tractId = tractInfo.getId()
445 tractRegion = tractInfo.getOuterSkyPolygon()
446 centroid = SpherePoint(tractRegion.getCentroid())
447 tractWcs = tractInfo.getWcs()
448 tractRecord = dict(
449 skymap=name,
450 tract=tractId,
451 region=tractRegion,
452 ra=centroid.getRa().asDegrees(),
453 dec=centroid.getDec().asDegrees(),
454 )
455 butler.registry.insertDimensionData("tract", tractRecord)
456
457 patchRecords = []
458 for patchInfo in tractInfo:
459 xx, yy = patchInfo.getIndex()
460 patchRecords.append(
461 dict(
462 skymap=name,
463 tract=tractId,
464 patch=tractInfo.getSequentialPatchIndex(patchInfo),
465 cell_x=xx,
466 cell_y=yy,
467 region=patchInfo.getOuterSkyPolygon(tractWcs),
468 )
469 )
470 butler.registry.insertDimensionData("patch", *patchRecords)
471
472 butler.put(self, datasetType, {"skymap": name}, run=self.SKYMAP_RUN_COLLECTION_NAMESKYMAP_RUN_COLLECTION_NAME)
473
474 def pack_data_id(self, tract, patch, band=None):
475 """Pack a skymap-based data ID into an integer.
476
477 Parameters
478 ----------
479 tract : `int`
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.
491
492 Returns
493 -------
494 packed : `int`
495 Integer that corresponds to the data ID.
496 max_bits : `int`
497 Maximum number of bits that ``packed`` could have, assuming this
498 skymap and presence or absence of ``band``.
499
500 Notes
501 -----
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"
506 dimension.
507
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.
513 """
514 from lsst.daf.butler import DataCoordinate, DimensionUniverse
515 universe = DimensionUniverse()
516 dummy_skymap_name = "unimportant" # only matters to Gen3 registry
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,
522 hash=self.getSha1getSha1(),
523 tract_max=len(self),
524 patch_nx_max=nx, # assuming these are the same for all tracts for now
525 patch_ny_max=ny,
526 )
527 skymap_data_id = DataCoordinate.standardize(
528 skymap=dummy_skymap_name,
529 universe=universe,
530 ).expanded(
531 records={"skymap": skymap_record},
532 )
533 full_data_id = DataCoordinate.standardize(
534 skymap=dummy_skymap_name,
535 tract=tract_info.getId(),
536 patch=tract_info.getSequentialPatchIndex(patch_info),
537 universe=universe,
538 )
539 if band is None:
540 packer = universe.makePacker("tract_patch", skymap_data_id)
541 else:
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)
int max
table::Key< int > type
Definition: Detector.cc:163
A class representing an angle.
Definition: Angle.h:128
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
This static class includes a variety of methods for interacting with the the logging module.
Definition: Log.h:724
def register(self, name, butler)
Definition: baseSkyMap.py:381
def pack_data_id(self, tract, patch, band=None)
Definition: baseSkyMap.py:474
def findClosestTractPatchList(self, coordList)
Definition: baseSkyMap.py:270
def findTractPatchList(self, coordList)
Definition: baseSkyMap.py:242
def findTractIdArray(self, ra, dec, degrees=False)
Definition: baseSkyMap.py:203
def __init__(self, config=None)
Definition: baseSkyMap.py:156