diff --git a/wres-config/nonsrc/schema.yml b/wres-config/nonsrc/schema.yml index 8232558304..6f7da2758b 100644 --- a/wres-config/nonsrc/schema.yml +++ b/wres-config/nonsrc/schema.yml @@ -1489,13 +1489,16 @@ definitions: is present, evaluates common events by reference time and valid time. If absent, retains all events of each type. With fuzzy matching, each time- series is matched with its nearest, corresponding, time-series according to - the total duration between all reference times of a corresponding type. In - other words, if there is an exact match, that will be used, else the time- - series whose reference times are nearest overall. Once a time-series has - been matched, it cannot be re-used. Always uses exact matching for valid - times. The scope of the cross-pairing always includes the predicted and - baseline datasets, where defined, and may be further controlled using the - 'scope' parameter." + the total duration between all reference times across the candidate time- + series. In other words, if there is an exact match, that will be used, else + the time-series whose reference times are nearest overall. With exact + matching, both the type of reference time and the time itself must + correspond. With fuzzy matching, all types of reference time are considered + equal and hence used to calculate the total duration between all reference + times of the candidate series. Once a time-series has been matched, it + cannot be re-used. Always uses exact matching for valid times. The scope of + the cross-pairing always includes the predicted and baseline datasets, where + defined, and may be further controlled using the 'scope' parameter." type: string enum: - exact diff --git a/wres-datamodel/src/wres/datamodel/time/TimeSeriesCrossPairer.java b/wres-datamodel/src/wres/datamodel/time/TimeSeriesCrossPairer.java index 5091cddbbd..6ee5a37910 100644 --- a/wres-datamodel/src/wres/datamodel/time/TimeSeriesCrossPairer.java +++ b/wres-datamodel/src/wres/datamodel/time/TimeSeriesCrossPairer.java @@ -359,9 +359,31 @@ private Duration getTotalDurationBetweenCommonTimeTypes( TimeSeries

fi Set common = new HashSet<>( firstTimes.keySet() ); common.retainAll( secondTimes.keySet() ); - // Filter non-matching reference time types - if ( method != CrossPairMethod.FUZZY ) + // For exact matching, the reference time types must match + if ( method == CrossPairMethod.EXACT ) { + if ( common.isEmpty() ) + { + throw new PairingException( "Encountered an error while inspecting time-series to cross-pair. " + + "Attempted to calculate the total duration between the commonly typed " + + "reference times of two time-series, but no commonly typed reference " + + "times were discovered, which is not allowed. For lenient cross-pairing " + + "that considers all types of reference time equivalent, declare the " + + "'fuzzy' cross-pairing method instead of 'exact'. The first time-series " + + "was: " + + first.getMetadata() + + ". The second time-series was: " + + second.getMetadata() + + ". The first time-series had reference time types of: " + + first.getReferenceTimes() + .keySet() + + ". The second time-series had reference time types of: " + + second.getReferenceTimes() + .keySet() + + "." ); + } + + // Filter non-matching reference time types firstTimes = firstTimes.entrySet() .stream() .filter( e -> common.contains( e.getKey() ) ) @@ -372,36 +394,10 @@ private Duration getTotalDurationBetweenCommonTimeTypes( TimeSeries

fi .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) ); } - if ( firstTimes.isEmpty() || secondTimes.isEmpty() ) - { - String append = ""; - if ( method != CrossPairMethod.FUZZY ) - { - append = "For lenient cross-pairing that considers all types of reference time equivalent, declare the " - + "'fuzzy' cross-pairing method. "; - } - - throw new PairingException( "Encountered an error while inspecting time-series to cross-pair. Attempted to " - + "calculate the total duration between the commonly typed " - + "reference times of two time-series, but no commonly typed reference times " - + "were discovered, which is not allowed. " - + append - + "The first time-series was: " - + first.getMetadata() - + ". The second time-series was: " - + second.getMetadata() - + ". The first time-series had reference time types of: " - + first.getReferenceTimes() - .keySet() - + ". The second time-series had reference time types of: " - + second.getReferenceTimes() - .keySet() - + "." ); - } - // The neutral difference Duration returnMe = Duration.ZERO; + // Iterate through the differences and sum them for ( Instant firstInstant : firstTimes.values() ) { for ( Instant secondInstant : secondTimes.values() ) diff --git a/wres-datamodel/test/wres/datamodel/time/TimeSeriesCrossPairerTest.java b/wres-datamodel/test/wres/datamodel/time/TimeSeriesCrossPairerTest.java index 23b4dfa9ea..2c6c420f4c 100644 --- a/wres-datamodel/test/wres/datamodel/time/TimeSeriesCrossPairerTest.java +++ b/wres-datamodel/test/wres/datamodel/time/TimeSeriesCrossPairerTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -57,7 +58,7 @@ void runBeforeEachTest() } @Test - void testCrossPairTwoTimeSeriesWithEqualReferenceTimesThatEachAppearTwice() + void testCrossPairTwoTimeSeriesWithEqualReferenceTimesThatEachAppearTwiceAndFuzzyMatching() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); Event> second = Event.of( SECOND, Pair.of( 2, 2 ) ); @@ -120,7 +121,7 @@ void testCrossPairTwoTimeSeriesWithEqualReferenceTimesThatEachAppearTwice() } @Test - void testCrossPairTimeSeriesWithSomeEqualReferenceTimes() + void testCrossPairTimeSeriesWithSomeEqualReferenceTimesOfDifferentTypesAndFuzzyMatching() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); @@ -139,7 +140,7 @@ void testCrossPairTimeSeriesWithSomeEqualReferenceTimes() Event> second = Event.of( SECOND, Pair.of( 2, 2 ) ); TimeSeriesMetadata secondMetadata = - TimeSeriesMetadata.of( Collections.singletonMap( ReferenceTimeType.T0, + TimeSeriesMetadata.of( Collections.singletonMap( ReferenceTimeType.ISSUED_TIME, FIRST ), TimeScaleOuter.of(), CHICKENS, @@ -181,7 +182,7 @@ void testCrossPairTimeSeriesWithSomeEqualReferenceTimes() } @Test - void testCrossPairTimeSeriesWithNoEqualReferenceTimesOrValidTimes() + void testCrossPairTimeSeriesWithNoEqualReferenceTimesOrValidTimesWhenFuzzyMatching() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); @@ -222,7 +223,7 @@ void testCrossPairTimeSeriesWithNoEqualReferenceTimesOrValidTimes() } @Test - void testCrossPairTwoTimeSeriesWithEqualReferenceTimesAndNoEqualValidTimes() + void testCrossPairTwoTimeSeriesWithEqualReferenceTimesAndNoEqualValidTimesWhenFuzzyMatching() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); @@ -263,7 +264,7 @@ void testCrossPairTwoTimeSeriesWithEqualReferenceTimesAndNoEqualValidTimes() } @Test - void testCrossPairTimeSeriesWithNoEqualReferenceTimesAndSomeEqualValidTimes() + void testCrossPairTimeSeriesWithNoEqualReferenceTimesAndSomeEqualValidTimesWhenFuzzyMatching() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); @@ -353,7 +354,7 @@ void testCrossPairTimeSeriesWithNoEqualReferenceTimesAndSomeEqualValidTimesWhenE } @Test - void testCrossPairTwoTimeSeriesWithNoReferenceTimes() + void testCrossPairTwoTimeSeriesWithNoReferenceTimesAndFuzzyMatching() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); @@ -391,7 +392,7 @@ void testCrossPairTwoTimeSeriesWithNoReferenceTimes() } @Test - void testCrossPairTimeSeriesWithSomeNearbyReferenceTimes() + void testCrossPairTimeSeriesWithSomeNearbyReferenceTimesAndFuzzyMatching() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); @@ -489,7 +490,7 @@ void testCrossPairTimeSeriesWithSomeNearbyReferenceTimes() } @Test - void testCrossPairTimeSeriesWithNoEqualReferenceTimeTypes() + void testCrossPairTimeSeriesWithNoEqualReferenceTimeTypesThrowsPairingException() { Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); @@ -535,6 +536,31 @@ void testCrossPairTimeSeriesWithNoEqualReferenceTimeTypes() .contains( "no commonly typed reference times" ) ); } + @Test + void testCrossPairWithEmptyBaselineProducesEmptyCrossPairsWhenFuzzyMatching() + { + Event> first = Event.of( FIRST, Pair.of( 1, 1 ) ); + + TimeSeriesMetadata firstMetadata = + TimeSeriesMetadata.of( Collections.singletonMap( ReferenceTimeType.T0, + ZEROTH ), + TimeScaleOuter.of(), + CHICKENS, + GEORGIA, + KG_H ); + + TimeSeries> firstSeries = + new Builder>().setMetadata( firstMetadata ) + .addEvent( first ) + .build(); + + CrossPairs, Pair> cp = this.instance.apply( List.of( firstSeries ), + List.of() ); + + assertAll( () -> assertTrue( cp.getFirstPairs().isEmpty() ), + () -> assertTrue( cp.getSecondPairs().isEmpty() ) ); + } + @Test void testCrossPairProducesSymmetricallyShapedPairs() {