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