package org.yamcs.web.rest.archive;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* One-pass downsampler for time-series data (i.e. numeric archived parameters),
* where the number of recorded data points are not known upfront.
* <p>
* The output is not a bunch of parameter values, but instead a range of values
* limited to n, which should be fit for inclusion in plots.
* <p>
* This is *NOT* perfect. The range of returned items is not known upfront, so
* a rough assumption is made based on the first result, and up until validEnd.
*/
public class RestDownsampler {
private static final Logger log = LoggerFactory.getLogger(RestDownsampler.class);
private static final int DEFAULT_INTERVAL_COUNT = 500;
private TreeMap<Long, Sample> samplesByTime;
private long start;
private final long projectedEnd;
private final int intervalCount;
private long lastSampleTime;
public RestDownsampler(long projectedEnd) {
this(projectedEnd, DEFAULT_INTERVAL_COUNT);
}
public RestDownsampler(long projectedEnd, int intervalCount) {
this.projectedEnd = projectedEnd;
this.intervalCount = intervalCount;
}
private void initializeIntervals(long start) {
this.start = start;
samplesByTime = new TreeMap<>();
long step = (projectedEnd - start) / intervalCount;
for (long i = start; i < projectedEnd; i+=step) {
samplesByTime.put(i, null);
}
}
/**
* Assumes timesorted processing, as the first entry will be used to
* determine the time spread of the buckets, up until validEnd. :-/
*/
public void process(long time, double value) {
if (time > projectedEnd || time < start) {
return;
}
lastSampleTime = time;
if (samplesByTime == null) {
initializeIntervals(time);
}
Entry<Long, Sample> entry = samplesByTime.floorEntry(time);
if (entry == null) {
log.warn("No interval for value " + value);
return;
}
Sample sample = entry.getValue();
if (sample == null) {
samplesByTime.put(entry.getKey(), new Sample(time, value));
}
else sample.process(time, value);
}
public List<Sample> collect() {
if (samplesByTime == null) {
return Collections.emptyList();
}
return samplesByTime.values().stream().filter(s -> s != null).collect(Collectors.toList());
}
/**
* A cumulative sample that keeps track of a rolling average
* among others.
*/
public static class Sample {
long avgt;
double min;
double max;
double avg;
int n;
public Sample(long t, double value) {
avgt = t;
min = avg = max = value;
n = 1;
}
public void process(long t, double value) {
if (value < min) min = value;
if (value > max) max = value;
n++;
avgt -= (avgt / n);
avgt += (t / n);
avg -= (avg / n);
avg += (value / n);
}
@Override
public String toString() {
return String.format("%s (min=%s, max=%s, n=%s)", avg, min, max, n);
}
}
public void process(org.yamcs.parameter.ParameterValue pval) {
if (pval.getEngValue() == null) {
return;
}
switch (pval.getEngValue().getType()) {
case DOUBLE:
process(pval.getGenerationTime(), pval.getEngValue().getDoubleValue());
break;
case FLOAT:
process(pval.getGenerationTime(), pval.getEngValue().getFloatValue());
break;
case SINT32:
process(pval.getGenerationTime(), pval.getEngValue().getSint32Value());
break;
case SINT64:
process(pval.getGenerationTime(), pval.getEngValue().getSint64Value());
break;
case UINT32:
process(pval.getGenerationTime(), pval.getEngValue().getUint32Value()&0xFFFFFFFFL);
break;
case UINT64:
process(pval.getGenerationTime(), pval.getEngValue().getUint64Value());
break;
default:
log.warn("Unexpected value type {}", pval.getEngValue().getType());
}
}
public long lastSampleTime() {
return lastSampleTime;
}
}