From 51f6f24c76c971c3b59a1317fa16d48293af987c Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 13 May 2024 14:01:00 +0200 Subject: [PATCH] feat: initial measure module to help client-side processing --- measure/pom.xml | 40 ++++++++ .../power/measure/AbstractPowerMeasure.java | 33 +++++++ .../power/measure/OngoingPowerMeasure.java | 67 ++++++++++++++ .../power/measure/PowerMeasure.java | 91 +++++++++++++++++++ .../power/measure/StoppedPowerMeasure.java | 43 +++++++++ pom.xml | 3 +- 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 measure/pom.xml create mode 100644 measure/src/main/java/net/laprun/sustainability/power/measure/AbstractPowerMeasure.java create mode 100644 measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java create mode 100644 measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java create mode 100644 measure/src/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java diff --git a/measure/pom.xml b/measure/pom.xml new file mode 100644 index 0000000..27ea832 --- /dev/null +++ b/measure/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + net.laprun.sustainability + power-server-parent + 0.0.9-SNAPSHOT + + + power-server-measure + power-server : measure + A library to gather and analyze power consumption measures + + + + net.laprun.sustainability + power-server-metadata + ${project.version} + + + + + + + + net.revelc.code.formatter + formatter-maven-plugin + + + net.laprun.sustainability + build-tools + 0.0.9-SNAPSHOT + + + + + + + diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/AbstractPowerMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/AbstractPowerMeasure.java new file mode 100644 index 0000000..29a75fe --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/AbstractPowerMeasure.java @@ -0,0 +1,33 @@ +package net.laprun.sustainability.power.measure; + +import java.util.List; + +import net.laprun.sustainability.power.SensorMetadata; + +public abstract class AbstractPowerMeasure implements PowerMeasure { + private final SensorMetadata sensorMetadata; + private final List measures; + + protected AbstractPowerMeasure(SensorMetadata sensorMetadata, List measures) { + this.sensorMetadata = sensorMetadata; + this.measures = measures; + } + + @Override + public List measures() { + return measures; + } + + @Override + public SensorMetadata metadata() { + return sensorMetadata; + } + + public int numberOfSamples() { + return measures.size(); + } + + double[] getComponentData(int componentIndex) { + return measures.parallelStream().mapToDouble(measure -> measure[componentIndex]).toArray(); + } +} 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 new file mode 100644 index 0000000..5dbdebd --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java @@ -0,0 +1,67 @@ +package net.laprun.sustainability.power.measure; + +import java.util.ArrayList; + +import net.laprun.sustainability.power.SensorMetadata; + +public class OngoingPowerMeasure extends AbstractPowerMeasure { + private final long startedAt; + private double minTotal = Double.MAX_VALUE; + private double maxTotal; + private final double[] totals; + private final double[] averages; + + public OngoingPowerMeasure(SensorMetadata sensorMetadata, long duration, long frequency) { + super(sensorMetadata, new ArrayList<>((int) (duration / frequency))); + startedAt = System.currentTimeMillis(); + final var numComponents = metadata().componentCardinality(); + totals = new double[numComponents]; + averages = new double[numComponents]; + } + + public void recordMeasure(double[] components) { + final var recorded = new double[components.length]; + System.arraycopy(components, 0, recorded, 0, components.length); + final var previousSize = numberOfSamples(); + measures().add(recorded); + + for (int i = 0; i < recorded.length; i++) { + totals[i] += recorded[i]; + averages[i] = averages[i] == 0 ? recorded[i] + : (previousSize * averages[i] + recorded[i]) / numberOfSamples(); + } + + // record min / max totals + final var recordedTotal = PowerMeasure.sumOfComponents(recorded); + if (recordedTotal < minTotal) { + minTotal = recordedTotal; + } + if (recordedTotal > maxTotal) { + maxTotal = recordedTotal; + } + } + + @Override + public double total() { + return PowerMeasure.sumOfComponents(totals); + } + + public long duration() { + return System.currentTimeMillis() - startedAt; + } + + @Override + public double minMeasuredTotal() { + return minTotal; + } + + @Override + public double maxMeasuredTotal() { + return maxTotal; + } + + @Override + public double[] averagesPerComponent() { + return averages; + } +} 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 new file mode 100644 index 0000000..fe52192 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java @@ -0,0 +1,91 @@ +package net.laprun.sustainability.power.measure; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +import net.laprun.sustainability.power.SensorMetadata; + +public interface PowerMeasure { + int numberOfSamples(); + + long duration(); + + default double average() { + return total() / numberOfSamples(); + } + + double total(); + + SensorMetadata metadata(); + + double[] averagesPerComponent(); + + double minMeasuredTotal(); + + double maxMeasuredTotal(); + + static double sumOfComponents(double[] recorded) { + var componentSum = 0.0; + for (double value : recorded) { + componentSum += value; + } + return componentSum; + } + + default StdDev standardDeviations() { + final var cardinality = metadata().componentCardinality(); + 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 + 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 = sumOfComponents(m); + 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); + }); + 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)); + } + + /** + * Records the standard deviations for the aggregated energy comsumption value (as returned by {@link #total()}) and + * per component + * + * @param aggregate + * @param perComponent + */ + record StdDev(double aggregate, double[] perComponent) { + } + + static String asString(PowerMeasure measure) { + final var durationInSeconds = measure.duration() / 1000; + 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/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java new file mode 100644 index 0000000..1aa3f99 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java @@ -0,0 +1,43 @@ +package net.laprun.sustainability.power.measure; + +public class StoppedPowerMeasure extends AbstractPowerMeasure { + private final long duration; + private final double total; + private final double min; + private final double max; + private final double[] averages; + + public StoppedPowerMeasure(PowerMeasure powerMeasure) { + super(powerMeasure.metadata(), powerMeasure.measures()); + this.duration = powerMeasure.duration(); + this.total = powerMeasure.total(); + this.min = powerMeasure.minMeasuredTotal(); + this.max = powerMeasure.maxMeasuredTotal(); + this.averages = powerMeasure.averagesPerComponent(); + } + + @Override + public long duration() { + return duration; + } + + @Override + public double minMeasuredTotal() { + return min; + } + + @Override + public double maxMeasuredTotal() { + return max; + } + + @Override + public double total() { + return total; + } + + @Override + public double[] averagesPerComponent() { + return averages; + } +} diff --git a/pom.xml b/pom.xml index 6ed5b02..602a97d 100644 --- a/pom.xml +++ b/pom.xml @@ -66,8 +66,9 @@ build-tools - server metadata + measure + server