diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/ComponentMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/ComponentMeasure.java new file mode 100644 index 0000000..45e1e6c --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/ComponentMeasure.java @@ -0,0 +1,11 @@ +package net.laprun.sustainability.power.measure; + +public interface ComponentMeasure { + void recordComponentValue(double value); + + double[] getComponentRawValues(); + + interface Factory { + T create(); + } +} diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/DescriptiveStatisticsComponentMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/DescriptiveStatisticsComponentMeasure.java new file mode 100644 index 0000000..4518620 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/DescriptiveStatisticsComponentMeasure.java @@ -0,0 +1,25 @@ +package net.laprun.sustainability.power.measure; + +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; + +public class DescriptiveStatisticsComponentMeasure implements ComponentMeasure { + private final DescriptiveStatistics statistics; + + public DescriptiveStatisticsComponentMeasure(int initialWindow) { + statistics = new DescriptiveStatistics(initialWindow); + } + + @Override + public void recordComponentValue(double value) { + statistics.addValue(value); + } + + @Override + public double[] getComponentRawValues() { + return statistics.getValues(); + } + + public static Factory factory(int initialWindow) { + return () -> new DescriptiveStatisticsComponentMeasure(initialWindow); + } +} diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/DescriptiveStatisticsMeasureStore.java b/measure/src/main/java/net/laprun/sustainability/power/measure/DescriptiveStatisticsMeasureStore.java deleted file mode 100644 index 0e08df8..0000000 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/DescriptiveStatisticsMeasureStore.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.laprun.sustainability.power.measure; - -import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; - -class DescriptiveStatisticsMeasureStore implements MeasureStore { - private final int totalIndex; - private final DescriptiveStatistics[] measures; - - public DescriptiveStatisticsMeasureStore(int componentsNumber, int initialWindow) { - this.measures = new DescriptiveStatistics[componentsNumber + 1]; - totalIndex = componentsNumber; - for (int i = 0; i < measures.length; i++) { - measures[i] = new DescriptiveStatistics(initialWindow); - } - } - - @Override - public void recordComponentValue(int component, double value) { - getMeasure(component).addValue(value); - } - - private DescriptiveStatistics getMeasure(int component) { - return measures[component]; - } - - private DescriptiveStatistics getTotalMeasure() { - return getMeasure(totalIndex); - } - - @Override - public void recordTotal(double value) { - getTotalMeasure().addValue(value); - } - - @Override - public double getMeasuredTotal() { - return getTotalMeasure().getSum(); - } - - @Override - public double getComponentStandardDeviation(int component) { - return getMeasure(component).getStandardDeviation(); - } - - @Override - public double getTotalStandardDeviation() { - return getTotalMeasure().getStandardDeviation(); - } - - @Override - public double[] getComponentRawValues(int component) { - return getMeasure(component).getValues(); - } -} diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/HdrHistogramComponentMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/HdrHistogramComponentMeasure.java new file mode 100644 index 0000000..1f9d175 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/HdrHistogramComponentMeasure.java @@ -0,0 +1,35 @@ +package net.laprun.sustainability.power.measure; + +import org.HdrHistogram.HistogramIterationValue; +import org.HdrHistogram.IntCountsHistogram; + +public class HdrHistogramComponentMeasure implements ComponentMeasure { + private static final int HIGHEST_TRACKABLE_VALUE = 1_000_000; + private static final int NUMBER_OF_SIGNIFICANT_VALUE_DIGITS = 4; + private static final int CONVERSION_FACTOR = 1000; + private final IntCountsHistogram histogram; + + public HdrHistogramComponentMeasure() { + histogram = new IntCountsHistogram(HIGHEST_TRACKABLE_VALUE, NUMBER_OF_SIGNIFICANT_VALUE_DIGITS); + } + + @Override + public void recordComponentValue(double value) { + histogram.recordValue((long) (CONVERSION_FACTOR * value)); + } + + @Override + public double[] getComponentRawValues() { + final var totalCount = histogram.getTotalCount(); + if (totalCount == 0) { + return new double[0]; + } + + final var result = new double[(int) totalCount]; + int index = 0; + for (HistogramIterationValue value : histogram.recordedValues()) { + result[index++] = (double) value.getValueIteratedTo() / CONVERSION_FACTOR; + } + return result; + } +} diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/HdrHistogramMeasureStore.java b/measure/src/main/java/net/laprun/sustainability/power/measure/HdrHistogramMeasureStore.java deleted file mode 100644 index 1d5c566..0000000 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/HdrHistogramMeasureStore.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.laprun.sustainability.power.measure; - -import org.HdrHistogram.HistogramIterationValue; -import org.HdrHistogram.IntCountsHistogram; - -public class HdrHistogramMeasureStore implements MeasureStore { - private static final int HIGHEST_TRACKABLE_VALUE = 1_000_000; - private static final int NUMBER_OF_SIGNIFICANT_VALUE_DIGITS = 4; - private static final int CONVERSION_FACTOR = 1000; - private final IntCountsHistogram[] measures; - private final int totalIndex; - private double accumulatedTotal; - - public HdrHistogramMeasureStore(int componentsNumber, int initialWindow) { - totalIndex = componentsNumber; - measures = new IntCountsHistogram[componentsNumber + 1]; - for (int i = 0; i < measures.length; i++) { - measures[i] = new IntCountsHistogram(HIGHEST_TRACKABLE_VALUE, - NUMBER_OF_SIGNIFICANT_VALUE_DIGITS); - } - } - - private IntCountsHistogram getMeasure(int component) { - return measures[component]; - } - - private IntCountsHistogram getTotalMeasure() { - return getMeasure(totalIndex); - } - - @Override - public void recordComponentValue(int component, double value) { - getMeasure(component).recordValue((long) (CONVERSION_FACTOR * value)); - } - - @Override - public void recordTotal(double value) { - getTotalMeasure().recordValue((long) (CONVERSION_FACTOR * value)); - accumulatedTotal += value; - } - - @Override - public double getMeasuredTotal() { - return accumulatedTotal; - } - - @Override - public double getComponentStandardDeviation(int component) { - return getMeasure(component).getStdDeviation() / CONVERSION_FACTOR; - } - - @Override - public double getTotalStandardDeviation() { - // not unbiased so tests will fail - return getTotalMeasure().getStdDeviation(); - } - - @Override - public double[] getComponentRawValues(int component) { - final var measure = getMeasure(component); - final var totalCount = measure.getTotalCount(); - final var result = new double[(int) totalCount]; - int index = 0; - for (HistogramIterationValue value : measure.recordedValues()) { - result[index++] = (double) value.getValueIteratedTo() / CONVERSION_FACTOR; - } - return result; - } -} diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/MeasureStore.java b/measure/src/main/java/net/laprun/sustainability/power/measure/MeasureStore.java deleted file mode 100644 index c878b43..0000000 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/MeasureStore.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.laprun.sustainability.power.measure; - -interface MeasureStore { - void recordComponentValue(int component, double value); - - void recordTotal(double value); - - double getMeasuredTotal(); - - double getComponentStandardDeviation(int component); - - double getTotalStandardDeviation(); - - double[] getComponentRawValues(int component); -} diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java index fef448f..3b15e26 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java @@ -4,28 +4,37 @@ import java.util.HashSet; import java.util.Set; +import org.apache.commons.math3.util.FastMath; + import net.laprun.sustainability.power.SensorMetadata; public class OngoingPowerMeasure implements PowerMeasure { private final SensorMetadata sensorMetadata; - private final MeasureStore measures; + private final ComponentMeasure[] measures; private final long startedAt; private final double[] averages; private final Set nonZeroComponents; private final int[] totalComponents; + private final int totalIndex; private double minTotal = Double.MAX_VALUE; private double maxTotal; + private double accumulatedTotal; private int samples; - public OngoingPowerMeasure(SensorMetadata sensorMetadata, Duration duration, Duration frequency) { + public OngoingPowerMeasure(SensorMetadata sensorMetadata, ComponentMeasure.Factory componentMeasureFactory) { this.sensorMetadata = sensorMetadata; startedAt = System.currentTimeMillis(); - final var numComponents = metadata().componentCardinality(); - averages = new double[numComponents]; - - final var initialWindow = (int) (duration.toMillis() / frequency.toMillis()); - measures = new DescriptiveStatisticsMeasureStore(numComponents, initialWindow); + final var numComponents = sensorMetadata.componentCardinality(); + // we also record the aggregated total for each component participating in the aggregated value + final var measuresNb = numComponents + 1; + measures = new ComponentMeasure[measuresNb]; + for (int i = 0; i < measuresNb; i++) { + measures[i] = componentMeasureFactory.create(); + } + totalIndex = numComponents; + averages = new double[measuresNb]; + // we don't need to record the total component as a non-zero component since it's almost never zero and we compute the std dev separately nonZeroComponents = new HashSet<>(numComponents); totalComponents = sensorMetadata.totalComponents(); } @@ -49,14 +58,17 @@ public void recordMeasure(double[] components) { if (componentValue != 0) { nonZeroComponents.add(component); } - measures.recordComponentValue(component, componentValue); + measures[component].recordComponentValue(componentValue); averages[component] = averages[component] == 0 ? componentValue : (previousSize * averages[component] + componentValue) / samples; } // record min / max totals final var recordedTotal = PowerMeasure.sumOfSelectedComponents(components, totalComponents); - measures.recordTotal(recordedTotal); + measures[totalIndex].recordComponentValue(recordedTotal); + accumulatedTotal += recordedTotal; + averages[components.length] = averages[components.length] == 0 ? recordedTotal + : (previousSize * averages[components.length] + recordedTotal) / samples; if (recordedTotal < minTotal) { minTotal = recordedTotal; } @@ -67,7 +79,7 @@ public void recordMeasure(double[] components) { @Override public double total() { - return measures.getMeasuredTotal(); + return accumulatedTotal; } public Duration duration() { @@ -94,13 +106,28 @@ public StdDev standardDeviations() { final var stdDevs = new double[cardinality]; nonZeroComponents.stream() .parallel() - .forEach(component -> stdDevs[component] = measures.getComponentStandardDeviation(component)); + .forEach(component -> stdDevs[component] = standardDeviation(component)); - return new StdDev(measures.getTotalStandardDeviation(), stdDevs); + final double aggregate = maxTotal == 0 ? 0 : standardDeviation(totalIndex); + return new StdDev(aggregate, stdDevs); + } + + private double standardDeviation(int component) { + final var values = measures[component].getComponentRawValues(); + if (samples <= 1) { + return 0.0; + } + final double mean = averages[component]; + double geometric_deviation_total = 0.0; + for (double value : values) { + double deviation = value - mean; + geometric_deviation_total += (deviation * deviation); + } + return FastMath.sqrt(geometric_deviation_total / (samples - 1)); } @Override public double[] getMeasuresFor(int component) { - return measures.getComponentRawValues(component); + return measures[component].getComponentRawValues(); } } diff --git a/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java b/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java index 8205e9b..dbf2ed0 100644 --- a/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java +++ b/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java @@ -2,31 +2,38 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.time.Duration; +import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import net.laprun.sustainability.power.SensorMetadata; public class OngoingPowerMeasureTest { + private final static SensorMetadata metadata = new SensorMetadata(Map.of(), null, new int[0]) { - @Test - void testStatistics() { + @Override + public int componentCardinality() { + return 3; + } + }; + + static List> factories() { + return List.of(DescriptiveStatisticsComponentMeasure.factory(2), HdrHistogramComponentMeasure::new); + } + + @ParameterizedTest + @MethodSource("factories") + void testStatistics(ComponentMeasure.Factory factory) { final var m1c1 = 10.0; final var m1c2 = 12.0; final var m1c3 = 0.0; final var m2c1 = 8.0; final var m2c2 = 17.0; final var m2c3 = 0.0; - final var metadata = new SensorMetadata(Map.of(), null, new int[0]) { - - @Override - public int componentCardinality() { - return 3; - } - }; - final var measure = new OngoingPowerMeasure(metadata, Duration.ofSeconds(1), Duration.ofMillis(500)); + + final var measure = new OngoingPowerMeasure(metadata, factory); final var components = new double[metadata.componentCardinality()]; components[0] = m1c1;