Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-11957: Cannot round-trip >7th degree Chebyshev photometry models #277

Merged
merged 7 commits into from
Oct 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions include/lsst/afw/formatters/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@
#ifndef LSST_AFW_FORMATTERS_UTILS_H
#define LSST_AFW_FORMATTERS_UTILS_H

#include <cstdint>
#include <set>
#include <string>
#include <vector>

#include "ndarray.h"

#include "lsst/base.h"
#include "lsst/daf/base.h"
Expand Down Expand Up @@ -167,6 +171,16 @@ std::string formatFitsProperties(lsst::daf::base::PropertyList const& prop,
std::set<std::string> const& excludeNames = {});
int countFitsHeaderCards(lsst::daf::base::PropertySet const& prop);

/**
* Encode a std::string as a vector of uint8
*/
ndarray::Array<std::uint8_t, 1, 1> stringToBytes(std::string const &str);

/**
* Decode a std::string from a vector of uint8 returned by stringToBytes
*/
std::string bytesToString(ndarray::Array<std::uint8_t const, 1, 1> const& bytes);

}
}
} // namespace lsst::afw::formatters
Expand Down
4 changes: 2 additions & 2 deletions include/lsst/afw/image/ImageBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -385,15 +385,15 @@ class ImageBase : public lsst::daf::base::Persistable, public lsst::daf::base::C
* @throws lsst::pex::exceptions::RuntimeError Argument `contiguous` is false, or the pixels are not in
* fact contiguous
*/
fast_iterator begin(bool) const;
fast_iterator begin(bool contiguous) const;
/** Return a fast STL compliant iterator to the end of the %image which must be contiguous
*
* @param contiguous Pixels are contiguous (must be true)
*
* @throws lsst::pex::exceptions::RuntimeError Argument `contiguous` is false, or the pixels are not in
* fact contiguous
*/
fast_iterator end(bool) const;
fast_iterator end(bool contiguous) const;

/** Return an `x_iterator` to the start of the `y`'th row
*
Expand Down
4 changes: 2 additions & 2 deletions include/lsst/afw/image/MaskedImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ class MaskedImage : public lsst::daf::base::Persistable, public lsst::daf::base:
* @throws lsst::pex::exceptions::RuntimeError Argument `contiguous` is false, or the pixels are not in
* fact contiguous
*/
fast_iterator begin(bool) const;
fast_iterator begin(bool contiguous) const;
/** Return a fast `iterator` to the end of the %image, which must be contiguous
* Note that the order in which pixels are visited is undefined.
*
Expand All @@ -1192,7 +1192,7 @@ class MaskedImage : public lsst::daf::base::Persistable, public lsst::daf::base:
* @throws lsst::pex::exceptions::RuntimeError Argument `contiguous` is false, or the pixels are not in
* fact contiguous
*/
fast_iterator end(bool) const;
fast_iterator end(bool contiguous) const;

/// Return an `x_iterator` to the start of the %image
x_iterator row_begin(int y) const;
Expand Down
12 changes: 6 additions & 6 deletions include/lsst/afw/table/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@
*/

// Scalar types: those that can serve as elements for other types, and use the default FieldBase template.
#define AFW_TABLE_SCALAR_FIELD_TYPE_N 6
#define AFW_TABLE_SCALAR_FIELD_TYPES RecordId, std::uint16_t, std::int32_t, float, double, Angle
#define AFW_TABLE_SCALAR_FIELD_TYPE_N 7
#define AFW_TABLE_SCALAR_FIELD_TYPES RecordId, std::uint16_t, std::int32_t, float, double, Angle, std::uint8_t
#define AFW_TABLE_SCALAR_FIELD_TYPE_TUPLE BOOST_PP_LPAREN() AFW_TABLE_SCALAR_FIELD_TYPES BOOST_PP_RPAREN()

// Arrays types: the types we allow for Array fields.
#define AFW_TABLE_ARRAY_FIELD_TYPE_N 4
#define AFW_TABLE_ARRAY_FIELD_TYPES std::uint16_t, int, float, double
#define AFW_TABLE_ARRAY_FIELD_TYPE_N 5
#define AFW_TABLE_ARRAY_FIELD_TYPES std::uint16_t, int, float, double, std::uint8_t
#define AFW_TABLE_ARRAY_FIELD_TYPE_TUPLE BOOST_PP_LPAREN() AFW_TABLE_ARRAY_FIELD_TYPES BOOST_PP_RPAREN()

// Field types: all the types we allow for fields.
#define AFW_TABLE_FIELD_TYPE_N 12
#define AFW_TABLE_FIELD_TYPE_N 14
#define AFW_TABLE_FIELD_TYPES \
AFW_TABLE_SCALAR_FIELD_TYPES, Flag, std::string, Array<std::uint16_t>, Array<int>, Array<float>, \

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but is there any reason why we use Array<int> while the scalar is specifically qualified as std::int32_t?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea. @TallJimbo?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::int32_t would be better, but changing it would require concerted changes across a lot of the codebase. Should probably be spun off into another ticket.

Array<double>
Array<double>, Array<std::uint8_t>

#define AFW_TABLE_FIELD_TYPE_TUPLE BOOST_PP_LPAREN() AFW_TABLE_FIELD_TYPES BOOST_PP_RPAREN()

Expand Down
9 changes: 7 additions & 2 deletions python/lsst/afw/formatters/SConscript
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## -*- python -*-
#from lsst.sconsUtils import scripts
#scripts.BasicSConscript.pybind11([])
from lsst.sconsUtils import scripts
scripts.BasicSConscript.pybind11(
[
"utils",
],
addUnderscore=False,
)
2 changes: 1 addition & 1 deletion python/lsst/afw/formatters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
"""lsst.afw.formatters
"""
from __future__ import absolute_import
from .detectionLib import *
from .utils import *
1 change: 0 additions & 1 deletion python/lsst/afw/formatters/formattersLib.py

This file was deleted.

41 changes: 27 additions & 14 deletions python/lsst/afw/formatters/utils.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* LSST Data Management System
* Copyright 2008-2016 AURA/LSST.
* Copyright 2017 AURA/LSST.
*
* This product includes software developed by the
* LSST Project (http://www.lsst.org/).
Expand All @@ -21,25 +21,38 @@
*/

#include <pybind11/pybind11.h>
//#include <pybind11/operators.h>
//#include <pybind11/stl.h>
#include <pybind11/stl.h>

namespace py = pybind11;

using namespace lsst::afw::formatters;
#include "numpy/arrayobject.h"
#include "ndarray/pybind11.h"

PYBIND11_PLUGIN(_utils) {
py::module mod("_utils", "Python wrapper for afw _utils library");
#include "lsst/afw/formatters/Utils.h"

/* Module level */
namespace py = pybind11;
using namespace py::literals;

/* Member types and enums */
namespace py = pybind11;
namespace lsst {
namespace afw {
namespace formatters {
namespace {

/* Constructors */
PYBIND11_PLUGIN(utils) {
py::module mod("utils");

/* Operators */
// Need to import numpy for ndarray and eigen conversions
if (_import_array() < 0) {
PyErr_SetString(PyExc_ImportError, "numpy.core.multiarray failed to import");
return nullptr;
}

/* Members */
mod.def("stringToBytes", stringToBytes);
mod.def("bytesToString", bytesToString);

return mod.ptr();
}
}

} // namespace
} // namespace formatters
} // namespace afw
} // namespace lsst
2 changes: 2 additions & 0 deletions python/lsst/afw/table/base/base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ PyBaseRecord declareBaseRecord(py::module &mod) {
declareBaseRecordOverloads<double>(cls, "D");
declareBaseRecordOverloads<float>(cls, "F");
declareBaseRecordOverloads<lsst::afw::table::Flag>(cls, "Flag");
declareBaseRecordOverloads<std::uint8_t>(cls, "B");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think we should add some kind of type trait at some point to standardize the suffixes. Such that we can say lsst::utils::default_suffix<std::uint8_t>::value and always get the same one.
Maybe not now, since it leads to the slightly tricky problem of compile-time string concatenation, but perhaps file a new ticket for it? If you agree that is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this code would benefit from a cleanup pass. But I'm not comfortable enough with type traits to have an opinion on this exact issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another issue that is worth doing as a separate ticket.

declareBaseRecordOverloads<std::uint16_t>(cls, "U");
declareBaseRecordOverloads<std::int32_t>(cls, "I");
declareBaseRecordOverloads<std::int64_t>(cls, "L");
declareBaseRecordOverloads<std::string>(cls, "String");
declareBaseRecordOverloads<lsst::afw::geom::Angle>(cls, "Angle");
declareBaseRecordArrayOverloads<std::uint8_t>(cls, "ArrayB");
declareBaseRecordArrayOverloads<std::uint16_t>(cls, "ArrayU");
declareBaseRecordArrayOverloads<int>(cls, "ArrayI");
declareBaseRecordArrayOverloads<float>(cls, "ArrayF");
Expand Down
2 changes: 2 additions & 0 deletions python/lsst/afw/table/baseColumnView/baseColumnView.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ static void declareBaseColumnView(py::module &mod) {
// _getBits supports a Python version of getBits that accepts None and field names as keys
cls.def("_getBits", &BaseColumnView::getBits);
cls.def("getAllBits", &BaseColumnView::getAllBits);
declareBaseColumnViewOverloads<std::uint8_t>(cls);
declareBaseColumnViewOverloads<std::uint16_t>(cls);
declareBaseColumnViewOverloads<std::int32_t>(cls);
declareBaseColumnViewOverloads<std::int64_t>(cls);
Expand All @@ -94,6 +95,7 @@ static void declareBaseColumnView(py::module &mod) {
// std::string columns are not supported, because numpy string arrays
// do not have the same memory model as ours.
declareBaseColumnViewOverloads<lsst::afw::geom::Angle>(cls);
declareBaseColumnViewArrayOverloads<std::uint8_t>(cls);
declareBaseColumnViewArrayOverloads<std::uint16_t>(cls);
declareBaseColumnViewArrayOverloads<int>(cls);
declareBaseColumnViewArrayOverloads<float>(cls);
Expand Down
2 changes: 2 additions & 0 deletions python/lsst/afw/table/schema/schema.cc
Original file line number Diff line number Diff line change
Expand Up @@ -407,13 +407,15 @@ PYBIND11_PLUGIN(schema) {
mod.attr("_Key") = py::dict();
mod.attr("_SchemaItem") = py::dict();

declareSchemaType<std::uint8_t>(mod);
declareSchemaType<std::uint16_t>(mod);
declareSchemaType<std::int32_t>(mod);
declareSchemaType<std::int64_t>(mod);
declareSchemaType<float>(mod);
declareSchemaType<double>(mod);
declareSchemaType<std::string>(mod);
declareSchemaType<geom::Angle>(mod);
declareSchemaType<Array<std::uint8_t>>(mod);
declareSchemaType<Array<std::uint16_t>>(mod);
declareSchemaType<Array<int>>(mod);
declareSchemaType<Array<float>>(mod);
Expand Down
1 change: 1 addition & 0 deletions python/lsst/afw/table/schema/schemaContinued.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
# Key/Field/SchemaItem types.
_dtypes = {
"String": str,
"B": np.uint8,
"U": np.uint16,
"I": np.int32,
"L": np.int64,
Expand Down
2 changes: 2 additions & 0 deletions python/lsst/afw/table/schemaMapper/schemaMapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ PYBIND11_PLUGIN(schemaMapper) {
cls.def("addMinimalSchema", &SchemaMapper::addMinimalSchema, "minimal"_a, "doMap"_a = true);
cls.def_static("removeMinimalSchema", &SchemaMapper::removeMinimalSchema);
cls.def_static("join", &SchemaMapper::join, "inputs"_a, "prefixes"_a = std::vector<std::string>());
declareSchemaMapperOverloads<std::uint8_t>(cls, "B");
declareSchemaMapperOverloads<std::uint16_t>(cls, "U");
declareSchemaMapperOverloads<std::int32_t>(cls, "I");
declareSchemaMapperOverloads<std::int64_t>(cls, "L");
Expand All @@ -68,6 +69,7 @@ PYBIND11_PLUGIN(schemaMapper) {
declareSchemaMapperOverloads<std::string>(cls, "String");
declareSchemaMapperOverloads<lsst::afw::geom::Angle>(cls, "Angle");
declareSchemaMapperOverloads<lsst::afw::table::Flag>(cls, "Flag");
declareSchemaMapperOverloads<lsst::afw::table::Array<std::uint8_t>>(cls, "ArrayB");
declareSchemaMapperOverloads<lsst::afw::table::Array<std::uint16_t>>(cls, "ArrayU");
declareSchemaMapperOverloads<lsst::afw::table::Array<int>>(cls, "ArrayI");
declareSchemaMapperOverloads<lsst::afw::table::Array<float>>(cls, "ArrayF");
Expand Down
13 changes: 13 additions & 0 deletions src/fits.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ struct FitsType<char> {
static int const CONSTANT = TSTRING;
};
template <>
struct FitsType<signed char> {
static int const CONSTANT = TSBYTE;
};
template <>
struct FitsType<unsigned char> {
static int const CONSTANT = TBYTE;
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we don't specifically also specialize for std::uint8_t? I know it maps on most platforms, but it doesn't have to.

Copy link
Contributor Author

@r-owen r-owen Oct 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. I am copying existing code which may be based on this entry in the CFITSIO documentation: https://heasarc.gsfc.nasa.gov/docs/software/fitsio/quick/node9.html. It is true that the entry refers to images (it was the only description of 'TSBYTE' I found).

(begin quote)
The datatype parameter specifies the datatype of the C array in the program, which need not be the same as the datatype of the FITS image itself. If the datatypes differ then CFITSIO will convert the data as it is read or written. The following symbolic constants are allowed for the value of datatype:
TBYTE unsigned char
TSBYTE signed char
TSHORT signed short
TUSHORT unsigned short
TINT signed int
TUINT unsigned int
TLONG signed long
TLONGLONG signed 8-byte integer
TULONG unsigned long
TFLOAT float
TDOUBLE double
(end quote)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as @r-owen said, this is limited by what cfitsio supports.

Expand Down Expand Up @@ -690,6 +694,15 @@ void writeKeyFromProperty(Fits &fits, daf::base::PropertySet const &metadata, st
} else {
writeKeyImpl(fits, key.c_str(), metadata.get<bool>(key), comment);
}
} else if (valueType == typeid(std::uint8_t)) {
if (metadata.isArray(key)) {
std::vector<std::uint8_t> tmp = metadata.getArray<std::uint8_t>(key);
for (std::size_t i = 0; i != tmp.size(); ++i) {
writeKeyImpl(fits, key.c_str(), tmp[i], comment);
}
} else {
writeKeyImpl(fits, key.c_str(), metadata.get<std::uint8_t>(key), comment);
}
} else if (valueType == typeid(int)) {
if (metadata.isArray(key)) {
std::vector<int> tmp = metadata.getArray<int>(key);
Expand Down
2 changes: 2 additions & 0 deletions src/fitsCompression.cc
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ struct Bzero<T, typename std::enable_if<std::numeric_limits<T>::is_integer &&
};


#ifndef DOXYGEN // suppress a bogus Doxygen complaint about an documented symbol
template <typename T, int N>
ImageScale ImageScalingOptions::determine(
ndarray::Array<T const, N, N> const& image,
Expand All @@ -322,6 +323,7 @@ ImageScale ImageScalingOptions::determine(
std::abort(); // should never get here
}
}
#endif

namespace {

Expand Down
18 changes: 18 additions & 0 deletions src/formatters/Utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,24 @@ std::string formatFitsProperties(daf::base::PropertyList const& prop,
int countFitsHeaderCards(lsst::daf::base::PropertySet const& prop) {
return prop.paramNames(false).size();
}

ndarray::Array<std::uint8_t, 1, 1> stringToBytes(std::string const& str) {
auto nbytes = str.size() * sizeof(char) / sizeof(std::uint8_t);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizeof(typename std::string::value_type)? Or do we trust the user to update this if they switch the input to some other string type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string::data is documented to return const char * so I think the current code is safest.

std::uint8_t const* byteCArr = reinterpret_cast<std::uint8_t const*>(str.data());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we don't do any formal encoding? Hmm, I'm very worried that this will create problems when the data is read on a different platform then the one it was written.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed on the JIRA ticket with the resolution that we will file another ticket to look at this and use this code for now.

auto shape = ndarray::makeVector(nbytes);
auto strides = ndarray::makeVector(1);
// Make an Array that shares memory with `str` (and does not free that memory when destroyed),
// then return a copy; this is simpler than manually copying the data into a newly allocated array
ndarray::Array<std::uint8_t const, 1, 1> localArray = ndarray::external(byteCArr, shape, strides);
return ndarray::copy(localArray);
}

std::string bytesToString(ndarray::Array<std::uint8_t const, 1, 1> const& bytes) {
auto nchars = bytes.size() * sizeof(std::uint8_t) / sizeof(char);
char const* charCArr = reinterpret_cast<char const*>(bytes.getData());
return std::string(charCArr, nchars);
}

}
}
} // namespace lsst::afw::formatters
13 changes: 8 additions & 5 deletions src/math/TransformBoundedField.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
* see <http://www.lsstcorp.org/LegalNotices/>.
*/

#include <cstdint>
#include <memory>
#include <string>

#include "ndarray/eigen.h"
#include "astshim.h"
#include "lsst/afw/formatters/Utils.h"
#include "lsst/afw/math/TransformBoundedField.h"
#include "lsst/afw/table/io/InputArchive.h"
#include "lsst/afw/table/io/OutputArchive.h"
Expand Down Expand Up @@ -81,16 +83,17 @@ struct PersistenceHelper {
table::Schema schema;
table::PointKey<int> bboxMin;
table::PointKey<int> bboxMax;
table::Key<std::string> frameSet;
// store the FrameSet as string encoded as a variable-length vector of bytes
table::Key<table::Array<std::uint8_t>> frameSet;

PersistenceHelper()
: schema(),
bboxMin(table::PointKey<int>::addFields(schema, "bbox_min", "lower-left corner of bounding box",
"pixel")),
bboxMax(table::PointKey<int>::addFields(schema, "bbox_max",
"upper-right corner of bounding box", "pixel")),
frameSet(schema.addField<std::string>("frameSet", "FrameSet contained in the Transform", 0,
false)) {}
frameSet(schema.addField<table::Array<std::uint8_t>>(
"frameSet", "FrameSet contained in the Transform", "", 0)) {}

PersistenceHelper(table::Schema const& s)
: schema(s), bboxMin(s["bbox_min"]), bboxMax(s["bbox_max"]), frameSet(s["frameSet"]) {}
Expand All @@ -109,7 +112,7 @@ class TransformBoundedFieldFactory : public table::io::PersistableFactory {
PersistenceHelper const keys(record.getSchema());
// NOTE: needed invert=false in case min=-1, max=0 (empty bbox). See RFC-324 and DM-10200
geom::Box2I bbox(record.get(keys.bboxMin), record.get(keys.bboxMax), false);
auto frameSetStr = record.get(keys.frameSet);
auto frameSetStr = formatters::bytesToString(record.get(keys.frameSet));
auto transform =
geom::Transform<geom::Point2Endpoint, geom::GenericEndpoint>::readString(frameSetStr);
return std::make_shared<TransformBoundedField>(bbox, transform);
Expand All @@ -134,7 +137,7 @@ void TransformBoundedField::write(OutputArchiveHandle& handle) const {
std::shared_ptr<table::BaseRecord> record = catalog.addNew();
record->set(keys.bboxMin, getBBox().getMin());
record->set(keys.bboxMax, getBBox().getMax());
record->set(keys.frameSet, getTransform().writeString());
record->set(keys.frameSet, formatters::stringToBytes(getTransform().writeString()));
handle.saveCatalog(catalog);
}

Expand Down
4 changes: 4 additions & 0 deletions src/table/FieldBase.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ namespace {
template <typename T>
struct TypeTraits;

template <>
struct TypeTraits<std::uint8_t> {
static char const *getName() { return "B"; }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so we have one. Then why don't we use it everywhere? Preferably pulling it up to utils?
In that case I would still modify it slightly such that it can be used in constexpr contexts (e.g. one can use it in a switch or something).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These characters are FITS table characters. I'm not sure I'd want to use it anywhere else.

I don't understand your suggested modification but respectfully suggest that any significant refactoring should go into a new ticket.

};
template <>
struct TypeTraits<std::uint16_t> {
static char const *getName() { return "U"; }
Expand Down
12 changes: 12 additions & 0 deletions src/table/io/FitsSchemaInputMapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,18 @@ std::unique_ptr<FitsColumnReader> makeColumnReader(Schema &schema, FitsSchemaIte
}
// switch code over FITS codes that correspond to different element types
switch (code) {
case 'B': // 8-bit unsigned integers -- can only be scalars or Arrays
if (size == 1) {
if (item.tccls == "Array") {
return StandardReader<Array<std::uint8_t>>::make(schema, item, size);
}
return StandardReader<std::uint8_t>::make(schema, item);
}
if (size == 0) {
return VariableLengthArrayReader<std::uint8_t>::make(schema, item);
}
return StandardReader<Array<std::uint8_t>>::make(schema, item, size);

case 'I': // 16-bit integers - can only be scalars or Arrays (we assume they're unsigned, since
// that's all we ever write, and CFITSIO will complain later if they aren't)
if (size == 1) {
Expand Down