net.volcanite.util.DoubleStatisticsSync Maven / Gradle / Ivy
package net.volcanite.util;
import java.util.Objects;
/**
* A state object for collecting statistics such as count, min, max, sum,
* average and variance (or standard deviation).
*
* Implementation Note:
* This implementation is thread-safe.
*/
public class DoubleStatisticsSync implements DoubleStatistics {
private long count;
private double sum;
private double sumCompensation; // Negative low order bits of sum
private double min = Double.POSITIVE_INFINITY;
private double max = Double.NEGATIVE_INFINITY;
/**
* the sum of squares of differences from the (current) mean
* http://en.wikipedia
* .org/wiki/Algorithms_for_calculating_variance#On-line_algorithm
* (Welford's algorithm)
*/
private double sumDiffFromCurrMeanSquared;
private double sumDiffFromCurrMeanSquaredCompensation;
/**
* the variance - recursively calculated via Welford's algorithm
*/
private double variance;
/**
* Construct an empty instance with zero count, zero sum,
* {@code Double.POSITIVE_INFINITY} min, {@code Double.NEGATIVE_INFINITY}
* max and zero average.
*/
public DoubleStatisticsSync() {
}
/**
* Constructs a non-empty instance with an initial state that corresponds to
* the current state of the specified {@code other} DoubleStatisticsSync
* instance.
*
*
* If {@code other.count} is zero then the remaining arguments are ignored
* and an empty instance is constructed.
*
*
* If the state of {@code other} is inconsistent then an
* {@code IllegalArgumentException} is thrown. The necessary conditions for
* a consistent state are:
*
* - {@code other.count >= 0}
* - {@code (other.min <= other.max && !isNaN(other.sum)) || (isNaN(other.min) && isNaN(other.max) && isNaN(other.sum))}
*
*
* API Note:
* The enforcement of state correctness means that the retrieved set of
* recorded values obtained from a {@code DoubleStatisticsSync} source
* instance may not be a legal state for this constructor due to arithmetic
* overflow of the source's recorded count of values. The consistency
* conditions are not sufficient to prevent the creation of an internally
* inconsistent instance. An example of such a state would be an instance
* with: {@code other.count} = 2, {@code other.min} = 1, {@code other.max} =
* 2, and {@code other.sum} = 0.
*
* @param other
* the DoubleStatisticsSync instance whose state should be
* replicated
* @throws NullPointerException
* if {@code other} is null
* @throws IllegalArgumentException
* if the internal state of the {@code other} object is
* inconsistent
*/
public DoubleStatisticsSync(DoubleStatisticsSync other) throws IllegalArgumentException {
Objects.requireNonNull(other);
synchronized (other) {
if (other.count < 0L) {
throw new IllegalArgumentException("Negative count value");
} else if (other.count > 0L) {
if (other.min > other.max) {
throw new IllegalArgumentException("Minimum greater than maximum");
}
// All NaN or non NaN
int ncount = 0;
if (Double.isNaN(other.min)) {
++ncount;
}
if (Double.isNaN(other.max)) {
++ncount;
}
if (Double.isNaN(other.sum)) {
++ncount;
}
if (ncount > 0 && ncount < 3) {
throw new IllegalArgumentException("Some, not all, of the minimum, maximum, or sum is NaN");
}
this.count = other.count;
this.sum = other.sum;
this.sumCompensation = 0.0d;
this.min = other.min;
this.max = other.max;
this.sumDiffFromCurrMeanSquared = other.sumDiffFromCurrMeanSquared;
this.sumDiffFromCurrMeanSquaredCompensation = other.sumDiffFromCurrMeanSquaredCompensation;
this.variance = other.variance;
}
// Use default field values if count == 0
}
}
/**
* {@inheritDoc}
*/
@Override
public synchronized void accept(double value) {
long countSoFar = count;
double average = getAverage();
++count;
sumWithCompensation(value);
min = Math.min(min, value);
max = Math.max(max, value);
double delta = value - average;
average = ((countSoFar * average) / count) + (value / count);
sumDiffFromCurrMeanSquaredWithCompensation(delta * (value - average));
if (count > 1L) {
variance = (sumDiffFromCurrMeanSquared - sumDiffFromCurrMeanSquaredCompensation) / countSoFar;
}
}
/**
* Incorporate a new double value using Kahan summation / compensated
* summation.
*/
private void sumWithCompensation(double value) {
// https://en.wikipedia.org/wiki/Kahan_summation_algorithm
double tmp = value - sumCompensation;
double velvel = sum + tmp; // Little wolf of rounding error
sumCompensation = (velvel - sum) - tmp;
sum = velvel;
}
/**
* Incorporate a new double value using Kahan summation / compensated
* summation.
*/
private void sumDiffFromCurrMeanSquaredWithCompensation(double value) {
double tmp = value - sumDiffFromCurrMeanSquaredCompensation;
double velvel = sumDiffFromCurrMeanSquared + tmp;
sumDiffFromCurrMeanSquaredCompensation = (velvel - sumDiffFromCurrMeanSquared) - tmp;
sumDiffFromCurrMeanSquared = velvel;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final long getCount() {
return count;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final double getSum() {
// Better error bounds to add both terms as the final sum
return sum - sumCompensation;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final double getMin() {
return min;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final double getMax() {
return max;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final double getAverage() {
return getCount() > 0 ? getSum() / getCount() : 0.0d;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final double getVariance() {
double var = variance;
if (var < 0.0) {
var = 0.0;
}
return var;
}
/**
* {@inheritDoc}
*/
@Override
public synchronized final double getStandardDeviation() {
return Math.sqrt(getVariance());
}
/**
* {@inheritDoc}
*/
@Override
public synchronized String toString() {
return String.format("%s{count=%d, sum=%f, min=%f, average=%f, max=%f, stddev=%f}",
this.getClass().getSimpleName(), getCount(), getSum(), getMin(), getAverage(), getMax(),
getStandardDeviation());
}
}