Skip to content

Commit f6b11f4

Browse files
authored
Merge pull request #1514 from ie3-institute/sp/#1511-weather-source-timeseries
Implementing time series retrieval for `WeatherSource`
2 parents cfebac9 + 879e77e commit f6b11f4

File tree

6 files changed

+142
-129
lines changed

6 files changed

+142
-129
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Updated Authors.md file [#1503](https://github.com/ie3-institute/simona/issues/1503)
1616
- Quantity to squants conversion in WeatherService [#1506](https://github.com/ie3-institute/simona/issues/1506)
1717
- Considering primary data that start before simulation [#1034](https://github.com/ie3-institute/simona/issues/1034)
18+
- Implement time series retrieval for WeatherSource [#1511](https://github.com/ie3-institute/simona/issues/1511)
1819

1920
### Changed
2021
- Upgraded `scala2` to `scala3` [#53](https://github.com/ie3-institute/simona/issues/53)

src/main/scala/edu/ie3/simona/service/weather/SampleWeatherSource.scala

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package edu.ie3.simona.service.weather
99
import edu.ie3.datamodel.io.source.IdCoordinateSource
1010
import edu.ie3.datamodel.models.input.NodeInput
1111
import edu.ie3.simona.service.Data.SecondaryData.WeatherData
12+
import edu.ie3.simona.service.weather.WeatherSource.WeightedCoordinates
1213
import edu.ie3.simona.util.TickUtil
1314
import edu.ie3.simona.util.TickUtil.*
1415
import edu.ie3.util.geo.CoordinateDistance
@@ -26,6 +27,7 @@ import java.time.temporal.ChronoField.{HOUR_OF_DAY, MONTH_OF_YEAR, YEAR}
2627
import java.util
2728
import java.util.{Collections, Optional}
2829
import javax.measure.quantity.Length
30+
import scala.collection.SortedMap
2931
import scala.jdk.CollectionConverters.*
3032

3133
final class SampleWeatherSource(
@@ -38,20 +40,17 @@ final class SampleWeatherSource(
3840
override val maxCoordinateDistance: ComparableQuantity[Length] =
3941
Quantities.getQuantity(50000d, Units.METRE)
4042

41-
/** Get the weather data for the given tick as a weighted average taking into
42-
* account the given weighting of weather coordinates.
43-
*
44-
* @param tick
45-
* Simulation date in question
46-
* @param weightedCoordinates
47-
* The coordinate in question
48-
* @return
49-
* Matching weather data
50-
*/
5143
override def getWeather(
52-
tick: Long,
53-
weightedCoordinates: WeatherSource.WeightedCoordinates,
54-
): WeatherData = getWeather(tick)
44+
startTick: Long,
45+
endTick: Long,
46+
weightedCoordinates: WeightedCoordinates,
47+
): SortedMap[ZonedDateTime, WeatherData] =
48+
Range.Long
49+
.inclusive(startTick, endTick, resolution)
50+
.map { tick =>
51+
tick.toDateTime -> getWeather(tick)
52+
}
53+
.to(SortedMap)
5554

5655
/** Get the weather data for the given tick and coordinate. Here, the weather
5756
* data is taken repeatedly from a store The coordinate is not considered at

src/main/scala/edu/ie3/simona/service/weather/WeatherSource.scala

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import tech.units.indriya.unit.Units
4646
import java.nio.file.Paths
4747
import java.time.ZonedDateTime
4848
import javax.measure.quantity.{Dimensionless, Length}
49+
import scala.collection.SortedMap
4950
import scala.jdk.CollectionConverters.*
5051
import scala.jdk.OptionConverters.RichOptional
5152
import scala.util.{Failure, Success, Try}
@@ -197,11 +198,29 @@ trait WeatherSource {
197198
}
198199
}
199200

201+
/** Get the weather data between (and including) given ticks as a weighted
202+
* average taking into account the given weighting of weather coordinates.
203+
*
204+
* @param startTick
205+
* The first tick to retrieve weather for
206+
* @param endTick
207+
* The last tick to retrieve weather for
208+
* @param weightedCoordinates
209+
* The coordinate in question
210+
* @return
211+
* Matching weather data
212+
*/
213+
def getWeather(
214+
startTick: Long,
215+
endTick: Long,
216+
weightedCoordinates: WeightedCoordinates,
217+
): SortedMap[ZonedDateTime, WeatherData]
218+
200219
/** Get the weather data for the given tick as a weighted average taking into
201220
* account the given weighting of weather coordinates.
202221
*
203222
* @param tick
204-
* Simulation date in question
223+
* Simulation tick in question
205224
* @param weightedCoordinates
206225
* The coordinate in question
207226
* @return
@@ -210,7 +229,13 @@ trait WeatherSource {
210229
def getWeather(
211230
tick: Long,
212231
weightedCoordinates: WeightedCoordinates,
213-
): WeatherData
232+
): WeatherData = {
233+
getWeather(tick, tick, weightedCoordinates).values.headOption.getOrElse(
234+
throw new SourceException(
235+
s"No weather data received for tick $tick."
236+
)
237+
)
238+
}
214239

215240
/** Get the weather data for the given tick and agent coordinates having a
216241
* weighted average of weather values.

src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala

Lines changed: 45 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,20 @@ import edu.ie3.simona.service.weather.WeatherSource as SimonaWeatherSource
4444
import edu.ie3.simona.util.TickUtil.{RichZonedDateTime, TickLong}
4545
import edu.ie3.util.DoubleUtils.!~=
4646
import edu.ie3.util.interval.ClosedInterval
47+
import org.locationtech.jts.geom.Point
4748
import squants.thermal.Kelvin
4849
import tech.units.indriya.ComparableQuantity
4950

5051
import java.nio.file.Paths
5152
import java.time.ZonedDateTime
5253
import java.time.format.DateTimeFormatter
5354
import javax.measure.quantity.Length
55+
import scala.collection.SortedMap
5456
import scala.jdk.CollectionConverters.{
5557
CollectionHasAsScala,
5658
IterableHasAsJava,
5759
MapHasAsScala,
5860
}
59-
import scala.jdk.OptionConverters.RichOptional
6061
import scala.util.{Failure, Success, Try}
6162

6263
/** This class provides an implementation of the SIMONA trait
@@ -83,58 +84,58 @@ private[weather] final case class WeatherSourceWrapper private (
8384
) extends SimonaWeatherSource
8485
with LazyLogging {
8586

86-
/** Get the weather data for the given tick as a weighted average taking into
87-
* account the given weighting of weather coordinates.
88-
*
89-
* @param tick
90-
* Simulation date in question
91-
* @param weightedCoordinates
92-
* The coordinate in question
93-
* @return
94-
* Matching weather data
95-
*/
9687
override def getWeather(
97-
tick: Long,
88+
startTick: Long,
89+
endTick: Long,
9890
weightedCoordinates: WeatherSource.WeightedCoordinates,
99-
): WeatherData = {
100-
val dateTime = tick.toDateTime
101-
val interval = new ClosedInterval(dateTime, dateTime)
91+
): SortedMap[ZonedDateTime, WeatherData] = {
92+
val interval = new ClosedInterval(startTick.toDateTime, endTick.toDateTime)
10293
val coordinates = weightedCoordinates.weighting.keys.toList.asJavaCollection
103-
val results = source
94+
95+
source
10496
.getWeather(
10597
interval,
10698
coordinates,
10799
)
108100
.asScala
109-
.toMap
110-
val weatherDataMap = results.flatMap { case (point, timeSeries) =>
111-
// change temperature scale for the upcoming calculations
112-
timeSeries
113-
.getValue(dateTime)
114-
.toScala
115-
.map(weatherValue => point -> toWeatherData(weatherValue))
116-
}
117-
118-
weatherDataMap.foldLeft((EMPTY_WEATHER_DATA, WeightSum.EMPTY_WEIGHT_SUM)) {
119-
case ((averagedWeather, currentWeightSum), (point, currentWeather)) =>
120-
/** Calculate the contribution of a single coordinate value to the
121-
* averaged weather information. If we got an empty quantity (which can
122-
* be the case, as this particular value might be missing in the
123-
* weather data), we do let it out and also return the "effective"
124-
* weight of 0d.
125-
*/
126-
127-
/* Get pre-calculated weight for this coordinate */
101+
.map { case (coordinate, timeSeries) =>
102+
timeSeries.getEntries.asScala.map { weatherValue =>
103+
(weatherValue.getTime, coordinate) -> weatherValue.getValue
104+
}.toMap
105+
}
106+
.flatten
107+
.groupMap { case ((time, _), _) =>
108+
time
109+
} { case ((_, location), weatherValue) =>
128110
val weight = weightedCoordinates.weighting.getOrElse(
129-
point, {
130-
logger.warn(s"Received an unexpected point: $point")
111+
location, {
112+
logger.warn(s"Received an unexpected point: $location")
131113
0d
132114
},
133115
)
134-
/* Sum up weight and contributions */
116+
val weatherData = toWeatherData(weatherValue)
117+
(location, weatherData, weight)
118+
}
119+
.map { case (time, weather) =>
120+
time -> spatialDataInterpolation(weather)
121+
}
122+
.to(SortedMap)
123+
}
124+
125+
private def spatialDataInterpolation(
126+
weatherData: Iterable[(Point, WeatherData, Double)]
127+
): WeatherData =
128+
weatherData.foldLeft((EMPTY_WEATHER_DATA, WeightSum.EMPTY_WEIGHT_SUM)) {
129+
case ((averagedWeather, weightSum), (point, weather, weight)) =>
130+
/* Calculate the contribution of a single coordinate value to the
131+
* averaged weather information. If we got an empty quantity (which can
132+
* be the case, as this particular value might be missing in the
133+
* weather data), we do let it out and also return the "effective"
134+
* weight of 0d.
135+
*/
135136

136137
/* Determine actual weights and contributions */
137-
val (diffIrradiance, diffIrrWeight) = currentWeather.diffIrr match {
138+
val (diffIrradiance, diffIrrWeight) = weather.diffIrr match {
138139
case EMPTY_WEATHER_DATA.diffIrr =>
139140
// Some data sets do not provide diffuse irradiance, so we do not
140141
// warn here
@@ -144,15 +145,15 @@ private[weather] final case class WeatherSourceWrapper private (
144145
(averagedWeather.diffIrr + nonEmptyDiffIrr * weight, weight)
145146
}
146147

147-
val (dirIrradience, dirIrrWeight) = currentWeather.dirIrr match {
148+
val (dirIrradiance, dirIrrWeight) = weather.dirIrr match {
148149
case EMPTY_WEATHER_DATA.`dirIrr` =>
149150
logger.warn(s"Direct solar irradiance not available at $point.")
150151
(averagedWeather.dirIrr, 0d)
151152
case nonEmptyDirIrr =>
152153
(averagedWeather.dirIrr + nonEmptyDirIrr * weight, weight)
153154
}
154155

155-
val (temperature, tempWeight) = currentWeather.temp match {
156+
val (temperature, tempWeight) = weather.temp match {
156157
case EMPTY_WEATHER_DATA.temp =>
157158
logger.warn(s"Temperature not available at $point.")
158159
(averagedWeather.temp, 0d)
@@ -162,7 +163,7 @@ private[weather] final case class WeatherSourceWrapper private (
162163
(averagedWeather.temp + nonEmptyTemp.in(Kelvin) * weight, weight)
163164
}
164165

165-
val (windVelocity, windVelWeight) = currentWeather.windVel match {
166+
val (windVelocity, windVelWeight) = weather.windVel match {
166167
case EMPTY_WEATHER_DATA.windVel =>
167168
logger.warn(s"Wind velocity not available at $point.")
168169
(averagedWeather.windVel, 0d)
@@ -171,8 +172,8 @@ private[weather] final case class WeatherSourceWrapper private (
171172
}
172173

173174
(
174-
WeatherData(diffIrradiance, dirIrradience, temperature, windVelocity),
175-
currentWeightSum.add(
175+
WeatherData(diffIrradiance, dirIrradiance, temperature, windVelocity),
176+
weightSum.add(
176177
diffIrrWeight,
177178
dirIrrWeight,
178179
tempWeight,
@@ -183,18 +184,7 @@ private[weather] final case class WeatherSourceWrapper private (
183184
case (weatherData: WeatherData, weightSum: WeightSum) =>
184185
weightSum.scale(weatherData)
185186
}
186-
}
187187

188-
/** Determine an Array with all ticks between the request frame's start and
189-
* end on which new data is available
190-
*
191-
* @param requestFrameStart
192-
* Beginning of the announced request frame
193-
* @param requestFrameEnd
194-
* End of the announced request frame
195-
* @return
196-
* Array with data ticks
197-
*/
198188
override def getDataTicks(
199189
requestFrameStart: Long,
200190
requestFrameEnd: Long,

src/test/scala/edu/ie3/simona/service/weather/WeatherSourceSpec.scala

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import tech.units.indriya.ComparableQuantity
2222
import tech.units.indriya.quantity.Quantities
2323
import tech.units.indriya.unit.Units
2424

25+
import java.time.ZonedDateTime
2526
import java.util
2627
import java.util.Optional
2728
import javax.measure.quantity.Length
29+
import scala.collection.SortedMap
2830
import scala.jdk.CollectionConverters.*
2931
import scala.jdk.OptionConverters.*
3032
import scala.util.{Failure, Success}
@@ -298,35 +300,15 @@ case object WeatherSourceSpec {
298300
override protected val maxCoordinateDistance: ComparableQuantity[Length] =
299301
Quantities.getQuantity(400000, Units.METRE)
300302

301-
/** Get the weather data for the given tick as a weighted average taking
302-
* into account the given weighting of weather coordinates.
303-
*
304-
* @param tick
305-
* Simulation date in question
306-
* @param weightedCoordinates
307-
* The coordinate in question
308-
* @return
309-
* Matching weather data
310-
*/
311303
override def getWeather(
312-
tick: Long,
304+
startTick: Long,
305+
endTick: Long,
313306
weightedCoordinates: WeightedCoordinates,
314-
): WeatherData =
307+
): SortedMap[ZonedDateTime, WeatherData] =
315308
throw new UnsupportedOperationException(
316309
"This is not supported by the dummy source."
317310
)
318311

319-
/** Determine an Array with all ticks between the request frame's start and
320-
* end on which new data is available. Bot the request frame's start and
321-
* end are INCLUDED.
322-
*
323-
* @param requestFrameStart
324-
* Beginning of the announced request frame
325-
* @param requestFrameEnd
326-
* End of the announced request frame
327-
* @return
328-
* Array with data ticks
329-
*/
330312
override def getDataTicks(
331313
requestFrameStart: Long,
332314
requestFrameEnd: Long,

0 commit comments

Comments
 (0)