Skip to content

Commit 538ca55

Browse files
committed
Simple impl for LTTB array down sampling function
1 parent c823646 commit 538ca55

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package org.csstudio.apputil.formula.array;
2+
3+
import org.epics.util.array.*;
4+
import org.epics.vtype.*;
5+
import org.phoebus.core.vtypes.VTypeHelper;
6+
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
/**
12+
* @author Kunal Shroff
13+
*/
14+
public class ArraySampleWithLTTBFunction extends BaseArrayFunction {
15+
16+
@Override
17+
public String getName() {
18+
return "arraySampleWithLTTB";
19+
}
20+
21+
@Override
22+
public String getDescription() {
23+
return "Downsample the array using LTTB";
24+
}
25+
26+
@Override
27+
public List<String> getArguments() {
28+
return List.of("array", "buckets");
29+
}
30+
31+
32+
protected VType getArrayData(final VNumberArray array, final double buckets) {
33+
return VNumberArray.of(sampleWithLTTB(array.getData(), buckets), Alarm.none(), array.getTime(), Display.none());
34+
}
35+
36+
// TODO at some point we might want to support non uniform x values
37+
private static final class Point {
38+
public final double x, y;
39+
public Point(double x, double y) { this.x = x; this.y = y; }
40+
}
41+
42+
static ListNumber sampleWithLTTB(final ListNumber data, final double threshold) {
43+
final int n = data.size();
44+
if (threshold >= n || threshold <= 0) return data;
45+
if (threshold == 1) return CollectionNumbers.toListDouble(data.getDouble(0));
46+
if (threshold == 2) return CollectionNumbers.toListDouble(data.getDouble(0), data.getDouble(1));
47+
48+
final double[] out = new double[(int) threshold];
49+
out[0] = data.getDouble(0); // keep first
50+
out[(int) (threshold-1)] = data.getDouble(n-1);
51+
52+
int aIdx = 0; // index of last selected point
53+
double bucketSize = (double)(n - 2) / (threshold - 2);
54+
55+
for (int i = 0; i < threshold - 2; i++) {
56+
// range of current bucket
57+
int cs = (int)Math.floor(i * bucketSize) + 1;
58+
int ce = (int)Math.floor((i + 1) * bucketSize) + 1;
59+
if (ce > n - 1) ce = n - 1;
60+
61+
// range of next bucket
62+
int ns = (int)Math.floor((i + 1) * bucketSize) + 1;
63+
int ne = (int)Math.floor((i + 2) * bucketSize) + 1;
64+
if (ne > n - 1) ne = n - 1;
65+
66+
// average of next bucket (x = index, y = value)
67+
double avgX;
68+
double avgY;
69+
if (ns == ne && ns < n) {
70+
avgX = ns;
71+
avgY = data.getDouble(ns);
72+
} else if (ns >= ne) {
73+
avgX = ns;
74+
avgY = data.getDouble(Math.min(ns, n - 2));
75+
} else {
76+
double sx = 0, sy = 0;
77+
for (int j = ns; j < ne; j++) { sx += j; sy += data.getDouble(j); }
78+
int cnt = ne - ns;
79+
avgX = sx / cnt;
80+
avgY = sy / cnt;
81+
}
82+
83+
// find point in current bucket with max triangle area
84+
double ax = aIdx, ay = data.getDouble(aIdx);
85+
double bestArea = -1;
86+
int bestIdx = cs;
87+
for (int j = cs; j < ce; j++) {
88+
double bx = j, by = data.getDouble(j);
89+
double area = Math.abs(
90+
(bx - ax) * (avgY - ay) - (avgX - ax) * (by - ay)
91+
);
92+
if (area >= bestArea) { // prefer last index in case of tie
93+
bestArea = area;
94+
bestIdx = j;
95+
}
96+
}
97+
98+
out[i+1] = data.getDouble(bestIdx);
99+
aIdx = bestIdx;
100+
}
101+
102+
return CollectionNumbers.toList(out);
103+
}
104+
105+
@Override
106+
public VType compute(final VType... args) throws Exception {
107+
if (args.length != 2) {
108+
throw new Exception("Function " + getName() +
109+
" requires 2 arguments but received " + Arrays.toString(args));
110+
}
111+
if (!VTypeHelper.isNumericArray(args[0]))
112+
throw new Exception("Function " + getName() +
113+
" takes array but received " + Arrays.toString(args));
114+
115+
double buckets = VTypeHelper.toDouble(args[1]);
116+
117+
return getArrayData((VNumberArray) args[0], buckets);
118+
}
119+
}

core/formula/src/main/resources/META-INF/services/org.csstudio.apputil.formula.spi.FormulaFunction

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ org.csstudio.apputil.formula.array.ArrayScalarDivisionFunction
5959
org.csstudio.apputil.formula.array.ArrayInverseScalarDivisionFunction
6060
org.csstudio.apputil.formula.array.ArrayOfFunction
6161
org.csstudio.apputil.formula.array.ArrayRangeOfFunction
62+
org.csstudio.apputil.formula.array.ArraySampleWithLTTBFunction
6263
org.csstudio.apputil.formula.array.ArraySampleWithStrideFunction
6364
org.csstudio.apputil.formula.array.ArrayStatsFunction
6465
org.csstudio.apputil.formula.array.ArrayMaxFunction
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.csstudio.apputil.formula.array;
2+
3+
import org.epics.util.array.*;
4+
import org.junit.jupiter.api.Test;
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
class ArraySampleWithLTTBFunctionTest {
8+
@Test
9+
void testThresholdEqualsOne() {
10+
ListNumber data = CollectionNumbers.toListDouble(1.0, 2.0, 3.0, 4.0);
11+
ListNumber result = ArraySampleWithLTTBFunction.sampleWithLTTB(data, 1);
12+
assertEquals(1, result.size());
13+
assertEquals(1.0, result.getDouble(0));
14+
}
15+
16+
@Test
17+
void testThresholdEqualsTwo() {
18+
ListNumber data = CollectionNumbers.toListDouble(1.0, 2.0, 3.0, 4.0);
19+
ListNumber result = ArraySampleWithLTTBFunction.sampleWithLTTB(data, 2);
20+
assertEquals(2, result.size());
21+
assertEquals(1.0, result.getDouble(0));
22+
assertEquals(2.0, result.getDouble(1));
23+
}
24+
25+
@Test
26+
void testThresholdGreaterThanDataSize() {
27+
ListNumber data = CollectionNumbers.toListDouble(1.0, 2.0, 3.0);
28+
ListNumber result = ArraySampleWithLTTBFunction.sampleWithLTTB(data, 5);
29+
assertEquals(data.size(), result.size());
30+
for (int i = 0; i < data.size(); i++) {
31+
assertEquals(data.getDouble(i), result.getDouble(i));
32+
}
33+
}
34+
35+
@Test
36+
void testMonotonicIncreasing() {
37+
ListNumber data = CollectionNumbers.toListDouble(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
38+
ListNumber result = ArraySampleWithLTTBFunction.sampleWithLTTB(data, 3);
39+
assertEquals(3, result.size());
40+
assertEquals(1.0, result.getDouble(0));
41+
assertEquals(5.0, result.getDouble(1));
42+
assertEquals(6.0, result.getDouble(2));
43+
}
44+
45+
@Test
46+
void testConstantData() {
47+
ListNumber data = CollectionNumbers.toListDouble(5.0, 5.0, 5.0, 5.0, 5.0);
48+
ListNumber result = ArraySampleWithLTTBFunction.sampleWithLTTB(data, 3);
49+
assertEquals(3, result.size());
50+
assertEquals(5.0, result.getDouble(0));
51+
assertEquals(5.0, result.getDouble(1));
52+
assertEquals(5.0, result.getDouble(2));
53+
}
54+
55+
}
56+

0 commit comments

Comments
 (0)