Skip to content

Commit

Permalink
Identify forecast time-series consistently when estimating sampling u…
Browse files Browse the repository at this point in the history
…ncertainties, #426.
  • Loading branch information
james-d-brown committed Feb 24, 2025
1 parent 5dd62f5 commit 1fd9f86
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 27 deletions.
2 changes: 2 additions & 0 deletions src/wres/pipeline/Evaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -682,11 +682,13 @@ private Pair<Set<Path>, String> evaluate( SystemSettings systemSettings,
.map( FeatureTuple::getGeometryTuple )
.collect( Collectors.toUnmodifiableSet() );
Set<FeatureGroup> featureGroups = new TreeSet<>( project.getFeatureGroups() );

Set<FeatureGroup> adjustedFeatureGroups =
EvaluationUtilities.adjustFeatureGroupsForSummaryStatistics( featureGroups,
unwrappedFeatures,
declaration.summaryStatistics(),
doNotPublish );

EvaluationUtilities.createNetcdfBlobs( netcdfWriters,
adjustedFeatureGroups,
metricsAndThresholds );
Expand Down
33 changes: 29 additions & 4 deletions wres-config/src/wres/config/yaml/DeclarationInterpolator.java
Original file line number Diff line number Diff line change
Expand Up @@ -2452,8 +2452,13 @@ private static List<EvaluationStatusEvent> interpolateObservedDataTypeWhenUndecl

DataType calculatedDataType;

// Analysis durations present? If so, assume analyses
if ( DeclarationUtilities.hasAnalysisTimes( builder ) )
// Analysis durations present? If so, assume analyses, unless there is an analysis interface declared for
// another side of data
if ( DeclarationUtilities.hasAnalysisTimes( builder )
&& DeclarationInterpolator.noAnalysisInterface( builder.right() )
&& ( !DeclarationUtilities.hasBaseline( builder )
|| DeclarationInterpolator.noAnalysisInterface( builder.baseline()
.dataset() ) ) )
{
calculatedDataType = DataType.ANALYSES;

Expand Down Expand Up @@ -2496,15 +2501,16 @@ private static List<EvaluationStatusEvent> interpolateObservedDataTypeWhenUndecl
// Is it consistent with the type inferred from the declaration? If not, we only emit an error if the
// type inferred from the declaration is ANALYSES because this requires definitive/unique declaration
// options. Otherwise, we emit a warning.
if ( ingestedDataType != calculatedDataType && calculatedDataType == DataType.ANALYSES )
if ( ingestedDataType != calculatedDataType
&& calculatedDataType == DataType.ANALYSES )
{
EvaluationStatusEvent event
= EvaluationStatusEvent.newBuilder()
.setStatusLevel( EvaluationStatusEvent.StatusLevel.ERROR )
.setEventMessage( THE_DATA_TYPE_INFERRED_FROM_THE_TIME_SERIES_DATA
+ "for "
+ article
+ "'"
+ " '"
+ orientation
+ DATASET_WAS
+ ingestedDataType
Expand Down Expand Up @@ -2554,6 +2560,25 @@ else if ( ingestedDataType != calculatedDataType )
return Collections.unmodifiableList( events );
}

/**
* @param dataset the dataset
* @return whether the dataset has an analysis interface
*/

private static boolean noAnalysisInterface( Dataset dataset )
{
if ( Objects.isNull( dataset ) )
{
return true;
}

return dataset.sources()
.stream()
.noneMatch( s -> Objects.nonNull( s.sourceInterface() )
&& s.sourceInterface()
.isAnalysisInterface() );
}

/**
* Interpolates the predicted data type.
* @param builder the builder
Expand Down
22 changes: 18 additions & 4 deletions wres-config/src/wres/config/yaml/components/SourceInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,20 @@ public enum SourceInterface
NWM_LONG_RANGE_CHANNEL_RT_CONUS( Set.of( DataType.SINGLE_VALUED_FORECASTS ), FeatureAuthority.NWM_FEATURE_ID ),
/** nwm short range channel rt alaska. */
@JsonProperty( "nwm short range channel rt alaska" )
NWM_SHORT_RANGE_CHANNEL_RT_CONUS_ALASKA( Set.of( DataType.SINGLE_VALUED_FORECASTS ), FeatureAuthority.NWM_FEATURE_ID ),
NWM_SHORT_RANGE_CHANNEL_RT_CONUS_ALASKA( Set.of( DataType.SINGLE_VALUED_FORECASTS ),
FeatureAuthority.NWM_FEATURE_ID ),
/** nwm medium range ensemble channel rt alaska. */
@JsonProperty( "nwm medium range ensemble channel rt alaska" )
NWM_MEDIUM_RANGE_ENSEMBLE_CHANNEL_RT_ALASKA( Set.of( DataType.ENSEMBLE_FORECASTS ),
FeatureAuthority.NWM_FEATURE_ID ),
FeatureAuthority.NWM_FEATURE_ID ),
/** nwm medium range deterministic channel rt alaska. */
@JsonProperty( "nwm medium range deterministic channel rt alaska" )
NWM_MEDIUM_RANGE_DETERMINISTIC_CHANNEL_RT_ALASKA( Set.of( DataType.SINGLE_VALUED_FORECASTS ),
FeatureAuthority.NWM_FEATURE_ID ),
FeatureAuthority.NWM_FEATURE_ID ),
/** nwm medium range no da deterministic channel rt alaska. */
@JsonProperty( "nwm medium range no da deterministic channel rt alaska" )
NWM_MEDIUM_RANGE_NO_DA_DETERMINISTIC_CHANNEL_RT_ALASKA( Set.of( DataType.SINGLE_VALUED_FORECASTS ),
FeatureAuthority.NWM_FEATURE_ID ),
FeatureAuthority.NWM_FEATURE_ID ),
/** nwm analysis assim channel rt alaska. */
@JsonProperty( "nwm analysis assim channel rt alaska" )
NWM_ANALYSIS_ASSIM_CHANNEL_RT_ALASKA( Set.of( DataType.ANALYSES ), FeatureAuthority.NWM_FEATURE_ID ),
Expand Down Expand Up @@ -169,4 +170,17 @@ public boolean isNwmInterface()
return this.name()
.startsWith( "NWM_" );
}

/**
* Convenience method that inspects the interface {@link #name()} and returns <code>true</code> when the name
* contains 'ANALYSIS_', otherwise <code>false</code>.
*
* @return whether the source interface is an analysis interface
*/

public boolean isAnalysisInterface()
{
return this.name()
.contains( "ANALYSIS_" );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ List<List<Event<T>>> getTimeSeriesWithAtLeastThisManyEvents( int minimumEventCou
/**
* Returns the time-series with all events present
*
* @return the time-series with at least the number if requested events
* @return the time-series with all events
*/

List<List<Event<T>>> getTimeSeriesWithAllEvents()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ private List<ResampleIndexes> generateResampleIndexes( BootstrapPool<T> pool )
// ordering
List<List<TimeSeries<T>>> groupedBySize = pool.getOrderedTimeSeries();

// One ResampledIndexes for each time-series, indicating where to obtain the event values for that series
// One ResampleIndexes for each time-series, indicating where to obtain the event values for that series
List<ResampleIndexes> outerIndexes = new ArrayList<>();
for ( List<TimeSeries<T>> poolSeries : groupedBySize )
{
Expand All @@ -211,8 +211,7 @@ private List<ResampleIndexes> generateResampleIndexes( BootstrapPool<T> pool )

// Forecast time-series which are probably non-stationary across lead durations, unless they are based
// on climatology
if ( !nextSeries.getReferenceTimes()
.isEmpty() )
if ( pool.hasForecasts() )
{
nextIndexes = this.generateResampleIndexesForForecastSeries( nextSeries,
i,
Expand Down Expand Up @@ -756,6 +755,29 @@ private UnaryOperator<TimeSeries<T>> getTimeSeriesResampler( BootstrapPool<T> po
Event<T> nextEvent = events.get( j );
int[] index = indexes.indexes()
.get( j );

if ( eventsToSample.size() <= index[0] )
{
throw new IndexOutOfBoundsException( "While attempting to resample a time-series at index "
+ index[0]
+ ", discovered a maximum time-series index of "
+ ( eventsToSample.size() - 1 )
+ ", which is smaller than the required index." );
}

if ( eventsToSample.get( index[0] )
.size() <= index[1] )
{
throw new IndexOutOfBoundsException( "While attempting to resample a time-series event at index "
+ index[1]
+ " of the time-series at index "
+ index[0]
+ ", discovered a maximum time-series event index of "
+ ( eventsToSample.get( index[0] )
.size() - 1 )
+ ", which is smaller than the required index." );
}

Event<T> resampledValue = eventsToSample.get( index[0] )
.get( index[1] );
Event<T> resampled = Event.of( nextEvent.getTime(), resampledValue.getValue() );
Expand Down
3 changes: 2 additions & 1 deletion wres-datamodel/src/wres/datamodel/time/TimeSeriesSlicer.java
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,8 @@ public static boolean hasForecasts( Set<ReferenceTimeType> referenceTimeTypes )
Objects.requireNonNull( referenceTimeTypes );

return referenceTimeTypes.stream()
.anyMatch( t -> t == ReferenceTimeType.T0 || t == ReferenceTimeType.ISSUED_TIME );
.anyMatch( t -> t == ReferenceTimeType.T0
|| t == ReferenceTimeType.ISSUED_TIME );
}

/**
Expand Down
23 changes: 10 additions & 13 deletions wres-io/src/wres/io/retrieving/database/AnalysisRetriever.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@
import wres.io.retrieving.RetrieverUtilities;

/**
* <p>Retrieves data from the wres.TimeSeries and wres.TimeSeriesValue tables but
* in the pattern expected for treating the nth timestep of each analysis as if
* it were an event in a timeseries across analyses, sort of like observations.
* <p>Retrieves data from the wres.TimeSeries and wres.TimeSeriesValue tables but in the pattern expected for treating
* the nth timestep of each analysis as if it were an event in a time-series across analyses, sort of like observations.
*
* <p>The reason for separating it from forecast and observation timeseries
* retrieval is that each analysis has N events in an actual timeseries, but the
* structure and use of the analyses and origin of analyses differs from both
* observation and timeseries. The structure of an NWM analysis, for example, is
* akin to an NWM forecast, with a reference datetime and valid datetimes.
* However, when using the analyses in an evaluation of forecasts, one event
* <p>The reason for separating it from forecast and observation timeseries retrieval is that each analysis has N
* events in an actual timeseries, but the structure and use of the analyses and origin of analyses differs from both
* observation and timeseries. The structure of an NWM analysis, for example, is akin to an NWM forecast, with a
* reference datetime and valid datetimes. However, when using the analyses in an evaluation of forecasts, one event
* from each analysis is picked out and a broader timeseries is created.
*/

Expand Down Expand Up @@ -103,7 +100,7 @@ static class Builder extends TimeSeriesRetriever.Builder<Double>

/**
* Sets the earliest analysis hour, if not <code>null</null>.
*
*
* @param earliestAnalysisDuration duration
* @return A builder
*/
Expand All @@ -119,7 +116,7 @@ Builder setEarliestAnalysisDuration( Duration earliestAnalysisDuration )

/**
* Set the latest analysis hour, if not <code>null</null>.
*
*
* @param latestAnalysisDuration duration
* @return A builder
*/
Expand Down Expand Up @@ -173,7 +170,7 @@ public Stream<TimeSeries<Double>> get()

/**
* Returns the earliest analysis duration or null.
*
*
* @return the earliest analysis duration or null
*/

Expand All @@ -184,7 +181,7 @@ private Duration getEarliestAnalysisDuration()

/**
* Returns the latest analysis duration or null.
*
*
* @return the latest analysis duration or null
*/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ private static void validate( URI uri,
"reconciled with features to evaluate. Features without ",
"thresholds will be skipped. If the number of features ",
"without thresholds is larger than expected, ensure that ",
"the source of feature names (featureNameFrom) is properly ",
"the source of feature names (feature_name_from) is properly ",
"declared for the external thresholds. The ",
"declared features without thresholds are: ",
featureNamesWithoutThresholds,
Expand Down

0 comments on commit 1fd9f86

Please sign in to comment.