LSST Applications  21.0.0+04719a4bac,21.0.0-1-ga51b5d4+f5e6047307,21.0.0-11-g2b59f77+a9c1acf22d,21.0.0-11-ga42c5b2+86977b0b17,21.0.0-12-gf4ce030+76814010d2,21.0.0-13-g1721dae+760e7a6536,21.0.0-13-g3a573fe+768d78a30a,21.0.0-15-g5a7caf0+f21cbc5713,21.0.0-16-g0fb55c1+b60e2d390c,21.0.0-19-g4cded4ca+71a93a33c0,21.0.0-2-g103fe59+bb20972958,21.0.0-2-g45278ab+04719a4bac,21.0.0-2-g5242d73+3ad5d60fb1,21.0.0-2-g7f82c8f+8babb168e8,21.0.0-2-g8f08a60+06509c8b61,21.0.0-2-g8faa9b5+616205b9df,21.0.0-2-ga326454+8babb168e8,21.0.0-2-gde069b7+5e4aea9c2f,21.0.0-2-gecfae73+1d3a86e577,21.0.0-2-gfc62afb+3ad5d60fb1,21.0.0-25-g1d57be3cd+e73869a214,21.0.0-3-g357aad2+ed88757d29,21.0.0-3-g4a4ce7f+3ad5d60fb1,21.0.0-3-g4be5c26+3ad5d60fb1,21.0.0-3-g65f322c+e0b24896a3,21.0.0-3-g7d9da8d+616205b9df,21.0.0-3-ge02ed75+a9c1acf22d,21.0.0-4-g591bb35+a9c1acf22d,21.0.0-4-g65b4814+b60e2d390c,21.0.0-4-gccdca77+0de219a2bc,21.0.0-4-ge8a399c+6c55c39e83,21.0.0-5-gd00fb1e+05fce91b99,21.0.0-6-gc675373+3ad5d60fb1,21.0.0-64-g1122c245+4fb2b8f86e,21.0.0-7-g04766d7+cd19d05db2,21.0.0-7-gdf92d54+04719a4bac,21.0.0-8-g5674e7b+d1bd76f71f,master-gac4afde19b+a9c1acf22d,w.2021.13
LSST Data Management Base Package
FitsSchemaInputMapper.cc
Go to the documentation of this file.
1 // -*- lsst-c++ -*-
2 
3 #include <array>
4 #include <cmath>
5 #include <cstdint>
6 #include <cstdio>
7 #include <string>
8 #include <algorithm>
9 #include <cctype>
10 
11 #include "boost/regex.hpp"
12 #include "boost/multi_index_container.hpp"
13 #include "boost/multi_index/sequenced_index.hpp"
14 #include "boost/multi_index/ordered_index.hpp"
15 #include "boost/multi_index/hashed_index.hpp"
16 #include "boost/multi_index/member.hpp"
17 
18 #include "lsst/log/Log.h"
19 #include "lsst/geom.h"
22 
23 namespace lsst {
24 namespace afw {
25 namespace table {
26 namespace io {
27 
28 namespace {
29 
30 // A quirk of Boost.MultiIndex (which we use for our container of FitsSchemaItems)
31 // that you have to use a special functor (like this one) to set data members
32 // in a container with set indices (because setting those values might require
33 // the element to be moved to a different place in the set). Check out
34 // the Boost.MultiIndex docs for more information.
35 template <std::string FitsSchemaItem::*Member>
36 struct SetFitsSchemaString {
37  void operator()(FitsSchemaItem &item) { item.*Member = _v; }
38  explicit SetFitsSchemaString(std::string const &v) : _v(v) {}
39 
40 private:
41  std::string const &_v;
42 };
43 
44 } // namespace
45 
47 public:
48  // A container class (based on Boost.MultiIndex) that provides three sort orders,
49  // on column number, flag bit, and name (ttype). This allows us to insert fields into the
50  // schema in the correct order, regardless of which order they appear in the
51  // FITS header.
52  typedef boost::multi_index_container<
54  boost::multi_index::indexed_by<
55  boost::multi_index::ordered_non_unique<
56  boost::multi_index::member<FitsSchemaItem, int, &FitsSchemaItem::column>>,
57  boost::multi_index::ordered_non_unique<
58  boost::multi_index::member<FitsSchemaItem, int, &FitsSchemaItem::bit>>,
59  boost::multi_index::hashed_unique<
60  boost::multi_index::member<FitsSchemaItem, std::string, &FitsSchemaItem::ttype>>,
61  boost::multi_index::sequenced<>>>
63 
64  // Typedefs for the special functors used to set data members.
65  typedef SetFitsSchemaString<&FitsSchemaItem::ttype> SetTTYPE;
66  typedef SetFitsSchemaString<&FitsSchemaItem::tform> SetTFORM;
67  typedef SetFitsSchemaString<&FitsSchemaItem::tccls> SetTCCLS;
68  typedef SetFitsSchemaString<&FitsSchemaItem::tunit> SetTUNIT;
69  typedef SetFitsSchemaString<&FitsSchemaItem::doc> SetDoc;
70 
71  // Typedefs for the different indices.
76 
77  // Getters for the different indices.
78  ByColumn &byColumn() { return inputs.get<0>(); }
79  ByBit &byBit() { return inputs.get<1>(); }
80  ByName &byName() { return inputs.get<2>(); }
81  AsList &asList() { return inputs.get<3>(); }
82 
83  Impl() : version(0), flagColumn(0), archiveHdu(-1) {}
84 
85  int version;
96 };
97 
98 std::size_t FitsSchemaInputMapper::PREPPED_ROWS_FACTOR = 1 << 15; // determined empirically; see DM-19461.
99 
101  : _impl(std::make_shared<Impl>()) {
102  // Set the table version. If AFW_TABLE_VERSION tag exists, use that
103  // If not, set to 0 if it has an AFW_TYPE, Schema default otherwise (DM-590)
104  if (!metadata.exists("AFW_TYPE")) {
105  _impl->version = lsst::afw::table::Schema::VERSION;
106  }
107  _impl->version = metadata.get("AFW_TABLE_VERSION", _impl->version);
108  _impl->type = metadata.get("AFW_TYPE", _impl->type);
109  if (stripMetadata) {
110  metadata.remove("AFW_TABLE_VERSION");
111  }
112  if (stripMetadata) {
113  metadata.remove("AFW_TYPE");
114  }
115 
116  // Find a key that indicates an Archive stored on other HDUs
117  _impl->archiveHdu = metadata.get("AR_HDU", -1);
118  if (_impl->archiveHdu > 0) {
119  --_impl->archiveHdu; // AR_HDU is 1-indexed for historical reasons (RFC-304; see Source.cc)
120  if (stripMetadata) {
121  metadata.remove("AR_HDU");
122  }
123  }
124 
125  // Read aliases, stored as header entries with key 'ALIAS'
126  try {
127  std::vector<std::string> rawAliases = metadata.getArray<std::string>("ALIAS");
128  for (std::vector<std::string>::const_iterator i = rawAliases.begin(); i != rawAliases.end(); ++i) {
129  std::size_t pos = i->find_first_of(':');
130  if (pos == std::string::npos) {
132  (boost::format("Malformed alias definition: '%s'") % (*i)).str());
133  }
134  _impl->schema.getAliasMap()->set(i->substr(0, pos), i->substr(pos + 1, std::string::npos));
135  }
136  if (stripMetadata) {
137  metadata.remove("ALIAS");
138  }
139  } catch (pex::exceptions::NotFoundError &) {
140  // if there are no aliases, just move on
141  }
142 
143  if (_impl->version == 0) {
144  // Read slots saved using an old mechanism in as aliases, since the new slot mechanism delegates
145  // slot definition to the AliasMap.
146  static std::array<std::pair<std::string, std::string>, 7> oldSlotKeys = {
147  {std::make_pair("PSF_FLUX", "slot_PsfFlux"), std::make_pair("AP_FLUX", "slot_ApFlux"),
148  std::make_pair("INST_FLUX", "slot_GaussianFlux"),
149  std::make_pair("MODEL_FLUX", "slot_ModelFlux"),
150  std::make_pair("CALIB_FLUX", "slot_CalibFlux"), std::make_pair("CENTROID", "slot_Centroid"),
151  std::make_pair("SHAPE", "slot_Shape")}};
152  for (std::size_t i = 0; i < oldSlotKeys.size(); ++i) {
153  std::string target = metadata.get(oldSlotKeys[i].first + "_SLOT", std::string(""));
154  if (!target.empty()) {
155  _impl->schema.getAliasMap()->set(oldSlotKeys[i].second, target);
156  if (stripMetadata) {
157  metadata.remove(oldSlotKeys[i].first);
158  metadata.remove(oldSlotKeys[i].first + "_ERR_SLOT");
159  metadata.remove(oldSlotKeys[i].first + "_FLAG_SLOT");
160  }
161  }
162  }
163  }
164 
165  // Read the rest of the header into the intermediate inputs container.
166  std::vector<std::string> keyList = metadata.getOrderedNames();
167  for (std::vector<std::string>::const_iterator key = keyList.begin(); key != keyList.end(); ++key) {
168  if (key->compare(0, 5, "TTYPE") == 0) {
169  int column = std::stoi(key->substr(5)) - 1;
170  auto iter = _impl->byColumn().lower_bound(column);
171  if (iter == _impl->byColumn().end() || iter->column != column) {
172  iter = _impl->byColumn().insert(iter, FitsSchemaItem(column, -1));
173  }
174  std::string v = metadata.get<std::string>(*key);
175  _impl->byColumn().modify(iter, Impl::SetTTYPE(v));
176  if (iter->doc.empty()) { // don't overwrite if already set with TDOCn
177  _impl->byColumn().modify(iter, Impl::SetDoc(metadata.getComment(*key)));
178  }
179  if (stripMetadata) {
180  metadata.remove(*key);
181  }
182  } else if (key->compare(0, 5, "TFLAG") == 0) {
183  int bit = std::stoi(key->substr(5)) - 1;
184  auto iter = _impl->byBit().lower_bound(bit);
185  if (iter == _impl->byBit().end() || iter->bit != bit) {
186  iter = _impl->byBit().insert(iter, FitsSchemaItem(-1, bit));
187  }
188  std::string v = metadata.get<std::string>(*key);
189  _impl->byBit().modify(iter, Impl::SetTTYPE(v));
190  if (iter->doc.empty()) { // don't overwrite if already set with TFDOCn
191  _impl->byBit().modify(iter, Impl::SetDoc(metadata.getComment(*key)));
192  }
193  if (stripMetadata) {
194  metadata.remove(*key);
195  }
196  } else if (key->compare(0, 4, "TDOC") == 0) {
197  int column = std::stoi(key->substr(4)) - 1;
198  auto iter = _impl->byColumn().lower_bound(column);
199  if (iter == _impl->byColumn().end() || iter->column != column) {
200  iter = _impl->byColumn().insert(iter, FitsSchemaItem(column, -1));
201  }
202  _impl->byColumn().modify(iter, Impl::SetDoc(metadata.get<std::string>(*key)));
203  if (stripMetadata) {
204  metadata.remove(*key);
205  }
206  } else if (key->compare(0, 5, "TFDOC") == 0) {
207  int bit = std::stoi(key->substr(5)) - 1;
208  auto iter = _impl->byBit().lower_bound(bit);
209  if (iter == _impl->byBit().end() || iter->bit != bit) {
210  iter = _impl->byBit().insert(iter, FitsSchemaItem(-1, bit));
211  }
212  _impl->byBit().modify(iter, Impl::SetDoc(metadata.get<std::string>(*key)));
213  if (stripMetadata) {
214  metadata.remove(*key);
215  }
216  } else if (key->compare(0, 5, "TUNIT") == 0) {
217  int column = std::stoi(key->substr(5)) - 1;
218  auto iter = _impl->byColumn().lower_bound(column);
219  if (iter == _impl->byColumn().end() || iter->column != column) {
220  iter = _impl->byColumn().insert(iter, FitsSchemaItem(column, -1));
221  }
222  _impl->byColumn().modify(iter, Impl::SetTUNIT(metadata.get<std::string>(*key)));
223  if (stripMetadata) {
224  metadata.remove(*key);
225  }
226  } else if (key->compare(0, 5, "TCCLS") == 0) {
227  int column = std::stoi(key->substr(5)) - 1;
228  auto iter = _impl->byColumn().lower_bound(column);
229  if (iter == _impl->byColumn().end() || iter->column != column) {
230  iter = _impl->byColumn().insert(iter, FitsSchemaItem(column, -1));
231  }
232  _impl->byColumn().modify(iter, Impl::SetTCCLS(metadata.get<std::string>(*key)));
233  if (stripMetadata) {
234  metadata.remove(*key);
235  }
236  } else if (key->compare(0, 5, "TFORM") == 0) {
237  int column = std::stoi(key->substr(5)) - 1;
238  auto iter = _impl->byColumn().lower_bound(column);
239  if (iter == _impl->byColumn().end() || iter->column != column) {
240  iter = _impl->byColumn().insert(iter, FitsSchemaItem(column, -1));
241  }
242  _impl->byColumn().modify(iter, Impl::SetTFORM(metadata.get<std::string>(*key)));
243  if (stripMetadata) {
244  metadata.remove(*key);
245  }
246  } else if (key->compare(0, 5, "TZERO") == 0) {
247  if (stripMetadata) {
248  metadata.remove(*key);
249  }
250  } else if (key->compare(0, 5, "TSCAL") == 0) {
251  if (stripMetadata) {
252  metadata.remove(*key);
253  }
254  } else if (key->compare(0, 5, "TNULL") == 0) {
255  if (stripMetadata) {
256  metadata.remove(*key);
257  }
258  } else if (key->compare(0, 5, "TDISP") == 0) {
259  if (stripMetadata) {
260  metadata.remove(*key);
261  }
262  }
263  }
264 
265  // Find the column used to store flags, and setup the flag-handling data members from it.
266  _impl->flagColumn = metadata.get("FLAGCOL", 0);
267  if (_impl->flagColumn > 0) {
268  if (stripMetadata) {
269  metadata.remove("FLAGCOL");
270  }
271  --_impl->flagColumn; // switch from 1-indexed to 0-indexed
272  auto iter = _impl->byColumn().find(_impl->flagColumn);
273  if (iter == _impl->byColumn().end()) {
274  throw LSST_EXCEPT(
276  (boost::format("Column for flag data not found; FLAGCOL=%d") % _impl->flagColumn).str());
277  }
278  // Regex to unpack a FITS TFORM value for a bit array column (TFORM code 'X'). The number
279  // that precedes the code is the size of the array; the number that follows it (if present)
280  // is ignored.
281  static boost::regex const regex("(\\d+)?X\\(?(\\d)*\\)?", boost::regex::perl);
282  boost::smatch m;
283  if (!boost::regex_match(iter->tform, m, regex)) {
284  throw LSST_EXCEPT(
286  (boost::format("Invalid TFORM key for flags column: '%s'") % iter->tform).str());
287  }
288  int nFlags = 1;
289  if (m[1].matched) {
290  nFlags = std::stoi(m[1].str());
291  }
292  _impl->flagKeys.resize(nFlags);
293  _impl->flagWorkspace.reset(new bool[nFlags]);
294  // Delete the flag column from the input list so we don't interpret it as a
295  // regular field.
296  _impl->byColumn().erase(iter);
297  }
298 }
299 
305 
306 void FitsSchemaInputMapper::setArchive(std::shared_ptr<InputArchive> archive) { _impl->archive = archive; }
307 
309  int oldHdu = fits.getHdu();
310  if (_impl->archiveHdu < 0) _impl->archiveHdu = oldHdu + 1;
311  try {
312  fits.setHdu(_impl->archiveHdu);
313  _impl->archive.reset(new io::InputArchive(InputArchive::readFits(fits)));
314  fits.setHdu(oldHdu);
315  return true;
316  } catch (afw::fits::FitsError &) {
317  fits.status = 0;
318  fits.setHdu(oldHdu);
319  _impl->archiveHdu = -1;
320  return false;
321  }
322 }
323 
324 bool FitsSchemaInputMapper::hasArchive() const { return static_cast<bool>(_impl->archive); }
325 
327  auto iter = _impl->byName().find(ttype);
328  if (iter == _impl->byName().end()) {
329  return 0;
330  }
331  return &(*iter);
332 }
333 
334 FitsSchemaItem const *FitsSchemaInputMapper::find(int column) const {
335  auto iter = _impl->byColumn().lower_bound(column);
336  if (iter == _impl->byColumn().end() || iter->column != column) {
337  return 0;
338  }
339  return &(*iter);
340 }
341 
343  auto iter = _impl->byColumn().lower_bound(item->column);
344  assert(iter != _impl->byColumn().end() && iter->column == item->column);
345  _impl->byColumn().erase(iter);
346 }
347 
349  auto iter = _impl->byName().find(ttype);
350  if (iter != _impl->byName().end() && iter->ttype == ttype) {
351  _impl->byName().erase(iter);
352  }
353 }
354 
356  auto iter = _impl->byColumn().lower_bound(column);
357  if (iter != _impl->byColumn().end() && iter->column == column) {
358  _impl->byColumn().erase(iter);
359  }
360 }
361 
362 void erase(int column);
363 
365  _impl->readers.push_back(std::move(reader));
366 }
367 
368 namespace {
369 
370 template <typename T>
371 class StandardReader : public FitsColumnReader {
372 public:
374  FieldBase<T> const &base = FieldBase<T>()) {
375  return std::unique_ptr<FitsColumnReader>(new StandardReader(schema, item, base));
376  }
377 
378  StandardReader(Schema &schema, FitsSchemaItem const &item, FieldBase<T> const &base)
379  : _column(item.column), _key(schema.addField<T>(item.ttype, item.doc, item.tunit, base)),
380  _cache(), _cacheFirstRow(0)
381  {}
382 
383  void prepRead(std::size_t firstRow, std::size_t nRows, fits::Fits & fits) override {
384  // We only prep and cache scalar-valued columns, not array-valued
385  // columns, as apparently the order CFITSIO reads array-valued columns
386  // is not the order we want.
387  if (_key.getElementCount() == 1u) {
388  std::size_t nElements = nRows*_key.getElementCount();
389  _cache.resize(nElements);
390  _cacheFirstRow = firstRow;
391  fits.readTableArray(firstRow, _column, nElements, &_cache.front());
392  }
393  }
394 
395  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
396  std::shared_ptr<InputArchive> const &archive) const override {
397  if (_cache.empty()) {
398  fits.readTableArray(row, _column, _key.getElementCount(), record.getElement(_key));
399  } else {
400  assert(row >= _cacheFirstRow);
401  std::size_t offset = row - _cacheFirstRow;
402  assert(offset < _cache.size());
403  std::copy_n(_cache.begin() + offset, _key.getElementCount(), record.getElement(_key));
404  }
405  }
406 
407 private:
408  int _column;
409  Key<T> _key;
411  std::size_t _cacheFirstRow;
412  std::size_t _nRowsToPrep;
413 };
414 
415 class AngleReader : public FitsColumnReader {
416 public:
418  Schema &schema, FitsSchemaItem const &item,
419  FieldBase<lsst::geom::Angle> const &base = FieldBase<lsst::geom::Angle>()) {
420  return std::unique_ptr<FitsColumnReader>(new AngleReader(schema, item, base));
421  }
422 
423  AngleReader(Schema &schema, FitsSchemaItem const &item, FieldBase<lsst::geom::Angle> const &base)
424  : _column(item.column), _key(schema.addField<lsst::geom::Angle>(item.ttype, item.doc, "", base)) {
425  // We require an LSST-specific key in the headers before parsing a column
426  // as Angle at all, so we don't need to worry about other units or other
427  // spellings of radians. We do continue to support no units for backwards
428  // compatibility.
429  if (!item.tunit.empty() && item.tunit != "rad") {
430  throw LSST_EXCEPT(afw::fits::FitsError,
431  "Angle fields must be persisted in radians (TUNIT='rad').");
432  }
433  }
434 
435  void prepRead(std::size_t firstRow, std::size_t nRows, fits::Fits & fits) override {
436  assert(_key.getElementCount() == 1u);
437  _cache.resize(nRows);
438  _cacheFirstRow = firstRow;
439  fits.readTableArray(firstRow, _column, nRows, &_cache.front());
440  }
441 
442  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
443  std::shared_ptr<InputArchive> const &archive) const override {
444  if (_cache.empty()) {
445  double tmp = 0;
446  fits.readTableScalar(row, _column, tmp);
447  record.set(_key, tmp * lsst::geom::radians);
448  } else {
449  assert(row >= _cacheFirstRow);
450  std::size_t offset = row - _cacheFirstRow;
451  assert(offset < _cache.size());
452  record.set(_key, _cache[offset] * lsst::geom::radians);
453  }
454  }
455 
456 private:
457  int _column;
458  Key<lsst::geom::Angle> _key;
459  std::vector<double> _cache;
460  std::size_t _cacheFirstRow;
461 };
462 
463 class StringReader : public FitsColumnReader {
464 public:
465  static std::unique_ptr<FitsColumnReader> make(Schema &schema, FitsSchemaItem const &item, int size) {
466  return std::unique_ptr<FitsColumnReader>(new StringReader(schema, item, size));
467  }
468 
469  StringReader(Schema &schema, FitsSchemaItem const &item, int size)
470  : _column(item.column),
471  _key(schema.addField<std::string>(item.ttype, item.doc, item.tunit, size)),
472  _isVariableLength(size == 0) {}
473 
474  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
475  std::shared_ptr<InputArchive> const &archive) const override {
476  std::string s;
477  fits.readTableScalar(row, _column, s, _isVariableLength);
478  record.set(_key, s);
479  }
480 
481 private:
482  int _column;
483  Key<std::string> _key;
484  bool _isVariableLength;
485 };
486 
487 template <typename T>
488 class VariableLengthArrayReader : public FitsColumnReader {
489 public:
490  static std::unique_ptr<FitsColumnReader> make(Schema &schema, FitsSchemaItem const &item) {
491  return std::unique_ptr<FitsColumnReader>(new VariableLengthArrayReader(schema, item));
492  }
493 
494  VariableLengthArrayReader(Schema &schema, FitsSchemaItem const &item)
495  : _column(item.column), _key(schema.addField<Array<T>>(item.ttype, item.doc, item.tunit, 0)) {}
496 
497  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
498  std::shared_ptr<InputArchive> const &archive) const override {
499  int size = fits.getTableArraySize(row, _column);
500  ndarray::Array<T, 1, 1> array = ndarray::allocate(size);
501  fits.readTableArray(row, _column, size, array.getData());
502  record.set(_key, array);
503  }
504 
505 private:
506  int _column;
507  Key<Array<T>> _key;
508 };
509 
510 // Read a 2-element FITS array column as separate x and y Schema fields (hence converting
511 // from the old Point compound field to the new PointKey FunctorKey).
512 template <typename T>
513 class PointConversionReader : public FitsColumnReader {
514 public:
515  static std::unique_ptr<FitsColumnReader> make(Schema &schema, FitsSchemaItem const &item) {
516  return std::unique_ptr<FitsColumnReader>(new PointConversionReader(schema, item));
517  }
518 
519  PointConversionReader(Schema &schema, FitsSchemaItem const &item)
520  : _column(item.column), _key(PointKey<T>::addFields(schema, item.ttype, item.doc, item.tunit)) {}
521 
522  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
523  std::shared_ptr<InputArchive> const &archive) const override {
524  std::array<T, 2> buffer;
525  fits.readTableArray(row, _column, 2, buffer.data());
526  record.set(_key, lsst::geom::Point<T, 2>(buffer[0], buffer[1]));
527  }
528 
529 private:
530  int _column;
531  PointKey<T> _key;
532 };
533 
534 // Read a 2-element FITS array column as separate ra and dec Schema fields (hence converting
535 // from the old Coord compound field to the new CoordKey FunctorKey).
536 class CoordConversionReader : public FitsColumnReader {
537 public:
538  static std::unique_ptr<FitsColumnReader> make(Schema &schema, FitsSchemaItem const &item) {
539  return std::unique_ptr<FitsColumnReader>(new CoordConversionReader(schema, item));
540  }
541 
542  CoordConversionReader(Schema &schema, FitsSchemaItem const &item)
543  : _column(item.column), _key(CoordKey::addFields(schema, item.ttype, item.doc)) {}
544 
545  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
546  std::shared_ptr<InputArchive> const &archive) const override {
548  fits.readTableArray(row, _column, 2, buffer.data());
549  record.set(_key, lsst::geom::SpherePoint(buffer[0], buffer[1]));
550  }
551 
552 private:
553  int _column;
554  CoordKey _key;
555 };
556 
557 // Read a 3-element FITS array column as separate xx, yy, and xy Schema fields (hence converting
558 // from the old Moments compound field to the new QuadrupoleKey FunctorKey).
559 class MomentsConversionReader : public FitsColumnReader {
560 public:
561  static std::unique_ptr<FitsColumnReader> make(Schema &schema, FitsSchemaItem const &item) {
562  return std::unique_ptr<FitsColumnReader>(new MomentsConversionReader(schema, item));
563  }
564 
565  MomentsConversionReader(Schema &schema, FitsSchemaItem const &item)
566  : _column(item.column),
567  _key(QuadrupoleKey::addFields(schema, item.ttype, item.doc, CoordinateType::PIXEL)) {}
568 
569  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
570  std::shared_ptr<InputArchive> const &archive) const override {
571  std::array<double, 3> buffer;
572  fits.readTableArray(row, _column, 3, buffer.data());
573  record.set(_key, geom::ellipses::Quadrupole(buffer[0], buffer[1], buffer[2], false));
574  }
575 
576 private:
577  int _column;
578  QuadrupoleKey _key;
579 };
580 
581 // Read a FITS array column representing a packed symmetric matrix into
582 // Schema fields for each element (hence converting from the old Covariance
583 // compound field to the new CovarianceMatrixKey FunctorKey).
584 template <typename T, int N>
585 class CovarianceConversionReader : public FitsColumnReader {
586 public:
587  static std::string guessUnits(std::string const &oldUnits) {
588  static boost::regex const regex("(.*)(\\^(\\d+))?", boost::regex::perl);
589  boost::smatch m;
590  if (!boost::regex_match(oldUnits, m, regex)) {
591  int oldPower = std::stoi(m[2]);
592  int newPower = std::sqrt(oldPower);
593  return std::to_string(newPower);
594  }
595  return oldUnits;
596  }
597 
598  static std::unique_ptr<FitsColumnReader> make(Schema &schema, FitsSchemaItem const &item,
599  std::vector<std::string> const &names) {
600  return std::unique_ptr<FitsColumnReader>(new CovarianceConversionReader(schema, item, names));
601  }
602 
603  CovarianceConversionReader(Schema &schema, FitsSchemaItem const &item,
604  std::vector<std::string> const &names)
605  : _column(item.column),
606  _size(names.size()),
607  _key(CovarianceMatrixKey<T, N>::addFields(schema, item.ttype, names, guessUnits(item.tunit))),
608  _buffer(new T[detail::computeCovariancePackedSize(names.size())]) {}
609 
610  void readCell(BaseRecord &record, std::size_t row, afw::fits::Fits &fits,
611  std::shared_ptr<InputArchive> const &archive) const override {
612  fits.readTableArray(row, _column, detail::computeCovariancePackedSize(_size), _buffer.get());
613  for (int i = 0; i < _size; ++i) {
614  for (int j = i; j < _size; ++j) {
615  _key.setElement(record, i, j, _buffer[detail::indexCovariance(i, j)]);
616  }
617  }
618  }
619 
620 private:
621  int _column;
622  int _size;
623  CovarianceMatrixKey<T, N> _key;
624  std::unique_ptr<T[]> _buffer;
625 };
626 
627 std::unique_ptr<FitsColumnReader> makeColumnReader(Schema &schema, FitsSchemaItem const &item) {
628  // Regex to unpack a FITS TFORM value. The first number is the size of the array (1 if not present),
629  // followed by an alpha code indicating the type (preceded by P or Q for variable size array).
630  // The last number is ignored.
631  static boost::regex const regex("(\\d+)?([PQ])?(\\u)\\(?(\\d)*\\)?", boost::regex::perl);
632  // start by parsing the format; this tells the element type of the field and the number of elements
633  boost::smatch m;
634  if (!boost::regex_match(item.tform, m, regex)) {
636  }
637  int size = 1;
638  if (m[1].matched) {
639  size = std::stoi(m[1].str());
640  }
641  char code = m[3].str()[0];
642  if (m[2].matched) {
643  // P or Q presence indicates a variable-length array, which we can get by just setting the
644  // size to zero and letting the rest of the logic run its course.
645  size = 0;
646  }
647  // switch code over FITS codes that correspond to different element types
648  switch (code) {
649  case 'B': // 8-bit unsigned integers -- can only be scalars or Arrays
650  if (size == 1) {
651  if (item.tccls == "Array") {
652  return StandardReader<Array<std::uint8_t>>::make(schema, item, size);
653  }
654  return StandardReader<std::uint8_t>::make(schema, item);
655  }
656  if (size == 0) {
657  return VariableLengthArrayReader<std::uint8_t>::make(schema, item);
658  }
659  return StandardReader<Array<std::uint8_t>>::make(schema, item, size);
660 
661  case 'I': // 16-bit integers - can only be scalars or Arrays (we assume they're unsigned, since
662  // that's all we ever write, and CFITSIO will complain later if they aren't)
663  if (size == 1) {
664  if (item.tccls == "Array") {
665  return StandardReader<Array<std::uint16_t>>::make(schema, item, size);
666  }
667  return StandardReader<std::uint16_t>::make(schema, item);
668  }
669  if (size == 0) {
670  return VariableLengthArrayReader<std::uint16_t>::make(schema, item);
671  }
672  return StandardReader<Array<std::uint16_t>>::make(schema, item, size);
673  case 'J': // 32-bit integers - can only be scalars, Point fields, or Arrays
674  if (size == 0) {
675  return VariableLengthArrayReader<std::int32_t>::make(schema, item);
676  }
677  if (item.tccls == "Point") {
678  return PointConversionReader<std::int32_t>::make(schema, item);
679  }
680  if (size > 1 || item.tccls == "Array") {
681  return StandardReader<Array<std::int32_t>>::make(schema, item, size);
682  }
683  return StandardReader<std::int32_t>::make(schema, item);
684  case 'K': // 64-bit integers - can only be scalars.
685  if (size == 1) {
686  return StandardReader<std::int64_t>::make(schema, item);
687  }
688  case 'E': // floats
689  if (size == 0) {
690  return VariableLengthArrayReader<float>::make(schema, item);
691  }
692  if (size == 1) {
693  if (item.tccls == "Array") {
694  return StandardReader<Array<float>>::make(schema, item, 1);
695  }
696  // Just use scalars for Covariances of size 1, since that results in more
697  // natural field names (essentially never happens anyway).
698  return StandardReader<float>::make(schema, item);
699  }
700  if (size == 3 && item.tccls == "Covariance(Point)") {
701  std::vector<std::string> names = {"x", "y"};
702  return CovarianceConversionReader<float, 2>::make(schema, item, names);
703  }
704  if (size == 6 && item.tccls == "Covariance(Moments)") {
705  std::vector<std::string> names = {"xx", "yy", "xy"};
706  return CovarianceConversionReader<float, 3>::make(schema, item, names);
707  }
708  if (item.tccls == "Covariance") {
709  double v = 0.5 * (std::sqrt(1 + 8 * size) - 1);
710  int n = std::lround(v);
711  if (n * (n + 1) != size * 2) {
712  throw LSST_EXCEPT(afw::fits::FitsError, "Covariance field has invalid size.");
713  }
714  std::vector<std::string> names(n);
715  for (int i = 0; i < n; ++i) {
716  names[i] = std::to_string(i);
717  }
718  return CovarianceConversionReader<float, Eigen::Dynamic>::make(schema, item, names);
719  }
720  return StandardReader<Array<float>>::make(schema, item, size);
721  case 'D': // doubles
722  if (size == 0) {
723  return VariableLengthArrayReader<double>::make(schema, item);
724  }
725  if (size == 1) {
726  if (item.tccls == "Angle") {
727  return AngleReader::make(schema, item);
728  }
729  if (item.tccls == "Array") {
730  return StandardReader<Array<double>>::make(schema, item, 1);
731  }
732  return StandardReader<double>::make(schema, item);
733  }
734  if (size == 2) {
735  if (item.tccls == "Point") {
736  return PointConversionReader<double>::make(schema, item);
737  }
738  if (item.tccls == "Coord") {
739  return CoordConversionReader::make(schema, item);
740  }
741  }
742  if (size == 3 && item.tccls == "Moments") {
743  return MomentsConversionReader::make(schema, item);
744  }
745  return StandardReader<Array<double>>::make(schema, item, size);
746  case 'A': // strings
747  // StringReader can read both fixed-length and variable-length (size=0) strings
748  return StringReader::make(schema, item, size);
749  default:
751  }
752 }
753 
754 bool endswith(std::string const &s, std::string const &suffix) {
755  return s.size() >= suffix.size() && s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
756 }
757 
758 bool isInstFlux(FitsSchemaItem const & item) {
759  // helper lambda to make reading the real logic easier
760  auto includes = [](std::string const & s, char const * target) {
761  return s.find(target) != std::string::npos;
762  };
763  if (!includes(item.ttype, "flux")) return false;
764  if (includes(item.ttype, "modelfit_CModel") && item.tunit.empty()) {
765  // CModel flux fields were written with no units prior to DM-16068,
766  // but should have been "count".
767  return true;
768  }
769  // transform units to lowercase.
770  std::string units(item.tunit);
771  std::transform(units.begin(), units.end(), units.begin(), [](char c) { return std::tolower(c); } );
772  return includes(units, "count") || includes(units, "dn") || includes (units, "adu");
773 }
774 
775 // Replace 'from' with 'to' in 'full', returning the result.
776 std::string replace(std::string full, std::string const & from, std::string const & to) {
777  return full.replace(full.find(from), from.size(), to);
778 }
779 
780 } // namespace
781 
783  if (_impl->version == 0) {
784  AliasMap &aliases = *_impl->schema.getAliasMap();
785  for (auto iter = _impl->asList().begin(); iter != _impl->asList().end(); ++iter) {
786  std::size_t flagPos = iter->ttype.find("flags");
787  if (flagPos != std::string::npos) {
788  // We want to create aliases that resolve "(.*)_flag" to "$1_flags"; old schemas will have
789  // the latter, but new conventions (including slots) expect the former.
790  // But we can't do that, because adding that alias directly results in a cycle in the
791  // aliases (since aliases do partial matches, and keep trying until there are no matches,
792  // we'd have "(.*)_flag" resolve to "$1_flagssssssssssssss...").
793  // Instead, we *rename* from "flags" to "flag", then create the reverse alias.
794  std::string ttype = iter->ttype;
795  std::string prefix = iter->ttype.substr(0, flagPos);
796  ttype.replace(flagPos, 5, "flag");
797  _impl->asList().modify(iter, Impl::SetTTYPE(ttype));
798  // Note that we're not aliasing the full field, just the first part - if we have multiple
799  // flag fields, one alias should be sufficient for all of them (because of partial matching).
800  // Of course, we'll try to recreate that alias every time we handle another flag field with
801  // the same prefix, but AliasMap know hows to handle that no-op set.
802  aliases.set(prefix + "flags", prefix + "flag");
803  } else if (isInstFlux(*iter)) {
804  // Create an alias that resolves "X_instFlux" to "X" or "X_instFluxErr" to "X_err".
805  if (endswith(iter->ttype, "_err")) {
806  aliases.set(replace(iter->ttype, "_err", "_instFluxErr"), iter->ttype);
807  } else {
808  aliases.set(iter->ttype + "_instFlux", iter->ttype);
809  }
810  } else if (endswith(iter->ttype, "_err")) {
811  // Create aliases that resolve "(.*)_(.*)Err" and "(.*)_(.*)_(.*)_Cov" to
812  // "$1_err_$2Err" and "$1_err_$2_$3_Cov", to make centroid and shape uncertainties
813  // available under the new conventions. We don't have to create aliases for the
814  // centroid and shape values themselves, as those will automatically be correct
815  // after the PointConversionReader and MomentsConversionReader do their work.
816  if (iter->tccls == "Covariance(Point)") {
817  aliases.set(replace(iter->ttype, "_err", "_yErr"), iter->ttype + "_yErr");
818  aliases.set(replace(iter->ttype, "_err", "_xErr"), iter->ttype + "_xErr");
819  aliases.set(replace(iter->ttype, "_err", "_x_y_Cov"), iter->ttype + "_x_y_Cov");
820  } else if (iter->tccls == "Covariance(Moments)") {
821  aliases.set(replace(iter->ttype, "_err", "_xxErr"), iter->ttype + "_xxErr");
822  aliases.set(replace(iter->ttype, "_err", "_yyErr"), iter->ttype + "_yyErr");
823  aliases.set(replace(iter->ttype, "_err", "_xyErr"), iter->ttype + "_xyErr");
824  aliases.set(replace(iter->ttype, "_err", "_xx_yy_Cov"), iter->ttype + "_xx_yy_Cov");
825  aliases.set(replace(iter->ttype, "_err", "_xx_xy_Cov"), iter->ttype + "_xx_xy_Cov");
826  aliases.set(replace(iter->ttype, "_err", "_yy_xy_Cov"), iter->ttype + "_yy_xy_Cov");
827  }
828  }
829  }
830  } else if (_impl->version < 3) {
831  // Version == 1 tables use Sigma when we should use Err (see RFC-333) and had no fields
832  // that should have been named Sigma. So provide aliases xErr -> xSigma.
833  // Version <= 2 tables used _flux when we should use _instFlux (see RFC-322).
834  AliasMap &aliases = *_impl->schema.getAliasMap();
835  for (auto iter = _impl->asList().begin(); iter != _impl->asList().end(); ++iter) {
836  std::string name = iter->ttype;
837  if (_impl->version < 2 && endswith(name, "Sigma")) {
838  name = replace(std::move(name), "Sigma", "Err");
839  }
840  if (_impl->version < 3 && isInstFlux(*iter)) {
841  name = replace(std::move(name), "flux", "instFlux");
842  }
843  if (name != iter->ttype) {
844  aliases.set(name, iter->ttype);
845  }
846  }
847  }
848  for (auto iter = _impl->asList().begin(); iter != _impl->asList().end(); ++iter) {
849  if (iter->bit < 0) { // not a Flag column
850  std::unique_ptr<FitsColumnReader> reader = makeColumnReader(_impl->schema, *iter);
851  if (reader) {
852  _impl->readers.push_back(std::move(reader));
853  } else {
854  LOGLS_WARN("afw.FitsSchemaInputMapper", "Format " << iter->tform << " for column "
855  << iter->ttype
856  << " not supported; skipping.");
857  }
858  } else { // is a Flag column
859  if (static_cast<std::size_t>(iter->bit) >= _impl->flagKeys.size()) {
861  (boost::format("Flag field '%s' is is in bit %d (0-indexed) of only %d") %
862  iter->ttype % iter->bit % _impl->flagKeys.size())
863  .str());
864  }
865  _impl->flagKeys[iter->bit] = _impl->schema.addField<Flag>(iter->ttype, iter->doc);
866  }
867  }
868  _impl->asList().clear();
869  if (_impl->schema.getRecordSize() <= 0) {
870  throw LSST_EXCEPT(
872  (boost::format("Non-positive record size: %d; file is corrupt or invalid.") %
873  _impl->schema.getRecordSize()).str()
874  );
875  }
876  _impl->nRowsToPrep = std::max(PREPPED_ROWS_FACTOR / _impl->schema.getRecordSize(), std::size_t(1));
877  return _impl->schema;
878 }
879 
881  if (!_impl->flagKeys.empty()) {
882  fits.readTableArray<bool>(row, _impl->flagColumn, _impl->flagKeys.size(), _impl->flagWorkspace.get());
883  for (std::size_t bit = 0; bit < _impl->flagKeys.size(); ++bit) {
884  record.set(_impl->flagKeys[bit], _impl->flagWorkspace[bit]);
885  }
886  }
887  if (_impl->nRowsToPrep != 1 && row % _impl->nRowsToPrep == 0) {
888  // Give readers a chance to read and cache up to nRowsToPrep rows-
889  // worth of values.
890  std::size_t size = std::min(_impl->nRowsToPrep, fits.countRows() - row);
891  for (auto & reader : _impl->readers) {
892  reader->prepRead(row, size, fits);
893  }
894  }
895  for (auto const & reader : _impl->readers) {
896  reader->readCell(record, row, fits, _impl->archive);
897  }
898 }
899 } // namespace io
900 } // namespace table
901 } // namespace afw
902 } // namespace lsst
table::Key< std::string > name
Definition: Amplifier.cc:116
Key< Flag > const & target
table::Key< int > type
Definition: Detector.cc:163
#define LSST_EXCEPT(type,...)
Create an exception with a given type.
Definition: Exception.h:48
Fits * fits
Definition: FitsWriter.cc:90
int nFlags
Definition: FitsWriter.cc:91
LSST DM logging module built on log4cxx.
#define LOGLS_WARN(logger, message)
Log a warn-level message using an iostream-based interface.
Definition: Log.h:648
Key< U > key
Definition: Schema.cc:281
std::string prefix
Definition: SchemaMapper.cc:79
int m
Definition: SpanSet.cc:49
table::Key< int > from
table::Key< int > to
table::Schema schema
Definition: python.h:134
T begin(T... args)
An exception thrown when problems are found when reading or writing FITS files.
Definition: fits.h:36
A simple struct that combines the two arguments that must be passed to most cfitsio routines and cont...
Definition: fits.h:297
Mapping class that holds aliases for a Schema.
Definition: AliasMap.h:36
void set(std::string const &alias, std::string const &target)
Add an alias to the schema or replace an existing one.
Definition: AliasMap.cc:82
Base class for all records.
Definition: BaseRecord.h:31
void set(Key< T > const &key, U const &value)
Set value of a field for the given key.
Definition: BaseRecord.h:164
Defines the fields and offsets for a table.
Definition: Schema.h:50
static int const VERSION
Definition: Schema.h:56
Polymorphic reader interface used to read different kinds of objects from one or more FITS binary tab...
std::vector< std::unique_ptr< FitsColumnReader > > readers
SetFitsSchemaString<&FitsSchemaItem::tccls > SetTCCLS
SetFitsSchemaString<&FitsSchemaItem::ttype > SetTTYPE
boost::multi_index_container< FitsSchemaItem, boost::multi_index::indexed_by< boost::multi_index::ordered_non_unique< boost::multi_index::member< FitsSchemaItem, int, &FitsSchemaItem::column > >, boost::multi_index::ordered_non_unique< boost::multi_index::member< FitsSchemaItem, int, &FitsSchemaItem::bit > >, boost::multi_index::hashed_unique< boost::multi_index::member< FitsSchemaItem, std::string, &FitsSchemaItem::ttype > >, boost::multi_index::sequenced<> > > InputContainer
SetFitsSchemaString<&FitsSchemaItem::tunit > SetTUNIT
SetFitsSchemaString<&FitsSchemaItem::tform > SetTFORM
SetFitsSchemaString<&FitsSchemaItem::doc > SetDoc
A class that describes a mapping from a FITS binary table to an afw::table Schema.
FitsSchemaInputMapper & operator=(FitsSchemaInputMapper const &)
static std::size_t PREPPED_ROWS_FACTOR
When processing each column, divide this number by the record size (in bytes) and ask CFITSIO to read...
bool readArchive(afw::fits::Fits &fits)
Set the Archive by reading from the HDU specified by the AR_HDU header entry.
Schema finalize()
Map any remaining items into regular Schema items, and return the final Schema.
void customize(std::unique_ptr< FitsColumnReader > reader)
Customize a mapping by providing a FitsColumnReader instance that will be invoked by readRecords().
void readRecord(BaseRecord &record, afw::fits::Fits &fits, std::size_t row)
Fill a record from a FITS binary table row.
FitsSchemaInputMapper(daf::base::PropertyList &metadata, bool stripMetadata)
Construct a mapper from a PropertyList of FITS header values, stripping recognized keys if desired.
void erase(Item const *item)
Remove the given item (which should have been retrieved via find()) from the mapping,...
bool hasArchive() const
Return true if the mapper has an InputArchive.
Item const * find(std::string const &ttype) const
Find an item with the given column name (ttype), returning nullptr if no such column exists.
void setArchive(std::shared_ptr< InputArchive > archive)
Set the Archive to an externally-provided one, overriding any that may have been read.
A multi-catalog archive object used to load table::io::Persistable objects.
Definition: InputArchive.h:31
static InputArchive readFits(fits::Fits &fitsfile)
Read an object from an already open FITS object.
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
T get(std::string const &name) const
Get the last value for a property name (possibly hierarchical).
Definition: PropertyList.cc:61
std::string const & getComment(std::string const &name) const
Get the comment for a string property name (possibly hierarchical).
Definition: PropertyList.cc:77
std::vector< T > getArray(std::string const &name) const
Get the vector of values for a property name (possibly hierarchical).
Definition: PropertyList.cc:73
std::vector< std::string > getOrderedNames() const
Get the list of property names, in the order they were added.
Definition: PropertyList.cc:81
virtual void remove(std::string const &name)
Remove all values for a property name (possibly hierarchical).
bool exists(std::string const &name) const
Determine if a name (possibly hierarchical) exists.
A coordinate class intended to represent absolute positions (2-d specialization).
Definition: Point.h:211
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
Reports attempts to exceed implementation-defined length limits for some classes.
Definition: Runtime.h:76
Reports attempts to access elements using an invalid key.
Definition: Runtime.h:151
T compare(T... args)
T copy_n(T... args)
T data(T... args)
T end(T... args)
T find(T... args)
T includes(T... args)
T make_pair(T... args)
T max(T... args)
T min(T... args)
T move(T... args)
int computeCovariancePackedSize(int size)
Defines the packed size of a covariance matrices.
Definition: FieldBase.h:35
int indexCovariance(int i, int j)
Defines the ordering of packed covariance matrices.
Definition: FieldBase.h:32
void erase(int column)
CoordinateType
Enum used to set units for geometric FunctorKeys.
Definition: aggregates.h:277
lsst::geom::Angle Angle
Definition: misc.h:33
constexpr AngleUnit radians
constant with units of radians
Definition: Angle.h:108
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:174
A base class for image defects.
STL namespace.
T replace(T... args)
T lround(T... args)
T size(T... args)
T sqrt(T... args)
int row
Definition: CR.cc:145
table::Key< table::Array< int > > _size
Definition: PsfexPsf.cc:364
T stoi(T... args)
Field base class default implementation (used for numeric scalars and lsst::geom::Angle).
Definition: FieldBase.h:43
A structure that describes a field as a collection of related strings read from the FITS header.
T to_string(T... args)
T transform(T... args)