LSST Applications  21.0.0-147-g0e635eb1+1acddb5be5,22.0.0+052faf71bd,22.0.0+1ea9a8b2b2,22.0.0+6312710a6c,22.0.0+729191ecac,22.0.0+7589c3a021,22.0.0+9f079a9461,22.0.1-1-g7d6de66+b8044ec9de,22.0.1-1-g87000a6+536b1ee016,22.0.1-1-g8e32f31+6312710a6c,22.0.1-10-gd060f87+016f7cdc03,22.0.1-12-g9c3108e+df145f6f68,22.0.1-16-g314fa6d+c825727ab8,22.0.1-19-g93a5c75+d23f2fb6d8,22.0.1-19-gb93eaa13+aab3ef7709,22.0.1-2-g8ef0a89+b8044ec9de,22.0.1-2-g92698f7+9f079a9461,22.0.1-2-ga9b0f51+052faf71bd,22.0.1-2-gac51dbf+052faf71bd,22.0.1-2-gb66926d+6312710a6c,22.0.1-2-gcb770ba+09e3807989,22.0.1-20-g32debb5+b8044ec9de,22.0.1-23-gc2439a9a+fb0756638e,22.0.1-3-g496fd5d+09117f784f,22.0.1-3-g59f966b+1e6ba2c031,22.0.1-3-g849a1b8+f8b568069f,22.0.1-3-gaaec9c0+c5c846a8b1,22.0.1-32-g5ddfab5d3+60ce4897b0,22.0.1-4-g037fbe1+64e601228d,22.0.1-4-g8623105+b8044ec9de,22.0.1-5-g096abc9+d18c45d440,22.0.1-5-g15c806e+57f5c03693,22.0.1-7-gba73697+57f5c03693,master-g6e05de7fdc+c1283a92b8,master-g72cdda8301+729191ecac,w.2021.39
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 """
23 todo: Consider tweaking pixel scale so the average scale is as specified,
24 rather than the scale at the center.
25 """
26 
27 __all__ = ["BaseSkyMapConfig", "BaseSkyMap"]
28 
29 import hashlib
30 import struct
31 import numpy as np
32 
33 import lsst.geom as geom
34 import lsst.pex.config as pexConfig
35 from lsst.geom import SpherePoint, Angle, arcseconds, degrees
36 from . import detail
37 
38 
39 class BaseSkyMapConfig(pexConfig.Config):
40  patchInnerDimensions = pexConfig.ListField(
41  doc="dimensions of inner region of patches (x,y pixels)",
42  dtype=int,
43  length=2,
44  default=(4000, 4000),
45  )
46  patchBorder = pexConfig.Field(
47  doc="border between patch inner and outer bbox (pixels)",
48  dtype=int,
49  default=100,
50  )
51  tractOverlap = pexConfig.Field(
52  doc="minimum overlap between adjacent sky tracts, on the sky (deg)",
53  dtype=float,
54  default=1.0,
55  )
56  pixelScale = pexConfig.Field(
57  doc="nominal pixel scale (arcsec/pixel)",
58  dtype=float,
59  default=0.333
60  )
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",
66  dtype=str,
67  default="STG",
68  )
69  rotation = pexConfig.Field(
70  doc="Rotation for WCS (deg)",
71  dtype=float,
72  default=0,
73  )
74 
75 
76 class BaseSkyMap:
77  """A collection of overlapping Tracts that map part or all of the sky.
78 
79  See TractInfo for more information.
80 
81  Parameters
82  ----------
83  config : `BaseSkyMapConfig` or None (optional)
84  The configuration for this SkyMap; if None use the default config.
85 
86  Notes
87  -----
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.
96 
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.
100  """
101 
102  ConfigClass = BaseSkyMapConfig
103 
104  def __init__(self, config=None):
105  if config is None:
106  config = self.ConfigClassConfigClass()
107  config.freeze() # just to be sure, e.g. for pickling
108  self.configconfig = config
109  self._tractInfoList_tractInfoList = []
110  self._wcsFactory_wcsFactory = detail.WcsFactory(
111  pixelScale=Angle(self.configconfig.pixelScale, arcseconds),
112  projection=self.configconfig.projection,
113  rotation=Angle(self.configconfig.rotation, degrees),
114  )
115  self._sha1_sha1 = None
116 
117  def findTract(self, coord):
118  """Find the tract whose center is nearest the specified coord.
119 
120  Parameters
121  ----------
122  coord : `lsst.geom.SpherePoint`
123  ICRS sky coordinate to search for.
124 
125  Returns
126  -------
127  result : `TractInfo`
128  TractInfo of tract whose center is nearest the specified coord.
129 
130  Notes
131  -----
132  - If coord is equidistant between multiple sky tract centers then one
133  is arbitrarily chosen.
134 
135  - The default implementation is not very efficient; subclasses may wish
136  to override.
137 
138  **Warning:**
139  If tracts do not cover the whole sky then the returned tract may not
140  include the coord.
141  """
142  distTractInfoList = []
143  for i, tractInfo in enumerate(self):
144  angSep = coord.separation(tractInfo.getCtrCoord()).asDegrees()
145  # include index in order to disambiguate identical angSep values
146  distTractInfoList.append((angSep, i, tractInfo))
147  distTractInfoList.sort()
148  return distTractInfoList[0][2]
149 
150  def findTractIdArray(self, ra, dec, degrees=False):
151  """Find array of tract IDs with vectorized operations (where supported).
152 
153  If a given sky map does not support vectorized operations, then a loop
154  over findTract will be called.
155 
156  Parameters
157  ----------
158  ra : `np.ndarray`
159  Array of Right Ascension. Units are radians unless
160  degrees=True.
161  dec : `np.ndarray`
162  Array of Declination. Units are radians unless
163  degrees=True.
164  degrees : `bool`, optional
165  Input ra, dec arrays are degrees if True.
166 
167  Returns
168  -------
169  tractId : `np.ndarray`
170  Array of tract IDs
171 
172  Notes
173  -----
174  - If coord is equidistant between multiple sky tract centers then one
175  is arbitrarily chosen.
176 
177  **Warning:**
178  If tracts do not cover the whole sky then the returned tract may not
179  include the given ra/dec.
180  """
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),
183  np.atleast_1d(dec))]
184 
185  tractId = np.array([self.findTractfindTract(coord).getId() for coord in coords])
186 
187  return tractId
188 
189  def findTractPatchList(self, coordList):
190  """Find tracts and patches that overlap a region.
191 
192  Parameters
193  ----------
194  coordList : `list` of `lsst.geom.SpherePoint`
195  List of ICRS sky coordinates to search for.
196 
197  Returns
198  -------
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.
202 
203  Notes
204  -----
205  **warning:**
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).
209  """
210  retList = []
211  for tractInfo in self:
212  patchList = tractInfo.findPatchList(coordList)
213  if patchList:
214  retList.append((tractInfo, patchList))
215  return retList
216 
217  def findClosestTractPatchList(self, coordList):
218  """Find closest tract and patches that overlap coordinates.
219 
220  Parameters
221  ----------
222  coordList : `lsst.geom.SpherePoint`
223  List of ICRS sky coordinates to search for.
224 
225  Returns
226  -------
227  retList : `list`
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.
231  """
232  retList = []
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))
238  return retList
239 
240  def __getitem__(self, ind):
241  return self._tractInfoList_tractInfoList[ind]
242 
243  def __iter__(self):
244  return iter(self._tractInfoList_tractInfoList)
245 
246  def __len__(self):
247  return len(self._tractInfoList_tractInfoList)
248 
249  def __hash__(self):
250  return hash(self.getSha1getSha1())
251 
252  def __eq__(self, other):
253  try:
254  return self.getSha1getSha1() == other.getSha1()
255  except AttributeError:
256  return NotImplemented
257 
258  def __ne__(self, other):
259  return not (self == other)
260 
261  def logSkyMapInfo(self, log):
262  """Write information about a sky map to supplied log
263 
264  Parameters
265  ----------
266  log : `lsst.log.Log`
267  Log object that information about skymap will be written
268  """
269  log.info("sky map has %s tracts" % (len(self),))
270  for tractInfo in self:
271  wcs = tractInfo.getWcs()
272  posBox = geom.Box2D(tractInfo.getBBox())
273  pixelPosList = (
274  posBox.getMin(),
275  geom.Point2D(posBox.getMaxX(), posBox.getMinY()),
276  posBox.getMax(),
277  geom.Point2D(posBox.getMinX(), posBox.getMaxY()),
278  )
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]))
284 
285  def getSha1(self):
286  """Return a SHA1 hash that uniquely identifies this SkyMap instance.
287 
288  Returns
289  -------
290  sha1 : `bytes`
291  A 20-byte hash that uniquely identifies this SkyMap instance.
292 
293  Notes
294  -----
295  Subclasses should almost always override ``updateSha1`` instead of
296  this function to add subclass-specific state to the hash.
297  """
298  if self._sha1_sha1 is None:
299  sha1 = hashlib.sha1()
300  sha1.update(type(self).__name__.encode('utf-8'))
301  configPacked = struct.pack(
302  "<iiidd3sd",
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
310  )
311  sha1.update(configPacked)
312  self.updateSha1updateSha1(sha1)
313  self._sha1_sha1 = sha1.digest()
314  return self._sha1_sha1
315 
316  def updateSha1(self, sha1):
317  """Add subclass-specific state or configuration options to the SHA1.
318 
319  Parameters
320  ----------
321  sha1 : `hashlib.sha1`
322  A hashlib object on which `update` can be called to add
323  additional state to the hash.
324 
325  Notes
326  -----
327  This method is conceptually "protected" : it should be reimplemented by
328  all subclasses, but called only by the base class implementation of
329  `getSha1` .
330  """
331  raise NotImplementedError()
332 
333  SKYMAP_RUN_COLLECTION_NAME = "skymaps"
334 
335  SKYMAP_DATASET_TYPE_NAME = "skyMap"
336 
337  def register(self, name, butler):
338  """Add skymap, tract, and patch Dimension entries to the given Gen3
339  Butler.
340 
341  Parameters
342  ----------
343  name : `str`
344  The name of the skymap.
345  butler : `lsst.daf.butler.Butler`
346  The butler to add to.
347 
348  Raises
349  ------
350  lsst.daf.butler.registry.ConflictingDefinitionError
351  Raised if a different skymap exists with the same name.
352 
353  Notes
354  -----
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.
359 
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.
366  """
367  nxMax = 0
368  nyMax = 0
369  tractRecords = []
370  patchRecords = []
371  for tractInfo in self:
372  nx, ny = tractInfo.getNumPatches()
373  nxMax = max(nxMax, nx)
374  nyMax = max(nyMax, ny)
375  region = tractInfo.getOuterSkyPolygon()
376  centroid = SpherePoint(region.getCentroid())
377  tractRecords.append({
378  "skymap": name,
379  "tract": tractInfo.getId(),
380  "region": region,
381  "ra": centroid.getRa().asDegrees(),
382  "dec": centroid.getDec().asDegrees(),
383  })
384  for patchInfo in tractInfo:
385  cellX, cellY = patchInfo.getIndex()
386  patchRecords.append({
387  "skymap": name,
388  "tract": tractInfo.getId(),
389  "patch": tractInfo.getSequentialPatchIndex(patchInfo),
390  "cell_x": cellX,
391  "cell_y": cellY,
392  "region": patchInfo.getOuterSkyPolygon(tractInfo.getWcs()),
393  })
394  skyMapRecord = {
395  "skymap": name,
396  "hash": self.getSha1getSha1(),
397  "tract_max": len(self),
398  "patch_nx_max": nxMax,
399  "patch_ny_max": nyMax,
400  }
401  butler.registry.registerRun(self.SKYMAP_RUN_COLLECTION_NAMESKYMAP_RUN_COLLECTION_NAME)
402  # Kind of crazy that we've got three different capitalizations of
403  # "skymap" here, but that's what the various conventions (or at least
404  # precedents) dictate.
405  from lsst.daf.butler import DatasetType
406  from lsst.daf.butler.registry import ConflictingDefinitionError
407  datasetType = DatasetType(
408  name=self.SKYMAP_DATASET_TYPE_NAMESKYMAP_DATASET_TYPE_NAME,
409  dimensions=["skymap"],
410  storageClass="SkyMap",
411  universe=butler.registry.dimensions
412  )
413  butler.registry.registerDatasetType(datasetType)
414  with butler.transaction():
415  try:
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."
420  ) from err
421  if inserted:
422  butler.registry.insertDimensionData("tract", *tractRecords)
423  butler.registry.insertDimensionData("patch", *patchRecords)
424  butler.put(self, datasetType, {"skymap": name}, run=self.SKYMAP_RUN_COLLECTION_NAMESKYMAP_RUN_COLLECTION_NAME)
int max
table::Key< int > type
Definition: Detector.cc:163
A class representing an angle.
Definition: Angle.h:127
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
def register(self, name, butler)
Definition: baseSkyMap.py:337
def findClosestTractPatchList(self, coordList)
Definition: baseSkyMap.py:217
def findTractPatchList(self, coordList)
Definition: baseSkyMap.py:189
def findTractIdArray(self, ra, dec, degrees=False)
Definition: baseSkyMap.py:150
def __init__(self, config=None)
Definition: baseSkyMap.py:104