/* Copyright 2006 by Sean Luke and George Mason University Licensed under the Academic Free License version 3.0 See the file "LICENSE" for more information */ package sim.util.media.chart; import sim.util.*; /** * This is meant as an on-line algorithm for keeping a constant number of data points * from an on-going time series. It only looks at the X values of the time series data points. * * <p>Specifically, this algorithm eliminates the point the leaves the smallest gap * (i.e. has the closest neighbors). The first and last data point are never touched. * * <p>In case of ties, it chooses the first. This is meant to make the older data sparse while keeping * more/most of the fresh data. * * * Few gaps change between removes (2 old are merged, 1 new is introduced; * 1. I could cache the gap sums * 2. I could use a heap (although the tie breaking might be off) * * * <p> For efficiency reasons, multiple points are dropped in one culling. * The suggested (default) amount of points dropped in one culling is 50%. * * * @author Gabriel Balan */ public class MinGapDataCuller implements DataCuller { int maxPointCount; int pointCountAfterCulling; IntBag reusableIntBag; public MinGapDataCuller(int maxPointCount) { this(maxPointCount, maxPointCount/2+1); } public MinGapDataCuller(int maxPointCount, int pointCountAfterCulling) { setMaxAndMinCounts(maxPointCount, pointCountAfterCulling); this.reusableIntBag = new IntBag(maxPointCount-pointCountAfterCulling+1); //+1 cause you need 1 over maxPointCount to trigger the culling } public boolean tooManyPoints(int currentPointCount) { return currentPointCount >maxPointCount; } void setMaxAndMinCounts(int maxPointCount, int pointCountAfterCulling) { this.maxPointCount = maxPointCount; this.pointCountAfterCulling = pointCountAfterCulling; } // O(maxPoints) static void sort(IntBag indices, int maxPoints) { boolean[] map = new boolean[maxPoints]; for(int i=0;i<indices.numObjs;i++) map[indices.objs[i]]=true; indices.clear(); for(int i=0;i<maxPoints;i++) if(map[i]) indices.add(i); } public IntBag cull(double[] xValues, boolean sortedOutput) { return cull(xValues, reusableIntBag, sortedOutput); } IntBag cull(double[] xValues, IntBag droppedIndices, boolean sortOutput) { return cull(xValues, pointCountAfterCulling, droppedIndices, sortOutput); } //this ignores size!!! IntBag cull(double[] xValues, int size, IntBag droppedIndices, boolean sortOutput) { IntBag bag = cullToSize(xValues, size, droppedIndices); if(sortOutput) sort(bag, xValues.length); return bag; } /* I expect the xValues to be sorted! * I don't sort the output, but I offer <code>sort</code> to do it. * * This works by finding the element that would leave the * smallest gap (if dropped), then repeat. This means I won't touch * the first and last element. **/ static public IntBag cullToSize(double[] xValues, int size, IntBag droppedIndices) { droppedIndices.clear(); int pointsToDrop = xValues.length-size; if(pointsToDrop<=0) return droppedIndices; if(xValues.length<=2) { //I shouldn't be in this situation. I'll just drop something and get out. for(int i=0;i<pointsToDrop;i++) droppedIndices.add(i); return droppedIndices; } if(pointsToDrop==1) { //no need for a heap, just the best point to drop //i.e. the points with the neighbors closest together. double bestGapSumSoFar = Double.MAX_VALUE; int index = -1; double lastX = xValues[1]; double lastGap = xValues[1]-xValues[0]; for(int i=2;i<xValues.length;i++) { double xi = xValues[i]; double gap = xi-lastX; lastX = xi; double gapSum = gap+lastGap; lastGap = gap; if(gapSum<bestGapSumSoFar) { index = i-1; bestGapSumSoFar = gapSum; } } droppedIndices.add(index); return droppedIndices; } //now for the main case: let's make the heap Heap h = new Heap(xValues); for(int i=0;i<pointsToDrop;i++) { droppedIndices.add(h.extractMin().xValueIndex); } return droppedIndices; } static class Record implements Comparable { int xValueIndex; double leftGap, rightGap; double key;//leftGap+rightGap. Record leftRecord, rightRecord; int heapPosition;//actually, it's the index in 1based counting. Record(int xValueIndex, double leftGap, double rightGap, int heapPosition) { this.xValueIndex = xValueIndex; this.leftGap = leftGap; this.rightGap = rightGap; this.key = leftGap+rightGap; this.heapPosition = heapPosition; } //I prefer to drop the point the leaves behind the smallest gap (key=leftGap+rightGap) //In case of a tie, I prefer to dop the first point (so I keep more of the fresh data on) public int compareTo(Object o) { Record r = (Record)o; double keydiff = key-r.key; if(keydiff==0) return xValueIndex - r.xValueIndex; else return keydiff<=0?-1:1; } void setLeftGap(double lg) { leftGap = lg; key = leftGap+rightGap; } void setRightGap(double rg) { rightGap = rg; key = leftGap+rightGap; } } static class Heap { int heapsize; Record[] heap; Heap(double[] xValues) { this.heapsize = xValues.length-2; //of all the data points I can delete, the first and last are taboo. heap = new Record[heapsize]; double currentX = xValues[1]; double lastGap = currentX-xValues[0]; if(lastGap<=0) throw new RuntimeException("I expect xValues in strictly increasing order."); Record lastRecord = null; for(int i=1;i<xValues.length-1;i++) { double nextX = xValues[i+1]; double nextGap = nextX - currentX; if(nextGap<=0) throw new RuntimeException("I expect xValues in strictly increasing order."); Record ri = new Record(i,lastGap,nextGap,i); ri.leftRecord = lastRecord; if(lastRecord!=null) lastRecord.rightRecord = ri; lastRecord = ri; currentX = nextX; lastGap = nextGap; heap[i-1]=ri; } for( int i = heapsize/2 ; i >= 1 ; i-- ) heapify(i); } Record extractMin() { if( heapsize == 0 ) return null; // remove the info Record result = heap[1-1]; heap[1-1] = heap[heapsize-1]; heap[1-1].heapPosition=1; heap[heapsize-1] = null; heapsize--; // rebuild heap if (heapsize > 1) {// no need to heapify if there's only zero or one element! heapify(1); //lets update the previous and next record. Record leftRecord = result.leftRecord; Record rightRecord = result.rightRecord; if(rightRecord!=null) { if(rightRecord.leftGap!=result.rightGap) throw new RuntimeException("BUG");//TODO delete these checks } if(leftRecord!=null) { if(leftRecord.rightGap!=result.leftGap) throw new RuntimeException("BUG");//TODO delete these checks //leftRecord.rightGap+=result.rightGap; leftRecord.setRightGap(result.key); leftRecord.rightRecord = result.rightRecord; heapify(leftRecord.heapPosition); } if(rightRecord!=null) { rightRecord.setLeftGap(result.key); rightRecord.leftRecord = result.leftRecord; heapify(rightRecord.heapPosition); } } return result; } void heapify(int i) { while(true) { int l = 2*i; int r = 2*i+1; int smallest; if( l <= heapsize && heap[l-1].compareTo(heap[i-1])<0) smallest = l; else smallest = i; if( r <= heapsize && heap[r-1].compareTo(heap[smallest-1]) < 0) smallest = r; if( smallest != i ) { // swap records Record tmp = heap[i-1]; heap[i-1] = heap[smallest-1]; heap[smallest-1] = tmp; heap[i-1].heapPosition=i; heap[smallest-1].heapPosition = smallest; // recursive call.... :) i = smallest; } else return; } } } }