Skip to content

Interpolate functionality and InterpolationTest. #1323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed SonarQube junit path issue in GitHub Actions [#1284](https://github.com/ie3-institute/PowerSystemDataModel/issues/1284)
- Fixed no errors thrown in `getMapping()` in `TimeSeriesMappingSource` [#1287](https://github.com/ie3-institute/PowerSystemDataModel/issues/1287)
- Consider None-equivalent value for missing data points in weather [#1304](https://github.com/ie3-institute/PowerSystemDataModel/issues/1304)

### Changed
- Replaced `return this` with `return thisInstance` in CopyBuilders [#1250](https://github.com/ie3-institute/PowerSystemDataModel/issues/1250)
Expand Down
139 changes: 139 additions & 0 deletions src/main/java/edu/ie3/datamodel/io/source/WeatherSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
import edu.ie3.datamodel.models.value.WeatherValue;
import edu.ie3.datamodel.utils.Try;
import edu.ie3.util.interval.ClosedInterval;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.measure.Quantity;
import org.apache.commons.lang3.tuple.Pair;
import org.locationtech.jts.geom.Point;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tech.units.indriya.ComparableQuantity;

/** Abstract class for WeatherSource by Csv and Sql Data */
public abstract class WeatherSource extends EntitySource {
Expand Down Expand Up @@ -52,6 +55,83 @@ public void validate() throws ValidationException {
validate(WeatherValue.class, this::getSourceFields, weatherFactory);
}

/**
* Method for interpolating weather values.
*
* @return a new quantity
*/
protected List<TimeBasedValue<WeatherValue>> interpolateMissingValues(
List<TimeBasedValue<WeatherValue>> timeSeries) {

List<TimeBasedValue<WeatherValue>> result = new ArrayList<>();
int i = 0;
while (i < timeSeries.size()) {
TimeBasedValue<WeatherValue> current = timeSeries.get(i);

if (current.getValue() != null) {
result.add(current);
i++;
continue;
}
int prevIdx = i - 1;
int nextIdx = i + 1;
while (nextIdx < timeSeries.size() && timeSeries.get(nextIdx).getValue() == null) {
nextIdx++;
}

if (prevIdx >= 0 && nextIdx < timeSeries.size()) {
TimeBasedValue<WeatherValue> prev = timeSeries.get(prevIdx);
TimeBasedValue<WeatherValue> next = timeSeries.get(nextIdx);
Duration total = Duration.between(prev.getTime(), next.getTime());
for (int j = i; j < nextIdx; j++) {
TimeBasedValue<WeatherValue> missing = timeSeries.get(j);
Duration fromPrev = Duration.between(prev.getTime(), missing.getTime());
double ratio = (double) fromPrev.toSeconds() / total.toSeconds();
WeatherValue interpolated =
interpolateWeatherValue(prev.getValue(), next.getValue(), ratio);
result.add(new TimeBasedValue<>(missing.getTime(), interpolated));
}
i = nextIdx;
} else {
result.add(current);
i++;
}
}

return result;
}

private WeatherValue interpolateWeatherValue(WeatherValue start, WeatherValue end, double ratio) {
var direct = interpolateOptional(start.getDirectIrradiance(), end.getDirectIrradiance(), ratio);
var diffuse = interpolateOptional(start.getDiffuseIrradiance(), end.getDiffuseIrradiance(), ratio);

var temp = interpolateDirect(start.getTemperature(), end.getTemperature(), ratio);
var dir = interpolateDirect(start.getWindDirection(), end.getWindDirection(), ratio);
var vel = interpolateDirect(start.getWindVelocity(), end.getWindVelocity(), ratio);

return new WeatherValue(start.getCoordinate(), direct, diffuse, temp, dir, vel);
}


private <Q extends Quantity<Q>> ComparableQuantity<Q> interpolateOptional(
Optional<ComparableQuantity<Q>> startOpt,
Optional<ComparableQuantity<Q>> endOpt,
double ratio) {
return startOpt
.flatMap(startVal -> endOpt.map(endVal -> interpolateQuantity(startVal, endVal, ratio)))
.orElse(null);
}

private <Q extends Quantity<Q>> ComparableQuantity<Q> interpolateDirect(
ComparableQuantity<Q> start, ComparableQuantity<Q> end, double ratio) {
return interpolateQuantity(start, end, ratio);
}

private <Q extends Quantity<Q>> ComparableQuantity<Q> interpolateQuantity(
ComparableQuantity<Q> a, ComparableQuantity<Q> b, double ratio) {
return a.add(b.subtract(a).multiply(ratio));
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

public abstract Map<Point, IndividualTimeSeries<WeatherValue>> getWeather(
Expand All @@ -64,6 +144,52 @@ public abstract Map<Point, IndividualTimeSeries<WeatherValue>> getWeather(
public abstract Optional<TimeBasedValue<WeatherValue>> getWeather(
ZonedDateTime date, Point coordinate) throws SourceException;

public Optional<WeatherValue> getWeatherInterpolated(
ZonedDateTime date, Point coordinate, int plus, int minus) throws SourceException {

ClosedInterval<ZonedDateTime> interpolationInterval =
new ClosedInterval<>(date.minusHours(minus), date.plusHours(plus));
IndividualTimeSeries<WeatherValue> ts =
getWeather(interpolationInterval, List.of(coordinate)).get(coordinate);

if (ts == null) {
log.warn("No time series available for coordinate {}", coordinate);
return Optional.empty();
}

Optional<WeatherValue> value = ts.getValue(date);

if (value.isPresent() && value.get().isComplete()) {
return value;
}

Optional<TimeBasedValue<WeatherValue>> prevValue = ts.getPreviousTimeBasedValue(date);
Optional<TimeBasedValue<WeatherValue>> nextValue = ts.getNextTimeBasedValue(date);

if (prevValue.isEmpty() || nextValue.isEmpty()) {
log.warn(
"Not enough data to interpolate weather value at {} for coordinate {}", date, coordinate);
return Optional.empty();
}

TimeBasedValue<WeatherValue> prev = prevValue.get();
TimeBasedValue<WeatherValue> next = nextValue.get();

Duration totalDuration = Duration.between(prev.getTime(), next.getTime());
Duration partialDuration = Duration.between(prev.getTime(), date);

if (totalDuration.isZero()) {
return Optional.of(prev.getValue());
}

double ratio = (double) partialDuration.toSeconds() / totalDuration.toSeconds();

WeatherValue interpolated = interpolateWeatherValue(prev.getValue(), next.getValue(), ratio);

return Optional.of(interpolated);
}


public abstract Map<Point, List<ZonedDateTime>> getTimeKeysAfter(ZonedDateTime time)
throws SourceException;

Expand All @@ -72,6 +198,19 @@ public List<ZonedDateTime> getTimeKeysAfter(ZonedDateTime time, Point coordinate
return getTimeKeysAfter(time).getOrDefault(coordinate, Collections.emptyList());
}

protected Map<Point, IndividualTimeSeries<WeatherValue>> interpolateWeatherData(
Map<Point, IndividualTimeSeries<WeatherValue>> rawData) {
return rawData.entrySet().stream()
.collect(
Collectors.toMap(
Map.Entry::getKey,
entry ->
new IndividualTimeSeries<>(
new LinkedHashSet<>(
interpolateMissingValues(
new ArrayList<>(entry.getValue().getEntries()))))));
}

// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

/**
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/edu/ie3/datamodel/models/value/WeatherValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import edu.ie3.util.quantities.interfaces.Irradiance;
import java.util.Objects;
import java.util.Optional;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Speed;
import javax.measure.quantity.Temperature;
Expand Down Expand Up @@ -81,6 +82,34 @@ public WindValue getWind() {
return wind;
}

public Optional<ComparableQuantity<Irradiance>> getDirectIrradiance() {
return solarIrradiance.getDirectIrradiance();
}

public Optional<ComparableQuantity<Irradiance>> getDiffuseIrradiance() {
return solarIrradiance.getDiffuseIrradiance();
}

public ComparableQuantity<Temperature> getTemperature() {
return temperature.getTemperature();
}

public ComparableQuantity<Angle> getWindDirection() {
return wind.getDirection();
}
public ComparableQuantity<Speed> getWindVelocity() {
return wind.getVelocity();
}

/**
* Checks if all mandatory values are present.
*
* @return true if all values are present, false otherwise
*/
public boolean isComplete() {
return solarIrradiance != null && temperature != null && wind != null;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* © 2025. TU Dortmund University,
* Institute of Energy Systems, Energy Efficiency and Energy Economics,
* Research group Distribution grid planning and operation
*/
package edu.ie3.datamodel.io.source

import edu.ie3.datamodel.models.timeseries.individual.TimeBasedValue
import edu.ie3.datamodel.models.value.WeatherValue
import edu.ie3.util.TimeUtil
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.geom.Point
import spock.lang.Shared
import spock.lang.Specification

import java.time.ZonedDateTime

class WeatherSourceInterpolationTest extends Specification {

@Shared
def geometryFactory = new GeometryFactory()

def "interpolateMissingValues fills gaps between known weather values"() {
given:
def coordinate = geometryFactory.createPoint(new Coordinate(7.0, 51.0))
def t1 = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T00:00:00Z")
def t2 = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T01:00:00Z")
def t3 = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T02:00:00Z")
def t4 = TimeUtil.withDefaults.toZonedDateTime("2020-01-01T03:00:00Z")
def value1 = new WeatherValue(coordinate, 100, 200, 10, 5, 180)
def value3 = new WeatherValue(coordinate, 300 , 400, 20, 10, 270)

def series = [
new TimeBasedValue(t1, value1),
new TimeBasedValue(t3, value3),
new TimeBasedValue(t4, value3)
] as Set<TimeBasedValue>

def mockSource = new InterpolatingWeatherSource()

when:
def interpolatedSeries = mockSource.interpolateMissingValues(series)

then:
interpolatedSeries.size() == 4
def interpolated = interpolatedSeries.find { it.time == t2 }
interpolated != null
with(interpolated.value as WeatherValue) {
getTemperature().value.doubleValue() == 15.0
getWindVelocity().value.doubleValue() == 7.5
getWindDirection().value.doubleValue() == 225.0
}
}

private static class InterpolatingWeatherSource extends WeatherSource {
@Override
Optional<TimeBasedValue<WeatherValue>> getWeather(ZonedDateTime dateTime, Point coordinate) {
return Optional.empty()
}
}
}
Loading