In case you need to write the code yourself, here's a trick for a simple 'delta' encoding scheme that avoids losing accuracy unnecessarily.
The idea is that the compressor should compute deltas NOT between the current data point and the last, but between the current data point and what the decompressor will have computed for the last. This means that the quantisation errors will not add up.
As a simple example that you might want to peruse/experiment with here's c code that compresses doubles into a (start double) and a sequence of floats:
typedef struct
{ double start;
int n;
float* delta;
} compT;
compT compress( int n, const double* data)
{
compT c = (compT){ data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
double r = c.start;
for( int i=1; i<n; ++i)
{
float d = (float)(data[i]-r);
c.delta[i-1] = d;
r += d;
}
return c;
}
static double* uncompress( compT c)
{
double* d = malloc( (c.n+1)*sizeof *d);
double r = c.start;
d[0] = r;
for( int i=1; i<=c.n; ++i)
{ r += c.delta[i-1];
d[i] = r;
}
return d;
}
compT bad_compress( int n, const double* data)
{
compT c = (compT){ data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
for( int i=1; i<n; ++i)
{
float d = (float)(data[i]-data[i-1]);
c.delta[i-1] = d;
}
return c;
}
When using floats the quantization error build up is really only noticeable on long (millions) data sequences.
However when hoping to compress more, the effects are more noticeable. The code below uses an int_16t for the deltas. I've used this in cases where the data values were guaranteed to be around 1Km, so I used a scale (in the code below) of 16, so could cope with differences of 2km.
typedef struct
{ float scale;
double start;
int n;
int16_t* delta;
} compiT;
compiT compressi( int n, const double* data, float scale)
{
compiT c = (compiT){ scale, data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
double r = c.start;
for( int i=1; i<n; ++i)
{
int16_t d = (int16_t)round(c.scale*(data[i]-r));
c.delta[i-1] = d;
r += ((double)d)/c.scale;
}
return c;
}
compiT bad_compressi( int n, const double* data, float scale)
{
compiT c = (compiT){ scale, data[0], n-1, malloc( (n-1)*sizeof *c.delta)};
for( int i=1; i<n; ++i)
{
int16_t d = (int16_t)round(c.scale*(data[i]-data[i-1]));
c.delta[i-1] = d;
}
return c;
}
static double* uncompressi( compiT c)
{
double* d = malloc( (c.n+1)*sizeof *d);
double r = c.start;
d[0] = r;
for( int i=1; i<=c.n; ++i)
{
double delta = ((double)c.delta[i-1])/c.scale;
r += delta;
d[i] = r;
}
return d;
}
In runs of arbitrary length, the maximum error (ie the differences between the original data and the decompressed compressed data) was, as it should be, around 3cm, whereas using the bad_compressor the errors were around 0.5m on runs of 1000, 2.5m on runs of 10000
Of course where you have no guarantees of boundedness of the differences, you'll need to incorporate some sort of restart.