Skip to content

Commit 91067c7

Browse files
committed
Avoid NaN in sum computation
1 parent 5a9e788 commit 91067c7

File tree

3 files changed

+60
-16
lines changed

3 files changed

+60
-16
lines changed

libs/exponential-histogram/src/main/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramUtils.java

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,46 @@ public class ExponentialHistogramUtils {
2525

2626
/**
2727
* Estimates the sum of all values of a histogram just based on the populated buckets.
28+
* Will never return NaN, but might return +/-Infinity if the histogram is too big.
2829
*
2930
* @param negativeBuckets the negative buckets of the histogram
3031
* @param positiveBuckets the positive buckets of the histogram
31-
* @return the estimated sum of all values in the histogram, guaranteed to be zero if there are no buckets
32+
* @return the estimated sum of all values in the histogram, guaranteed to be zero if there are no buckets.
3233
*/
3334
public static double estimateSum(BucketIterator negativeBuckets, BucketIterator positiveBuckets) {
35+
assert negativeBuckets.scale() == positiveBuckets.scale();
3436
double sum = 0.0;
35-
while (negativeBuckets.hasNext()) {
36-
double bucketMidPoint = ExponentialScaleUtils.getPointOfLeastRelativeError(
37-
negativeBuckets.peekIndex(),
38-
negativeBuckets.scale()
39-
);
40-
sum += -bucketMidPoint * negativeBuckets.peekCount();
41-
negativeBuckets.advance();
42-
}
43-
while (positiveBuckets.hasNext()) {
37+
while (negativeBuckets.hasNext() || positiveBuckets.hasNext()) {
38+
long negativeIndex = negativeBuckets.hasNext() ? negativeBuckets.peekIndex() : Long.MAX_VALUE;
39+
long positiveIndex = positiveBuckets.hasNext() ? positiveBuckets.peekIndex() : Long.MAX_VALUE;
40+
4441
double bucketMidPoint = ExponentialScaleUtils.getPointOfLeastRelativeError(
45-
positiveBuckets.peekIndex(),
42+
Math.min(negativeIndex, positiveIndex),
4643
positiveBuckets.scale()
4744
);
48-
sum += bucketMidPoint * positiveBuckets.peekCount();
49-
positiveBuckets.advance();
45+
46+
long countWithSign;
47+
if (negativeIndex == positiveIndex) {
48+
countWithSign = positiveBuckets.peekCount() - negativeBuckets.peekCount();
49+
positiveBuckets.advance();
50+
negativeBuckets.advance();
51+
} else if (negativeIndex < positiveIndex){
52+
countWithSign = -negativeBuckets.peekCount();
53+
negativeBuckets.advance();
54+
} else { // positiveIndex > negativeIndex
55+
countWithSign = positiveBuckets.peekCount();
56+
positiveBuckets.advance();
57+
}
58+
if (countWithSign != 0) {
59+
double toAdd = bucketMidPoint * countWithSign;
60+
if (Double.isFinite(toAdd)) {
61+
sum += toAdd;
62+
} else {
63+
// Avoid NaN in case we end up with e.g. -Infinity+Infinity
64+
// we consider the bucket with the bigger index the winner for the sign
65+
sum = toAdd;
66+
}
67+
}
5068
}
5169
return sum;
5270
}

libs/exponential-histogram/src/test/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramUtilsTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
package org.elasticsearch.exponentialhistogram;
2323

2424
import static org.hamcrest.Matchers.closeTo;
25+
import static org.hamcrest.Matchers.equalTo;
2526

2627
public class ExponentialHistogramUtilsTests extends ExponentialHistogramTestCase {
2728

@@ -55,4 +56,31 @@ public void testRandomDataSumEstimation() {
5556
assertThat(estimatedAverage, closeTo(correctAverage, allowedError));
5657
}
5758
}
59+
60+
public void testInfinityHandling() {
61+
FixedCapacityExponentialHistogram morePositiveValues = createAutoReleasedHistogram(100);
62+
morePositiveValues.resetBuckets(0);
63+
morePositiveValues.tryAddBucket(1999, 1,false);
64+
morePositiveValues.tryAddBucket(2000, 2, false);
65+
morePositiveValues.tryAddBucket(1999, 2,true);
66+
morePositiveValues.tryAddBucket(2000, 2,true);
67+
68+
double sum = ExponentialHistogramUtils.estimateSum(
69+
morePositiveValues.negativeBuckets().iterator(),
70+
morePositiveValues.positiveBuckets().iterator()
71+
);
72+
assertThat(sum, equalTo(Double.POSITIVE_INFINITY));
73+
FixedCapacityExponentialHistogram moreNegativeValues = createAutoReleasedHistogram(100);
74+
moreNegativeValues.resetBuckets(0);
75+
moreNegativeValues.tryAddBucket(1999, 2,false);
76+
moreNegativeValues.tryAddBucket(2000, 2, false);
77+
moreNegativeValues.tryAddBucket(1999, 1,true);
78+
moreNegativeValues.tryAddBucket(2000, 2,true);
79+
80+
sum = ExponentialHistogramUtils.estimateSum(
81+
moreNegativeValues.negativeBuckets().iterator(),
82+
moreNegativeValues.positiveBuckets().iterator()
83+
);
84+
assertThat(sum, equalTo(Double.NEGATIVE_INFINITY));
85+
}
5886
}

x-pack/plugin/mapper-exponential-histogram/src/test/java/org/elasticsearch/xpack/exponentialhistogram/ExponentialHistogramFieldMapperTests.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,7 @@ private static Map<String, Object> createRandomHistogramValue(int maxBucketCount
129129
Map.of("indices", negativeIndices, "counts", negativeCounts)
130130
)
131131
);
132-
if ((positiveIndices.isEmpty() == false || negativeIndices.isEmpty() == false)) {
133-
// we always add the sum field to avoid numeric problems with the estimation due to random buckets
134-
// sum generation is tested in the yaml tests
132+
if (randomBoolean() && (positiveIndices.isEmpty() == false || negativeIndices.isEmpty() == false)) {
135133
result.put("sum", randomDoubleBetween(-1000, 1000, true));
136134
}
137135
return result;

0 commit comments

Comments
 (0)