Skip to content

Commit 189c8bc

Browse files
authored
Merge pull request #3578 from ControlSystemStudio/LTTB
array down sampling with LTTB
2 parents 5cf6e87 + 0ca72a9 commit 189c8bc

File tree

5 files changed

+196
-5
lines changed

5 files changed

+196
-5
lines changed

core/formula/doc/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ This includes the average, min, max, and element count
155155

156156
**arrayMin(VNumberArray array)** - Returns a VDouble with the smallest value of the given array
157157

158+
**arraySampleWithLTTB(VNumberArray array, VNumber threshold)** - Returns a VNumberArray which is a down-sampled version of the input array.
159+
The threshold parameter defines the maximum number of data points to return.
160+
The down-sampling is performed using the Largest-Triangle-Three-Buckets (LTTB) algorithm.
161+
158162
**arraySampleWithStride(VNumberArray array, VNumber stride, VNumber offset)** - Returns a VNumberArray where each element is defined as array\[x \* stride + offset\].
159163

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

core/formula/src/main/java/org/csstudio/apputil/formula/array/ArraySampleWithStrideFunction.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.csstudio.apputil.formula.array;
22

3+
import org.csstudio.apputil.formula.Formula;
34
import org.epics.util.array.ListDouble;
45
import org.epics.util.array.ListNumber;
56
import org.epics.vtype.Alarm;
@@ -10,6 +11,7 @@
1011

1112
import java.util.Arrays;
1213
import java.util.List;
14+
import java.util.logging.Level;
1315

1416
/**
1517
* A formula function for extracting elements from the given array at regular intervals (stride),
@@ -104,13 +106,16 @@ public VType compute(final VType... args) throws Exception {
104106
throw new Exception("Function " + getName() +
105107
" requires 2 or 3 arguments but received " + Arrays.toString(args));
106108
}
107-
if (!VTypeHelper.isNumericArray(args[0]))
108-
throw new Exception("Function " + getName() +
109+
if (!VTypeHelper.isNumericArray(args[0])) {
110+
Formula.logger.log(Level.WARNING, "Function " + getName() +
109111
" takes array but received " + Arrays.toString(args));
112+
return DEFAULT_NAN_DOUBLE_ARRAY;
110113

111-
double stride = VTypeHelper.toDouble(args[1]);
112-
double offset = (args.length == 3) ? VTypeHelper.toDouble(args[2]) : 0; // Default offset to 0
114+
} else {
115+
double stride = VTypeHelper.toDouble(args[1]);
116+
double offset = (args.length == 3) ? VTypeHelper.toDouble(args[2]) : 0; // Default offset to 0
113117

114-
return getArrayData((VNumberArray) args[0], stride, offset);
118+
return getArrayData((VNumberArray) args[0], stride, offset);
119+
}
115120
}
116121
}

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)