diff --git a/CHANGELOG.md b/CHANGELOG.md index a76218f58..8a52e2c94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Enhance `TimeSeriesSource` with method to retrieve all time keys after a given key [#543](https://github.com/ie3-institute/PowerSystemDataModel/issues/543) - Enhance `WeatherSource` with method to retrieve all time keys after a given key [#572](https://github.com/ie3-institute/PowerSystemDataModel/issues/572) +- Adding timeseries for voltage values [#1128](https://github.com/ie3-institute/PowerSystemDataModel/issues/1128) ### Fixed diff --git a/docs/readthedocs/io/csvfiles.md b/docs/readthedocs/io/csvfiles.md index afeb737d8..3e96eb59d 100644 --- a/docs/readthedocs/io/csvfiles.md +++ b/docs/readthedocs/io/csvfiles.md @@ -122,30 +122,34 @@ This is the UUID from the example above `2fcb3e53-b94a-4b96-bea4-c469e499f1a1`. The following keys are supported until now: ```{list-table} :widths: auto + :class: wrapping :header-rows: 1 * - Key - - Information and supported head line + - Information and supported head line. * - c - An energy price (e.g. in €/MWh; c stands for charge). Permissible head line: ``time,price`` * - p - - Active power + - Active power. Permissible head line: ``time,p`` * - pq - - Active and reactive power + - Active and reactive power. Permissible head line: ``time,p,q`` * - h - - Heat power demand + - Heat power demand. Permissible head line: ``time,h`` * - ph - - Active and heat power + - Active and heat power. Permissible head line: ``time,p,h`` * - pqh - - Active, reactive and heat power + - Active, reactive and heat power. Permissible head line: ``time,p,q,h`` + * - v + - Voltage mangnitude in pu and angle in °. + Permissible head line: ``time,vMag,vAng`` * - weather - - Weather information + - Weather information. Permissible head line: ``time,coordinate,direct_irradiation,diffuse_irradiation,temperature,wind_velocity,wind_direction`` ``` diff --git a/docs/readthedocs/models/input/additionaldata/timeseries.md b/docs/readthedocs/models/input/additionaldata/timeseries.md index a082ba2fb..991568b4b 100644 --- a/docs/readthedocs/models/input/additionaldata/timeseries.md +++ b/docs/readthedocs/models/input/additionaldata/timeseries.md @@ -48,7 +48,10 @@ The following different values are available: * - `WindValue` - Combination of wind direction and wind velocity - + + * - `VoltageValue` + - Combination of voltage magnitude in p.u. and angle in ° + * - `WeatherValue` - Combination of irradiance, temperature and wind information diff --git a/src/main/java/edu/ie3/datamodel/exceptions/EntityProcessorException.java b/src/main/java/edu/ie3/datamodel/exceptions/EntityProcessorException.java index 37736b231..749b190d8 100644 --- a/src/main/java/edu/ie3/datamodel/exceptions/EntityProcessorException.java +++ b/src/main/java/edu/ie3/datamodel/exceptions/EntityProcessorException.java @@ -6,7 +6,7 @@ package edu.ie3.datamodel.exceptions; /** - * Is thrown, when an something went wrong during entity field mapping creation in a {@link + * Is thrown, when something went wrong during entity field mapping creation in a {@link * edu.ie3.datamodel.io.processor.EntityProcessor} */ public class EntityProcessorException extends Exception { diff --git a/src/main/java/edu/ie3/datamodel/io/factory/FactoryData.java b/src/main/java/edu/ie3/datamodel/io/factory/FactoryData.java index 6ed7dcb7e..cc0cdd357 100644 --- a/src/main/java/edu/ie3/datamodel/io/factory/FactoryData.java +++ b/src/main/java/edu/ie3/datamodel/io/factory/FactoryData.java @@ -78,6 +78,22 @@ public > ComparableQuantity getQuantity(String field, U return Quantities.getQuantity(getDouble(field), unit); } + /** + * Returns field value for given field name, or empty Optional if field does not exist. + * + * @param field field name + * @param unit unit of Quantity + * @param unit type parameter + * @return field value + */ + public > Optional> getQuantityOptional( + String field, Unit unit) { + return Optional.ofNullable(fieldsToAttributes.get(field)) + .filter(str -> !str.isEmpty()) + .map(Double::parseDouble) + .map(value -> Quantities.getQuantity(value, unit)); + } + /** * Returns int value for given field name. Throws {@link FactoryException} if field does not exist * or parsing fails. diff --git a/src/main/java/edu/ie3/datamodel/io/factory/timeseries/TimeBasedSimpleValueFactory.java b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/TimeBasedSimpleValueFactory.java index ab128d1e6..52b39e2d7 100644 --- a/src/main/java/edu/ie3/datamodel/io/factory/timeseries/TimeBasedSimpleValueFactory.java +++ b/src/main/java/edu/ie3/datamodel/io/factory/timeseries/TimeBasedSimpleValueFactory.java @@ -25,6 +25,10 @@ public class TimeBasedSimpleValueFactory private static final String REACTIVE_POWER = "q"; private static final String HEAT_DEMAND = "heatDemand"; + /* voltage */ + private static final String VMAG = "vMag"; + private static final String VANG = "VAng"; + public TimeBasedSimpleValueFactory(Class valueClasses) { super(valueClasses); } @@ -35,6 +39,7 @@ public TimeBasedSimpleValueFactory( } @Override + @SuppressWarnings("unchecked") protected TimeBasedValue buildModel(SimpleTimeBasedValueData data) { ZonedDateTime time = timeUtil.toZonedDateTime(data.getField(TIME)); V value; @@ -64,6 +69,12 @@ protected TimeBasedValue buildModel(SimpleTimeBasedValueData data) { data.getQuantity(REACTIVE_POWER, REACTIVE_POWER_IN)); } else if (PValue.class.isAssignableFrom(data.getTargetClass())) { value = (V) new PValue(data.getQuantity(ACTIVE_POWER, ACTIVE_POWER_IN)); + } else if (VoltageValue.class.isAssignableFrom(data.getTargetClass())) { + value = + (V) + new VoltageValue( + data.getQuantity(VMAG, VOLTAGE_MAGNITUDE), + data.getQuantityOptional(VANG, VOLTAGE_ANGLE)); } else { throw new FactoryException( "The given factory cannot handle target class '" + data.getTargetClass() + "'."); @@ -88,6 +99,8 @@ protected List> getFields(Class entityClass) { minConstructorParams.addAll(Arrays.asList(ACTIVE_POWER, REACTIVE_POWER)); } else if (PValue.class.isAssignableFrom(entityClass)) { minConstructorParams.add(ACTIVE_POWER); + } else if (VoltageValue.class.isAssignableFrom(entityClass)) { + minConstructorParams.addAll(List.of(VMAG, VANG)); } else { throw new FactoryException( "The given factory cannot handle target class '" + entityClass + "'."); diff --git a/src/main/java/edu/ie3/datamodel/io/naming/timeseries/ColumnScheme.java b/src/main/java/edu/ie3/datamodel/io/naming/timeseries/ColumnScheme.java index 44359e34b..6ea64b458 100644 --- a/src/main/java/edu/ie3/datamodel/io/naming/timeseries/ColumnScheme.java +++ b/src/main/java/edu/ie3/datamodel/io/naming/timeseries/ColumnScheme.java @@ -19,7 +19,8 @@ public enum ColumnScheme { HEAT_DEMAND("h", HeatDemandValue.class), ACTIVE_POWER_AND_HEAT_DEMAND("ph", HeatAndPValue.class), APPARENT_POWER_AND_HEAT_DEMAND("pqh", HeatAndSValue.class), - WEATHER("weather", WeatherValue.class); + WEATHER("weather", WeatherValue.class), + VOLTAGE("v", VoltageValue.class); private final String scheme; private final Class valueClass; @@ -57,6 +58,7 @@ public static Optional parse(Class valueClass if (PValue.class.isAssignableFrom(valueClass)) return Optional.of(ACTIVE_POWER); if (HeatDemandValue.class.isAssignableFrom(valueClass)) return Optional.of(HEAT_DEMAND); if (WeatherValue.class.isAssignableFrom(valueClass)) return Optional.of(WEATHER); + if (VoltageValue.class.isAssignableFrom(valueClass)) return Optional.of(VOLTAGE); return Optional.empty(); } } diff --git a/src/main/java/edu/ie3/datamodel/io/processor/EntityProcessor.java b/src/main/java/edu/ie3/datamodel/io/processor/EntityProcessor.java index d756c00cf..ac7380793 100644 --- a/src/main/java/edu/ie3/datamodel/io/processor/EntityProcessor.java +++ b/src/main/java/edu/ie3/datamodel/io/processor/EntityProcessor.java @@ -55,7 +55,7 @@ protected EntityProcessor(Class registeredClass) throws EntityProce * during processing */ public LinkedHashMap handleEntity(T entity) throws EntityProcessorException { - if (!registeredClass.equals(entity.getClass())) + if (!registeredClass.isAssignableFrom(entity.getClass())) throw new EntityProcessorException( "Cannot process " + entity.getClass().getSimpleName() diff --git a/src/main/java/edu/ie3/datamodel/models/value/VoltageValue.java b/src/main/java/edu/ie3/datamodel/models/value/VoltageValue.java new file mode 100644 index 000000000..d66420756 --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/models/value/VoltageValue.java @@ -0,0 +1,90 @@ +/* + * © 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; + +import static edu.ie3.datamodel.models.StandardUnits.VOLTAGE_ANGLE; +import static edu.ie3.util.quantities.PowerSystemUnits.DEGREE_GEOM; +import static edu.ie3.util.quantities.PowerSystemUnits.PU; +import static java.lang.Math.*; + +import java.util.Objects; +import java.util.Optional; +import javax.measure.quantity.Angle; +import javax.measure.quantity.Dimensionless; +import tech.units.indriya.ComparableQuantity; +import tech.units.indriya.quantity.Quantities; + +/** Describes a voltage value as a pair of magnitude and angle */ +public class VoltageValue implements Value { + + /** Magnitude of the voltage in p.u. */ + private final ComparableQuantity magnitude; + /** Angle of the voltage in degree */ + private final ComparableQuantity angle; + + /** + * @param magnitude of the voltage in p.u. + * @param angleOption option for the angle of this voltage in degree + */ + public VoltageValue( + ComparableQuantity magnitude, + Optional> angleOption) { + this.magnitude = magnitude; + this.angle = angleOption.orElse(Quantities.getQuantity(0.0, VOLTAGE_ANGLE)); + } + + /** + * @param magnitude of the voltage in p.u. + * @param angle of the voltage in degree + */ + public VoltageValue( + ComparableQuantity magnitude, ComparableQuantity angle) { + this.magnitude = magnitude; + this.angle = angle; + } + + public Optional> getMagnitude() { + return Optional.ofNullable(magnitude); + } + + public Optional> getAngle() { + return Optional.ofNullable(angle); + } + + public Optional> getRealPart() { + double mag = magnitude.to(PU).getValue().doubleValue(); + double ang = angle.to(DEGREE_GEOM).getValue().doubleValue(); + + double eInPu = mag * cos(toRadians(ang)); + return Optional.of(Quantities.getQuantity(eInPu, PU)); + } + + public Optional> getImagPart() { + double mag = magnitude.to(PU).getValue().doubleValue(); + double ang = angle.to(DEGREE_GEOM).getValue().doubleValue(); + + double fInPu = mag * sin(toRadians(ang)); + return Optional.of(Quantities.getQuantity(fInPu, PU)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VoltageValue that = (VoltageValue) o; + return Objects.equals(magnitude, that.magnitude) && Objects.equals(angle, that.angle); + } + + @Override + public int hashCode() { + return Objects.hash(magnitude, angle); + } + + @Override + public String toString() { + return "VoltageValue{" + "magnitude=" + magnitude + ", angle=" + angle + '}'; + } +} diff --git a/src/main/java/edu/ie3/datamodel/utils/TimeSeriesUtils.java b/src/main/java/edu/ie3/datamodel/utils/TimeSeriesUtils.java index d6ce28049..16fdcb24f 100644 --- a/src/main/java/edu/ie3/datamodel/utils/TimeSeriesUtils.java +++ b/src/main/java/edu/ie3/datamodel/utils/TimeSeriesUtils.java @@ -25,7 +25,8 @@ public class TimeSeriesUtils { ENERGY_PRICE, APPARENT_POWER_AND_HEAT_DEMAND, ACTIVE_POWER_AND_HEAT_DEMAND, - HEAT_DEMAND); + HEAT_DEMAND, + VOLTAGE); /** Private Constructor as this class is not meant to be instantiated */ private TimeSeriesUtils() { diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesMetaInformationSourceIT.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesMetaInformationSourceIT.groovy index 2fa70a2f6..6623e76ca 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesMetaInformationSourceIT.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesMetaInformationSourceIT.groovy @@ -30,14 +30,15 @@ class CsvTimeSeriesMetaInformationSourceIT extends Specification implements CsvT new CsvIndividualTimeSeriesMetaInformation(UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), ColumnScheme.ACTIVE_POWER, Path.of('its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5')), new CsvIndividualTimeSeriesMetaInformation(UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26"), ColumnScheme.APPARENT_POWER, Path.of('its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26')), new CsvIndividualTimeSeriesMetaInformation(UUID.fromString("46be1e57-e4ed-4ef7-95f1-b2b321cb2047"), ColumnScheme.APPARENT_POWER_AND_HEAT_DEMAND, Path.of('its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047')), - new CsvIndividualTimeSeriesMetaInformation(UUID.fromString("1061af70-1c03-46e1-b960-940b956c429f"), ColumnScheme.APPARENT_POWER, Path.of('its_pq_1061af70-1c03-46e1-b960-940b956c429f')) + new CsvIndividualTimeSeriesMetaInformation(UUID.fromString("1061af70-1c03-46e1-b960-940b956c429f"), ColumnScheme.APPARENT_POWER, Path.of('its_pq_1061af70-1c03-46e1-b960-940b956c429f')), + new CsvIndividualTimeSeriesMetaInformation(UUID.fromString("eeccbe3c-a47e-448e-8eca-1f369d3c24e6"), ColumnScheme.VOLTAGE, Path.of("its_v_eeccbe3c-a47e-448e-8eca-1f369d3c24e6")) ) when: def actual = source.timeSeriesMetaInformation then: - actual.size() == 7 + actual.size() == 8 actual.every { it.key == it.value.uuid && expectedTimeSeries.contains(it.value) @@ -62,6 +63,7 @@ class CsvTimeSeriesMetaInformationSourceIT extends Specification implements CsvT "3fbfaa97-cff4-46d4-95ba-a95665e87c26" || "pq" "46be1e57-e4ed-4ef7-95f1-b2b321cb2047" || "pqh" "1061af70-1c03-46e1-b960-940b956c429f" || "pq" + "eeccbe3c-a47e-448e-8eca-1f369d3c24e6" || "v" } def "The CSV time series meta information source returns an empty optional for an unknown time series UUID"() { diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesSourceTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesSourceTest.groovy index 445ddc403..47d662b9a 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesSourceTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvTimeSeriesSourceTest.groovy @@ -94,5 +94,6 @@ class CsvTimeSeriesSourceTest extends Specification implements CsvTestDataMeta { UUID.fromString("76c9d846-797c-4f07-b7ec-2245f679f5c7") | ColumnScheme.ACTIVE_POWER_AND_HEAT_DEMAND | Path.of("its_ph_76c9d846-797c-4f07-b7ec-2245f679f5c7") || 2 | HeatAndPValue UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") | ColumnScheme.APPARENT_POWER | Path.of("its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26") || 2 | SValue UUID.fromString("46be1e57-e4ed-4ef7-95f1-b2b321cb2047") | ColumnScheme.APPARENT_POWER_AND_HEAT_DEMAND | Path.of("its_pqh_46be1e57-e4ed-4ef7-95f1-b2b321cb2047") || 2 | HeatAndSValue + UUID.fromString("eeccbe3c-a47e-448e-8eca-1f369d3c24e6") | ColumnScheme.VOLTAGE | Path.of("its_v_eeccbe3c-a47e-448e-8eca-1f369d3c24e6") || 2 | VoltageValue } } \ No newline at end of file diff --git a/src/test/groovy/edu/ie3/datamodel/models/value/VoltageValueTest.groovy b/src/test/groovy/edu/ie3/datamodel/models/value/VoltageValueTest.groovy new file mode 100644 index 000000000..471bc8891 --- /dev/null +++ b/src/test/groovy/edu/ie3/datamodel/models/value/VoltageValueTest.groovy @@ -0,0 +1,50 @@ +/* + * © 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 + +import static edu.ie3.util.quantities.PowerSystemUnits.* + +import edu.ie3.util.quantities.QuantityUtil +import spock.lang.Shared +import spock.lang.Specification +import tech.units.indriya.quantity.Quantities + +class VoltageValueTest extends Specification { + + @Shared + double tolerance = 1e-10 + + def "A VoltageValue should return the real part correctly"() { + when: + def actual = value.realPart + + then: + actual.isPresent() + QuantityUtil.isEquivalentAbs(actual.get(), expected, tolerance) + + where: + value | expected + new VoltageValue(Quantities.getQuantity(1, PU), Quantities.getQuantity(0, DEGREE_GEOM)) | Quantities.getQuantity(1, PU) + new VoltageValue(Quantities.getQuantity(1, PU), Quantities.getQuantity(45, DEGREE_GEOM)) | Quantities.getQuantity(0.7071067811865476, PU) + new VoltageValue(Quantities.getQuantity(1, PU), Quantities.getQuantity(90, DEGREE_GEOM)) | Quantities.getQuantity(0, PU) + } + + + def "A VoltageValue should return the imaginary part correctly"() { + when: + def actual = value.imagPart + + then: + actual.isPresent() + QuantityUtil.isEquivalentAbs(actual.get(), expected, tolerance) + + where: + value | expected + new VoltageValue(Quantities.getQuantity(1, PU), Quantities.getQuantity(0, DEGREE_GEOM)) | Quantities.getQuantity(0, PU) + new VoltageValue(Quantities.getQuantity(1, PU), Quantities.getQuantity(45, DEGREE_GEOM)) | Quantities.getQuantity(0.7071067811865475, PU) + new VoltageValue(Quantities.getQuantity(1, PU), Quantities.getQuantity(90, DEGREE_GEOM)) | Quantities.getQuantity(1, PU) + } +} diff --git a/src/test/resources/edu/ie3/datamodel/io/source/csv/_timeseries/its_v_eeccbe3c-a47e-448e-8eca-1f369d3c24e6.csv b/src/test/resources/edu/ie3/datamodel/io/source/csv/_timeseries/its_v_eeccbe3c-a47e-448e-8eca-1f369d3c24e6.csv new file mode 100644 index 000000000..1d776f689 --- /dev/null +++ b/src/test/resources/edu/ie3/datamodel/io/source/csv/_timeseries/its_v_eeccbe3c-a47e-448e-8eca-1f369d3c24e6.csv @@ -0,0 +1,3 @@ +"time";"vMag";"vAng" +2020-01-01T00:00:00Z;1.0;45.0 +2020-01-01T00:15:00Z;0.9; \ No newline at end of file