diff --git a/CHANGELOG.md b/CHANGELOG.md index 38bb3e6b6..2aed99080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extend ValidationUtils for validating ThermalGrids [#1216](https://github.com/ie3-institute/PowerSystemDataModel/issues/1216) - Enhance `TimeSeriesSource` with method to retrieve the previous value before a given key [#1182](https://github.com/ie3-institute/PowerSystemDataModel/issues/1182) - Added `BdewLoadProfileTimeSeries` [#1230](https://github.com/ie3-institute/PowerSystemDataModel/issues/1230) +- Added `RandomLoadProfileTimeSeries` [#1232](https://github.com/ie3-institute/PowerSystemDataModel/issues/1232) ### Fixed - Removing opened `SwitchInput` during connectivity check [#1221](https://github.com/ie3-institute/PowerSystemDataModel/issues/1221) diff --git a/build.gradle b/build.gradle index 95cff35db..609d0872c 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ plugins { id 'jacoco' // java code coverage plugin id "org.sonarqube" version "6.0.1.5171" // sonarqube id 'net.thauvin.erik.gradle.semver' version '1.0.4' // semantic versioning + id "com.github.johnrengelman.shadow" version "8.1.1" // fat jar } ext { @@ -67,6 +68,9 @@ dependencies { // Graphs implementation 'org.jgrapht:jgrapht-core:1.5.2' + // Statistics (for random load model) + implementation 'de.lmu.ifi.dbs.elki:elki:0.7.5' + // testing testImplementation "org.apache.groovy:groovy:$groovyBinaryVersion" diff --git a/docs/uml/main/TimeSeriesDatamodelConcept.puml b/docs/uml/main/TimeSeriesDatamodelConcept.puml index 7f0cda6d7..0acc0cf5f 100644 --- a/docs/uml/main/TimeSeriesDatamodelConcept.puml +++ b/docs/uml/main/TimeSeriesDatamodelConcept.puml @@ -32,6 +32,7 @@ package models { } DefaultLoadProfiles --|> LoadProfile + RandomLoadProfile --|> LoadProfile interface StandardLoadProfile { + {static} parse(String): StandardLoadProfile @@ -130,12 +131,22 @@ package models { } RepetitiveTimeSeries --|> TimeSeries - class LoadProfileInput { - - type: StandardLoadProfile - - dayOfWeekToHourlyValues: Map> + abstract class LoadProfileTimeSeries { + - loadProfile: StandardLoadProfile + - valueMapping: Map> + + getLoadProfile(): LoadProfile + # fromTime(ZonedDateTime): Key } - LoadProfileInput --|> RepetitiveTimeSeries - LoadProfileInput *-- StandardLoadProfile + LoadProfileTimeSeries --|> RepetitiveTimeSeries + LoadProfileTimeSeries *-- LoadProfile + + class BDEWLoadProfileTimeSeries {} + BDEWLoadProfileTimeSeries --|> LoadProfileTimeSeries + BDEWLoadProfileTimeSeries *-- BdewLoadProfileEntry + + class RandomLoadProfileTimeSeries {} + RandomLoadProfileTimeSeries --|> LoadProfileTimeSeries + RandomLoadProfileTimeSeries *-- RandomLoadProfileEntry abstract class TimeSeriesEntry { # value: V @@ -152,8 +163,21 @@ package models { class LoadProfileEntry { - dayOfWeek: DayOfWeek - quarterHourOfDay: int + + getDayOfWeek(): DayOfWeek + + getQuarterHourOfDay(): Integer } LoadProfileEntry --|> TimeSeriesEntry: <>:PValue + + class BdewLoadProfileEntry { + - season: Season + + getSeason(): Season + } + BdewLoadProfileEntry --|> LoadProfileEntry + + class RandomLoadProfileEntry { + - gev: GeneralizedExtremeValueDistribution + } + RandomLoadProfileEntry --|> LoadProfileEntry } } diff --git a/docs/uml/main/input/InputDatamodelConcept.puml b/docs/uml/main/input/InputDatamodelConcept.puml index 91f11c107..15badfeb0 100644 --- a/docs/uml/main/input/InputDatamodelConcept.puml +++ b/docs/uml/main/input/InputDatamodelConcept.puml @@ -142,20 +142,6 @@ package models { MeasurementUnitInput --|> AssetInput MeasurementUnitInput ..|> HasNodes - class RandomLoadParameter { - - quarterHour: int - - kWd: Double - - kSa: Double - - kSu: Double - - myWd: Double - - mySa: Double - - mySu: Double - - sigmaWd: Double - - sigmaSa: Double - - sigmaSu: Double - } - RandomLoadParameter --|> InputEntity - abstract class AssetTypeInput { - id: String } diff --git a/src/main/java/edu/ie3/datamodel/io/factory/timeseries/RandomLoadProfileFactory.java b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/RandomLoadProfileFactory.java new file mode 100644 index 000000000..440639388 --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/RandomLoadProfileFactory.java @@ -0,0 +1,117 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation +*/ +package edu.ie3.datamodel.io.factory.timeseries; + +import static edu.ie3.datamodel.models.profile.LoadProfile.RandomLoadProfile.RANDOM_LOAD_PROFILE; +import static tech.units.indriya.unit.Units.WATT; + +import edu.ie3.datamodel.io.naming.timeseries.LoadProfileMetaInformation; +import edu.ie3.datamodel.models.profile.LoadProfile.RandomLoadProfile; +import edu.ie3.datamodel.models.timeseries.repetitive.LoadProfileEntry; +import edu.ie3.datamodel.models.timeseries.repetitive.RandomLoadProfileTimeSeries; +import edu.ie3.datamodel.models.value.load.RandomLoadValues; +import edu.ie3.util.quantities.PowerSystemUnits; +import java.util.List; +import java.util.Set; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; +import tech.units.indriya.ComparableQuantity; +import tech.units.indriya.quantity.Quantities; + +public class RandomLoadProfileFactory + extends LoadProfileFactory { + public static final String K_WEEKDAY = "kWd"; + public static final String K_SATURDAY = "kSa"; + public static final String K_SUNDAY = "kSu"; + public static final String MY_WEEKDAY = "myWd"; + public static final String MY_SATURDAY = "mySa"; + public static final String MY_SUNDAY = "mySu"; + public static final String SIGMA_WEEKDAY = "sigmaWd"; + public static final String SIGMA_SATURDAY = "sigmaSa"; + public static final String SIGMA_SUNDAY = "sigmaSu"; + + public RandomLoadProfileFactory() { + super(RandomLoadValues.class); + } + + @Override + protected LoadProfileEntry buildModel(LoadProfileData data) { + int quarterHour = data.getInt(QUARTER_HOUR); + + return new LoadProfileEntry<>( + new RandomLoadValues( + data.getDouble(K_SATURDAY), + data.getDouble(K_SUNDAY), + data.getDouble(K_WEEKDAY), + data.getDouble(MY_SATURDAY), + data.getDouble(MY_SUNDAY), + data.getDouble(MY_WEEKDAY), + data.getDouble(SIGMA_SATURDAY), + data.getDouble(SIGMA_SUNDAY), + data.getDouble(SIGMA_WEEKDAY)), + quarterHour); + } + + @Override + protected List> getFields(Class entityClass) { + return List.of( + newSet( + QUARTER_HOUR, + K_WEEKDAY, + K_SATURDAY, + K_SUNDAY, + MY_WEEKDAY, + MY_SATURDAY, + MY_SUNDAY, + SIGMA_WEEKDAY, + SIGMA_SATURDAY, + SIGMA_SUNDAY)); + } + + @Override + public RandomLoadProfileTimeSeries build( + LoadProfileMetaInformation metaInformation, Set> entries) { + RandomLoadProfile profile = RANDOM_LOAD_PROFILE; + + ComparableQuantity maxPower = calculateMaxPower(profile, entries); + ComparableQuantity profileEnergyScaling = getLoadProfileEnergyScaling(profile); + + return new RandomLoadProfileTimeSeries( + metaInformation.getUuid(), profile, entries, maxPower, profileEnergyScaling); + } + + @Override + public RandomLoadProfile parseProfile(String profile) { + return RANDOM_LOAD_PROFILE; + } + + /** + * This is the 95 % quantile resulting from 10,000 evaluations of the year 2019. It is only + * needed, when the load is meant to be scaled to rated active power. + * + * @return Reference active power to use for later model calculations + */ + @Override + public ComparableQuantity calculateMaxPower( + RandomLoadProfile loadProfile, Set> loadProfileEntries) { + return Quantities.getQuantity(159d, WATT); + } + + /** + * Returns the profile energy scaling factor, the random profile is scaled to. + * + *

It is said in 'Kays - Agent-based simulation environment for improving the planning of + * distribution grids', that the Generalized Extreme Value distribution's parameters are sampled + * from input data, that is normalized to 1,000 kWh annual energy consumption. However, due to + * inaccuracies in random data reproduction, the sampled values will lead to an average annual + * energy consumption of approx. this value. It has been found by 1,000 evaluations of the year + * 2019. + */ + @Override + public ComparableQuantity getLoadProfileEnergyScaling(RandomLoadProfile loadProfile) { + return Quantities.getQuantity(716.5416966513656, PowerSystemUnits.KILOWATTHOUR); + } +} diff --git a/src/main/java/edu/ie3/datamodel/io/processor/Processor.java b/src/main/java/edu/ie3/datamodel/io/processor/Processor.java index dc09ca026..194edd8f7 100644 --- a/src/main/java/edu/ie3/datamodel/io/processor/Processor.java +++ b/src/main/java/edu/ie3/datamodel/io/processor/Processor.java @@ -261,8 +261,8 @@ protected String processMethodResult(Object methodReturnObject, Method method, S processVoltageLevel((VoltageLevel) methodReturnObject, fieldName)); case "Point", "LineString" -> resultStringBuilder.append( geoJsonWriter.write((Geometry) methodReturnObject)); - case "LoadProfile", "BdewStandardLoadProfile" -> resultStringBuilder.append( - ((LoadProfile) methodReturnObject).getKey()); + case "LoadProfile", "BdewStandardLoadProfile", "RandomLoadProfile" -> resultStringBuilder + .append(((LoadProfile) methodReturnObject).getKey()); case "AssetTypeInput", "BmTypeInput", "ChpTypeInput", diff --git a/src/main/java/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessor.java b/src/main/java/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessor.java index 1f5eb7583..d536bbb02 100644 --- a/src/main/java/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessor.java +++ b/src/main/java/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessor.java @@ -16,6 +16,7 @@ import edu.ie3.datamodel.models.timeseries.repetitive.*; import edu.ie3.datamodel.models.value.*; import edu.ie3.datamodel.models.value.load.BdewLoadValues; +import edu.ie3.datamodel.models.value.load.RandomLoadValues; import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; @@ -54,7 +55,9 @@ public class TimeSeriesProcessor< new TimeSeriesProcessorKey( IndividualTimeSeries.class, TimeBasedValue.class, HeatAndSValue.class), new TimeSeriesProcessorKey( - BdewLoadProfileTimeSeries.class, LoadProfileEntry.class, BdewLoadValues.class)); + BdewLoadProfileTimeSeries.class, LoadProfileEntry.class, BdewLoadValues.class), + new TimeSeriesProcessorKey( + RandomLoadProfileTimeSeries.class, LoadProfileEntry.class, RandomLoadValues.class)); /** * Specific combination of time series class, entry class and value class, this processor is diff --git a/src/main/java/edu/ie3/datamodel/models/profile/LoadProfile.java b/src/main/java/edu/ie3/datamodel/models/profile/LoadProfile.java index 5159c9940..c68ba3435 100644 --- a/src/main/java/edu/ie3/datamodel/models/profile/LoadProfile.java +++ b/src/main/java/edu/ie3/datamodel/models/profile/LoadProfile.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.util.Arrays; import java.util.stream.Collectors; +import java.util.stream.Stream; public interface LoadProfile extends Serializable { /** @return The identifying String */ @@ -28,12 +29,12 @@ static LoadProfile parse(String key) throws ParsingException { } static LoadProfile[] getAllProfiles() { - final LoadProfile[][] all = - new LoadProfile[][] { - BdewStandardLoadProfile.values(), NbwTemperatureDependantLoadProfile.values() - }; - - return Arrays.stream(all).flatMap(Arrays::stream).toArray(LoadProfile[]::new); + return Stream.of( + BdewStandardLoadProfile.values(), + NbwTemperatureDependantLoadProfile.values(), + (LoadProfile[]) RandomLoadProfile.values()) + .flatMap(Arrays::stream) + .toArray(LoadProfile[]::new); } /** @@ -70,4 +71,13 @@ public String getKey() { return "No load profile assigned"; } } + + enum RandomLoadProfile implements LoadProfile { + RANDOM_LOAD_PROFILE; + + @Override + public String getKey() { + return "random"; + } + } } diff --git a/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/LoadProfileTimeSeries.java b/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/LoadProfileTimeSeries.java index 3de25d9c2..c024e4efc 100644 --- a/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/LoadProfileTimeSeries.java +++ b/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/LoadProfileTimeSeries.java @@ -25,13 +25,13 @@ public class LoadProfileTimeSeries private final Map valueMapping; /** - * The maximum average power consumption per quarter-hour for a given calculated over all seasons - * and weekday types of given load profile. + * The maximum average power consumption per quarter-hour calculated over all seasons and weekday + * types of given load profile. */ - public final ComparableQuantity maxPower; + private final ComparableQuantity maxPower; /** The profile energy scaling in kWh. */ - public final ComparableQuantity profileEnergyScaling; + private final ComparableQuantity profileEnergyScaling; public LoadProfileTimeSeries( UUID uuid, @@ -50,6 +50,19 @@ public LoadProfileTimeSeries( this.profileEnergyScaling = profileEnergyScaling; } + /** + * Returns the maximum average power consumption per quarter-hour calculated over all seasons and + * weekday types of given load profile in Watt. + */ + public Optional> maxPower() { + return Optional.ofNullable(maxPower); + } + + /** Returns the profile energy scaling in kWh. */ + public Optional> loadProfileScaling() { + return Optional.ofNullable(profileEnergyScaling); + } + /** Returns the {@link LoadProfile}. */ public LoadProfile getLoadProfile() { return loadProfile; diff --git a/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/RandomLoadProfileTimeSeries.java b/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/RandomLoadProfileTimeSeries.java new file mode 100644 index 000000000..de9142fd9 --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/RandomLoadProfileTimeSeries.java @@ -0,0 +1,41 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation +*/ +package edu.ie3.datamodel.models.timeseries.repetitive; + +import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution; +import edu.ie3.datamodel.models.profile.LoadProfile; +import edu.ie3.datamodel.models.value.load.RandomLoadValues; +import java.util.Set; +import java.util.UUID; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; +import tech.units.indriya.ComparableQuantity; + +/** + * Describes a random load profile time series based on a {@link + * GeneralizedExtremeValueDistribution}. Each value of this# timeseries is given in kW. + */ +public class RandomLoadProfileTimeSeries extends LoadProfileTimeSeries { + + public RandomLoadProfileTimeSeries( + UUID uuid, + LoadProfile loadProfile, + Set> entries, + ComparableQuantity maxPower, + ComparableQuantity profileEnergyScaling) { + super(uuid, loadProfile, entries, maxPower, profileEnergyScaling); + } + + @Override + public LoadProfile.RandomLoadProfile getLoadProfile() { + return (LoadProfile.RandomLoadProfile) super.getLoadProfile(); + } + + @Override + public String toString() { + return "Random" + super.toString(); + } +} diff --git a/src/main/java/edu/ie3/datamodel/models/input/RandomLoadParameters.java b/src/main/java/edu/ie3/datamodel/models/value/load/RandomLoadValues.java similarity index 60% rename from src/main/java/edu/ie3/datamodel/models/input/RandomLoadParameters.java rename to src/main/java/edu/ie3/datamodel/models/value/load/RandomLoadValues.java index 2233cb144..01f1f2c1b 100644 --- a/src/main/java/edu/ie3/datamodel/models/input/RandomLoadParameters.java +++ b/src/main/java/edu/ie3/datamodel/models/value/load/RandomLoadValues.java @@ -3,10 +3,19 @@ * Institute of Energy Systems, Energy Efficiency and Energy Economics, * Research group Distribution grid planning and operation */ -package edu.ie3.datamodel.models.input; +package edu.ie3.datamodel.models.value.load; +import static edu.ie3.util.quantities.PowerSystemUnits.KILOWATT; + +import de.lmu.ifi.dbs.elki.math.statistics.distribution.GeneralizedExtremeValueDistribution; +import de.lmu.ifi.dbs.elki.utilities.random.RandomFactory; +import edu.ie3.datamodel.models.profile.LoadProfile; +import edu.ie3.datamodel.models.value.PValue; +import java.time.DayOfWeek; +import java.time.ZonedDateTime; import java.util.Objects; -import java.util.UUID; +import java.util.Random; +import tech.units.indriya.quantity.Quantities; /** * Data model to describe the parameters of a probability density function to draw random power @@ -14,22 +23,15 @@ * sampled for each quarter hour of a day, subdivided into workdays, Saturdays and Sundays. In * general the GEV is described by the three parameters "location", "scale" and "shape" */ -public class RandomLoadParameters extends UniqueInputEntity { - - /** The respective quarter hour of the day */ - private final int quarterHour; - - /** Shape parameter for a working day */ - private final double kWd; - +public class RandomLoadValues implements LoadValues { /** Shape parameter for a Saturday */ private final double kSa; /** Shape parameter for a Sunday */ private final double kSu; - /** Location parameter for a working day */ - private final double myWd; + /** Shape parameter for a working day */ + private final double kWd; /** Location parameter for a Saturday */ private final double mySa; @@ -37,8 +39,8 @@ public class RandomLoadParameters extends UniqueInputEntity { /** Location parameter for a Sunday */ private final double mySu; - /** Scale parameter for a working day */ - private final double sigmaWd; + /** Location parameter for a working day */ + private final double myWd; /** Scale parameter for a Saturday */ private final double sigmaSa; @@ -46,33 +48,34 @@ public class RandomLoadParameters extends UniqueInputEntity { /** Scale parameter for a Sunday */ private final double sigmaSu; + /** Scale parameter for a working day */ + private final double sigmaWd; + + private final transient GeneralizedExtremeValueDistribution gevWd; + private final transient GeneralizedExtremeValueDistribution gevSa; + private final transient GeneralizedExtremeValueDistribution gevSu; + /** - * @param uuid of the input entity - * @param quarterHour The respective quarter hour of the day - * @param kWd Shape parameter for a working day * @param kSa Shape parameter for a Saturday * @param kSu Shape parameter for a Sunday - * @param myWd Location parameter for a working day + * @param kWd Shape parameter for a working day * @param mySa Location parameter for a Saturday * @param mySu Location parameter for a Sunday - * @param sigmaWd Scale parameter for a working day + * @param myWd Location parameter for a working day * @param sigmaSa Scale parameter for a Saturday * @param sigmaSu Scale parameter for a Sunday + * @param sigmaWd Scale parameter for a working day */ - public RandomLoadParameters( - UUID uuid, - int quarterHour, - double kWd, + public RandomLoadValues( double kSa, double kSu, - double myWd, + double kWd, double mySa, double mySu, - double sigmaWd, + double myWd, double sigmaSa, - double sigmaSu) { - super(uuid); - this.quarterHour = quarterHour; + double sigmaSu, + double sigmaWd) { this.kWd = kWd; this.kSa = kSa; this.kSu = kSu; @@ -82,10 +85,38 @@ public RandomLoadParameters( this.sigmaWd = sigmaWd; this.sigmaSa = sigmaSa; this.sigmaSu = sigmaSu; + + RandomFactory factory = RandomFactory.get(new Random().nextLong()); + + this.gevWd = new GeneralizedExtremeValueDistribution(myWd, sigmaWd, kWd, factory.getRandom()); + + this.gevSa = new GeneralizedExtremeValueDistribution(mySa, sigmaSa, kSa, factory.getRandom()); + this.gevSu = new GeneralizedExtremeValueDistribution(mySu, sigmaSu, kSu, factory.getRandom()); } - public int getQuarterHour() { - return quarterHour; + @Override + public PValue getValue(ZonedDateTime time, LoadProfile loadProfile) { + return new PValue(Quantities.getQuantity(getValue(time.getDayOfWeek()), KILOWATT)); + } + + /** + * Method to get the next random double value. + * + * @param day of the week + * @return a suitable random double + */ + private double getValue(DayOfWeek day) { + double randomValue = + switch (day) { + case SATURDAY -> gevSa.nextRandom(); + case SUNDAY -> gevSu.nextRandom(); + default -> gevWd.nextRandom(); + }; + + while (randomValue < 0) { + randomValue = getValue(day); + } + return randomValue; } public double getMyWd() { @@ -127,51 +158,46 @@ public double getkSu() { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof RandomLoadParameters that)) return false; + if (!(o instanceof RandomLoadValues that)) return false; if (!super.equals(o)) return false; - return Objects.equals(quarterHour, that.quarterHour) - && Objects.equals(kWd, that.kWd) - && Objects.equals(kSa, that.kSa) + return Objects.equals(kSa, that.kSa) && Objects.equals(kSu, that.kSu) - && Objects.equals(myWd, that.myWd) + && Objects.equals(kWd, that.kWd) && Objects.equals(mySa, that.mySa) && Objects.equals(mySu, that.mySu) - && Objects.equals(sigmaWd, that.sigmaWd) + && Objects.equals(myWd, that.myWd) && Objects.equals(sigmaSa, that.sigmaSa) - && Objects.equals(sigmaSu, that.sigmaSu); + && Objects.equals(sigmaSu, that.sigmaSu) + && Objects.equals(sigmaWd, that.sigmaWd); } @Override public int hashCode() { return Objects.hash( - super.hashCode(), quarterHour, kWd, kSa, kSu, myWd, mySa, mySu, sigmaWd, sigmaSa, sigmaSu); + super.hashCode(), kSa, kSu, kWd, mySa, mySu, myWd, sigmaSa, sigmaSu, sigmaWd); } @Override public String toString() { - return "RandomLoadParameters{" - + "uuid=" - + getUuid() - + ", quarterHour=" - + quarterHour - + ", kWd=" - + kWd - + ", kSa=" + return "RandomLoadValues{" + + "kSa=" + kSa + ", kSu=" + kSu - + ", myWd=" - + myWd + + ", kWd=" + + kWd + ", mySa=" + mySa + ", mySu=" + mySu - + ", sigmaWd=" - + sigmaWd + + ", myWd=" + + myWd + ", sigmaSa=" + sigmaSa + ", sigmaSu=" + sigmaSu + + ", sigmaWd=" + + sigmaWd + '}'; } } diff --git a/src/test/groovy/edu/ie3/datamodel/io/factory/timeseries/RandomLoadProfileFactoryTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/factory/timeseries/RandomLoadProfileFactoryTest.groovy new file mode 100644 index 000000000..901a3a3b1 --- /dev/null +++ b/src/test/groovy/edu/ie3/datamodel/io/factory/timeseries/RandomLoadProfileFactoryTest.groovy @@ -0,0 +1,161 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ +package edu.ie3.datamodel.io.factory.timeseries + +import edu.ie3.datamodel.io.naming.timeseries.LoadProfileMetaInformation +import edu.ie3.datamodel.models.profile.LoadProfile +import edu.ie3.datamodel.models.timeseries.repetitive.LoadProfileEntry +import edu.ie3.datamodel.models.value.load.RandomLoadValues +import edu.ie3.util.quantities.PowerSystemUnits +import spock.lang.Shared +import spock.lang.Specification +import tech.units.indriya.quantity.Quantities + +class RandomLoadProfileFactoryTest extends Specification { + @Shared + RandomLoadProfileFactory factory + + @Shared + private Set> allEntries + + def setupSpec() { + factory = new RandomLoadProfileFactory() + + def data0 = new LoadProfileData([ + "kSa": "0.266806721687317", + "kSu": "0.295997023582459", + "kWd": "0.279087692499161", + "mySa": "0.0610353946685791", + "mySu": "0.0630703344941139", + "myWd": "0.053140863776207", + "sigmaSa": "0.0357091873884201", + "sigmaSu": "0.0370676517486572", + "sigmaWd": "0.0293692331761122", + "quarterHour": "0" + ] as Map, RandomLoadValues) + + def data1 = new LoadProfileData([ + "kSa": "0.281179457902908", + "kSu": "0.299608528614044", + "kWd": "0.275292456150055", + "mySa": "0.0560021996498108", + "mySu": "0.058424074202776", + "myWd": "0.0498424917459488", + "sigmaSa": "0.0319067053496838", + "sigmaSu": "0.0334825366735458", + "sigmaWd": "0.0265011098235846", + "quarterHour": "1" + ] as Map, RandomLoadValues) + + def data2 = new LoadProfileData([ + "kSa": "0.275563269853592", + "kSu": "0.29670587182045", + "kWd": "0.252942383289337", + "mySa": "0.0528385005891323", + "mySu": "0.0547995530068874", + "myWd": "0.0472154095768929", + "sigmaSa": "0.0286294519901276", + "sigmaSu": "0.0310499873012304", + "sigmaWd": "0.0245211906731129", + "quarterHour": "2" + ] as Map, RandomLoadValues) + + allEntries = [ + factory.buildModel(data0), + factory.buildModel(data1), + factory.buildModel(data2) + ].flatten() as Set> + } + + def "A RandomLoadProfileFactory returns the correct fields"() { + given: + def expectedFields = [ + "kSa", + "kSu", + "kWd", + "mySa", + "mySu", + "myWd", + "sigmaSa", + "sigmaSu", + "sigmaWd", + "quarterHour" + ] as Set + + when: + def actual = factory.getFields(RandomLoadValues) + + then: + actual.size() == 1 + actual.head() == expectedFields + } + + def "A RandomLoadProfileFactory refuses to build from invalid data"() { + given: + def actualFields = factory.newSet("Wd", "Sa", "Su") + + when: + def actual = factory.validate(actualFields, RandomLoadValues) + + then: + actual.failure + actual.exception.get().message == "The provided fields [Sa, Su, Wd] are invalid for instance of 'RandomLoadValues'. \n" + + "The following fields (without complex objects e.g. nodes, operators, ...) to be passed to a constructor of 'RandomLoadValues' are possible (NOT case-sensitive!):\n" + + "0: [kSa, kSu, kWd, mySa, mySu, myWd, quarterHour, sigmaSa, sigmaSu, sigmaWd] or [k_sa, k_su, k_wd, my_sa, my_su, my_wd, quarter_hour, sigma_sa, sigma_su, sigma_wd]\n" + } + + def "A RandomLoadProfileFactory builds model from valid data"() { + given: + def data = [ + "kSa": "0.266806721687317", + "kSu": "0.295997023582459", + "kWd": "0.279087692499161", + "mySa": "0.0610353946685791", + "mySu": "0.0630703344941139", + "myWd": "0.053140863776207", + "sigmaSa": "0.0357091873884201", + "sigmaSu": "0.0370676517486572", + "sigmaWd": "0.0293692331761122", + "quarterHour": "0" + ] as Map + + when: + def entry = factory.buildModel(new LoadProfileData<>(data, RandomLoadValues)) + + then: + entry.value.class == RandomLoadValues + } + + def "A RandomLoadProfileFactory builds time series from entries"() { + given: + UUID uuid = UUID.fromString("fa3894c1-25af-479c-8a40-1323bb9150a9") + LoadProfileMetaInformation metaInformation = new LoadProfileMetaInformation(uuid, "random") + + + when: + def lpts = factory.build(metaInformation, allEntries) + + then: + lpts.loadProfile == LoadProfile.RandomLoadProfile.RANDOM_LOAD_PROFILE + lpts.entries.size() == 3 + } + + def "A RandomLoadProfileFactory does return the max power correctly"() { + when: + def maxPower = factory.calculateMaxPower(LoadProfile.RandomLoadProfile.RANDOM_LOAD_PROFILE, allEntries) + + then: + maxPower == Quantities.getQuantity(159d, PowerSystemUnits.WATT) + } + + def "A RandomLoadProfileFactory does return an energy scaling correctly"() { + when: + def energyScaling = factory.getLoadProfileEnergyScaling(LoadProfile.RandomLoadProfile.RANDOM_LOAD_PROFILE) + + then: + energyScaling == Quantities.getQuantity(716.5416966513656, PowerSystemUnits.KILOWATTHOUR) + } +} diff --git a/src/test/groovy/edu/ie3/datamodel/io/processor/ProcessorProviderTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/processor/ProcessorProviderTest.groovy index 0b30b13a5..7d63bcfae 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/processor/ProcessorProviderTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/processor/ProcessorProviderTest.groovy @@ -42,8 +42,10 @@ import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries import edu.ie3.datamodel.models.timeseries.individual.TimeBasedValue import edu.ie3.datamodel.models.timeseries.repetitive.BdewLoadProfileTimeSeries import edu.ie3.datamodel.models.timeseries.repetitive.LoadProfileEntry +import edu.ie3.datamodel.models.timeseries.repetitive.RandomLoadProfileTimeSeries import edu.ie3.datamodel.models.value.* import edu.ie3.datamodel.models.value.load.BdewLoadValues +import edu.ie3.datamodel.models.value.load.RandomLoadValues import edu.ie3.datamodel.utils.Try import edu.ie3.test.common.TimeSeriesTestData import edu.ie3.util.TimeUtil @@ -143,6 +145,7 @@ class ProcessorProviderTest extends Specification implements TimeSeriesTestData new TimeSeriesProcessorKey(IndividualTimeSeries, TimeBasedValue, SValue), new TimeSeriesProcessorKey(IndividualTimeSeries, TimeBasedValue, HeatAndSValue), new TimeSeriesProcessorKey(BdewLoadProfileTimeSeries, LoadProfileEntry, BdewLoadValues), + new TimeSeriesProcessorKey(RandomLoadProfileTimeSeries, LoadProfileEntry, RandomLoadValues) ] as Set when: diff --git a/src/test/groovy/edu/ie3/datamodel/models/profile/LoadProfileTest.groovy b/src/test/groovy/edu/ie3/datamodel/models/profile/LoadProfileTest.groovy index 47b5e6fdf..0265d24c4 100644 --- a/src/test/groovy/edu/ie3/datamodel/models/profile/LoadProfileTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/models/profile/LoadProfileTest.groovy @@ -5,6 +5,8 @@ */ package edu.ie3.datamodel.models.profile +import static edu.ie3.datamodel.models.profile.LoadProfile.RandomLoadProfile.RANDOM_LOAD_PROFILE + import edu.ie3.datamodel.exceptions.ParsingException import spock.lang.Specification @@ -17,77 +19,78 @@ class LoadProfileTest extends Specification { actual == expected where: - key || expected - "h0" || BdewStandardLoadProfile.H0 - "h-0" || BdewStandardLoadProfile.H0 - "h_0" || BdewStandardLoadProfile.H0 - "H0" || BdewStandardLoadProfile.H0 - "H-0" || BdewStandardLoadProfile.H0 - "H_0" || BdewStandardLoadProfile.H0 - "l0" || BdewStandardLoadProfile.L0 - "l-0" || BdewStandardLoadProfile.L0 - "l_0" || BdewStandardLoadProfile.L0 - "L0" || BdewStandardLoadProfile.L0 - "L-0" || BdewStandardLoadProfile.L0 - "L_0" || BdewStandardLoadProfile.L0 - "l1" || BdewStandardLoadProfile.L1 - "l-1" || BdewStandardLoadProfile.L1 - "l_1" || BdewStandardLoadProfile.L1 - "L1" || BdewStandardLoadProfile.L1 - "L-1" || BdewStandardLoadProfile.L1 - "L_1" || BdewStandardLoadProfile.L1 - "l2" || BdewStandardLoadProfile.L2 - "l-2" || BdewStandardLoadProfile.L2 - "l_2" || BdewStandardLoadProfile.L2 - "L2" || BdewStandardLoadProfile.L2 - "L-2" || BdewStandardLoadProfile.L2 - "L_2" || BdewStandardLoadProfile.L2 - "g0" || BdewStandardLoadProfile.G0 - "g-0" || BdewStandardLoadProfile.G0 - "g_0" || BdewStandardLoadProfile.G0 - "G0" || BdewStandardLoadProfile.G0 - "G-0" || BdewStandardLoadProfile.G0 - "G_0" || BdewStandardLoadProfile.G0 - "g1" || BdewStandardLoadProfile.G1 - "g-1" || BdewStandardLoadProfile.G1 - "g_1" || BdewStandardLoadProfile.G1 - "G1" || BdewStandardLoadProfile.G1 - "G-1" || BdewStandardLoadProfile.G1 - "G_1" || BdewStandardLoadProfile.G1 - "g2" || BdewStandardLoadProfile.G2 - "g-2" || BdewStandardLoadProfile.G2 - "g_2" || BdewStandardLoadProfile.G2 - "G2" || BdewStandardLoadProfile.G2 - "G-2" || BdewStandardLoadProfile.G2 - "G_2" || BdewStandardLoadProfile.G2 - "g3" || BdewStandardLoadProfile.G3 - "g-3" || BdewStandardLoadProfile.G3 - "g_3" || BdewStandardLoadProfile.G3 - "G3" || BdewStandardLoadProfile.G3 - "G-3" || BdewStandardLoadProfile.G3 - "G_3" || BdewStandardLoadProfile.G3 - "g4" || BdewStandardLoadProfile.G4 - "g-4" || BdewStandardLoadProfile.G4 - "g_4" || BdewStandardLoadProfile.G4 - "G4" || BdewStandardLoadProfile.G4 - "G-4" || BdewStandardLoadProfile.G4 - "G_4" || BdewStandardLoadProfile.G4 - "g5" || BdewStandardLoadProfile.G5 - "g-5" || BdewStandardLoadProfile.G5 - "g_5" || BdewStandardLoadProfile.G5 - "G5" || BdewStandardLoadProfile.G5 - "G-5" || BdewStandardLoadProfile.G5 - "G_5" || BdewStandardLoadProfile.G5 - "g6" || BdewStandardLoadProfile.G6 - "g-6" || BdewStandardLoadProfile.G6 - "g_6" || BdewStandardLoadProfile.G6 - "G6" || BdewStandardLoadProfile.G6 - "G-6" || BdewStandardLoadProfile.G6 - "G_6" || BdewStandardLoadProfile.G6 - "ep1" || NbwTemperatureDependantLoadProfile.EP1 - "ez2" || NbwTemperatureDependantLoadProfile.EZ2 - "" || LoadProfile.DefaultLoadProfiles.NO_LOAD_PROFILE - null || LoadProfile.DefaultLoadProfiles.NO_LOAD_PROFILE + key || expected + "h0" || BdewStandardLoadProfile.H0 + "h-0" || BdewStandardLoadProfile.H0 + "h_0" || BdewStandardLoadProfile.H0 + "H0" || BdewStandardLoadProfile.H0 + "H-0" || BdewStandardLoadProfile.H0 + "H_0" || BdewStandardLoadProfile.H0 + "l0" || BdewStandardLoadProfile.L0 + "l-0" || BdewStandardLoadProfile.L0 + "l_0" || BdewStandardLoadProfile.L0 + "L0" || BdewStandardLoadProfile.L0 + "L-0" || BdewStandardLoadProfile.L0 + "L_0" || BdewStandardLoadProfile.L0 + "l1" || BdewStandardLoadProfile.L1 + "l-1" || BdewStandardLoadProfile.L1 + "l_1" || BdewStandardLoadProfile.L1 + "L1" || BdewStandardLoadProfile.L1 + "L-1" || BdewStandardLoadProfile.L1 + "L_1" || BdewStandardLoadProfile.L1 + "l2" || BdewStandardLoadProfile.L2 + "l-2" || BdewStandardLoadProfile.L2 + "l_2" || BdewStandardLoadProfile.L2 + "L2" || BdewStandardLoadProfile.L2 + "L-2" || BdewStandardLoadProfile.L2 + "L_2" || BdewStandardLoadProfile.L2 + "g0" || BdewStandardLoadProfile.G0 + "g-0" || BdewStandardLoadProfile.G0 + "g_0" || BdewStandardLoadProfile.G0 + "G0" || BdewStandardLoadProfile.G0 + "G-0" || BdewStandardLoadProfile.G0 + "G_0" || BdewStandardLoadProfile.G0 + "g1" || BdewStandardLoadProfile.G1 + "g-1" || BdewStandardLoadProfile.G1 + "g_1" || BdewStandardLoadProfile.G1 + "G1" || BdewStandardLoadProfile.G1 + "G-1" || BdewStandardLoadProfile.G1 + "G_1" || BdewStandardLoadProfile.G1 + "g2" || BdewStandardLoadProfile.G2 + "g-2" || BdewStandardLoadProfile.G2 + "g_2" || BdewStandardLoadProfile.G2 + "G2" || BdewStandardLoadProfile.G2 + "G-2" || BdewStandardLoadProfile.G2 + "G_2" || BdewStandardLoadProfile.G2 + "g3" || BdewStandardLoadProfile.G3 + "g-3" || BdewStandardLoadProfile.G3 + "g_3" || BdewStandardLoadProfile.G3 + "G3" || BdewStandardLoadProfile.G3 + "G-3" || BdewStandardLoadProfile.G3 + "G_3" || BdewStandardLoadProfile.G3 + "g4" || BdewStandardLoadProfile.G4 + "g-4" || BdewStandardLoadProfile.G4 + "g_4" || BdewStandardLoadProfile.G4 + "G4" || BdewStandardLoadProfile.G4 + "G-4" || BdewStandardLoadProfile.G4 + "G_4" || BdewStandardLoadProfile.G4 + "g5" || BdewStandardLoadProfile.G5 + "g-5" || BdewStandardLoadProfile.G5 + "g_5" || BdewStandardLoadProfile.G5 + "G5" || BdewStandardLoadProfile.G5 + "G-5" || BdewStandardLoadProfile.G5 + "G_5" || BdewStandardLoadProfile.G5 + "g6" || BdewStandardLoadProfile.G6 + "g-6" || BdewStandardLoadProfile.G6 + "g_6" || BdewStandardLoadProfile.G6 + "G6" || BdewStandardLoadProfile.G6 + "G-6" || BdewStandardLoadProfile.G6 + "G_6" || BdewStandardLoadProfile.G6 + "ep1" || NbwTemperatureDependantLoadProfile.EP1 + "ez2" || NbwTemperatureDependantLoadProfile.EZ2 + "random" || RANDOM_LOAD_PROFILE + "" || LoadProfile.DefaultLoadProfiles.NO_LOAD_PROFILE + null || LoadProfile.DefaultLoadProfiles.NO_LOAD_PROFILE } def "Standard load profiles can be parsed correctly"() { @@ -107,7 +110,7 @@ class LoadProfileTest extends Specification { "H_0" || BdewStandardLoadProfile.H0 } - def "Tempearture dependent load profiles can be parsed correctly"() { + def "Temperature dependent load profiles can be parsed correctly"() { when: TemperatureDependantLoadProfile actual = TemperatureDependantLoadProfile.parse(key) @@ -184,6 +187,6 @@ class LoadProfileTest extends Specification { then: def e = thrown(ParsingException) - e.message == "No predefined load profile with key 'not_a_key' found. Please provide one of the following keys: h0, l0, l1, l2, g0, g1, g2, g3, g4, g5, g6, ep1, ez2" + e.message == "No predefined load profile with key 'not_a_key' found. Please provide one of the following keys: h0, l0, l1, l2, g0, g1, g2, g3, g4, g5, g6, ep1, ez2, random" } } diff --git a/src/test/groovy/edu/ie3/test/common/TimeSeriesSourceTestData.groovy b/src/test/groovy/edu/ie3/test/common/TimeSeriesSourceTestData.groovy index 9df990efb..2e7cb5d4d 100644 --- a/src/test/groovy/edu/ie3/test/common/TimeSeriesSourceTestData.groovy +++ b/src/test/groovy/edu/ie3/test/common/TimeSeriesSourceTestData.groovy @@ -8,6 +8,7 @@ package edu.ie3.test.common import edu.ie3.datamodel.models.StandardUnits import edu.ie3.datamodel.models.value.HeatAndPValue import edu.ie3.datamodel.models.value.PValue +import edu.ie3.util.quantities.PowerSystemUnits import tech.units.indriya.quantity.Quantities import java.time.ZonedDateTime @@ -37,4 +38,12 @@ final class TimeSeriesSourceTestData { Quantities.getQuantity(1250.0d, StandardUnits.ACTIVE_POWER_IN), Quantities.getQuantity(12.0, StandardUnits.HEAT_DEMAND) ) + + public static final PValue G3_VALUE_00MIN = new PValue( + Quantities.getQuantity(94.7, PowerSystemUnits.WATT) + ) + + public static final PValue G3_VALUE_15MIN = new PValue( + Quantities.getQuantity(94.1, PowerSystemUnits.WATT) + ) }