Commit 87c834a4 authored by Paal Kvamme's avatar Paal Kvamme
Browse files

Merge branch 'kvamme62/constcube' into 'master'

Single value cubes

See merge request !24
parents 1b693ced f352ad0f
Pipeline #19539 passed with stages
in 9 minutes and 29 seconds
......@@ -12,7 +12,7 @@
</head>
<body bgcolor="#ffffff">
<p class="version">This is version 0.3 of the document, last updated 2020-09-07.</p>
<p class="version">This is version 0.4 of the document, last updated 2020-12-15.</p>
<!-- <h1 style="color: red">DRAFT DOCUMENT</h1> -->
......@@ -575,6 +575,197 @@ limitations under the License.
It is unspecified whether the padding samples written in this manner
are retained when the data is read back.
</p>
<div style="color: blue">
<h2>Inconsistency encountered in old files</h2>
<p>(Added 2020-12-15)</p>
<p>
This is a long explanation of something that is a fairly obscure case.
</p>
<p>
For int8 and int16 files the data range should always have min
&lt; max. The reason is that applications will then have less
corner cases to worry about. The rule is enforced when creating
new files from OpenZGY. For float files the accessor should
ignore the range. So it doesn't matter what it is set to in that
case.
</p>
<p>
If an older file is encountered with min &gt; max those
values should be assumed to be garbage and ignored.
not be possible. The unconverted integral storage values should be
returned if the application tries to read back data as
float. By default OpenZGY should hide the min and max values
stored on the file and instead return a range consistent
with having storage and float values being the same.
</p>
<p>
If an older file is encountered with min == max it is
ambiguous what the application intended. There is at least
one scenario where Petrel using the old ZGY accessor could
write such files when all samples were the same.
</p>
<ol style="list-style-type: lower-alpha">
<li value="1">
<p>
This cube has all the samples set to the same value. Reading
any sample as float returns that value. Reading raw storage
values returns unspecified values but will probably return
all zeros. The choice of storage values was made when the
file was written. The old code in Petrel and ZGY could have,
but doesn't, avoid the situation by widening the range to
make it non-empty. TODO-Low: verify what ZgyPublic did.
</p>
</li>
<li value="2">
<p>
This cube contains discrete values such as classification
enums. Reading samples as float doesn't make sense and the
result is unspecified but will probably return all zeros.
Reading raw storage values returns useful data. The old code
in Petrel might have avoided the situation by choosing an
arbitrary range instead of e.g. leaving the range as (0,0).
I don't know the current behaviour.
</p>
</li>
</ol>
<p>
So the behavior may need to differ depending on whether the data
is being read back as float or storage values. Or read back as
storage but converted by the application to float. This is
exactly the kind of ambiguity that I am trying to avoid. What
should OpenZGY do when reading these old files?
</p>
<ol style="list-style-type: decimal">
<li value="1">
<p>
Mimic the old behaviour as closely as possible. This
includes showing the min==max data range to the application.
Clients will need to handle that case, and any test plan for
the clients must remember to test this situation. This makes
it more difficult to use OpenZGY.
</p>
</li>
<li value="2">
<p>
Reset the coding range to a sane value.
</p>
<table>
<tr>
<th>From</th>
<th>To</th>
</tr>
<tr>
<td>(0, 0)</td>
<td>(-128, +127) or (-1, +1)</td>
</tr>
<tr>
<td>(+value, +value</td>
<td>(0, +value*2)</td>
</tr>
<tr>
<td>(-value, -value)</td>
<td>(-value*2, 0).</td>
</tr>
</table>
<p>
Plus a small adjustment so "value" maps exactly to zero.
Read will be handled differently depending on whether
storage is requested (return actual data) or float is
requested (return the constant value). The main caveat is
what happens if the writer wanted to store a constant-cube
but used something else than zeros as the raw value,
<em>and</em> the application wants float values but chooses
to do the integral to float conversion itself
</p>
</li>
<li value="3">
<p>
As (2) but always choose to interpret as (a), assuming that
applications writing discrete values actually did set the
range to non-empty and we don't need to worry about that
case. If the assumption was incorrect then files supposed to
contain discrete values will appear to be empty. The
difficulty of verifying the assumption and the risk of
getting it wrong probably disqualifies this alternative.
Also, in this case there is no way application can retrieve
the actual data even if they do choose to implement special
handling. Note that applications reading using OpenZGY are
free to assume (a) and run the risk of losing data, while
OpenZGY itself isn't.
</p>
</li>
<li value="4">
<p>
TODO-Low: This should perhaps be the chosen behaviour.
As (2) but always choose to interpret as (b). If the choice
was wrong then applications might still work <em>if</em> the
raw storage values were all written as zero and the adjusted
coding range was set to an appropriate value.
</p>
</li>
<li value="5">
<p>
Scan the entire file to see if all bricks contain the same
constant value on storage. If yes, assume (a) and change
that value to zero if in isn't already. If no, assume (b)
</p>
</li>
</ol>
<p>
In all cases it is unspecified what happens with the histogram.
I doubt anybody cares when reading old inconsistent files.
</p>
<p>
TODO-Low: Related issue: When creating a histogram for a new
all-constant file the histogram would look better if all the
samples end up in the center bin. This applies also to float
files. Otherwise the logic would be similar to (2).
</p>
<p>
Applications such as "zgycopy" will need special handling
because the OpenZGY library will refuse to set min==max in a
new file. The copy application must guess whether the writer had
intended (a) or (b) and the choice might differ from whet the
library chooses.
</p>
<ol style="list-style-type: lower-alpha">
<li value="1">
Store all constant zero for storage and constant value for
float as if copying the data as float
</li>
<li>
Store the same numbers in float and storage as if copying the
data as storage values.
</li>
</ol>
<p>
For the stand alone tools I choose (a). Applications that
perform a similar task must choose for themselves. When choosing
(a) the copied file will have all-zero storage values regardless
of what was originally stored. When choosing (b) reading float
values will now return the sane as storage instead of some
constant.
</p>
<p>
When "zgycopy" chooses (a) this case if fairly straight forward.
No bulk data should be read from the input. The application
simply needs to figure out an appropriate data range and then
issue a single call to writeconst(). OpenZGY always re-computes
the statistics and histogram so it doesn't matter if those are
inconsistent and the reader ignores that fact.
</p>
<p>
With choices (4) or (5) "zgycopy" might work unmodified but only
if some other assumptions as explained above are correct.
</p>
<p>
In the current OpenZGY implementation the min==max case is
treated the same as min&gt;max. It is recommended that zgycopy
and similar applications check for the special case. Other
applications that read ZGY might not care.
</p>
</div>
<img src="openzgy-fig1.png" alt="Figure showing physical layout" width="500" height="1000"/>
<!-- This version of fig1 looks better because it is vector graphics
......
......@@ -166,7 +166,16 @@ public:
if (_meta->ih().datatype() == InternalZGY::RawDataType::Float32)
return std::array<float32_t,2>{_meta->ih().smin(), _meta->ih().smax()};
else
return _meta->ih().codingrange();
return _meta->ih().safe_codingrange();
}
virtual std::array<float32_t,2>
raw_datarange() const override
{
if (_meta->ih().datatype() == InternalZGY::RawDataType::Float32)
return std::array<float32_t,2>{_meta->ih().smin(), _meta->ih().smax()};
else
return _meta->ih().raw_codingrange();
}
virtual UnitDimension
......
......@@ -553,6 +553,9 @@ public:
* point numbers that correspond to the lowest and highest
* representable integer value. So for int8 files, -128 will be
* converted to "lo" and +127 will be converted to "hi".
*
* ZGY files require that min<max. This is not enforced here,
* but is checked when the file is actually created.
*/
ZgyWriterArgs& datarange(float lo, float hi) { _datarange[0] = lo; _datarange[1] = hi; return *this; }
/**
......@@ -641,6 +644,7 @@ public:
virtual size3i_t size() const = 0; /**< \brief Size in inline, crossline, vertical directions. */
virtual SampleDataType datatype() const = 0; /**< \brief Type of samples in each brick. */
virtual std::array<float32_t,2> datarange() const = 0; /**< \brief Used for float to int scaling. */
virtual std::array<float32_t,2> raw_datarange() const = 0; /**< \brief datarange before adjustment. */
virtual UnitDimension zunitdim() const = 0; /**< \brief Vertical dimension. */
virtual UnitDimension hunitdim() const = 0; /**< \brief Horizontal dimension. */
virtual std::string zunitname() const = 0; /**< \brief For annotation only. Use hunitfactor, not the name, to convert to or from SI. */
......
......@@ -560,7 +560,7 @@ InfoHeaderAccess::dump(std::ostream& out, const std::string& prefix)
out
<< prefix << "bricksize(): " << array_to_string(bricksize()) << "\n"
<< prefix << "datatype(): " << (int)datatype() << "\n"
<< prefix << "codingrange(): " << array_to_string(codingrange()) << "\n"
<< prefix << "safe_codingrange(): " << array_to_string(safe_codingrange()) << "\n"
<< prefix << "dataid(): " << array_to_hex(dataid()) << "\n"
<< prefix << "verid(): " << array_to_hex(verid()) << "\n"
<< prefix << "previd(): " << array_to_hex(previd()) << "\n"
......@@ -650,7 +650,7 @@ InfoHeaderAccess::storagetofloat() const
double slope = 1.0, intercept = 0.0;
const RawDataTypeDetails info(this->datatype());
if (info.is_integer) {
const std::array<float,2> range = this->codingrange();
const std::array<float,2> range = this->safe_codingrange();
slope = (range[1] - range[0]) / (info.highest - info.lowest);
intercept = range[0] - slope * info.lowest;
}
......@@ -731,8 +731,10 @@ class InfoHeaderV1Access : public InfoHeaderAccess
{
public:
InfoHeaderV1POD _pod;
float _cached_sample_min;
float _cached_sample_max;
float _cached_file_sample_min;
float _cached_file_sample_max;
float _cached_safe_codingrange_min;
float _cached_safe_codingrange_max;
std::int32_t _cached_nlods;
std::vector<std::array<std::int64_t,3>> _cached_lodsizes;
std::vector<std::int64_t> _cached_alphaoffsets;
......@@ -759,7 +761,8 @@ public:
virtual RawDataType datatype() const override { return DecodeDataType(_pod._datatype); }
//special: virtual std::uint8_t coordtype() const override { return _pod._coordtype; }
virtual std::array<std::int64_t,3> bricksize() const override { return std::array<std::int64_t,3>{64, 64, 64}; }
virtual std::array<float,2> codingrange() const override { return std::array<float,2>{_cached_sample_min, _cached_sample_max}; }
virtual std::array<float,2> safe_codingrange() const override { return std::array<float,2>{_cached_safe_codingrange_min, _cached_safe_codingrange_max}; }
virtual std::array<float,2> raw_codingrange() const override { return std::array<float,2>{_cached_file_sample_min, _cached_file_sample_max}; }
virtual std::array<std::uint8_t,16>dataid() const override { return std::array<std::uint8_t,16>{0}; }
virtual std::array<std::uint8_t,16>verid() const override { return std::array<std::uint8_t,16>{0}; }
virtual std::array<std::uint8_t,16>previd() const override { return std::array<std::uint8_t,16>{0}; }
......@@ -771,8 +774,8 @@ public:
virtual std::int64_t scnt() const override { return 0; }
virtual double ssum() const override { return 0; }
virtual double sssq() const override { return 0; }
virtual float smin() const override { return align(_cached_sample_min); }
virtual float smax() const override { return align(_cached_sample_max); }
virtual float smin() const override { return align(_cached_file_sample_min); }
virtual float smax() const override { return align(_cached_file_sample_max); }
//unused: virtual std::array<float,3> srvorig() const override { throw OpenZGY::Errors::ZgyInternalError("Not implemented"); }
//unused: virtual std::array<float,3> srvsize() const override { throw OpenZGY::Errors::ZgyInternalError("Not implemented"); }
//unused: virtual std::uint8_t gdef() const override { throw OpenZGY::Errors::ZgyInternalError("Not implemented"); }
......@@ -867,7 +870,7 @@ class InfoHeaderV2POD
public:
std::int32_t _bricksize[3]; // Brick size. Values other than (64,64,64) will likely not work.
std::uint8_t _datatype; // Type of samples in each brick: int8 = 0, int16 = 2, float32 = 6.
float _codingrange[2]; // If datatype is integral, this is the value range samples will be scaled to when read as float. In this case it must be specified on file creation. If datatype is float then this is the value range of the data and should be set automatically when writing the file.
float _file_codingrange[2]; // If datatype is integral, this is the value range samples will be scaled to when read as float. In this case it must be specified on file creation. If datatype is float then this is the value range of the data and should be set automatically when writing the file.
std::uint8_t _dataid[16]; // GUID set on file creation.
std::uint8_t _verid[16]; // GUID set each time the file is changed.
std::uint8_t _previd[16]; // GUID before last change.
......@@ -926,6 +929,7 @@ public:
std::array<std::array<double,2>,4> _cached_index;
std::array<std::array<double,2>,4> _cached_annot;
std::array<std::array<double,2>,4> _cached_world;
float _cached_safe_codingrange[2];
InfoHeaderV2Access() { memset(&_pod, 0, sizeof(_pod)); }
virtual podbytes_t podbytes() const override { return pod2bytes(_pod); }
......@@ -937,7 +941,8 @@ public:
public:
virtual std::array<std::int64_t,3> bricksize() const override { return array_cast<std::int64_t,std::int32_t,3>(ptr_to_array<std::int32_t,3>(_pod._bricksize)); }
virtual RawDataType datatype() const override { return DecodeDataType(_pod._datatype); }
virtual std::array<float,2> codingrange() const override { return ptr_to_array<float,2>(_pod._codingrange); }
virtual std::array<float,2> safe_codingrange() const override { return ptr_to_array<float,2>(_cached_safe_codingrange); }
virtual std::array<float,2> raw_codingrange() const override { return ptr_to_array<float,2>(_pod._file_codingrange); }
virtual std::array<std::uint8_t,16>dataid() const override { return ptr_to_array<std::uint8_t,16>(_pod._dataid); }
virtual std::array<std::uint8_t,16>verid() const override { return ptr_to_array<std::uint8_t,16>(_pod._verid); }
virtual std::array<std::uint8_t,16>previd() const override { return ptr_to_array<std::uint8_t,16>(_pod._previd); }
......@@ -1005,7 +1010,7 @@ InfoHeaderV2Access::byteswap()
{
byteswapT(&_pod._bricksize[0], 3);
// byteswap not needed for std::uint8_t _datatype because of its type.
byteswapT(&_pod._codingrange[0], 2);
byteswapT(&_pod._file_codingrange[0], 2);
// byteswap not needed for std::uint8_t _dataid[16] because of its type.
// byteswap not needed for std::uint8_t _verid[16] because of its type.
// byteswap not needed for std::uint8_t _previd[16] because of its type.
......@@ -1452,16 +1457,18 @@ InfoHeaderV1Access::calculate_read(const podbytes_t& /*stringlist_in*/, const st
{
if (hh) {
// Note reason for cast: ZGY only deals with int8, int16, float.
_cached_sample_min = static_cast<float>(hh->minvalue());
_cached_sample_max = static_cast<float>(hh->maxvalue());
_cached_file_sample_min = static_cast<float>(hh->minvalue());
_cached_file_sample_max = static_cast<float>(hh->maxvalue());
}
else {
_cached_sample_min = 0;
_cached_sample_max = 0;
_cached_file_sample_min = 0;
_cached_file_sample_max = 0;
}
// Sanity check. If codingrange is bad, silently use a range
// that causes no conversion between storage and float.
_fix_codingrange(&_cached_sample_min, &_cached_sample_max, _pod._datatype);
_cached_safe_codingrange_min = _cached_file_sample_min;
_cached_safe_codingrange_max = _cached_file_sample_max;
_fix_codingrange(&_cached_safe_codingrange_min, &_cached_safe_codingrange_max, _pod._datatype);
calculate_cache();
}
......@@ -1532,8 +1539,10 @@ InfoHeaderV2Access::calculate_read(const podbytes_t& stringlist_in, const std::s
this->_vunitname = strings[4];
// Sanity check. If codingrange is bad, silently use a range
// that causes no conversion between storage and float.
_fix_codingrange(&this->_pod._codingrange[0],
&this->_pod._codingrange[1],
_cached_safe_codingrange[0] = this->_pod._file_codingrange[0],
_cached_safe_codingrange[1] = this->_pod._file_codingrange[1],
_fix_codingrange(&this->_cached_safe_codingrange[0],
&this->_cached_safe_codingrange[1],
this->_pod._datatype);
calculate_cache();
}
......@@ -1853,8 +1862,10 @@ ZgyInternalMeta::initFromScratch(const ZgyInternalWriterArgs& args_in)
// the parameters to this function wree set to match
// the existing Python wrapper for the old ZGY.
pod._codingrange[0] = args.datarange[0];
pod._codingrange[1] = args.datarange[1];
pod._file_codingrange[0] = args.datarange[0];
pod._file_codingrange[1] = args.datarange[1];
ih->_cached_safe_codingrange[0] = pod._file_codingrange[0];
ih->_cached_safe_codingrange[1] = pod._file_codingrange[1];
pod._vdim = static_cast<std::uint8_t>(args.zunitdim);
pod._hdim = static_cast<std::uint8_t>(args.hunitdim);
......
......@@ -253,7 +253,8 @@ public:
virtual podbytes_t calculate_write() = 0;
public:
virtual std::array<std::int64_t,3> bricksize() const = 0;
virtual std::array<float,2> codingrange() const = 0;
virtual std::array<float,2> safe_codingrange() const = 0;
virtual std::array<float,2> raw_codingrange() const = 0;
virtual std::array<std::uint8_t,16>dataid() const = 0;
virtual std::array<std::uint8_t,16>verid() const = 0;
virtual std::array<std::uint8_t,16>previd() const = 0;
......
......@@ -66,6 +66,13 @@ public:
return writer_->datarange();
}
virtual std::array<float32_t,2> raw_datarange() const override
{
// Not allowed to change after file is created.
// std::lock_guard<std::mutex> lk(mutex_);
return writer_->raw_datarange();
}
virtual UnitDimension zunitdim() const override
{
std::lock_guard<std::mutex> lk(mutex_);
......
......@@ -44,6 +44,7 @@ public:
virtual size3i_t size() const {return size_;}
virtual SampleDataType datatype() const {return datatype_;}
virtual std::array<float32_t,2> datarange() const {return std::array<float32_t,2>{-1,1};}
virtual std::array<float32_t,2> raw_datarange() const {return std::array<float32_t,2>{-1,1};}
virtual UnitDimension zunitdim() const {return UnitDimension::unknown;}
virtual UnitDimension hunitdim() const {return UnitDimension::unknown;}
virtual std::string zunitname() const {return std::string();}
......
......@@ -579,6 +579,59 @@ void copychunk(
}
}
/**
* Suggest a data range to specify when the entire file is constant value.
* Using low==high is not allowed because it triggers several corner cases.
* The result should be zero-centric and map storage-zero to the single value.
* Apart from that, just suggest something reasonable.
*
* This function might be used when creating a constant-cube from scratch.
* It is also useful when encountering an old ZGY file from Petrel where
* low==high was permitted. Adjusting old files could also be done inside
* the access library. As of this writing it isn't.
*/
void
suggestRange(float value, float dt_lo, float dt_hi, float *lo, float *hi)
{
if (value == 0) {
// Choose the range -1..+1, but slightly wider at the low end
// to make float-0 map exactly to int-0, zero centric.
*lo = dt_lo / dt_hi;
*hi = 1;
}
else if (value > 0) {
// Choose the range 0..2*value, but slightly less at the high end
// to make float-value map exactly to int-0, zero centric.
*lo = 0;
*hi = value * (1 - dt_hi / dt_lo);
}
else { // value < 0
// Choose the range 2*value..0, but slightly wider at the low end
// to make float-value map exactly to int-0, zero centric.
*lo = value * (1 - dt_lo / dt_hi);
*hi = 0;
}
}
/**
* See the overloaded version for details.
*/
void
suggestRange(float value, OpenZGY::SampleDataType dt, float *lo, float *hi)
{
switch (dt) {
case OpenZGY::SampleDataType::int8:
suggestRange(value, -128, +127, lo, hi);
break;
case OpenZGY::SampleDataType::int16:
suggestRange(value, -32768, +32767, lo, hi);
break;
default:
*lo = *hi = value;
break;
}
}
void
copy(const Options& opt, SummaryTimer& rtimer, SummaryTimer& wtimer, SummaryTimer& rwtimer, SummaryTimer& ftimer)
{
......@@ -599,6 +652,31 @@ copy(const Options& opt, SummaryTimer& rtimer, SummaryTimer& wtimer, SummaryTime
if (opt.obricksize[0]>0 && opt.obricksize[1]>0 && opt.obricksize[2]>0)
args.bricksize(opt.obricksize[0], opt.obricksize[1], opt.obricksize[2]);
args.iocontext(&context);
// Existing files with integral storage and all samples set to
// the same value need special handling to appear consistent.
// See explanation in the documentation of the format.
if ((r->datatype() == OpenZGY::SampleDataType::int8 ||
r->datatype() == OpenZGY::SampleDataType::int16) &&
r->raw_datarange()[0] == r->raw_datarange()[1] &&
!opt.output.empty()) {
const float value = r->raw_datarange()[0];
float lo=0, hi=0;
// TODO-Low, should be w->datatype() in case the user overrides
// the output datatype. Chicken and egg problem,
suggestRange(value, r->datatype(), &lo, &hi);
args.datarange(lo, hi);
if (opt.verbose > 0)
std::cerr << "Writing a constant-value file"
<< ", range " << lo << " .. " << hi
<< ", value " << value << "\n"
<< "With datarange() it would be "
<< r->datarange()[0] << " .. " << r->datarange()[1] << "\n";
std::shared_ptr<IZgyWriter> w = IZgyWriter::open(args);
w->writeconst(std::array<int64_t,3>{0,0,0}, w->size(), &value);
return;
}
std::shared_ptr<IZgyWriter> w = !opt.output.empty() ?
IZgyWriter::open(args) :
Test::ZgyWriterMock::mock(args);
......
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
int main()
{
// v1: offsetheader starts at 8
// skip ? bytes to get HistHeader offset, stored flipped
// start of histheader, in this particular file, 0xb512aa
// has range float max, float min (NOTE ORDER)
// which is used for both histogram, statistics, abd codingrange.
int fd = open("Empty-BadCR-v1.zgy", O_RDWR, 0666);
if (fd < 0) {
perror("open");
exit(1);
}
lseek(fd, 0xb512aa, SEEK_SET);
float limits[2] {101.0f, 101.0f};
write(fd, &limits[0], 2*sizeof(float));
close(fd);
// v3: Infoheader starts at 9, additional 13 bytes offset to coding range
// given as float min, float max.
fd = open("Empty-BadCR-v3.zgy", O_RDWR, 0666);
if (fd < 0) {
perror("open");
exit(1);
}
lseek(fd, 22, SEEK_SET);
write(fd, &limits[0], 2*sizeof(float));
close(fd);
return 0;
}
#!/bin/bash
# Writing a constant-value file from Petrel using the old ZGY library
# triggers a number of corner cases. OpenZGY doesn't do that, but we
# need to handle the old constant-value files somehow.
#
# The main problem is that a file with coding range min==max can be
# interpreted in two ways:
#
# (a) This cube has all the samples set to the same value. Reading any
# sample as float returns that value. Reading raw storage values
# returns unspecified values but will probably return all zeros.
# The choice was made when the file was written. The old code in
# Petrel and ZGY could have, but doesn't, avoid the situation by
# widening the range to make it non-empty.
#
# (b) This cube contains discrete values such as classification enums.
# Reading samples as float doesn't make sense and the result is
# unspecified but will probably return all zeros. Reading raw
# storage values returns useful data. The old code in Petrel might
# have avoided the situation by choosing an arbitrary range
# instead of e.g. leaving the range as (0,0). I don't know the
# current behaviour.
#
# Currently, part of this is handles inside the library, both
# C++, Python, and the Python wrapper. And some is handled inside
# the application. Notably the copy tools:
# C++ zgycopyc, Python simplecopy, copy, and gui_copy.
#
# This ends up in a pretty large number of test cases.
# For a scenario that is arguably rather obscure.
#
# Note that is little or no checking of the results in this script;
# for now the verification needs to be manual
#
# Done:
# - Data preparation by patching old files and checking them.
# - Python library and "copy"
# - Python library and "simplecopy"
# - C++ library and "zgycopyc"
# - zgycopy from old accessor, for comparison.
#
# Each of these test copy of v1(int8) -> v3(int8), v3(int8) -> v3(int8),
# and v3(int8) -> v3(float). Except "simplecopy" which doesn't allow changing
# the type. And except "zgycopyc" which doesn't but probably should allow
# changing the type.
#
# TODO-Test: More coverage of the constant-file case.
# - gui_copy (although that shares much code with "copy")
# - Test int16 files as well as int8.
# - Test copy from int8/16 to float using zgycopyc when available.
# - Test negative, zero, and positive constant value.
#
# Current results:
# - Writing a "float" constant file doesn't place the values in the