From 4cb0e19bcdfa0b214afb279d1ee407dadf5beb9a Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Sun, 7 Jul 2024 15:09:02 +0200 Subject: [PATCH] feat: use commons-math3 for standard deviation --- measure/pom.xml | 5 + .../power/measure/PowerMeasure.java | 109 +++++++++--------- .../measure/OngoingPowerMeasureTest.java | 4 +- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/measure/pom.xml b/measure/pom.xml index 7351c53..dff0b6e 100644 --- a/measure/pom.xml +++ b/measure/pom.xml @@ -18,6 +18,11 @@ power-server-metadata ${project.version} + + org.apache.commons + commons-math3 + 3.6.1 + diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java index 6c9fcfb..825fc57 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java @@ -1,31 +1,14 @@ package net.laprun.sustainability.power.measure; import java.time.Duration; -import java.util.Arrays; import java.util.List; import java.util.stream.IntStream; +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; + import net.laprun.sustainability.power.SensorMetadata; public interface PowerMeasure { - int numberOfSamples(); - - Duration duration(); - - default double average() { - return total() / numberOfSamples(); - } - - double total(); - - SensorMetadata metadata(); - - double[] averagesPerComponent(); - - double minMeasuredTotal(); - - double maxMeasuredTotal(); - private static double sumOfComponents(double[] recorded) { var componentSum = 0.0; for (double value : recorded) { @@ -45,35 +28,65 @@ static double sumOfSelectedComponents(double[] recorded, int... indices) { return componentSum; } + static String asString(PowerMeasure measure) { + final var durationInSeconds = measure.duration().getSeconds(); + final var samples = measure.numberOfSamples(); + final var measuredMilliWatts = measure.total(); + final var stdDevs = measure.standardDeviations(); + return String.format("%s / avg: %s / std dev: %.3f [min: %.3f, max: %.3f] (%ds, %s samples)", + readableWithUnit(measuredMilliWatts), readableWithUnit(measure.average()), stdDevs.aggregate, + measure.minMeasuredTotal(), measure.maxMeasuredTotal(), durationInSeconds, samples); + } + + static String readableWithUnit(double milliWatts) { + String unit = milliWatts >= 1000 ? "W" : "mW"; + double power = milliWatts >= 1000 ? milliWatts / 1000 : milliWatts; + return String.format("%.3f%s", power, unit); + } + + int numberOfSamples(); + + Duration duration(); + + default double average() { + return total() / numberOfSamples(); + } + + double total(); + + SensorMetadata metadata(); + + double[] averagesPerComponent(); + + double minMeasuredTotal(); + + double maxMeasuredTotal(); + default StdDev standardDeviations() { final var cardinality = metadata().componentCardinality(); final var totalComponents = metadata().totalComponents(); - final var stdDevs = new double[cardinality]; - final var aggregate = new double[1]; - final var samples = numberOfSamples() - 1; // unbiased so we remove one sample - final var sqrdAverages = Arrays.stream(averagesPerComponent()).map(m -> m * m).toArray(); - final var sqrdAverage = average() * average(); - // need to compute the average of variances then square root that to get the "aggregate" standard deviation, - // see: https://stats.stackexchange.com/a/26647 - // "vectorize" computation of variances: compute the variance for each component in parallel + final DescriptiveStatistics[] perComponent = new DescriptiveStatistics[cardinality]; + for (int i = 0; i < perComponent.length; i++) { + perComponent[i] = new DescriptiveStatistics(); + } + final DescriptiveStatistics total = new DescriptiveStatistics(); IntStream.range(0, cardinality).parallel() - // compute variances for each component of the measure .forEach(component -> { - final var sumOfSquares = measures().stream().parallel().peek(m -> { - // compute the std dev for total measure - final var total = sumOfSelectedComponents(m, totalComponents); - aggregate[0] += total * total; - }).mapToDouble(m -> m[component] * m[component]).sum(); - stdDevs[component] = stdDev(sumOfSquares, sqrdAverages[component], samples); - aggregate[0] = stdDev(aggregate[0], sqrdAverage, samples); + measures().stream().parallel().forEach(measure -> { + perComponent[component].addValue(measure[component]); + total.addValue(sumOfSelectedComponents(measure, totalComponents)); + }); }); - return new StdDev(aggregate[0], stdDevs); - } - private static double stdDev(double sumOfSquares, double squaredAvg, int samples) { - return Math.sqrt((sumOfSquares / samples) - (((samples + 1) * squaredAvg) / samples)); + final var stdDevs = new double[cardinality]; + for (int i = 0; i < perComponent.length; i++) { + stdDevs[i] = perComponent[i].getStandardDeviation(); + } + return new StdDev(total.getStandardDeviation(), stdDevs); } + List measures(); + /** * Records the standard deviations for the aggregated energy comsumption value (as returned by {@link #total()}) and * per component @@ -83,22 +96,4 @@ private static double stdDev(double sumOfSquares, double squaredAvg, int samples */ record StdDev(double aggregate, double[] perComponent) { } - - static String asString(PowerMeasure measure) { - final var durationInSeconds = measure.duration().getSeconds(); - final var samples = measure.numberOfSamples(); - final var measuredMilliWatts = measure.total(); - final var stdDevs = measure.standardDeviations(); - return String.format("%s / avg: %s / std dev: %.3f [min: %.3f, max: %.3f] (%ds, %s samples)", - readableWithUnit(measuredMilliWatts), readableWithUnit(measure.average()), stdDevs.aggregate, - measure.minMeasuredTotal(), measure.maxMeasuredTotal(), durationInSeconds, samples); - } - - static String readableWithUnit(double milliWatts) { - String unit = milliWatts >= 1000 ? "W" : "mW"; - double power = milliWatts >= 1000 ? milliWatts / 1000 : milliWatts; - return String.format("%.3f%s", power, unit); - } - - List measures(); } 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 9ab8fe6..f9a4d8a 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 @@ -43,8 +43,8 @@ public int componentCardinality() { assertEquals((m1c1 + m2c1) / 2, c1Avg); assertEquals((m1c2 + m2c2) / 2, c2Avg); - final var stdVarForC1 = Math.sqrt(Math.pow(m1c1 - c1Avg, 2) + Math.pow(m2c1 - c1Avg, 2)); - final var stdVarForC2 = Math.sqrt(Math.pow(m1c2 - c2Avg, 2) + Math.pow(m2c2 - c2Avg, 2)); + final var stdVarForC1 = Math.sqrt((Math.pow(m1c1 - c1Avg, 2) + Math.pow(m2c1 - c1Avg, 2)) / (2 - 1)); + final var stdVarForC2 = Math.sqrt((Math.pow(m1c2 - c2Avg, 2) + Math.pow(m2c2 - c2Avg, 2)) / (2 - 1)); assertEquals(stdVarForC1, measure.standardDeviations().perComponent()[0], 0.0001, "Standard Deviation did not match the expected value");