diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/DefaultProcessors.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/DefaultProcessors.java index 7e58165..e278a79 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/DefaultProcessors.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/DefaultProcessors.java @@ -23,14 +23,22 @@ public void recordMeasure(double[] components, long timestamp) { } for (var index = 0; index < components.length; index++) { - final var fIndex = index; - final var componentProcessors = processors[index]; - if (componentProcessors != null) { - componentProcessors.forEach(proc -> proc.recordComponentValue(components[fIndex], timestamp)); - } + recordComponentValue(components[index], timestamp, index); + } + } + + private void recordComponentValue(double value, long timestamp, int componentIndex) { + final var componentProcessors = processors[componentIndex]; + if (componentProcessors != null) { + componentProcessors.forEach(proc -> proc.recordComponentValue(value, timestamp)); } } + @Override + public void recordSyntheticComponentValue(double syntheticValue, long timestamp, int componentIndex) { + recordComponentValue(syntheticValue, componentIndex, componentIndex); + } + @Override public void registerProcessorFor(int componentIndex, ComponentProcessor processor) { if (processor != null) { diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/Processors.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/Processors.java index 6901d86..31649ac 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/Processors.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/Processors.java @@ -29,4 +29,7 @@ default List measureProcessors() { default String output(SensorMetadata metadata) { return ""; } + + default void recordSyntheticComponentValue(double syntheticValue, long timestamp, int componentIndex) { + } } diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/RegisteredSyntheticComponent.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/RegisteredSyntheticComponent.java new file mode 100644 index 0000000..2ddf628 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/RegisteredSyntheticComponent.java @@ -0,0 +1,27 @@ +package net.laprun.sustainability.power.analysis; + +import net.laprun.sustainability.power.SensorMetadata; + +public class RegisteredSyntheticComponent implements SyntheticComponent { + private final SyntheticComponent syntheticComponent; + private final int computedIndex; + + public RegisteredSyntheticComponent(SyntheticComponent syntheticComponent, int computedIndex) { + this.syntheticComponent = syntheticComponent; + this.computedIndex = computedIndex; + } + + @Override + public SensorMetadata.ComponentMetadata metadata() { + return syntheticComponent.metadata(); + } + + @Override + public double synthesizeFrom(double[] components, long timestamp) { + return syntheticComponent.synthesizeFrom(components, timestamp); + } + + public int computedIndex() { + return computedIndex; + } +} diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/SyntheticComponent.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/SyntheticComponent.java new file mode 100644 index 0000000..c124a41 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/SyntheticComponent.java @@ -0,0 +1,10 @@ +package net.laprun.sustainability.power.analysis; + +import net.laprun.sustainability.power.SensorMetadata; + +public interface SyntheticComponent { + + SensorMetadata.ComponentMetadata metadata(); + + double synthesizeFrom(double[] components, long timestamp); +} diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/TotalMeasureProcessor.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalMeasureProcessor.java similarity index 96% rename from measure/src/main/java/net/laprun/sustainability/power/analysis/TotalMeasureProcessor.java rename to measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalMeasureProcessor.java index 154d2af..b4acd28 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/TotalMeasureProcessor.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalMeasureProcessor.java @@ -1,4 +1,4 @@ -package net.laprun.sustainability.power.analysis; +package net.laprun.sustainability.power.analysis.total; import java.util.Arrays; import java.util.Objects; @@ -8,6 +8,7 @@ import net.laprun.sustainability.power.Errors; import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.SensorUnit; +import net.laprun.sustainability.power.analysis.MeasureProcessor; public class TotalMeasureProcessor implements MeasureProcessor { private final String name; diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalSyntheticComponent.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalSyntheticComponent.java new file mode 100644 index 0000000..0e669c6 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalSyntheticComponent.java @@ -0,0 +1,83 @@ +package net.laprun.sustainability.power.analysis.total; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import net.laprun.sustainability.power.Errors; +import net.laprun.sustainability.power.SensorMetadata; +import net.laprun.sustainability.power.SensorUnit; +import net.laprun.sustainability.power.analysis.SyntheticComponent; + +public class TotalSyntheticComponent implements SyntheticComponent { + private final Function formula; + private final SensorUnit expectedResultUnit; + private final SensorMetadata.ComponentMetadata metadata; + + public TotalSyntheticComponent(SensorMetadata metadata, SensorUnit expectedResultUnit, int... totalComponentIndices) { + Objects.requireNonNull(totalComponentIndices, "Must specify component indices that will aggregated in a total"); + this.expectedResultUnit = Objects.requireNonNull(expectedResultUnit, "Must specify expected result unit"); + + final var errors = new Errors(); + final var totalComponents = Arrays.stream(totalComponentIndices) + .mapToObj(i -> toTotalComponent(metadata, i, errors)) + .toArray(TotalComponent[]::new); + final String description = Arrays.stream(totalComponents) + .map(TotalComponent::name) + .collect(Collectors.joining(" + ", "Aggregated total from (", ")")); + final String name = Arrays.stream(totalComponents) + .map(TotalComponent::name) + .collect(Collectors.joining("_", "total", "")); + final var isAttributed = metadata.components().values().stream() + .map(SensorMetadata.ComponentMetadata::isAttributed) + .reduce(Boolean::logicalAnd).orElse(false); + formula = components -> { + double result = 0; + for (var totalComponent : totalComponents) { + result += components[totalComponent.index] * totalComponent.factor; + } + return result; + }; + + if (metadata.exists(name)) { + errors.addError("Component " + name + " already exists"); + } + + if (errors.hasErrors()) { + throw new IllegalArgumentException(errors.formatErrors()); + } + + this.metadata = new SensorMetadata.ComponentMetadata(name, description, isAttributed, expectedResultUnit); + } + + private double convertToExpectedUnit(double value) { + return value * expectedResultUnit.base().conversionFactorTo(expectedResultUnit); + } + + private record TotalComponent(String name, int index, double factor) { + } + + private TotalComponent toTotalComponent(SensorMetadata metadata, int index, Errors errors) { + final var cm = metadata.metadataFor(index); + final var name = cm.name(); + final var unit = cm.unit(); + if (!unit.isCommensurableWith(expectedResultUnit)) { + errors.addError("Component " + name + + " is not commensurable with the expected base unit: " + expectedResultUnit); + } + + final var factor = unit.factor(); + return new TotalComponent(name, index, factor); + } + + @Override + public SensorMetadata.ComponentMetadata metadata() { + return metadata; + } + + @Override + public double synthesizeFrom(double[] components, long timestamp) { + return convertToExpectedUnit(formula.apply(components)); + } +} 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 81e70c0..72ef9de 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 @@ -1,7 +1,9 @@ package net.laprun.sustainability.power.measure; import java.time.Duration; +import java.util.Arrays; import java.util.BitSet; +import java.util.List; import java.util.Optional; import java.util.stream.DoubleStream; import java.util.stream.IntStream; @@ -9,6 +11,8 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.analysis.Processors; +import net.laprun.sustainability.power.analysis.RegisteredSyntheticComponent; +import net.laprun.sustainability.power.analysis.SyntheticComponent; public class OngoingPowerMeasure extends ProcessorAware implements PowerMeasure { private static final int DEFAULT_SIZE = 32; @@ -16,20 +20,32 @@ public class OngoingPowerMeasure extends ProcessorAware implements PowerMeasure private final long startedAt; private final BitSet nonZeroComponents; private final double[][] measures; + private final List syntheticComponents; private int samples; private long[] timestamps; - public OngoingPowerMeasure(SensorMetadata metadata) { + public OngoingPowerMeasure(SensorMetadata metadata, SyntheticComponent... syntheticComponents) { super(Processors.empty); startedAt = System.currentTimeMillis(); - this.metadata = metadata; - final var numComponents = metadata.componentCardinality(); measures = new double[numComponents][DEFAULT_SIZE]; - timestamps = new long[DEFAULT_SIZE]; - // 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 BitSet(numComponents); + timestamps = new long[DEFAULT_SIZE]; + + if (syntheticComponents != null) { + final var builder = SensorMetadata.from(metadata); + for (var component : syntheticComponents) { + builder.withNewComponent(component.metadata()); + } + this.metadata = builder.build(); + this.syntheticComponents = Arrays.stream(syntheticComponents) + .map(sc -> new RegisteredSyntheticComponent(sc, this.metadata.metadataFor(sc.metadata().name()).index())) + .toList(); + } else { + this.syntheticComponents = List.of(); + this.metadata = metadata; + } } @Override @@ -54,7 +70,14 @@ public void recordMeasure(double[] components) { recordComponentValue(component, componentValue, timestamp); } - processors().recordMeasure(components, timestamp); + final var processors = processors(); + processors.recordMeasure(components, timestamp); + + if (!syntheticComponents.isEmpty()) { + syntheticComponents.forEach(sc -> processors.recordSyntheticComponentValue(sc.synthesizeFrom(components, timestamp), + timestamp, sc.computedIndex())); + + } } private void recordComponentValue(int component, double value, long timestamp) { diff --git a/measure/src/test/java/net/laprun/sustainability/power/analysis/TotalMeasureProcessorTest.java b/measure/src/test/java/net/laprun/sustainability/power/analysis/total/TotalComputationTest.java similarity index 71% rename from measure/src/test/java/net/laprun/sustainability/power/analysis/TotalMeasureProcessorTest.java rename to measure/src/test/java/net/laprun/sustainability/power/analysis/total/TotalComputationTest.java index 1f2cc0b..aec1351 100644 --- a/measure/src/test/java/net/laprun/sustainability/power/analysis/TotalMeasureProcessorTest.java +++ b/measure/src/test/java/net/laprun/sustainability/power/analysis/total/TotalComputationTest.java @@ -1,4 +1,4 @@ -package net.laprun.sustainability.power.analysis; +package net.laprun.sustainability.power.analysis.total; import static org.junit.jupiter.api.Assertions.*; @@ -9,7 +9,7 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.SensorUnit; -class TotalMeasureProcessorTest { +class TotalComputationTest { @Test void totalShouldFailIfAllComponentsAreNotCommensurable() { @@ -20,10 +20,15 @@ void totalShouldFailIfAllComponentsAreNotCommensurable() { .build(); final var expectedResultUnit = SensorUnit.W; - final var e = assertThrows(IllegalArgumentException.class, + var e = assertThrows(IllegalArgumentException.class, () -> new TotalMeasureProcessor(metadata, expectedResultUnit, 0, 1)); assertTrue(e.getMessage().contains("Component " + inError + " is not commensurable with the expected base unit: " + expectedResultUnit)); + + e = assertThrows(IllegalArgumentException.class, + () -> new TotalSyntheticComponent(metadata, expectedResultUnit, 0, 1)); + assertTrue(e.getMessage().contains("Component " + inError + + " is not commensurable with the expected base unit: " + expectedResultUnit)); } @Test @@ -48,22 +53,28 @@ void testTotal() { final var m3total = m3c1 + m3c2 + m3c3; final var totalProc = new TotalMeasureProcessor(metadata, SensorUnit.of("mW"), 0, 1, 2); + final var totalSyncComp = new TotalSyntheticComponent(metadata, SensorUnit.W, 0, 1, 2); final var components = new double[metadata.componentCardinality()]; components[0] = m1c1; components[1] = m1c2; components[2] = m1c3; totalProc.recordMeasure(components, System.currentTimeMillis()); + // original components use mW as unit but we're asking for a synthetic total in W so the resulting total should be factored + final var mWtoWFactor = SensorUnit.mW.conversionFactorTo(SensorUnit.W); + assertEquals(m1total * mWtoWFactor, totalSyncComp.synthesizeFrom(components, 0)); components[0] = m2c1; components[1] = m2c2; components[2] = m2c3; totalProc.recordMeasure(components, System.currentTimeMillis()); + assertEquals(m2total * mWtoWFactor, totalSyncComp.synthesizeFrom(components, 0)); components[0] = m3c1; components[1] = m3c2; components[2] = m3c3; totalProc.recordMeasure(components, System.currentTimeMillis()); + assertEquals(m3total * mWtoWFactor, totalSyncComp.synthesizeFrom(components, 0)); assertEquals(m1c1 + m1c2 + m1c3 + m2c1 + m2c2 + m2c3 + m3c1 + m3c2 + m3c3, totalProc.total()); assertEquals(Stream.of(m1total, m2total, m3total).min(Double::compareTo).orElseThrow(), totalProc.minMeasuredTotal()); 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 5fd2075..ce104ad 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 @@ -10,8 +10,10 @@ import org.junit.jupiter.api.Test; import net.laprun.sustainability.power.SensorMetadata; +import net.laprun.sustainability.power.SensorUnit; import net.laprun.sustainability.power.analysis.ComponentProcessor; import net.laprun.sustainability.power.analysis.MeasureProcessor; +import net.laprun.sustainability.power.analysis.SyntheticComponent; public class OngoingPowerMeasureTest { private final static SensorMetadata metadata = SensorMetadata @@ -97,6 +99,42 @@ void processorsShouldBeCalled() { assertThat(measureProc.values.getLast().measures()).isEqualTo(new double[] { m2c1, m2c2, 0 }); } + @Test + void syntheticComponentsShouldWork() { + final var random = Random.from(RandomGenerator.getDefault()); + final var m1c1 = random.nextDouble(); + final var m2c1 = random.nextDouble(); + + final var doublerName = "doubler"; + final var doubler = new SyntheticComponent() { + @Override + public SensorMetadata.ComponentMetadata metadata() { + return new SensorMetadata.ComponentMetadata(doublerName, "doubler desc", true, SensorUnit.mW); + } + + @Override + public double synthesizeFrom(double[] components, long timestamp) { + return components[0] * 2; + } + }; + + final var measure = new OngoingPowerMeasure(metadata, doubler); + final var testProc = new TestComponentProcessor(); + // need to get updated metadata + final var doublerIndex = measure.metadata().metadataFor(doublerName).index(); + measure.registerProcessorFor(doublerIndex, testProc); + + final var components = new double[metadata.componentCardinality()]; + components[0] = m1c1; + measure.recordMeasure(components); + + components[0] = m2c1; + measure.recordMeasure(components); + + assertThat(testProc.values.getFirst().value()).isEqualTo(m1c1 * 2); + assertThat(testProc.values.getLast().value()).isEqualTo(m2c1 * 2); + } + private static class TestComponentProcessor implements ComponentProcessor { final List values = new ArrayList<>(); diff --git a/measure/src/test/java/net/laprun/sustainability/power/measure/StoppedPowerMeasureTest.java b/measure/src/test/java/net/laprun/sustainability/power/measure/StoppedPowerMeasureTest.java index d1ff9e5..305031f 100644 --- a/measure/src/test/java/net/laprun/sustainability/power/measure/StoppedPowerMeasureTest.java +++ b/measure/src/test/java/net/laprun/sustainability/power/measure/StoppedPowerMeasureTest.java @@ -9,7 +9,7 @@ import net.laprun.sustainability.power.analysis.ComponentProcessor; import net.laprun.sustainability.power.analysis.MeasureProcessor; import net.laprun.sustainability.power.analysis.Processors; -import net.laprun.sustainability.power.analysis.TotalMeasureProcessor; +import net.laprun.sustainability.power.analysis.total.TotalMeasureProcessor; public class StoppedPowerMeasureTest { private final static SensorMetadata metadata = SensorMetadata diff --git a/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java b/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java index bddbb6d..cb38ce0 100644 --- a/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java +++ b/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java @@ -202,6 +202,15 @@ public Builder withDocumentation(String documentation) { public SensorMetadata build() { return new SensorMetadata(components, documentation); } + + public Builder withNewComponent(ComponentMetadata metadata) { + if (-1 == metadata.index) { + metadata = new SensorMetadata.ComponentMetadata(metadata.name, currentIndex++, metadata.description, + metadata.isAttributed, metadata.unit); + } + components.add(metadata); + return this; + } } /** @@ -229,6 +238,14 @@ public record ComponentMetadata(String name, int index, String description, bool } } + /** + * Creates a ComponentMetadata that will be automatically assigned an index whenever added to a {@link SensorMetadata}, + * based on the contextual order of how other components have been added. + */ + public ComponentMetadata(String name, String description, boolean isAttributed, SensorUnit unit) { + this(name, -1, description, isAttributed, unit); + } + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public ComponentMetadata(@JsonProperty("name") String name, @JsonProperty("index") int index, @JsonProperty("description") String description,