From 1fd9f861d58b07d1c2d231f38d2096d4c26425df Mon Sep 17 00:00:00 2001 From: James Brown <64858662+james-d-brown@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:46:12 +0000 Subject: [PATCH] Identify forecast time-series consistently when estimating sampling uncertainties, #426. --- src/wres/pipeline/Evaluator.java | 2 ++ .../config/yaml/DeclarationInterpolator.java | 33 ++++++++++++++++--- .../yaml/components/SourceInterface.java | 22 ++++++++++--- .../datamodel/bootstrap/BootstrapPool.java | 2 +- .../StationaryBootstrapResampler.java | 28 ++++++++++++++-- .../wres/datamodel/time/TimeSeriesSlicer.java | 3 +- .../database/AnalysisRetriever.java | 23 ++++++------- .../wrds/thresholds/WrdsThresholdReader.java | 2 +- 8 files changed, 88 insertions(+), 27 deletions(-) diff --git a/src/wres/pipeline/Evaluator.java b/src/wres/pipeline/Evaluator.java index 499c21ebf7..275db9a7d8 100644 --- a/src/wres/pipeline/Evaluator.java +++ b/src/wres/pipeline/Evaluator.java @@ -682,11 +682,13 @@ private Pair, String> evaluate( SystemSettings systemSettings, .map( FeatureTuple::getGeometryTuple ) .collect( Collectors.toUnmodifiableSet() ); Set featureGroups = new TreeSet<>( project.getFeatureGroups() ); + Set adjustedFeatureGroups = EvaluationUtilities.adjustFeatureGroupsForSummaryStatistics( featureGroups, unwrappedFeatures, declaration.summaryStatistics(), doNotPublish ); + EvaluationUtilities.createNetcdfBlobs( netcdfWriters, adjustedFeatureGroups, metricsAndThresholds ); diff --git a/wres-config/src/wres/config/yaml/DeclarationInterpolator.java b/wres-config/src/wres/config/yaml/DeclarationInterpolator.java index f20cdb3b29..9b75a9a4fd 100644 --- a/wres-config/src/wres/config/yaml/DeclarationInterpolator.java +++ b/wres-config/src/wres/config/yaml/DeclarationInterpolator.java @@ -2452,8 +2452,13 @@ private static List 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; @@ -2496,7 +2501,8 @@ private static List 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() @@ -2504,7 +2510,7 @@ private static List interpolateObservedDataTypeWhenUndecl .setEventMessage( THE_DATA_TYPE_INFERRED_FROM_THE_TIME_SERIES_DATA + "for " + article - + "'" + + " '" + orientation + DATASET_WAS + ingestedDataType @@ -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 diff --git a/wres-config/src/wres/config/yaml/components/SourceInterface.java b/wres-config/src/wres/config/yaml/components/SourceInterface.java index ea58c7bf33..8bb5533366 100644 --- a/wres-config/src/wres/config/yaml/components/SourceInterface.java +++ b/wres-config/src/wres/config/yaml/components/SourceInterface.java @@ -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 ), @@ -169,4 +170,17 @@ public boolean isNwmInterface() return this.name() .startsWith( "NWM_" ); } + + /** + * Convenience method that inspects the interface {@link #name()} and returns true when the name + * contains 'ANALYSIS_', otherwise false. + * + * @return whether the source interface is an analysis interface + */ + + public boolean isAnalysisInterface() + { + return this.name() + .contains( "ANALYSIS_" ); + } } diff --git a/wres-datamodel/src/wres/datamodel/bootstrap/BootstrapPool.java b/wres-datamodel/src/wres/datamodel/bootstrap/BootstrapPool.java index f1065a6d34..2c97c005d9 100644 --- a/wres-datamodel/src/wres/datamodel/bootstrap/BootstrapPool.java +++ b/wres-datamodel/src/wres/datamodel/bootstrap/BootstrapPool.java @@ -89,7 +89,7 @@ List>> 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>> getTimeSeriesWithAllEvents() diff --git a/wres-datamodel/src/wres/datamodel/bootstrap/StationaryBootstrapResampler.java b/wres-datamodel/src/wres/datamodel/bootstrap/StationaryBootstrapResampler.java index 71aec7b57e..4efa77f6de 100644 --- a/wres-datamodel/src/wres/datamodel/bootstrap/StationaryBootstrapResampler.java +++ b/wres-datamodel/src/wres/datamodel/bootstrap/StationaryBootstrapResampler.java @@ -198,7 +198,7 @@ private List generateResampleIndexes( BootstrapPool pool ) // ordering List>> 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 outerIndexes = new ArrayList<>(); for ( List> poolSeries : groupedBySize ) { @@ -211,8 +211,7 @@ private List generateResampleIndexes( BootstrapPool 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, @@ -756,6 +755,29 @@ private UnaryOperator> getTimeSeriesResampler( BootstrapPool po Event 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 resampledValue = eventsToSample.get( index[0] ) .get( index[1] ); Event resampled = Event.of( nextEvent.getTime(), resampledValue.getValue() ); diff --git a/wres-datamodel/src/wres/datamodel/time/TimeSeriesSlicer.java b/wres-datamodel/src/wres/datamodel/time/TimeSeriesSlicer.java index 7c32fa4421..93c73acd65 100644 --- a/wres-datamodel/src/wres/datamodel/time/TimeSeriesSlicer.java +++ b/wres-datamodel/src/wres/datamodel/time/TimeSeriesSlicer.java @@ -1384,7 +1384,8 @@ public static boolean hasForecasts( Set 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 ); } /** diff --git a/wres-io/src/wres/io/retrieving/database/AnalysisRetriever.java b/wres-io/src/wres/io/retrieving/database/AnalysisRetriever.java index 6f7f6ed074..da9c15b84c 100644 --- a/wres-io/src/wres/io/retrieving/database/AnalysisRetriever.java +++ b/wres-io/src/wres/io/retrieving/database/AnalysisRetriever.java @@ -16,16 +16,13 @@ import wres.io.retrieving.RetrieverUtilities; /** - *

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. + *

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. * - *

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 + *

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. */ @@ -103,7 +100,7 @@ static class Builder extends TimeSeriesRetriever.Builder /** * Sets the earliest analysis hour, if not null. - * + * * @param earliestAnalysisDuration duration * @return A builder */ @@ -119,7 +116,7 @@ Builder setEarliestAnalysisDuration( Duration earliestAnalysisDuration ) /** * Set the latest analysis hour, if not null. - * + * * @param latestAnalysisDuration duration * @return A builder */ @@ -173,7 +170,7 @@ public Stream> get() /** * Returns the earliest analysis duration or null. - * + * * @return the earliest analysis duration or null */ @@ -184,7 +181,7 @@ private Duration getEarliestAnalysisDuration() /** * Returns the latest analysis duration or null. - * + * * @return the latest analysis duration or null */ diff --git a/wres-reading/src/wres/reading/wrds/thresholds/WrdsThresholdReader.java b/wres-reading/src/wres/reading/wrds/thresholds/WrdsThresholdReader.java index 9f1bd906c6..92a0ea4a51 100644 --- a/wres-reading/src/wres/reading/wrds/thresholds/WrdsThresholdReader.java +++ b/wres-reading/src/wres/reading/wrds/thresholds/WrdsThresholdReader.java @@ -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,