LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
LSSTDataManagementBasePackage
_base.py
Go to the documentation of this file.
1 # This file is part of afw.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 import numpy as np
22 
23 from lsst.utils import continueClass, TemplateMeta
24 from ._table import BaseRecord, BaseCatalog
25 from ._schema import Key
26 
27 
28 __all__ = ["Catalog"]
29 
30 
31 @continueClass # noqa: F811
32 class BaseRecord:
33 
34  def extract(self, *patterns, **kwds):
35  """Extract a dictionary of {<name>: <field-value>} in which the field names
36  match the given shell-style glob pattern(s).
37 
38  Any number of glob patterns may be passed; the result will be the union of all
39  the result of each glob considered separately.
40 
41  Parameters
42  ----------
43  items : `dict`
44  The result of a call to self.schema.extract(); this will be used
45  instead of doing any new matching, and allows the pattern matching
46  to be reused to extract values from multiple records. This
47  keyword is incompatible with any position arguments and the regex,
48  sub, and ordered keyword arguments.
49  split : `bool`
50  If `True`, fields with named subfields (e.g. points) will be split
51  into separate items in the dict; instead of {"point":
52  lsst.geom.Point2I(2,3)}, for instance, you'd get {"point.x":
53  2, "point.y": 3}. Default is `False`.
54  regex : `str` or `re` pattern object
55  A regular expression to be used in addition to any glob patterns
56  passed as positional arguments. Note that this will be compared
57  with re.match, not re.search.
58  sub : `str`
59  A replacement string (see `re.MatchObject.expand`) used to set the
60  dictionary keys of any fields matched by regex.
61  ordered : `bool`
62  If `True`, a `collections.OrderedDict` will be returned instead of
63  a standard dict, with the order corresponding to the definition
64  order of the `Schema`. Default is `False`.
65  """
66  d = kwds.pop("items", None)
67  split = kwds.pop("split", False)
68  if d is None:
69  d = self.schema.extract(*patterns, **kwds).copy()
70  elif kwds:
71  raise ValueError(
72  "Unrecognized keyword arguments for extract: %s" % ", ".join(kwds.keys()))
73  # must use list because we might be adding/deleting elements
74  for name, schemaItem in list(d.items()):
75  key = schemaItem.key
76  if split and key.HAS_NAMED_SUBFIELDS:
77  for subname, subkey in zip(key.subfields, key.subkeys):
78  d["%s.%s" % (name, subname)] = self.get(subkey)
79  del d[name]
80  else:
81  d[name] = self.get(schemaItem.key)
82  return d
83 
84  def __repr__(self):
85  return "%s\n%s" % (type(self), str(self))
86 
87 
88 class Catalog(metaclass=TemplateMeta):
89 
90  def getColumnView(self):
91  self._columns = self._getColumnView()
92  return self._columns
93 
94  def __getColumns(self):
95  if not hasattr(self, "_columns") or self._columns is None:
96  self._columns = self._getColumnView()
97  return self._columns
98  columns = property(__getColumns, doc="a column view of the catalog")
99 
100  def __getitem__(self, key):
101  """Return the record at index key if key is an integer,
102  return a column if `key` is a string field name or Key,
103  or return a subset of the catalog if key is a slice
104  or boolean NumPy array.
105  """
106  if type(key) is slice:
107  (start, stop, step) = (key.start, key.stop, key.step)
108  if step is None:
109  step = 1
110  if start is None:
111  start = 0
112  if stop is None:
113  stop = len(self)
114  return self.subset(start, stop, step)
115  elif isinstance(key, np.ndarray):
116  if key.dtype == bool:
117  return self.subset(key)
118  raise RuntimeError("Unsupported array type for indexing non-contiguous Catalog: %s" %
119  (key.dtype,))
120  elif isinstance(key, Key) or isinstance(key, str):
121  if not self.isContiguous():
122  if isinstance(key, str):
123  key = self.schema[key].asKey()
124  array = self._getitem_(key)
125  # This array doesn't share memory with the Catalog, so don't let it be modified by
126  # the user who thinks that the Catalog itself is being modified.
127  # Just be aware that this array can only be passed down to C++ as an ndarray::Array<T const>
128  # instead of an ordinary ndarray::Array<T>. If pybind isn't letting it down into C++,
129  # you may have left off the 'const' in the definition.
130  array.flags.writeable = False
131  return array
132  return self.columns[key]
133  else:
134  return self._getitem_(key)
135 
136  def __setitem__(self, key, value):
137  """If ``key`` is an integer, set ``catalog[key]`` to
138  ``value``. Otherwise select column ``key`` and set it to
139  ``value``.
140  """
141  self._columns = None
142  if isinstance(key, Key) or isinstance(key, str):
143  self.columns[key] = value
144  else:
145  return self.set(key, value)
146 
147  def __delitem__(self, key):
148  self._columns = None
149  if isinstance(key, slice):
150  self._delslice_(key)
151  else:
152  self._delitem_(key)
153 
154  def append(self, record):
155  self._columns = None
156  self._append(record)
157 
158  def insert(self, key, value):
159  self._columns = None
160  self._insert(key, value)
161 
162  def clear(self):
163  self._columns = None
164  self._clear()
165 
166  def addNew(self):
167  self._columns = None
168  return self._addNew()
169 
170  def cast(self, type_, deep=False):
171  """Return a copy of the catalog with the given type.
172 
173  Parameters
174  ----------
175  type_ :
176  Type of catalog to return.
177  deep : `bool`, optional
178  If `True`, clone the table and deep copy all records.
179 
180  Returns
181  -------
182  copy :
183  Copy of catalog with the requested type.
184  """
185  if deep:
186  table = self.table.clone()
187  table.preallocate(len(self))
188  else:
189  table = self.table
190  copy = type_(table)
191  copy.extend(self, deep=deep)
192  return copy
193 
194  def copy(self, deep=False):
195  """
196  Copy a catalog (default is not a deep copy).
197  """
198  return self.cast(type(self), deep)
199 
200  def extend(self, iterable, deep=False, mapper=None):
201  """Append all records in the given iterable to the catalog.
202 
203  Parameters
204  ----------
205  iterable :
206  Any Python iterable containing records.
207  deep : `bool`, optional
208  If `True`, the records will be deep-copied; ignored if
209  mapper is not `None` (that always implies `True`).
210  mapper : `lsst.afw.table.schemaMapper.SchemaMapper`, optional
211  Used to translate records.
212  """
213  self._columns = None
214  # We can't use isinstance here, because the SchemaMapper symbol isn't available
215  # when this code is part of a subclass of Catalog in another package.
216  if type(deep).__name__ == "SchemaMapper":
217  mapper = deep
218  deep = None
219  if isinstance(iterable, type(self)):
220  if mapper is not None:
221  self._extend(iterable, mapper)
222  else:
223  self._extend(iterable, deep)
224  else:
225  for record in iterable:
226  if mapper is not None:
227  self._append(self.table.copyRecord(record, mapper))
228  elif deep:
229  self._append(self.table.copyRecord(record))
230  else:
231  self._append(record)
232 
233  def __reduce__(self):
234  import lsst.afw.fits
235  return lsst.afw.fits.reduceToFits(self)
236 
237  def asAstropy(self, cls=None, copy=False, unviewable="copy"):
238  """Return an astropy.table.Table (or subclass thereof) view into this catalog.
239 
240  Parameters
241  ----------
242  cls :
243  Table subclass to use; `None` implies `astropy.table.Table`
244  itself. Use `astropy.table.QTable` to get Quantity columns.
245  copy : bool, optional
246  If `True`, copy data from the LSST catalog to the astropy
247  table. Not copying is usually faster, but can keep memory
248  from being freed if columns are later removed from the
249  Astropy view.
250  unviewable : `str`, optional
251  One of the following options (which is ignored if
252  copy=`True` ), indicating how to handle field types (`str`
253  and `Flag`) for which views cannot be constructed:
254  - 'copy' (default): copy only the unviewable fields.
255  - 'raise': raise ValueError if unviewable fields are present.
256  - 'skip': do not include unviewable fields in the Astropy Table.
257 
258  Returns
259  -------
260  cls : `astropy.table.Table`
261  Astropy view into the catalog.
262 
263  Raises
264  ------
265  ValueError
266  Raised if the `unviewable` option is not a known value, or
267  if the option is 'raise' and an uncopyable field is found.
268 
269  """
270  import astropy.table
271  if cls is None:
272  cls = astropy.table.Table
273  if unviewable not in ("copy", "raise", "skip"):
274  raise ValueError(
275  "'unviewable'=%r must be one of 'copy', 'raise', or 'skip'" % (unviewable,))
276  ps = self.getMetadata()
277  meta = ps.toOrderedDict() if ps is not None else None
278  columns = []
279  items = self.schema.extract("*", ordered=True)
280  for name, item in items.items():
281  key = item.key
282  unit = item.field.getUnits() or None # use None instead of "" when empty
283  if key.getTypeString() == "String":
284  if not copy:
285  if unviewable == "raise":
286  raise ValueError("Cannot extract string "
287  "unless copy=True or unviewable='copy' or 'skip'.")
288  elif unviewable == "skip":
289  continue
290  data = np.zeros(
291  len(self), dtype=np.dtype((str, key.getSize())))
292  for i, record in enumerate(self):
293  data[i] = record.get(key)
294  elif key.getTypeString() == "Flag":
295  if not copy:
296  if unviewable == "raise":
297  raise ValueError("Cannot extract packed bit columns "
298  "unless copy=True or unviewable='copy' or 'skip'.")
299  elif unviewable == "skip":
300  continue
301  data = self.columns.get_bool_array(key)
302  elif key.getTypeString() == "Angle":
303  data = self.columns.get(key)
304  unit = "radian"
305  if copy:
306  data = data.copy()
307  elif "Array" in key.getTypeString() and key.isVariableLength():
308  # Can't get columns for variable-length array fields.
309  if unviewable == "raise":
310  raise ValueError("Cannot extract variable-length array fields unless unviewable='skip'.")
311  elif unviewable == "skip" or unviewable == "copy":
312  continue
313  else:
314  data = self.columns.get(key)
315  if copy:
316  data = data.copy()
317  columns.append(
318  astropy.table.Column(
319  data,
320  name=name,
321  unit=unit,
322  description=item.field.getDoc()
323  )
324  )
325  return cls(columns, meta=meta, copy=False)
326 
327  def __dir__(self):
328  """
329  This custom dir is necessary due to the custom getattr below.
330  Without it, not all of the methods available are returned with dir.
331  See DM-7199.
332  """
333  def recursive_get_class_dir(cls):
334  """
335  Return a set containing the names of all methods
336  for a given class *and* all of its subclasses.
337  """
338  result = set()
339  if cls.__bases__:
340  for subcls in cls.__bases__:
341  result |= recursive_get_class_dir(subcls)
342  result |= set(cls.__dict__.keys())
343  return result
344  return sorted(set(dir(self.columns)) | set(dir(self.table)) |
345  recursive_get_class_dir(type(self)) | set(self.__dict__.keys()))
346 
347  def __getattr__(self, name):
348  # Catalog forwards unknown method calls to its table and column view
349  # for convenience. (Feature requested by RHL; complaints about magic
350  # should be directed to him.)
351  if name == "_columns":
352  self._columns = None
353  return None
354  try:
355  return getattr(self.table, name)
356  except AttributeError:
357  return getattr(self.columns, name)
358 
359  def __str__(self):
360  if self.isContiguous():
361  return str(self.asAstropy())
362  else:
363  fields = ' '.join(x.field.getName() for x in self.schema)
364  string = "Non-contiguous afw.Catalog of %d rows.\ncolumns: %s" % (len(self), fields)
365  return string
366 
367  def __repr__(self):
368  return "%s\n%s" % (type(self), self)
369 
370 
371 Catalog.register("Base", BaseCatalog)
def extract(self, patterns, kwds)
Definition: _base.py:34
def __getattr__(self, name)
Definition: _base.py:347
def copy(self, deep=False)
Definition: _base.py:194
def __setitem__(self, key, value)
Definition: _base.py:136
def extend(self, iterable, deep=False, mapper=None)
Definition: _base.py:200
def insert(self, key, value)
Definition: _base.py:158
daf::base::PropertySet * set
Definition: fits.cc:902
def asAstropy(self, cls=None, copy=False, unviewable="copy")
Definition: _base.py:237
table::Key< int > type
Definition: Detector.cc:163
def __getitem__(self, key)
Definition: _base.py:100
def append(self, record)
Definition: _base.py:154
def __delitem__(self, key)
Definition: _base.py:147
def cast(self, type_, deep=False)
Definition: _base.py:170
def get(cls, key, default=None)
Definition: wrappers.py:477
daf::base::PropertyList * list
Definition: fits.cc:903