Skip to content

Commit

Permalink
feat: use commons-math3 for standard deviation
Browse files Browse the repository at this point in the history
  • Loading branch information
metacosm committed Jul 7, 2024
1 parent f0a9de5 commit 4cb0e19
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 59 deletions.
5 changes: 5 additions & 0 deletions measure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
<artifactId>power-server-metadata</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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<double[]> measures();

/**
* Records the standard deviations for the aggregated energy comsumption value (as returned by {@link #total()}) and
* per component
Expand All @@ -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<double[]> measures();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit 4cb0e19

Please sign in to comment.