diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1f6b57c1..8f0ff2d112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Re-organizing test resources into their respective packages [#105](https://github.com/ie3-institute/simona/issues/105) - BREAKING: Using snapshot version of PSDM - Simplified PrimaryServiceProxy due to changes in PSDM [#120](https://github.com/ie3-institute/simona/issues/120) +- Improved handling of weights and their sum in determination of weather data [#173](https://github.com/ie3-institute/simona/issues/173) ### Fixed - Location of `vn_simona` test grid (was partially in Berlin and Dortmund) - Let `ParticipantAgent` die after failed registration with secondary services (prevents stuck simulation) +- Fix default resolution of weather source wrapper [#78](https://github.com/ie3-institute/simona/issues/78) [Unreleased]: https://github.com/ie3-institute/simona/compare/a14a093239f58fca9b2b974712686b33e5e5f939...HEAD diff --git a/docs/uml/main/ParticipantModelling.puml b/docs/uml/main/ParticipantModelling.puml index 2d1d1ec79c..ec44203170 100644 --- a/docs/uml/main/ParticipantModelling.puml +++ b/docs/uml/main/ParticipantModelling.puml @@ -41,9 +41,9 @@ package edu.ie3.edu.ie3.simona { } DateTime --|> SecondaryData - Class Weather{ - + diffRad: Quantity[Irradiation] - + dirRad: Quantity[Irradiation] + Class WeatherData{ + + diffIrr: Quantity[Irradiation] + + dirIrr: Quantity[Irradiation] + temp: Quantity[Temperature] + windVel: Quantity[Speed] } diff --git a/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala b/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala index 16547f8693..f42d8aa8c9 100644 --- a/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala +++ b/src/main/scala/edu/ie3/simona/agent/participant/pv/PVAgentFundamentals.scala @@ -230,8 +230,8 @@ protected trait PVAgentFundamentals PVRelevantData( dateTime, tickInterval, - weatherData.diffRad, - weatherData.dirRad + weatherData.diffIrr, + weatherData.dirIrr ) val power = pvModel.calculatePower( diff --git a/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala b/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala index 140def4563..27a9e88e7f 100644 --- a/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala +++ b/src/main/scala/edu/ie3/simona/ontology/messages/services/WeatherMessage.scala @@ -56,11 +56,21 @@ object WeatherMessage { ) extends WeatherMessage with ProvisionMessage[WeatherData] - /** Hold entire weather result together + /** Container class for the entirety of weather information at a certain point + * in time and at a certain coordinate + * + * @param diffIrr + * Diffuse irradiance on the horizontal pane + * @param dirIrr + * Direct irradiance on the horizontal pane + * @param temp + * Temperature + * @param windVel + * Wind velocity */ final case class WeatherData( - diffRad: ComparableQuantity[Irradiance], - dirRad: ComparableQuantity[Irradiance], + diffIrr: ComparableQuantity[Irradiance], + dirIrr: ComparableQuantity[Irradiance], temp: ComparableQuantity[Temperature], windVel: ComparableQuantity[Speed] ) extends SecondaryData diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala index 9e8b7c7065..488ecd4e5e 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala @@ -164,7 +164,8 @@ trait WeatherSource { } } - /** Determine the weights of each coordinate + /** Determine the weights of each coordinate. It is ensured, that the entirety + * of weights sum up to 1.0 * * @param nearestCoordinates * Collection of nearest coordinates with their distances @@ -522,9 +523,9 @@ object WeatherSource { ): WeatherData = { WeatherData( weatherValue.getSolarIrradiance.getDiffuseIrradiance - .orElse(EMPTY_WEATHER_DATA.diffRad), + .orElse(EMPTY_WEATHER_DATA.diffIrr), weatherValue.getSolarIrradiance.getDirectIrradiance - .orElse(EMPTY_WEATHER_DATA.dirRad), + .orElse(EMPTY_WEATHER_DATA.dirIrr), weatherValue.getTemperature.getTemperature .orElse(EMPTY_WEATHER_DATA.temp), weatherValue.getWind.getVelocity.orElse(EMPTY_WEATHER_DATA.windVel) diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index e1fba71f73..2d61d4c1c5 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -13,8 +13,8 @@ import edu.ie3.datamodel.io.connectors.{ SqlConnector } import edu.ie3.datamodel.io.factory.timeseries.{ - IconTimeBasedWeatherValueFactory, - CosmoTimeBasedWeatherValueFactory + CosmoTimeBasedWeatherValueFactory, + IconTimeBasedWeatherValueFactory } import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.source.couchbase.CouchbaseWeatherSource @@ -45,7 +45,9 @@ import edu.ie3.simona.util.TickUtil import edu.ie3.simona.util.TickUtil.TickLong import edu.ie3.util.exceptions.EmptyQuantityException import edu.ie3.util.interval.ClosedInterval +import edu.ie3.util.scala.DoubleUtils.ImplicitDouble import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units import java.time.ZonedDateTime import javax.measure.Quantity @@ -140,21 +142,21 @@ private[weather] final case class WeatherSourceWrapper private ( ) /* Determine actual weights and contributions */ - val (diffRadContrib, diffRadWeight) = currentWeather.diffRad match { - case EMPTY_WEATHER_DATA.diffRad => (EMPTY_WEATHER_DATA.diffRad, 0d) - case nonEmptyDiffRad => + val (diffIrrContrib, diffIrrWeight) = currentWeather.diffIrr match { + case EMPTY_WEATHER_DATA.diffIrr => (EMPTY_WEATHER_DATA.diffIrr, 0d) + case nonEmptyDiffIrr => calculateContrib( - nonEmptyDiffRad, + nonEmptyDiffIrr, weight, StandardUnits.SOLAR_IRRADIANCE, s"Diffuse solar irradiance not available at $point." ) } - val (dirRadContrib, dirRadWeight) = currentWeather.dirRad match { - case EMPTY_WEATHER_DATA.dirRad => (EMPTY_WEATHER_DATA.dirRad, 0d) - case nonEmptyDirRad => + val (dirIrrContrib, dirIrrWeight) = currentWeather.dirIrr match { + case EMPTY_WEATHER_DATA.`dirIrr` => (EMPTY_WEATHER_DATA.dirIrr, 0d) + case nonEmptyDirIrr => calculateContrib( - nonEmptyDirRad, + nonEmptyDirIrr, weight, StandardUnits.SOLAR_IRRADIANCE, s"Direct solar irradiance not available at $point." @@ -164,7 +166,7 @@ private[weather] final case class WeatherSourceWrapper private ( case EMPTY_WEATHER_DATA.temp => (EMPTY_WEATHER_DATA.temp, 0d) case nonEmptyTemp => calculateContrib( - nonEmptyTemp, + nonEmptyTemp.to(Units.KELVIN), weight, StandardUnits.TEMPERATURE, s"Temperature not available at $point." @@ -184,29 +186,22 @@ private[weather] final case class WeatherSourceWrapper private ( /* Sum up weight and contributions */ ( WeatherData( - averagedWeather.diffRad.add(diffRadContrib), - averagedWeather.dirRad.add(dirRadContrib), + averagedWeather.diffIrr.add(diffIrrContrib), + averagedWeather.dirIrr.add(dirIrrContrib), averagedWeather.temp.add(tempContrib), averagedWeather.windVel.add(windVelContrib) ), currentWeightSum.add( - diffRadWeight, - dirRadWeight, + diffIrrWeight, + dirIrrWeight, tempWeight, windVelWeight ) ) } match { case (weatherData: WeatherData, weightSum: WeightSum) => - /* Divide by weight sum to correctly account for missing data. Change temperature scale back to absolute*/ - WeatherData( - weatherData.diffRad.divide(weightSum.diffRad), - weatherData.dirRad.divide(weightSum.dirRad), - weatherData.temp.divide(weightSum.temp), - weatherData.windVel.divide(weightSum.windVel) - ) + weightSum.scale(weatherData) } - } /** Determine an Array with all ticks between the request frame's start and @@ -227,7 +222,7 @@ private[weather] final case class WeatherSourceWrapper private ( } private[weather] object WeatherSourceWrapper extends LazyLogging { - private val DEFAULT_RESOLUTION = 360L + private val DEFAULT_RESOLUTION = 3600L def apply( csvSep: String, @@ -363,26 +358,64 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { ) } + /** Simple container class to allow for accumulating determination of the sum + * of weights for different weather properties for different locations + * surrounding a given coordinate of interest + * + * @param diffIrr + * Sum of weight for diffuse irradiance + * @param dirIrr + * Sum of weight for direct irradiance + * @param temp + * Sum of weight for temperature + * @param windVel + * Sum of weight for wind velocity + */ final case class WeightSum( - diffRad: Double, - dirRad: Double, + diffIrr: Double, + dirIrr: Double, temp: Double, windVel: Double ) { def add( - diffRad: Double, - dirRad: Double, + diffIrr: Double, + dirIrr: Double, temp: Double, windVel: Double ): WeightSum = WeightSum( - this.diffRad + diffRad, - this.dirRad + dirRad, + this.diffIrr + diffIrr, + this.dirIrr + dirIrr, this.temp + temp, this.windVel + windVel ) + + /** Scale the given [[WeatherData]] by dividing by the sum of weights per + * attribute of the weather data. If one of the weight sums is empty (and + * thus a division by zero would happen) the defined "empty" information + * for this attribute is returned. + * + * @param weatherData + * Weighted and accumulated weather information + * @return + * Weighted weather information, which are divided by the sum of weights + */ + def scale(weatherData: WeatherData): WeatherData = weatherData match { + case WeatherData(diffIrr, dirIrr, temp, windVel) => + implicit val precision: Double = 1e-3 + WeatherData( + if (this.diffIrr !~= 0d) diffIrr.divide(this.diffIrr) + else EMPTY_WEATHER_DATA.diffIrr, + if (this.dirIrr !~= 0d) dirIrr.divide(this.dirIrr) + else EMPTY_WEATHER_DATA.dirIrr, + if (this.temp !~= 0d) temp.divide(this.temp) + else EMPTY_WEATHER_DATA.temp, + if (this.windVel !~= 0d) windVel.divide(this.windVel) + else EMPTY_WEATHER_DATA.windVel + ) + } } - case object WeightSum { + object WeightSum { val EMPTY_WEIGHT_SUM: WeightSum = WeightSum(0d, 0d, 0d, 0d) } diff --git a/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala b/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala new file mode 100644 index 0000000000..67bc7cf7cf --- /dev/null +++ b/src/main/scala/edu/ie3/util/scala/DoubleUtils.scala @@ -0,0 +1,17 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.util.scala + +@deprecated("Use implementation in power system utils package") +object DoubleUtils { + implicit class ImplicitDouble(d: Double) { + def ~=(other: Double)(implicit precision: Double): Boolean = + (d - other).abs <= precision + def !~=(other: Double)(implicit precision: Double): Boolean = + (d - other).abs > precision + } +} diff --git a/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy b/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy index d0dbdc8048..2dd49af64d 100644 --- a/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy +++ b/src/test/groovy/edu/ie3/simona/model/participant/PVModelIT.groovy @@ -88,7 +88,7 @@ class PVModelIT extends Specification implements PVModelITHelper { "build the needed data" WeatherMessage.WeatherData weather = modelToWeatherMap.get(modelId) - PVModel.PVRelevantData neededData = new PVModel.PVRelevantData(dateTime,3600L, weather.diffRad() as ComparableQuantity, weather.dirRad() as ComparableQuantity) + PVModel.PVRelevantData neededData = new PVModel.PVRelevantData(dateTime,3600L, weather.diffIrr() as ComparableQuantity, weather.dirIrr() as ComparableQuantity) ComparableQuantity voltage = getQuantity(1.414213562, PU) "collect the results and calculate the difference between the provided results and the calculated ones" diff --git a/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala b/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala index cc54e60372..39edbf6921 100644 --- a/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala +++ b/src/test/scala/edu/ie3/simona/agent/participant/PVAgentModelCalculationSpec.scala @@ -585,8 +585,8 @@ class PVAgentModelCalculationSpec 0L -> PVRelevantData( 0L.toDateTime, 3600L, - weatherData.diffRad, - weatherData.dirRad + weatherData.diffIrr, + weatherData.dirIrr ) ) } @@ -737,8 +737,8 @@ class PVAgentModelCalculationSpec 0L -> PVRelevantData( 0L.toDateTime, 3600L, - weatherData.diffRad, - weatherData.dirRad + weatherData.diffIrr, + weatherData.dirIrr ) ) } diff --git a/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala index 0b84a7bda7..ef9987a3fd 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/SampleWeatherSourceSpec.scala @@ -83,16 +83,16 @@ class SampleWeatherSourceSpec val actual = source invokePrivate getWeatherPrivate(tick) /* Units meet expectation */ - actual.diffRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE - actual.dirRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + actual.diffIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + actual.dirIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE actual.temp.getUnit shouldBe StandardUnits.TEMPERATURE actual.windVel.getUnit shouldBe StandardUnits.WIND_VELOCITY /* Values meet expectations */ - actual.diffRad should equalWithTolerance( + actual.diffIrr should equalWithTolerance( Quantities.getQuantity(72.7656, StandardUnits.SOLAR_IRRADIANCE) ) - actual.dirRad should equalWithTolerance( + actual.dirIrr should equalWithTolerance( Quantities.getQuantity(80.1172, StandardUnits.SOLAR_IRRADIANCE) ) actual.windVel should equalWithTolerance( @@ -108,14 +108,14 @@ class SampleWeatherSourceSpec WeightedCoordinates(Map(NodeInput.DEFAULT_GEO_POSITION -> 1d)) source.getWeather(tick, weightedCoordinates) match { - case WeatherData(diffRad, dirRad, temp, windVel) => - diffRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE - diffRad should equalWithTolerance( + case WeatherData(diffIrr, dirIrr, temp, windVel) => + diffIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + diffIrr should equalWithTolerance( Quantities.getQuantity(72.7656, StandardUnits.SOLAR_IRRADIANCE) ) - dirRad.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE - dirRad should equalWithTolerance( + dirIrr.getUnit shouldBe StandardUnits.SOLAR_IRRADIANCE + dirIrr should equalWithTolerance( Quantities.getQuantity(80.1172, StandardUnits.SOLAR_IRRADIANCE) ) diff --git a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala index e619deb026..8b0502c6cf 100644 --- a/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/weather/WeatherSourceWrapperSpec.scala @@ -16,14 +16,20 @@ import edu.ie3.datamodel.models.timeseries.individual.{ TimeBasedValue } import edu.ie3.datamodel.models.value.WeatherValue -import edu.ie3.simona.service.weather.WeatherSource.WeightedCoordinates +import edu.ie3.simona.ontology.messages.services.WeatherMessage.WeatherData +import edu.ie3.simona.service.weather.WeatherSource.{ + EMPTY_WEATHER_DATA, + WeightedCoordinates +} import edu.ie3.simona.service.weather.WeatherSourceSpec.DummyIdCoordinateSource +import edu.ie3.simona.service.weather.WeatherSourceWrapper.WeightSum import edu.ie3.simona.service.weather.WeatherSourceWrapperSpec._ import edu.ie3.simona.test.common.UnitSpec import edu.ie3.util.geo.GeoUtils import edu.ie3.util.interval.ClosedInterval import org.locationtech.jts.geom.Point import tech.units.indriya.quantity.Quantities +import tech.units.indriya.unit.Units import java.time.{ZoneId, ZonedDateTime} import java.util @@ -60,10 +66,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { ) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) val sumOfAll = 1 + 1 + 1 + 13 - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -85,10 +91,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { ) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) val sumOfAll = 1 + 1 + 1 + 13 - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 4, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -110,10 +116,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { ) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) val sumOfAll = 1 + 1 + 1 - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 3, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(sumOfAll / 3, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -127,10 +133,10 @@ class WeatherSourceWrapperSpec extends UnitSpec { "calculate the correct weighted value for 1 coordinate with a weight of 1" in { val weightedCoordinates = WeightedCoordinates(Map(coordinate13 -> 1d)) val result = source.getWeather(date.toEpochSecond, weightedCoordinates) - result.dirRad should equalWithTolerance( + result.dirIrr should equalWithTolerance( Quantities.getQuantity(13, StandardUnits.SOLAR_IRRADIANCE) ) - result.diffRad should equalWithTolerance( + result.diffIrr should equalWithTolerance( Quantities.getQuantity(13, StandardUnits.SOLAR_IRRADIANCE) ) result.temp should equalWithTolerance( @@ -147,9 +153,142 @@ class WeatherSourceWrapperSpec extends UnitSpec { result.temp.getScale shouldBe Scale.ABSOLUTE } } + + "Handling the weighted weather" when { + "adding to the weight sum" should { + "produce correct results" in { + val weightSum = WeightSum(0.1d, 0.2d, 0.3d, 0.4d) + val weightSumAdded = weightSum.add(0.2d, 0.3d, 0.4d, 0.5d) + + weightSumAdded.diffIrr should ===(0.3d +- 1e-10) + weightSumAdded.dirIrr should ===(0.5d +- 1e-10) + weightSumAdded.temp should ===(0.7d +- 1e-10) + weightSumAdded.windVel should ===(0.9d +- 1e-10) + } + } + + "scaling the weighted attributes with the sum of weights" should { + "calculate proper information on proper input" in { + val weatherSeq = Seq( + (0.5, 0.75, 291d, 10d), + (12.3, 1.2, 293d, 12d), + (25.0, 5.7, 290d, 9d), + (26.3, 1.7, 289d, 11d) + ) + val weights = Seq( + (0.1, 0.2, 0.3, 0.4), + (0.25, 0.2, 0.25, 0.1), + (0.3, 0.4, 0.15, 0.05), + (0.35, 0.2, 0.3, 0.45) + ) + + val (weightedWeather, weightSum) = + prepareWeightTestData(weatherSeq, weights) + + weightSum.scale(weightedWeather) match { + case WeatherData(diffIrr, dirIrr, temp, windVel) => + diffIrr should equalWithTolerance( + Quantities.getQuantity(19.83, StandardUnits.SOLAR_IRRADIANCE), + 1e-6 + ) + dirIrr should equalWithTolerance( + Quantities.getQuantity(3.01, StandardUnits.SOLAR_IRRADIANCE), + 1e-6 + ) + temp should equalWithTolerance( + Quantities + .getQuantity(290.75, Units.KELVIN) + .to(StandardUnits.TEMPERATURE), + 1e-6 + ) + windVel should equalWithTolerance( + Quantities.getQuantity(10.6, StandardUnits.WIND_VELOCITY), + 1e-6 + ) + } + } + } + + "calculate proper input, if data is missing in one coordinate" in { + val weatherSeq = Seq( + (0.5, 0.75, 291d, 10d), + (12.3, 1.2, 293d, 12d), + (25.0, 5.7, 290d, 9d), + (26.3, 1.7, 289d, 11d) + ) + val weights = Seq( + (0.1, 0.2, 0d, 0.4), + (0.25, 0.2, 0d, 0.1), + (0.3, 0.4, 0d, 0.05), + (0.35, 0.2, 0d, 0.45) + ) + + val (weightedWeather, weightSum) = + prepareWeightTestData(weatherSeq, weights) + + weightSum.scale(weightedWeather) match { + case WeatherData(_, _, temp, _) => + temp shouldBe EMPTY_WEATHER_DATA.temp + } + } + + "return empty value for an attribute, if weight sum is zero" in { + val weatherSeq = Seq( + (0.5, 0.75, 291d, 10d), + (12.3, 1.2, 0d, 12d), + (25.0, 5.7, 290d, 9d), + (26.3, 1.7, 289d, 11d) + ) + val weights = Seq( + (0.1, 0.2, 0.3, 0.4), + (0.25, 0.2, 0d, 0.1), + (0.3, 0.4, 0.15, 0.05), + (0.35, 0.2, 0.3, 0.45) + ) + + val (weightedWeather, weightSum) = + prepareWeightTestData(weatherSeq, weights) + + weightSum.scale(weightedWeather) match { + case WeatherData(_, _, temp, _) => + temp should equalWithTolerance( + Quantities + .getQuantity(290d, Units.KELVIN) + .to(StandardUnits.TEMPERATURE) + ) + } + } + + "correctly calculate scaled properties if provided with varying weight components" in { + val weatherData = WeatherData( + Quantities.getQuantity(1.0, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(1.0, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(1.0, Units.KELVIN), + Quantities.getQuantity(1.0, StandardUnits.WIND_VELOCITY) + ) + val weightSum = WeightSum(0.25, 0.5, 0.8, 1.0) + + weightSum.scale(weatherData) match { + case WeatherData(diffIrr, dirIrr, temp, windVel) => + diffIrr should equalWithTolerance( + Quantities.getQuantity(4.0, StandardUnits.SOLAR_IRRADIANCE) + ) + dirIrr should equalWithTolerance( + Quantities.getQuantity(2.0, StandardUnits.SOLAR_IRRADIANCE) + ) + temp should equalWithTolerance( + Quantities + .getQuantity(1.25, Units.KELVIN) + ) + windVel should equalWithTolerance( + Quantities.getQuantity(1.0, StandardUnits.WIND_VELOCITY) + ) + } + } + } } -case object WeatherSourceWrapperSpec { +object WeatherSourceWrapperSpec { // lat/lon are irrelevant, we will manually create weights later on private val coordinate1a = GeoUtils.xyToPoint(6, 51) private val coordinate1b = GeoUtils.xyToPoint(7, 51) @@ -271,4 +410,56 @@ case object WeatherSourceWrapperSpec { } } + /** Prepare test data for WeightSum-related tests + * + * @param weatherSeq + * sequence of raw weather data + * @param weights + * the weights to use for averaging the weather data, with rows equivalent + * to the rows in weatherSeq + * @return + * A tuple of 1. the weighted average weather data and 2. the weight sum + */ + private def prepareWeightTestData( + weatherSeq: Seq[(Double, Double, Double, Double)], + weights: Seq[(Double, Double, Double, Double)] + ): (WeatherData, WeightSum) = { + val weatherData = weatherSeq.map { case (diff, dir, temp, wVel) => + WeatherData( + Quantities.getQuantity(diff, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(dir, StandardUnits.SOLAR_IRRADIANCE), + Quantities.getQuantity(temp, Units.KELVIN), + Quantities.getQuantity(wVel, StandardUnits.WIND_VELOCITY) + ) + } + + val weightedWeather = + weatherData.zip(weights).foldLeft(EMPTY_WEATHER_DATA) { + case ( + currentSum, + ( + WeatherData(diffIrr, dirIrr, temp, windVel), + (diffWeight, dirWeight, tempWeight, wVelWeight) + ) + ) => + currentSum.copy( + diffIrr = currentSum.diffIrr.add(diffIrr.multiply(diffWeight)), + dirIrr = currentSum.dirIrr.add(dirIrr.multiply(dirWeight)), + temp = currentSum.temp.add(temp.multiply(tempWeight)), + windVel = currentSum.windVel.add(windVel.multiply(wVelWeight)) + ) + } + val weightSum = weights.foldLeft(WeightSum.EMPTY_WEIGHT_SUM) { + case (currentSum, currentWeight) => + currentSum.add( + currentWeight._1, + currentWeight._2, + currentWeight._3, + currentWeight._4 + ) + } + + (weightedWeather, weightSum) + } + }