LSSTApplications  18.1.0
LSSTDataManagementBasePackage
multiindex.py
Go to the documentation of this file.
1 from __future__ import absolute_import, division, print_function
2 
3 __all__ = ["getIndexPath", "getConfigFromEnvironment", "AstrometryNetCatalog", "generateCache"]
4 
5 from builtins import zip
6 from builtins import range
7 from builtins import object
8 import os
9 
10 import numpy as np
11 from astropy.io import fits
12 
13 import lsst.utils
14 from lsst.log import Log
15 from .astrometry_net import MultiIndex, healpixDistance
16 from .astrometryNetDataConfig import AstrometryNetDataConfig
17 
18 
19 def getIndexPath(fn):
20  """!Get the path to the specified astrometry.net index file
21 
22  No effort is made to confirm that the file exists, so it may be used to
23  locate the path to a non-existent file (e.g., to write).
24 
25  @param[in] fn path to index file; if relative, then relative to
26  astrometry_net_data if that product is setup, else relative to the
27  current working directory
28  @return the absolute path to the index file
29  """
30  if os.path.isabs(fn):
31  return fn
32  try:
33  andir = lsst.utils.getPackageDir('astrometry_net_data')
34  except Exception:
35  # Relative to cwd
36  return os.path.abspath(fn)
37  return os.path.join(andir, fn)
38 
39 
41  """Find the config file from the environment
42 
43  The andConfig.py file is in the astrometry_net_data directory.
44  """
45  try:
46  anDir = lsst.utils.getPackageDir('astrometry_net_data')
47  except Exception:
48  anDir = os.getcwd()
49  andConfigPath = "andConfig.py"
50  if not os.path.exists(andConfigPath):
51  raise RuntimeError("Unable to find andConfig.py in the current directory. "
52  "Did you forget to setup astrometry_net_data?")
53  else:
54  andConfigPath = os.path.join(anDir, "andConfig.py")
55  if not os.path.exists(andConfigPath):
56  raise RuntimeError("Unable to find andConfig.py in astrometry_net_data directory %s" % (anDir,))
57 
58  andConfig = AstrometryNetDataConfig()
59  andConfig.load(andConfigPath)
60  return andConfig
61 
62 
64  """A wrapper for the multiindex_t, which only reads the data when it
65  needs to
66 
67  The MultiIndexCache may be instantiated directly, or via the
68  'fromFilenameList' class method, which loads it from a list of filenames.
69  """
70 
71  def __init__(self, filenameList, healpix, nside):
72  """!Constructor
73 
74  @param filenameList List of filenames; first is the multiindex, then
75  follows the individual index files
76  @param healpix Healpix number
77  @param nside Healpix nside
78  """
79  if len(filenameList) < 2:
80  raise RuntimeError("Insufficient filenames provided for multiindex (%s): expected >= 2" %
81  (filenameList,))
82  self._filenameList = filenameList
83  self._healpix = int(healpix)
84  self._nside = int(nside)
85  self._mi = None
86  self._loaded = False
87  self.log = Log.getDefaultLogger()
88 
89  @classmethod
90  def fromFilenameList(cls, filenameList):
91  """Construct from a list of filenames
92 
93  The list of filenames should contain the multiindex filename first,
94  then the individual index filenames. The healpix and nside are
95  determined by reading the indices, so this is not very efficient.
96  """
97  self = cls(filenameList, 0, 0)
98  self.reload()
99  healpixes = set(self[i].healpix for i in range(len(self)))
100  nsides = set(self[i].hpnside for i in range(len(self)))
101  assert len(healpixes) == 1
102  assert len(nsides) == 1
103  self._healpix = healpixes.pop()
104  self._nside = nsides.pop()
105  return self
106 
107  def read(self):
108  """Read the indices"""
109  if self._mi is not None:
110  return
111  fn = getIndexPath(self._filenameList[0])
112  if not os.path.exists(fn):
113  raise RuntimeError(
114  "Unable to get filename for astrometry star file %s" % (self._filenameList[0],))
115  self._mi = MultiIndex(fn)
116  if self._mi is None:
117  # Can't proceed at all without stars
118  raise RuntimeError('Failed to read stars from astrometry multiindex filename "%s"' % fn)
119  for i, fn in enumerate(self._filenameList[1:]):
120  if fn is None:
121  self.log.debug('Unable to find index part of multiindex %s', fn)
122  continue
123  fn = getIndexPath(fn)
124  if not os.path.exists(fn):
125  self.log.warn("Unable to get filename for astrometry index %s", fn)
126  continue
127  self.log.debug('Reading index from multiindex file "%s"', fn)
128  self._mi.addIndex(fn, False)
129  ind = self._mi[i]
130  self.log.debug(' index %i, hp %i (nside %i), nstars %i, nquads %i',
131  ind.indexid, ind.healpix, ind.hpnside, ind.nstars, ind.nquads)
132 
133  def reload(self):
134  """Reload the indices."""
135  if self._loaded:
136  return
137  if self._mi is None:
138  self.read()
139  else:
140  self._mi.reload()
141  self._loaded = True
142 
143  def unload(self):
144  """Unload the indices"""
145  if not self._loaded:
146  return
147  self._mi.unload()
148  self._loaded = False
149 
150  def isWithinRange(self, coord, distance):
151  """!Is the index within range of the provided coordinates?
152 
153  @param coord ICRS coordinate to check (lsst.afw.geom.SpherPoint)
154  @param distance Angular distance (lsst.afw.geom.Angle)
155  """
156  return (self._healpix == -1 or healpixDistance(self._healpix, self._nside, coord) <= distance)
157 
158  def __getitem__(self, i):
159  self.reload()
160  return self._mi[i]
161 
162  def __len__(self):
163  return len(self._filenameList) - 1 # The first is the multiindex; the rest are the indices
164 
165  def __iter__(self):
166  self.reload()
167  return iter(self._mi)
168 
169 
171  """An interface to an astrometry.net catalog
172 
173  Behaves like a list of MultiIndexCache (or multiindex_t).
174 
175  These should usually be constructed using the 'fromEnvironment'
176  class method, which wraps the 'fromIndexFiles' and 'fromCache'
177  alternative class methods.
178  """
179  _cacheFilename = "andCache.fits"
180 
181  def __init__(self, andConfig):
182  """!Constructor
183 
184  @param andConfig Configuration (an AstrometryNetDataConfig)
185  """
186  self.config = andConfig
187  cacheName = getIndexPath(self._cacheFilename)
188  if self.config.allowCache and os.path.exists(cacheName):
189  self._initFromCache(cacheName)
190  else:
191  self._initFromIndexFiles(self.config)
192 
193  def _initFromIndexFiles(self, andConfig):
194  """Initialise from the index files in an AstrometryNetDataConfig"""
195  indexFiles = list(zip(andConfig.indexFiles, andConfig.indexFiles)) + andConfig.multiIndexFiles
196  self._multiInds = [MultiIndexCache.fromFilenameList(fnList) for fnList in indexFiles]
197 
198  def writeCache(self):
199  """Write a cache file
200 
201  The cache file is a FITS file with all the required information to
202  build the AstrometryNetCatalog quickly. The first table extension
203  contains a row for each multiindex, storing the healpix and nside
204  values. The second table extension contains a row for each filename
205  in all the multiindexes. The two may be JOINed through the 'id'
206  column.
207  """
208  outName = getIndexPath(self._cacheFilename)
209  numFilenames = sum(len(ind._filenameList) for ind in self._multiInds)
210  maxLength = max(len(fn) for ind in self._multiInds for fn in ind._filenameList) + 1
211 
212  # First table
213  first = fits.BinTableHDU.from_columns([fits.Column(name="id", format="K"),
214  fits.Column(name="healpix", format="K"),
215  fits.Column(name="nside", format="K"),
216  ], nrows=len(self._multiInds))
217  first.data.field("id")[:] = np.arange(len(self._multiInds), dtype=int)
218  first.data.field("healpix")[:] = np.array([ind._healpix for ind in self._multiInds])
219  first.data.field("nside")[:] = np.array([ind._nside for ind in self._multiInds])
220 
221  # Second table
222  second = fits.BinTableHDU.from_columns([fits.Column(name="id", format="K"),
223  fits.Column(name="filename", format="%dA" % (maxLength)),
224  ], nrows=numFilenames)
225  ident = second.data.field("id")
226  filenames = second.data.field("filename")
227  i = 0
228  for j, ind in enumerate(self._multiInds):
229  for fn in ind._filenameList:
230  ident[i] = j
231  filenames[i] = fn
232  i += 1
233 
234  fits.HDUList([fits.PrimaryHDU(), first, second]).writeto(outName, overwrite=True)
235 
236  def _initFromCache(self, filename):
237  """Initialise from a cache file
238 
239  Ingest the cache file written by the 'writeCache' method and
240  use that to quickly instantiate the AstrometryNetCatalog.
241  """
242  with fits.open(filename) as hduList:
243  first = hduList[1].data
244  second = hduList[2].data
245 
246  # first JOIN second USING(id)
247  filenames = {i: [] for i in first.field("id")}
248  for id2, fn in zip(second.field("id"), second.field("filename")):
249  filenames[id2].append(fn)
250  self._multiInds = [MultiIndexCache(filenames[i], hp, nside) for i, hp, nside in
251  zip(first.field("id"), first.field("healpix"), first.field("nside"))]
252 
253  # Check for consistency
254  cacheFiles = set(second.field("filename"))
255  configFiles = set(sum(self.config.multiIndexFiles, []) + self.config.indexFiles)
256  assert(cacheFiles == configFiles)
257 
258  def __getitem__(self, ii):
259  return self._multiInds[ii]
260 
261  def __iter__(self):
262  return iter(self._multiInds)
263 
264  def __len__(self):
265  return len(self._multiInds)
266 
267 
268 def generateCache(andConfig=None):
269  """Generate a cache file"""
270  if andConfig is None:
271  andConfig = getConfigFromEnvironment()
272  catalog = AstrometryNetCatalog(andConfig)
273  try:
274  for index in catalog:
275  index.reload()
276  catalog.writeCache()
277  finally:
278  for index in catalog:
279  index.unload()
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
daf::base::PropertySet * set
Definition: fits.cc:884
def isWithinRange(self, coord, distance)
Is the index within range of the provided coordinates?
Definition: multiindex.py:150
std::string getPackageDir(std::string const &packageName)
return the root directory of a setup package
Definition: packaging.cc:33
lsst::afw::geom::Angle healpixDistance(int hp, int nside, lsst::afw::geom::SpherePoint const &coord)
Calculate the distance from coordinates to a healpix.
Definition: Log.h:691
def getIndexPath(fn)
Get the path to the specified astrometry.net index file.
Definition: multiindex.py:19
def __init__(self, filenameList, healpix, nside)
Constructor.
Definition: multiindex.py:71
int max
daf::base::PropertyList * list
Definition: fits.cc:885