diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c20c903d..38bb3e6b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Staudt to list of reviewers [#1190](https://github.com/ie3-institute/PowerSystemDataModel/issues/1190) - 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) ### Fixed - Removing opened `SwitchInput` during connectivity check [#1221](https://github.com/ie3-institute/PowerSystemDataModel/issues/1221) diff --git a/docs/uml/main/TimeSeriesDatamodelConcept.puml b/docs/uml/main/TimeSeriesDatamodelConcept.puml index 370f3b2e8..7f0cda6d7 100644 --- a/docs/uml/main/TimeSeriesDatamodelConcept.puml +++ b/docs/uml/main/TimeSeriesDatamodelConcept.puml @@ -22,11 +22,21 @@ package models { childNote .. input childNote .. voltagelevels - interface StandardLoadProfile { + interface LoadProfile { + getKey: String + + {static} parse(String): LoadProfile + + {static} getAllProfiles: LoadProfile[] + + {static} getProfile(T[], String): T + enum DefaultLoadProfiles + + enum RandomLoadProfile + } + + DefaultLoadProfiles --|> LoadProfile + + interface StandardLoadProfile { + {static} parse(String): StandardLoadProfile } + StandardLoadProfile ..|> LoadProfile enum BdewLoadProfile { - key: String diff --git a/src/main/java/edu/ie3/datamodel/io/factory/timeseries/BdewLoadProfileFactory.java b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/BdewLoadProfileFactory.java new file mode 100644 index 000000000..2d10a0c00 --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/BdewLoadProfileFactory.java @@ -0,0 +1,126 @@ +/* + * © 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 tech.units.indriya.unit.Units.WATT; + +import edu.ie3.datamodel.exceptions.FactoryException; +import edu.ie3.datamodel.exceptions.ParsingException; +import edu.ie3.datamodel.io.naming.timeseries.LoadProfileMetaInformation; +import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile; +import edu.ie3.datamodel.models.timeseries.TimeSeriesEntry; +import edu.ie3.datamodel.models.timeseries.repetitive.BdewLoadProfileTimeSeries; +import edu.ie3.datamodel.models.timeseries.repetitive.LoadProfileEntry; +import edu.ie3.datamodel.models.value.load.BdewLoadValues; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; +import tech.units.indriya.ComparableQuantity; +import tech.units.indriya.quantity.Quantities; + +public class BdewLoadProfileFactory + extends LoadProfileFactory { + public static final String SUMMER_SATURDAY = "SuSa"; + public static final String SUMMER_SUNDAY = "SuSu"; + public static final String SUMMER_WEEKDAY = "SuWd"; + public static final String TRANSITION_SATURDAY = "TrSa"; + public static final String TRANSITION_SUNDAY = "TrSu"; + public static final String TRANSITION_WEEKDAY = "TrWd"; + public static final String WINTER_SATURDAY = "WiSa"; + public static final String WINTER_SUNDAY = "WiSu"; + public static final String WINTER_WEEKDAY = "WiWd"; + + public BdewLoadProfileFactory() { + this(BdewLoadValues.class); + } + + public BdewLoadProfileFactory(Class valueClass) { + super(valueClass); + } + + @Override + protected LoadProfileEntry buildModel(LoadProfileData data) { + int quarterHour = data.getInt(QUARTER_HOUR); + + return new LoadProfileEntry<>( + new BdewLoadValues( + data.getDouble(SUMMER_SATURDAY), + data.getDouble(SUMMER_SUNDAY), + data.getDouble(SUMMER_WEEKDAY), + data.getDouble(TRANSITION_SATURDAY), + data.getDouble(TRANSITION_SUNDAY), + data.getDouble(TRANSITION_WEEKDAY), + data.getDouble(WINTER_SATURDAY), + data.getDouble(WINTER_SUNDAY), + data.getDouble(WINTER_WEEKDAY)), + quarterHour); + } + + @Override + protected List> getFields(Class entityClass) { + return List.of( + newSet( + QUARTER_HOUR, + SUMMER_SATURDAY, + SUMMER_SUNDAY, + SUMMER_WEEKDAY, + TRANSITION_SATURDAY, + TRANSITION_SUNDAY, + TRANSITION_WEEKDAY, + WINTER_SATURDAY, + WINTER_SUNDAY, + WINTER_WEEKDAY)); + } + + @Override + public BdewLoadProfileTimeSeries build( + LoadProfileMetaInformation metaInformation, Set> entries) { + + BdewStandardLoadProfile profile = parseProfile(metaInformation.getProfile()); + ComparableQuantity maxPower = calculateMaxPower(profile, entries); + ComparableQuantity profileEnergyScaling = getLoadProfileEnergyScaling(profile); + + return new BdewLoadProfileTimeSeries( + metaInformation.getUuid(), profile, entries, maxPower, profileEnergyScaling); + } + + @Override + public BdewStandardLoadProfile parseProfile(String profile) { + try { + return BdewStandardLoadProfile.get(profile); + } catch (ParsingException e) { + throw new FactoryException("An error occurred while parsing the profile: " + profile, e); + } + } + + @Override + public ComparableQuantity calculateMaxPower( + BdewStandardLoadProfile loadProfile, Set> entries) { + Function> valueExtractor; + + if (loadProfile == BdewStandardLoadProfile.H0) { + // maximum dynamization factor is on day 366 (leap year) or day 365 (regular year). + // The difference between day 365 and day 366 is negligible, thus pick 366 + valueExtractor = + v -> + Stream.of(v.getWiSa(), v.getWiSu(), v.getWiWd()) + .map(p -> BdewLoadValues.dynamization(p, 366)); + } else { + valueExtractor = v -> v.values().stream(); + } + + double maxPower = + entries.stream() + .map(TimeSeriesEntry::getValue) + .flatMap(valueExtractor) + .max(Comparator.naturalOrder()) + .orElse(0d); + + return Quantities.getQuantity(maxPower, WATT); + } +} diff --git a/src/main/java/edu/ie3/datamodel/io/factory/timeseries/LoadProfileFactory.java b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/LoadProfileFactory.java index f2b511eef..eea6e22d2 100644 --- a/src/main/java/edu/ie3/datamodel/io/factory/timeseries/LoadProfileFactory.java +++ b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/LoadProfileFactory.java @@ -55,6 +55,6 @@ public String getTimeFieldString() { /** Returns the load profile energy scaling. The default value is 1000 kWh */ public ComparableQuantity getLoadProfileEnergyScaling(P loadProfile) { - return Quantities.getQuantity(1000, PowerSystemUnits.KILOWATTHOUR); + return Quantities.getQuantity(1000d, 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 faf79daa8..dc09ca026 100644 --- a/src/main/java/edu/ie3/datamodel/io/processor/Processor.java +++ b/src/main/java/edu/ie3/datamodel/io/processor/Processor.java @@ -224,6 +224,7 @@ protected String processMethodResult(Object methodReturnObject, Method method, S "double", "String", "DayOfWeek", + "Season", "ChargingPointType", "EvcsLocationType" -> resultStringBuilder.append(methodReturnObject.toString()); case "Quantity", "ComparableQuantity" -> resultStringBuilder.append( @@ -260,7 +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" -> resultStringBuilder.append(((LoadProfile) methodReturnObject).getKey()); + case "LoadProfile", "BdewStandardLoadProfile" -> 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 77a85e69d..1f5eb7583 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 @@ -13,7 +13,9 @@ import edu.ie3.datamodel.models.timeseries.TimeSeriesEntry; import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries; import edu.ie3.datamodel.models.timeseries.individual.TimeBasedValue; +import edu.ie3.datamodel.models.timeseries.repetitive.*; import edu.ie3.datamodel.models.value.*; +import edu.ie3.datamodel.models.value.load.BdewLoadValues; import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; @@ -50,7 +52,9 @@ public class TimeSeriesProcessor< new TimeSeriesProcessorKey( IndividualTimeSeries.class, TimeBasedValue.class, SValue.class), new TimeSeriesProcessorKey( - IndividualTimeSeries.class, TimeBasedValue.class, HeatAndSValue.class)); + IndividualTimeSeries.class, TimeBasedValue.class, HeatAndSValue.class), + new TimeSeriesProcessorKey( + BdewLoadProfileTimeSeries.class, LoadProfileEntry.class, BdewLoadValues.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/BdewSeason.java b/src/main/java/edu/ie3/datamodel/models/BdewSeason.java new file mode 100644 index 000000000..eb7336c55 --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/models/BdewSeason.java @@ -0,0 +1,85 @@ +/* + * © 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; + +import edu.ie3.datamodel.exceptions.ParsingException; +import java.time.ZonedDateTime; + +public enum BdewSeason { + WINTER("Wi"), + SUMMER("Su"), + TRANSITION("Tr"); + + private final String key; + + BdewSeason(String key) { + this.key = key.toLowerCase(); + } + + public static BdewSeason parse(String key) throws ParsingException { + return switch (key) { + case "Wi", "Winter" -> WINTER; + case "Su", "Summer" -> SUMMER; + case "Tr", "Intermediate" -> TRANSITION; + default -> throw new ParsingException( + "There is no season for key:" + + key + + ". Permissible keys: 'Wi', 'Winter', 'Su', 'Summer', 'Tr', 'Intermediate'"); + }; + } + + /** + * Creates a season from given time + * + * @param time the time + * @return a season + */ + public static BdewSeason getSeason(ZonedDateTime time) { + int day = time.getDayOfMonth(); + + // winter: 1.11.-20.03. + // summer: 15.05.-14.09. + // transition: 21.03.-14.05. and + // 15.09.-31.10. + // (VDEW handbook) + + return switch (time.getMonth()) { + case NOVEMBER, DECEMBER, JANUARY, FEBRUARY -> WINTER; + case MARCH -> { + if (day <= 20) { + yield WINTER; + } else { + yield TRANSITION; + } + } + case MAY -> { + if (day >= 15) { + yield SUMMER; + } else { + yield TRANSITION; + } + } + case JUNE, JULY, AUGUST -> SUMMER; + case SEPTEMBER -> { + if (day <= 14) { + yield SUMMER; + } else { + yield TRANSITION; + } + } + default -> TRANSITION; + }; + } + + public String getKey() { + return key; + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/BdewLoadProfileTimeSeries.java b/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/BdewLoadProfileTimeSeries.java new file mode 100644 index 000000000..524697166 --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/models/timeseries/repetitive/BdewLoadProfileTimeSeries.java @@ -0,0 +1,60 @@ +/* + * © 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 edu.ie3.datamodel.models.profile.BdewStandardLoadProfile; +import edu.ie3.datamodel.models.value.load.BdewLoadValues; +import java.util.Objects; +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 bdew load profile time series with repetitive values that can be calculated from a + * pattern. Each value of this timeseries is given in W. + */ +public class BdewLoadProfileTimeSeries extends LoadProfileTimeSeries { + + public BdewLoadProfileTimeSeries( + UUID uuid, + BdewStandardLoadProfile loadProfile, + Set> values, + ComparableQuantity maxPower, + ComparableQuantity profileEnergyScaling) { + super(uuid, loadProfile, values, maxPower, profileEnergyScaling); + } + + @Override + public BdewStandardLoadProfile getLoadProfile() { + return (BdewStandardLoadProfile) super.getLoadProfile(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode()); + } + + @Override + public String toString() { + return "BDEWLoadProfileTimeSeries{" + + "uuid=" + + getUuid() + + "loadProfile=" + + getLoadProfile() + + ", valueMapping=" + + getValueMapping() + + '}'; + } +} diff --git a/src/main/java/edu/ie3/datamodel/models/value/load/BdewLoadValues.java b/src/main/java/edu/ie3/datamodel/models/value/load/BdewLoadValues.java new file mode 100644 index 000000000..278dcaa0e --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/models/value/load/BdewLoadValues.java @@ -0,0 +1,184 @@ +/* + * © 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.value.load; + +import static edu.ie3.datamodel.models.BdewSeason.*; +import static java.lang.Math.pow; +import static java.lang.Math.round; +import static tech.units.indriya.unit.Units.WATT; + +import edu.ie3.datamodel.models.BdewSeason; +import edu.ie3.datamodel.models.profile.BdewStandardLoadProfile; +import edu.ie3.datamodel.models.profile.LoadProfile; +import edu.ie3.datamodel.models.value.PValue; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import tech.units.indriya.quantity.Quantities; + +/** Load values for a {@link BdewStandardLoadProfile} */ +public class BdewLoadValues implements LoadValues { + private final double suSa; + private final double suSu; + private final double suWd; + private final double trSa; + private final double trSu; + private final double trWd; + private final double wiSa; + private final double wiSu; + private final double wiWd; + + public BdewLoadValues( + double suSa, + double suSu, + double suWd, + double trSa, + double trSu, + double trWd, + double wiSa, + double wiSu, + double wiWd) { + this.suSa = suSa; + this.suSu = suSu; + this.suWd = suWd; + this.trSa = trSa; + this.trSu = trSu; + this.trWd = trWd; + this.wiSa = wiSa; + this.wiSu = wiSu; + this.wiWd = wiWd; + } + + @Override + public PValue getValue(ZonedDateTime time, LoadProfile loadProfile) { + Map mapping = + switch (time.getDayOfWeek()) { + case SATURDAY -> Map.of( + SUMMER, suSa, + WINTER, wiSa, + TRANSITION, trSa); + case SUNDAY -> Map.of( + SUMMER, suSu, + WINTER, wiSu, + TRANSITION, trSu); + default -> Map.of( + SUMMER, suWd, + WINTER, wiWd, + TRANSITION, trWd); + }; + + double power = mapping.get(getSeason(time)); + + if (loadProfile == BdewStandardLoadProfile.H0) { + /* For the residential average profile, a dynamization has to be taken into account */ + power = dynamization(power, time.getDayOfYear()); // leap years are ignored + } + + return new PValue(Quantities.getQuantity(power, WATT)); + } + + /** + * Calculates the dynamization factor for given day of year. Cf. + * Anwendung der repräsentativen Lastprofile - Step by step page 19 + * + * @param load load value + * @param t day of year (1-366) + * @return dynamization factor + */ + public static double dynamization(double load, int t) { + double factor = + (-3.92e-10 * pow(t, 4) + 3.2e-7 * pow(t, 3) - 7.02e-5 * pow(t, 2) + 2.1e-3 * t + 1.24); + double rndFactor = round(factor * 1e4) / 1e4; // round to 4 decimal places + return round(load * rndFactor * 1e1) / 1e1; // rounded to 1 decimal place + } + + public double getSuSa() { + return suSa; + } + + public double getSuSu() { + return suSu; + } + + public double getSuWd() { + return suWd; + } + + public double getTrSa() { + return trSa; + } + + public double getTrSu() { + return trSu; + } + + public double getTrWd() { + return trWd; + } + + public double getWiSa() { + return wiSa; + } + + public double getWiSu() { + return wiSu; + } + + public double getWiWd() { + return wiWd; + } + + public List values() { + return List.of(suSa, suSu, suWd, trSa, trSu, trWd, wiSa, wiSu, wiWd); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BdewLoadValues that = (BdewLoadValues) o; + return Objects.equals(suSa, that.suSa) + && Objects.equals(suSu, that.suSu) + && Objects.equals(suWd, that.suWd) + && Objects.equals(trSa, that.trSa) + && Objects.equals(trSu, that.trSu) + && Objects.equals(trWd, that.trWd) + && Objects.equals(wiSa, that.wiSa) + && Objects.equals(wiSu, that.wiSu) + && Objects.equals(wiWd, that.wiWd); + } + + @Override + public int hashCode() { + return Objects.hash(suSa, suSu, suWd, trSa, trSu, trWd, wiSa, wiSu, wiWd); + } + + @Override + public String toString() { + return "BDEWLoadValues{" + + "suSa=" + + suSa + + ", suSu=" + + suSu + + ", suWd=" + + suWd + + ", trSa=" + + trSa + + ", trSu=" + + trSu + + ", trWd=" + + trWd + + ", wiSa=" + + wiSa + + ", wiSu=" + + wiSu + + ", wiWd=" + + wiWd + + '}'; + } +} diff --git a/src/test/groovy/edu/ie3/datamodel/io/factory/timeseries/BdewLoadProfileFactoryTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/factory/timeseries/BdewLoadProfileFactoryTest.groovy new file mode 100644 index 000000000..8c9c14048 --- /dev/null +++ b/src/test/groovy/edu/ie3/datamodel/io/factory/timeseries/BdewLoadProfileFactoryTest.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.BdewStandardLoadProfile +import edu.ie3.datamodel.models.timeseries.repetitive.LoadProfileEntry +import edu.ie3.datamodel.models.value.load.BdewLoadValues +import edu.ie3.util.quantities.PowerSystemUnits +import spock.lang.Shared +import spock.lang.Specification +import tech.units.indriya.quantity.Quantities + +class BdewLoadProfileFactoryTest extends Specification { + @Shared + BdewLoadProfileFactory factory + + @Shared + private Set> allEntries + + def setupSpec() { + factory = new BdewLoadProfileFactory() + + def data0 = new LoadProfileData([ + "SuSa": "74.6", + "SuSu": "68.8", + "SuWd": "71.5", + "TrSa": "75.8", + "TrSu": "68.3", + "TrWd": "73.0", + "WiSa": "70.0", + "WiSu": "63.2", + "WiWd": "65.5", + "quarterHour": "0" + ] as Map, BdewLoadValues) + + def data1 = new LoadProfileData([ + "SuSa": "76.2", + "SuSu": "67.4", + "SuWd": "69.0", + "TrSa": "76.7", + "TrSu": "66.5", + "TrWd": "70.1", + "WiSa": "73.0", + "WiSu": "61.0", + "WiWd": "62.6", + "quarterHour": "1" + ] as Map, BdewLoadValues) + + def data2 = new LoadProfileData([ + "SuSa": "77.7", + "SuSu": "65.7", + "SuWd": "66.3", + "TrSa": "77.7", + "TrSu": "64.6", + "TrWd": "67.1", + "WiSa": "75.9", + "WiSu": "58.9", + "WiWd": "59.6", + "quarterHour": "2" + ] as Map, BdewLoadValues) + + allEntries = [ + factory.buildModel(data0), + factory.buildModel(data1), + factory.buildModel(data2) + ].flatten() as Set> + } + + def "A BDEWLoadProfileFactory returns the correct fields"() { + given: + def expectedFields = [ + "SuSa", + "SuSu", + "SuWd", + "TrSa", + "TrSu", + "TrWd", + "WiSa", + "WiSu", + "WiWd", + "quarterHour" + ] as Set + + when: + def actual = factory.getFields(BdewLoadValues) + + then: + actual.size() == 1 + actual.head() == expectedFields + } + + def "A BDEWLoadProfileFactory refuses to build from invalid data"() { + given: + def actualFields = factory.newSet("Sa", "Su", "Wd") + + when: + def actual = factory.validate(actualFields, BdewLoadValues) + + then: + actual.failure + actual.exception.get().message == "The provided fields [Sa, Su, Wd] are invalid for instance of 'BdewLoadValues'. \n" + + "The following fields (without complex objects e.g. nodes, operators, ...) to be passed to a constructor of 'BdewLoadValues' are possible (NOT case-sensitive!):\n" + + "0: [quarterHour, SuSa, SuSu, SuWd, TrSa, TrSu, TrWd, WiSa, WiSu, WiWd] or [quarter_hour, su_sa, su_su, su_wd, tr_sa, tr_su, tr_wd, wi_sa, wi_su, wi_wd]\n" + } + + def "A BDEWLoadProfileFactory builds model from valid data"() { + given: + def data = [ + "SuSa": "74.6", + "SuSu": "68.8", + "SuWd": "71.5", + "TrSa": "75.8", + "TrSu": "68.3", + "TrWd": "73.0", + "WiSa": "70.0", + "WiSu": "63.2", + "WiWd": "65.5", + "quarterHour": "0" + ] as Map + + when: + def entry = factory.buildModel(new LoadProfileData<>(data, BdewLoadValues)) + + then: + entry.value.class == BdewLoadValues + } + + def "A BDEWLoadProfileFactory builds time series from entries"() { + given: + UUID uuid = UUID.fromString("fa3894c1-25af-479c-8a40-1323bb9150a9") + LoadProfileMetaInformation metaInformation = new LoadProfileMetaInformation(uuid, "g0") + + + when: + def lpts = factory.build(metaInformation, allEntries) + + then: + lpts.loadProfile == BdewStandardLoadProfile.G0 + lpts.entries.size() == 3 + } + + def "A BDEWLoadProfileFactory does return the max power correctly"() { + when: + def maxPower = factory.calculateMaxPower(BdewStandardLoadProfile.G0, allEntries) + + then: + maxPower == Quantities.getQuantity(77.7, PowerSystemUnits.WATT) + } + + def "A BDEWLoadProfileFactory does return an energy scaling correctly"() { + when: + def energyScaling = factory.getLoadProfileEnergyScaling(BdewStandardLoadProfile.G0) + + then: + energyScaling == Quantities.getQuantity(1000d, PowerSystemUnits.KILOWATTHOUR) + } +} diff --git a/src/test/groovy/edu/ie3/datamodel/io/naming/EntityPersistenceNamingStrategyTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/naming/EntityPersistenceNamingStrategyTest.groovy index 7986a3774..06765fdc1 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/naming/EntityPersistenceNamingStrategyTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/naming/EntityPersistenceNamingStrategyTest.groovy @@ -34,7 +34,7 @@ import edu.ie3.datamodel.models.result.thermal.ThermalHouseResult import edu.ie3.datamodel.models.timeseries.IntValue import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries import edu.ie3.datamodel.models.timeseries.individual.TimeBasedValue -import edu.ie3.datamodel.models.timeseries.repetitive.LoadProfileTimeSeries +import edu.ie3.datamodel.models.timeseries.repetitive.BdewLoadProfileTimeSeries import edu.ie3.datamodel.models.timeseries.repetitive.RepetitiveTimeSeries import edu.ie3.datamodel.models.value.EnergyPriceValue import edu.ie3.util.quantities.PowerSystemUnits @@ -409,11 +409,10 @@ class EntityPersistenceNamingStrategyTest extends Specification { IndividualTimeSeries | UUID.fromString("4881fda2-bcee-4f4f-a5bb-6a09bf785276") || "aa_its_c_4881fda2-bcee-4f4f-a5bb-6a09bf785276_zz" } - def "A EntityPersistenceNamingStrategy without pre- or suffix should return valid file name for load profile input" () { + def "A EntityPersistenceNamingStrategy without pre- or suffix should return valid file name for load profile time series" () { given: EntityPersistenceNamingStrategy strategy = new EntityPersistenceNamingStrategy() - LoadProfileTimeSeries timeSeries = Mock(LoadProfileTimeSeries) - timeSeries.uuid >> uuid + BdewLoadProfileTimeSeries timeSeries = Mock(BdewLoadProfileTimeSeries) timeSeries.loadProfile >> type when: @@ -424,8 +423,8 @@ class EntityPersistenceNamingStrategyTest extends Specification { actual.get() == expectedFileName where: - clazz | uuid | type || expectedFileName - LoadProfileTimeSeries | UUID.fromString("bee0a8b6-4788-4f18-bf72-be52035f7304") | BdewStandardLoadProfile.G3 || "lpts_g3" + clazz | type || expectedFileName + BdewLoadProfileTimeSeries | BdewStandardLoadProfile.G3 || "lpts_g3" } def "A EntityPersistenceNamingStrategy returns empty Optional, when there is no naming defined for a given time series class"() { 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 4f9b5c011..0b30b13a5 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/processor/ProcessorProviderTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/processor/ProcessorProviderTest.groovy @@ -40,7 +40,10 @@ import edu.ie3.datamodel.models.timeseries.TimeSeries import edu.ie3.datamodel.models.timeseries.TimeSeriesEntry 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.value.* +import edu.ie3.datamodel.models.value.load.BdewLoadValues import edu.ie3.datamodel.utils.Try import edu.ie3.test.common.TimeSeriesTestData import edu.ie3.util.TimeUtil @@ -139,6 +142,7 @@ class ProcessorProviderTest extends Specification implements TimeSeriesTestData new TimeSeriesProcessorKey(IndividualTimeSeries, TimeBasedValue, HeatAndPValue), new TimeSeriesProcessorKey(IndividualTimeSeries, TimeBasedValue, SValue), new TimeSeriesProcessorKey(IndividualTimeSeries, TimeBasedValue, HeatAndSValue), + new TimeSeriesProcessorKey(BdewLoadProfileTimeSeries, LoadProfileEntry, BdewLoadValues), ] as Set when: diff --git a/src/test/groovy/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessorTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessorTest.groovy index d551d35d0..d4f045201 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessorTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/processor/timeseries/TimeSeriesProcessorTest.groovy @@ -10,7 +10,10 @@ import edu.ie3.datamodel.io.processor.Processor import edu.ie3.datamodel.models.timeseries.IntValue 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.value.* +import edu.ie3.datamodel.models.value.load.BdewLoadValues import edu.ie3.test.common.TimeSeriesTestData import spock.lang.Specification @@ -207,4 +210,15 @@ class TimeSeriesProcessorTest extends Specification implements TimeSeriesTestDat then: actual == individualHeatAndSTimeSeriesProcessed } + + def "A TimeSeriesProcessors handles a complete LoadProfileTimeSeries correctly"() { + given: + TimeSeriesProcessor processor = new TimeSeriesProcessor<>(BdewLoadProfileTimeSeries, LoadProfileEntry, BdewLoadValues) + + when: + Set> actual = processor.handleTimeSeries(loadProfileTimeSeries) + + then: + actual == loadProfileTimeSeriesProcessed + } } diff --git a/src/test/groovy/edu/ie3/datamodel/io/sink/CsvFileSinkTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/sink/CsvFileSinkTest.groovy index 4b2be267b..7a0957607 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/sink/CsvFileSinkTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/sink/CsvFileSinkTest.groovy @@ -236,6 +236,7 @@ class CsvFileSinkTest extends Specification implements TimeSeriesTestData { testBaseFolderPath.resolve("its_ph_56c20b88-c001-4225-8dac-cd13a75c6b48.csv").toFile().exists() testBaseFolderPath.resolve("its_pqh_83b577cc-06b1-47a1-bfff-ad648a00784b.csv").toFile().exists() testBaseFolderPath.resolve("its_c_a4bbcb77-b9d0-4b88-92be-b9a14a3e332b.csv").toFile().exists() + testBaseFolderPath.resolve("lpts_g2.csv").toFile().exists() testBaseFolderPath.resolve("its_weather_4fcbdfcd-4ff0-46dd-b0df-f3af7ae3ed98.csv").toFile().exists() } diff --git a/src/test/groovy/edu/ie3/datamodel/io/sink/SqlSinkTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/sink/SqlSinkTest.groovy index db8828495..75a0859a2 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/sink/SqlSinkTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/sink/SqlSinkTest.groovy @@ -87,7 +87,7 @@ class SqlSinkTest extends Specification implements TestContainerHelper, TimeSeri "result_entities.sql", "input_entities.sql", "time_series.sql", - "load_profile.sql" + "load_profiles.sql" ) for (String file: importFiles) { Container.ExecResult res = postgreSQLContainer.execInContainer("psql", "-Utest", "-f/home/" + file) @@ -199,6 +199,7 @@ class SqlSinkTest extends Specification implements TestContainerHelper, TimeSeri source.executeQuery("SELECT * FROM " + schemaName + "." + "time_series_ph", ps -> {}).count() == 3 source.executeQuery("SELECT * FROM " + schemaName + "." + "time_series_pqh", ps -> {}).count() == 3 source.executeQuery("SELECT * FROM " + schemaName + "." + "time_series_weather", ps -> {}).count() == 3 + source.executeQuery("SELECT * FROM " + schemaName + "." + "load_profiles", ps -> {}).count() == 3 cleanup: sink.shutdown() diff --git a/src/test/groovy/edu/ie3/test/common/TimeSeriesTestData.groovy b/src/test/groovy/edu/ie3/test/common/TimeSeriesTestData.groovy index efa2ccdea..99925e5ab 100644 --- a/src/test/groovy/edu/ie3/test/common/TimeSeriesTestData.groovy +++ b/src/test/groovy/edu/ie3/test/common/TimeSeriesTestData.groovy @@ -17,8 +17,10 @@ import edu.ie3.datamodel.models.timeseries.IntValue import edu.ie3.datamodel.models.timeseries.TimeSeries 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.value.* +import edu.ie3.datamodel.models.value.load.BdewLoadValues import org.locationtech.jts.geom.Coordinate import org.locationtech.jts.geom.GeometryFactory import org.locationtech.jts.geom.Point @@ -399,6 +401,62 @@ trait TimeSeriesTestData { ] as LinkedHashMap ] as Set + BdewLoadProfileTimeSeries loadProfileTimeSeries = new BdewLoadProfileTimeSeries( + UUID.fromString("b0ad5ba2-0d5e-4c9b-b818-4079cebf59cc"), + BdewStandardLoadProfile.G2, + [ + new LoadProfileEntry<>( + new BdewLoadValues(63.1, 50.6, 60.8, 73.1, 64.2, 70.5, 80.6, 73.7, 77.4), 0 + ), + new LoadProfileEntry<>( + new BdewLoadValues(58.0, 47.4, 53.0, 67.6, 60.7, 61.9, 74.6, 68.7, 67.4), 1), + new LoadProfileEntry<>( + new BdewLoadValues(53.5, 44.3, 46.0, 62.8, 56.9, 54.4, 69.2, 63.6, 58.4), 2 + ), + ] as Set, + Quantities.getQuantity(80.6, WATT), + Quantities.getQuantity(1000d, KILOWATTHOUR) + ) + + Set> loadProfileTimeSeriesProcessed = [ + [ + "suSa": "63.1", + "suSu": "50.6", + "suWd": "60.8", + "trSa": "73.1", + "trSu": "64.2", + "trWd": "70.5", + "wiSa": "80.6", + "wiSu": "73.7", + "wiWd": "77.4", + "quarterHour": "0" + ] as LinkedHashMap, + [ + "suSa": "58.0", + "suSu": "47.4", + "suWd": "53.0", + "trSa": "67.6", + "trSu": "60.7", + "trWd": "61.9", + "wiSa": "74.6", + "wiSu": "68.7", + "wiWd": "67.4", + "quarterHour": "1" + ] as LinkedHashMap, + [ + "suSa": "53.5", + "suSu": "44.3", + "suWd": "46.0", + "trSa": "62.8", + "trSu": "56.9", + "trWd": "54.4", + "wiSa": "69.2", + "wiSu": "63.6", + "wiWd": "58.4", + "quarterHour": "2" + ] as LinkedHashMap + ] as Set + List allTimeSeries = [ individualPTimeSeries, individualEnergyPriceTimeSeries, @@ -408,5 +466,6 @@ trait TimeSeriesTestData { individualPTimeSeries, individualSTimeSeries, individualWeatherTimeSeries, + loadProfileTimeSeries ] } \ No newline at end of file diff --git a/src/test/resources/edu/ie3/datamodel/io/sink/_sql/load_profile.sql b/src/test/resources/edu/ie3/datamodel/io/sink/_sql/load_profile.sql deleted file mode 100644 index 474be75de..000000000 --- a/src/test/resources/edu/ie3/datamodel/io/sink/_sql/load_profile.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE public.load_profile_g2 -( - time_series UUID NOT NULL, - day_of_week TEXT NOT NULL, - quarter_hour_of_day TEXT NOT NULL, - p DOUBLE PRECISION, - grid_uuid UUID NOT NULL REFERENCES grids(uuid) -) - WITHOUT OIDS - TABLESPACE pg_default; - diff --git a/src/test/resources/edu/ie3/datamodel/io/sink/_sql/load_profiles.sql b/src/test/resources/edu/ie3/datamodel/io/sink/_sql/load_profiles.sql new file mode 100644 index 000000000..da883ded9 --- /dev/null +++ b/src/test/resources/edu/ie3/datamodel/io/sink/_sql/load_profiles.sql @@ -0,0 +1,24 @@ +CREATE TABLE public.load_profiles +( + load_profile VARCHAR(11) NOT NULL, + quarter_hour integer NOT NULL, + su_sa DOUBLE PRECISION NOT NULL, + su_su DOUBLE PRECISION NOT NULL, + su_wd DOUBLE PRECISION NOT NULL, + tr_sa DOUBLE PRECISION NOT NULL, + tr_su DOUBLE PRECISION NOT NULL, + tr_wd DOUBLE PRECISION NOT NULL, + wi_sa DOUBLE PRECISION NOT NULL, + wi_su DOUBLE PRECISION NOT NULL, + wi_wd DOUBLE PRECISION NOT NULL, + grid_uuid UUID NOT NULL REFERENCES grids(uuid) +) + WITHOUT OIDS + TABLESPACE pg_default; + +CREATE INDEX load_profiles_series_id ON load_profiles USING hash (load_profile); + +-- Order of columns is important when using btree: https://www.postgresql.org/docs/14/indexes-multicolumn.html +-- Column time_series needs to placed as the first argument since we at most use an equality constraint on +-- time_series and a range query on time. +CREATE UNIQUE INDEX load_profiles_series_time ON load_profiles USING btree (load_profile, quarter_hour); \ No newline at end of file diff --git a/src/test/resources/edu/ie3/datamodel/io/source/csv/_timeseries/lpts_g2.csv b/src/test/resources/edu/ie3/datamodel/io/source/csv/_timeseries/lpts_g2.csv new file mode 100644 index 000000000..b216cb1f6 --- /dev/null +++ b/src/test/resources/edu/ie3/datamodel/io/source/csv/_timeseries/lpts_g2.csv @@ -0,0 +1,3 @@ +SuSa,SuSu,SuWd,TrSa,TrSu,TrWd,WiSa,WiSu,WiWd,quarterHour +63.1,50.6,60.8,73.1,64.2,70.5,80.6,73.7,77.4,0 +58.0,47.4,53.0,67.6,60.7,61.9,74.6,68.7,67.4,1 \ No newline at end of file diff --git a/src/test/resources/edu/ie3/datamodel/io/source/sql/_timeseries/load_profiles.sql b/src/test/resources/edu/ie3/datamodel/io/source/sql/_timeseries/load_profiles.sql new file mode 100644 index 000000000..8402798ae --- /dev/null +++ b/src/test/resources/edu/ie3/datamodel/io/source/sql/_timeseries/load_profiles.sql @@ -0,0 +1,35 @@ +CREATE TABLE public.load_profiles +( + load_profile VARCHAR(11) NOT NULL, + quarter_hour integer NOT NULL, + su_sa DOUBLE PRECISION NOT NULL, + su_su DOUBLE PRECISION NOT NULL, + su_wd DOUBLE PRECISION NOT NULL, + tr_sa DOUBLE PRECISION NOT NULL, + tr_su DOUBLE PRECISION NOT NULL, + tr_wd DOUBLE PRECISION NOT NULL, + wi_sa DOUBLE PRECISION NOT NULL, + wi_su DOUBLE PRECISION NOT NULL, + wi_wd DOUBLE PRECISION NOT NULL +) + WITHOUT OIDS + TABLESPACE pg_default; + +CREATE INDEX load_profiles_series_id ON load_profiles USING hash (load_profile); + +-- Order of columns is important when using btree: https://www.postgresql.org/docs/14/indexes-multicolumn.html +-- Column time_series needs to placed as the first argument since we at most use an equality constraint on +-- time_series and a range query on time. +CREATE UNIQUE INDEX load_profiles_series_time ON load_profiles USING btree (load_profile, quarter_hour); + +INSERT INTO + public.load_profiles (load_profile, quarter_hour, su_sa, su_su, su_wd, tr_sa, tr_su, tr_wd, wi_sa, wi_su, wi_wd) +VALUES + +('g2', 0, 63.1, 50.6, 60.8, 73.1, 64.2, 70.5, 80.6, 73.7, 77.4), +('g2', 1, 58.0, 47.4, 53.0, 67.6, 60.7, 61.9, 74.6, 68.7, 67.4), +('g2', 2, 53.5, 44.3, 46.0, 62.8, 56.9, 54.4, 69.2, 63.6, 58.4), + +('g3', 0, 99.0, 94.6, 98.5, 92.5, 87.6, 91.2, 95.3, 87.8, 94.7), +('g3', 1, 100.0, 95.3, 99.0, 93.0, 87.3, 90.2, 95.5, 88.3, 94.1), +('g3', 2, 100.9, 96.0, 99.4, 93.7, 87.4, 89.4, 96.2, 89.0, 94.1);