diff --git a/src/wres/pipeline/pooling/PoolFactory.java b/src/wres/pipeline/pooling/PoolFactory.java index d8aa768c9..0651b40db 100644 --- a/src/wres/pipeline/pooling/PoolFactory.java +++ b/src/wres/pipeline/pooling/PoolFactory.java @@ -87,6 +87,7 @@ import wres.statistics.generated.GeometryTuple; import wres.statistics.generated.SummaryStatistic; import wres.statistics.generated.TimeScale; +import wres.statistics.generated.TimeWindow; /** * A factory class for generating the pools of pairs associated with an evaluation. @@ -438,11 +439,12 @@ public List getPoolRequests( Evaluation evaluation ) // Get the desired timescale TimeScaleOuter desiredTimeScale = innerProject.getDesiredTimeScale(); - // Get the time windows and sort them - Set timeWindows = DeclarationUtilities.getTimeWindows( declaration ) - .stream() - .map( TimeWindowOuter::of ) - .collect( Collectors.toCollection( TreeSet::new ) ); + // Get the declared time windows and sort them + Set timeWindows = this.getTimeWindows( declaration, + innerProject ) + .stream() + .map( TimeWindowOuter::of ) + .collect( Collectors.toCollection( TreeSet::new ) ); return featureGroups.stream() .flatMap( nextGroup -> this.getPoolRequests( evaluation, @@ -454,6 +456,55 @@ public List getPoolRequests( Evaluation evaluation ) .toList(); } + /** + * Generates the time windows for evaluation, including both the declared time windows from a pool sequence or + * explicit list of time pools and those associated with event detection. + * + * @param declaration the declaration + * @param project the project + * @return the time windows + */ + + private Set getTimeWindows( EvaluationDeclaration declaration, + Project project ) + { + // Declared time windows + Set timeWindows = DeclarationUtilities.getTimeWindows( declaration ); + + // Time windows associated with event detection + if ( Objects.nonNull( declaration.eventDetection() ) ) + { + Set events = this.doEventDetection( project ); + + // Add the lead time and reference date constraints to each event, if defined + if ( !timeWindows.isEmpty() ) + { + Set adjustedEvents = new HashSet<>(); + for ( TimeWindow next : events ) + { + for ( TimeWindow adjust : timeWindows ) + { + TimeWindow adjusted = next.toBuilder() + .setEarliestReferenceTime( adjust.getEarliestReferenceTime() ) + .setLatestReferenceTime( adjust.getLatestReferenceTime() ) + .setEarliestLeadDuration( adjust.getEarliestLeadDuration() ) + .setLatestLeadDuration( adjust.getLatestLeadDuration() ) + .build(); + adjustedEvents.add( adjusted ); + } + + timeWindows = Collections.unmodifiableSet( adjustedEvents ); + } + } + else + { + timeWindows = events; + } + } + + return timeWindows; + } + /** *

Create pools for single-valued data. This method will attempt to retrieve and re-use data that is common to * multiple pools. Thus, it is generally better to provide a list of pool requests that represent connected pools, @@ -1066,6 +1117,21 @@ private List>>>> return this.getComposedSuppliers( poolRequests, rawSuppliers ); } + /** + * Performs event detection on one or more declared time-series datasets. + * @param project the project whose time-series data should be used + * @return the detected events + */ + private Set doEventDetection( Project project ) + { + LOGGER.debug( "Performing event detection." ); + + + + // Unbounded time window, placeholder + return Set.of( wres.statistics.MessageFactory.getTimeWindow() ); + } + /** * Composes the raw suppliers with feature group information based on the pool requests, which are ordered * identically to the suppliers. diff --git a/wres-config/src/wres/config/yaml/DeclarationInterpolator.java b/wres-config/src/wres/config/yaml/DeclarationInterpolator.java index 884cc61e0..b65dcfb8a 100644 --- a/wres-config/src/wres/config/yaml/DeclarationInterpolator.java +++ b/wres-config/src/wres/config/yaml/DeclarationInterpolator.java @@ -943,14 +943,14 @@ private static Outputs.GraphicFormat getGraphicsFormatOptions( Outputs.GraphicFo // Reference date pools? if ( ( Objects.nonNull( builder.referenceDatePools() ) - || DeclarationInterpolator.hasExplicitReferenceDatePools( builder.timeWindows() ) ) + || DeclarationInterpolator.hasExplicitReferenceDatePools( builder.timePools() ) ) && options.getShape() == Outputs.GraphicFormat.GraphicShape.DEFAULT ) { newOptions.setShape( Outputs.GraphicFormat.GraphicShape.ISSUED_DATE_POOLS ); } // Valid date pools? else if ( ( Objects.nonNull( builder.validDatePools() ) - || DeclarationInterpolator.hasExplicitValidDatePools( builder.timeWindows() ) ) + || DeclarationInterpolator.hasExplicitValidDatePools( builder.timePools() ) ) && options.getShape() == Outputs.GraphicFormat.GraphicShape.DEFAULT ) { newOptions.setShape( Outputs.GraphicFormat.GraphicShape.VALID_DATE_POOLS ); @@ -1246,7 +1246,7 @@ private static void interpolateOutputFormatsWhenNoneDeclared( EvaluationDeclarat */ private static void interpolateTimeWindows( EvaluationDeclarationBuilder builder ) { - Set timeWindows = builder.timeWindows(); + Set timeWindows = builder.timePools(); Set adjustedTimeWindows = new HashSet<>(); // Set the earliest and latest lead durations @@ -1303,7 +1303,7 @@ private static void interpolateTimeWindows( EvaluationDeclarationBuilder builder adjustedTimeWindows.add( nextAdjusted ); } - builder.timeWindows( adjustedTimeWindows ); + builder.timePools( adjustedTimeWindows ); } /** diff --git a/wres-config/src/wres/config/yaml/DeclarationUtilities.java b/wres-config/src/wres/config/yaml/DeclarationUtilities.java index 0b9a2d54c..31942ae6c 100644 --- a/wres-config/src/wres/config/yaml/DeclarationUtilities.java +++ b/wres-config/src/wres/config/yaml/DeclarationUtilities.java @@ -111,8 +111,9 @@ public class DeclarationUtilities + "declaration."; /** - * Consumes a {@link EvaluationDeclaration} and returns a {@link Set} of {@link TimeWindow} for evaluation. - * Returns at least one {@link TimeWindow}. + * Consumes a {@link EvaluationDeclaration} and returns a {@link Set} of {@link TimeWindow} for evaluation. Only + * returns the time windows that can be inferred from the declaration alone and not from any ingested data sources, + * notably those associated with event detection. * * @param declaration the declaration, cannot be null * @return a set of one or more time windows for evaluation @@ -134,60 +135,12 @@ public static Set getTimeWindows( EvaluationDeclaration declaration || Objects.nonNull( referenceDatesPools ) || Objects.nonNull( validDatesPools ) ) { - // All dimensions - if ( Objects.nonNull( referenceDatesPools ) && Objects.nonNull( validDatesPools ) - && Objects.nonNull( leadDurationPools ) ) - { - LOGGER.debug( "Building time windows for reference dates and valid dates and lead durations." ); - - timeWindows = DeclarationUtilities.getReferenceDatesValidDatesAndLeadDurationTimeWindows( declaration ); - } - // Reference dates and valid dates - else if ( Objects.nonNull( referenceDatesPools ) && Objects.nonNull( validDatesPools ) ) - { - LOGGER.debug( "Building time windows for reference dates and valid dates." ); - - timeWindows = DeclarationUtilities.getReferenceDatesAndValidDatesTimeWindows( declaration ); - } - // Reference dates and lead durations - else if ( Objects.nonNull( referenceDatesPools ) && Objects.nonNull( leadDurationPools ) ) - { - LOGGER.debug( "Building time windows for reference dates and lead durations." ); - - timeWindows = DeclarationUtilities.getReferenceDatesAndLeadDurationTimeWindows( declaration ); - } - // Valid dates and lead durations - else if ( Objects.nonNull( validDatesPools ) && Objects.nonNull( leadDurationPools ) ) - { - LOGGER.debug( "Building time windows for valid dates and lead durations." ); - - timeWindows = DeclarationUtilities.getValidDatesAndLeadDurationTimeWindows( declaration ); - } - // Reference dates - else if ( Objects.nonNull( referenceDatesPools ) ) - { - LOGGER.debug( "Building time windows for reference dates." ); - - timeWindows = DeclarationUtilities.getReferenceDatesTimeWindows( declaration ); - } - // Lead durations - else if ( Objects.nonNull( leadDurationPools ) ) - { - LOGGER.debug( "Building time windows for lead durations." ); - - timeWindows = DeclarationUtilities.getLeadDurationTimeWindows( declaration ); - } - // Valid dates - else - { - LOGGER.debug( "Building time windows for valid dates." ); - - timeWindows = DeclarationUtilities.getValidDatesTimeWindows( declaration ); - } + timeWindows = DeclarationUtilities.getTimeWindowsFromPoolSequence( declaration ); } - // One big pool if no explicitly declared time windows - else if ( declaration.timeWindows() - .isEmpty() ) + // One big pool if no explicitly declared time windows and no event detection + else if ( declaration.timePools() + .isEmpty() + && Objects.isNull( declaration.eventDetection() ) ) { LOGGER.debug( "Building one big time window." ); @@ -198,10 +151,10 @@ else if ( declaration.timeWindows() Set finalWindows = new HashSet<>( timeWindows ); LOGGER.debug( "Added {} explicitly declared time pools to the overall group of time pools.", - declaration.timeWindows() + declaration.timePools() .size() ); - finalWindows.addAll( declaration.timeWindows() ); + finalWindows.addAll( declaration.timePools() ); return Collections.unmodifiableSet( finalWindows ); } @@ -1499,6 +1452,78 @@ static String getFirstLine( String lines ) return line; } + /** + * Generates time windows from an explicit pool sequence. + * + * @param declaration the declaration + * @return the time windows + */ + private static Set getTimeWindowsFromPoolSequence( EvaluationDeclaration declaration ) + { + TimePools leadDurationPools = declaration.leadTimePools(); + TimePools referenceDatesPools = declaration.referenceDatePools(); + TimePools validDatesPools = declaration.validDatePools(); + + Set timeWindows; + + // All dimensions + if ( Objects.nonNull( referenceDatesPools ) + && Objects.nonNull( validDatesPools ) + && Objects.nonNull( leadDurationPools ) ) + { + LOGGER.debug( "Building time windows for reference dates and valid dates and lead durations." ); + + timeWindows = DeclarationUtilities.getReferenceDatesValidDatesAndLeadDurationTimeWindows( declaration ); + } + // Reference dates and valid dates + else if ( Objects.nonNull( referenceDatesPools ) + && Objects.nonNull( validDatesPools ) ) + { + LOGGER.debug( "Building time windows for reference dates and valid dates." ); + + timeWindows = DeclarationUtilities.getReferenceDatesAndValidDatesTimeWindows( declaration ); + } + // Reference dates and lead durations + else if ( Objects.nonNull( referenceDatesPools ) + && Objects.nonNull( leadDurationPools ) ) + { + LOGGER.debug( "Building time windows for reference dates and lead durations." ); + + timeWindows = DeclarationUtilities.getReferenceDatesAndLeadDurationTimeWindows( declaration ); + } + // Valid dates and lead durations + else if ( Objects.nonNull( validDatesPools ) + && Objects.nonNull( leadDurationPools ) ) + { + LOGGER.debug( "Building time windows for valid dates and lead durations." ); + + timeWindows = DeclarationUtilities.getValidDatesAndLeadDurationTimeWindows( declaration ); + } + // Reference dates + else if ( Objects.nonNull( referenceDatesPools ) ) + { + LOGGER.debug( "Building time windows for reference dates." ); + + timeWindows = DeclarationUtilities.getReferenceDatesTimeWindows( declaration ); + } + // Lead durations + else if ( Objects.nonNull( leadDurationPools ) ) + { + LOGGER.debug( "Building time windows for lead durations." ); + + timeWindows = DeclarationUtilities.getLeadDurationTimeWindows( declaration ); + } + // Valid dates + else + { + LOGGER.debug( "Building time windows for valid dates." ); + + timeWindows = DeclarationUtilities.getValidDatesTimeWindows( declaration ); + } + + return timeWindows; + } + /** * @param builder the builder * @param groupType the group the metric should be part of diff --git a/wres-config/src/wres/config/yaml/DeclarationValidator.java b/wres-config/src/wres/config/yaml/DeclarationValidator.java index 9fc272e24..c38474acb 100644 --- a/wres-config/src/wres/config/yaml/DeclarationValidator.java +++ b/wres-config/src/wres/config/yaml/DeclarationValidator.java @@ -363,6 +363,9 @@ public static List validate( EvaluationDeclaration declar // Check that the time pools are valid List pools = DeclarationValidator.timePoolsAreValid( declaration ); events.addAll( pools ); + // Check that the event detection is valid + List eventDetection = DeclarationValidator.eventDetectionIsValid( declaration ); + events.addAll( eventDetection ); // Check that the feature declaration is valid List features = DeclarationValidator.featuresAreValid( declaration ); events.addAll( features ); @@ -2209,7 +2212,7 @@ private static List timePoolsAreValid( EvaluationDeclarat generated.add( "'lead_time_pools'" ); } - if ( !declaration.timeWindows() + if ( !declaration.timePools() .isEmpty() && !generated.isEmpty() ) { @@ -2231,6 +2234,105 @@ private static List timePoolsAreValid( EvaluationDeclarat return Collections.unmodifiableList( events ); } + /** + * Checks that any event detection is valid. + * @param declaration the evaluation declaration + * @return the validation events encountered + */ + private static List eventDetectionIsValid( EvaluationDeclaration declaration ) + { + if ( Objects.isNull( declaration.eventDetection() ) ) + { + LOGGER.debug( "No event detection declaration to validate." ); + + return List.of(); + } + + List events = new ArrayList<>(); + + // Error when also declaring valid date pools + if ( Objects.nonNull( declaration.validDatePools() ) ) + { + EvaluationStatusEvent error + = EvaluationStatusEvent.newBuilder() + .setStatusLevel( StatusLevel.ERROR ) + .setEventMessage( "Event detection was declared alongside valid date pools, " + + "which is not allowed because event detection " + + "also generates valid date pools. Please remove the " + + "declaration of either 'event_detection' or " + + "'valid_date_pools' and try again." ) + .build(); + events.add( error ); + } + + // Error when also declaring explicit time pools + if ( !declaration.timePools() + .isEmpty() ) + { + EvaluationStatusEvent error + = EvaluationStatusEvent.newBuilder() + .setStatusLevel( StatusLevel.ERROR ) + .setEventMessage( "Event detection was declared alongside explicit time " + + "pools, which is not allowed because event detection " + + "also generates time pools. Please remove the " + + "declaration of either 'event_detection' or " + + "'time_pools' and try again." ) + .build(); + events.add( error ); + } + + // Error when declaring feature groups or a feature service with pooling across features within a group + if ( DeclarationUtilities.hasFeatureGroups( declaration ) ) + { + EvaluationStatusEvent error + = EvaluationStatusEvent.newBuilder() + .setStatusLevel( StatusLevel.ERROR ) + .setEventMessage( "Event detection was declared alongside feature groups, " + + "which is not currently supported. Please remove the " + + "declaration of 'feature_groups' or a 'feature_service' " + + "with a 'group' whose features will be pooled together " + + "(i.e., 'pool: true'), as applicable, and try again. " + + "Alternatively, please remove 'event_detection'. Hint: " + + "summary statistics are supported alongside event " + + "detection if your goal is to compute statistics across " + + "events for multiple geographic features." ) + .build(); + events.add( error ); + } + + // Warning when declaring other types of explicit pool + if ( Objects.nonNull( declaration.leadTimePools() ) ) + { + EvaluationStatusEvent warn + = EvaluationStatusEvent.newBuilder() + .setStatusLevel( StatusLevel.WARN ) + .setEventMessage( "Event detection was declared alongside lead time pools, " + + "which is allowed, but may not be intended. A separate " + + "pool will be generated for each combination of event " + + "and lead time pool. If this is not intended, please " + + "remove either 'event_detection' or 'lead_time_pools' " + + "and try again." ) + .build(); + events.add( warn ); + } + if ( Objects.nonNull( declaration.referenceDatePools() ) ) + { + EvaluationStatusEvent warn + = EvaluationStatusEvent.newBuilder() + .setStatusLevel( StatusLevel.WARN ) + .setEventMessage( "Event detection was declared alongside reference date " + + "pools, which is allowed, but may not be intended. A " + + "separate pool will be generated for each combination " + + "of event and reference date pool. If this is not " + + "intended, please remove either 'event_detection' or " + + "'reference_date_pools' and try again." ) + .build(); + events.add( warn ); + } + + return Collections.unmodifiableList( events ); + } + /** * Checks that the feature declaration is valid. * @param declaration the evaluation declaration diff --git a/wres-config/src/wres/config/yaml/components/EvaluationDeclaration.java b/wres-config/src/wres/config/yaml/components/EvaluationDeclaration.java index 8b89b9c83..082b3fcd0 100644 --- a/wres-config/src/wres/config/yaml/components/EvaluationDeclaration.java +++ b/wres-config/src/wres/config/yaml/components/EvaluationDeclaration.java @@ -50,6 +50,7 @@ * @param spatialMask a spatial mask * @param unit the measurement unit * @param unitAliases aliases for measurement units + * @param timePools explicit time pools * @param referenceDates reference dates * @param referenceDatePools reference date pools * @param validDates valid dates @@ -92,7 +93,7 @@ public record EvaluationDeclaration( @JsonProperty( "label" ) String label, @JsonProperty( "unit" ) String unit, @JsonProperty( "unit_aliases" ) Set unitAliases, @JsonDeserialize( using = TimeWindowDeserializer.class ) - @JsonProperty( "time_pools" ) Set timeWindows, + @JsonProperty( "time_pools" ) Set timePools, @JsonProperty( "reference_dates" ) TimeInterval referenceDates, @JsonProperty( "reference_date_pools" ) TimePools referenceDatePools, @JsonProperty( "valid_dates" ) TimeInterval validDates, @@ -156,6 +157,7 @@ public record EvaluationDeclaration( @JsonProperty( "label" ) String label, * @param spatialMask a spatial mask * @param unit the measurement unit * @param unitAliases aliases for measurement units + * @param timePools explicit time pools * @param referenceDates reference dates * @param referenceDatePools reference date pools * @param validDates valid dates @@ -202,9 +204,9 @@ public record EvaluationDeclaration( @JsonProperty( "label" ) String label, unitAliases = this.emptyOrUnmodifiableSet( unitAliases, "unit aliases" ); } - if ( Objects.isNull( timeWindows ) ) + if ( Objects.isNull( timePools ) ) { - timeWindows = this.emptyOrUnmodifiableSet( timeWindows, "time pools" ); + timePools = this.emptyOrUnmodifiableSet( timePools, "time pools" ); } if ( Objects.isNull( rescaleLenience ) ) diff --git a/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java b/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java index f14902fcc..9dc8df81f 100644 --- a/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java +++ b/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java @@ -2353,7 +2353,7 @@ void testDeserializeWithExplicitTimePools() throws IOException Set expected = Set.of( expectedOne, expectedTwo ); - assertEquals( expected, actual.timeWindows() ); + assertEquals( expected, actual.timePools() ); } @Test @@ -2394,7 +2394,7 @@ void testDeserializeWithExplicitTimePoolsThatAreSparselyDeclared() throws IOExce Set expected = Set.of( expectedOne, expectedTwo ); - assertEquals( expected, actual.timeWindows() ); + assertEquals( expected, actual.timePools() ); } @Test diff --git a/wres-config/test/wres/config/yaml/DeclarationInterpolatorTest.java b/wres-config/test/wres/config/yaml/DeclarationInterpolatorTest.java index b18665f45..46763df0f 100644 --- a/wres-config/test/wres/config/yaml/DeclarationInterpolatorTest.java +++ b/wres-config/test/wres/config/yaml/DeclarationInterpolatorTest.java @@ -1654,12 +1654,12 @@ void testInterpolateMissingTimeWindowComponents() .leadTimes( new LeadTimeInterval( Duration.ofHours( 1 ), Duration.ofHours( 100 ) ) ) .validDates( new TimeInterval( validOne, validTwo ) ) - .timeWindows( timeWindows ) + .timePools( timeWindows ) .build(); EvaluationDeclaration interpolated = DeclarationInterpolator.interpolate( declaration, false ); - Set actual = interpolated.timeWindows(); + Set actual = interpolated.timePools(); TimeWindow expectedOne = TimeWindow.newBuilder() .setEarliestValidTime( MessageFactory.getTimestamp( validWindowOne ) ) diff --git a/wres-config/test/wres/config/yaml/DeclarationValidatorTest.java b/wres-config/test/wres/config/yaml/DeclarationValidatorTest.java index c1a944c3b..2b48db027 100644 --- a/wres-config/test/wres/config/yaml/DeclarationValidatorTest.java +++ b/wres-config/test/wres/config/yaml/DeclarationValidatorTest.java @@ -36,6 +36,9 @@ import wres.config.yaml.components.EnsembleFilter; import wres.config.yaml.components.EvaluationDeclaration; import wres.config.yaml.components.EvaluationDeclarationBuilder; +import wres.config.yaml.components.EventDetection; +import wres.config.yaml.components.EventDetectionBuilder; +import wres.config.yaml.components.EventDetectionDataset; import wres.config.yaml.components.FeatureAuthority; import wres.config.yaml.components.FeatureGroups; import wres.config.yaml.components.FeatureGroupsBuilder; @@ -67,10 +70,12 @@ import wres.config.yaml.components.TimeInterval; import wres.config.yaml.components.TimeIntervalBuilder; import wres.config.yaml.components.TimePools; +import wres.config.yaml.components.TimePoolsBuilder; import wres.config.yaml.components.TimeScaleBuilder; import wres.config.yaml.components.TimeScaleLenience; import wres.config.yaml.components.UnitAlias; import wres.config.yaml.components.Variable; +import wres.statistics.MessageFactory; import wres.statistics.generated.EvaluationStatus.EvaluationStatusEvent; import wres.statistics.generated.EvaluationStatus.EvaluationStatusEvent.StatusLevel; import wres.statistics.generated.Geometry; @@ -2644,6 +2649,100 @@ void testCovariatesWithInvalidFiltersProducesErrors() StatusLevel.ERROR ) ) ); } + @Test + void testExplicitPoolsAndEventDetectionProducesErrorAndWarnings() + { + EventDetection eventDetection = EventDetectionBuilder.builder() + .datasets( Set.of( EventDetectionDataset.OBSERVED ) ) + .build(); + TimePools validDatePools = TimePoolsBuilder.builder() + .period( java.time.Duration.ofHours( 1 ) ) + .build(); + Set timePools = Set.of( MessageFactory.getTimeWindow() ); + EvaluationDeclaration declaration + = EvaluationDeclarationBuilder.builder() + .left( this.defaultDataset ) + .right( this.defaultDataset ) + .validDates( new TimeInterval( Instant.MIN, Instant.MAX ) ) + .validDatePools( validDatePools ) + .referenceDatePools( validDatePools ) + .leadTimePools( validDatePools ) + .eventDetection( eventDetection ) + .timePools( timePools ) + .build(); + + List events = DeclarationValidator.validate( declaration ); + + assertAll( () -> assertTrue( DeclarationValidatorTest.contains( events, + "Event detection was declared alongside valid date " + + "pools, which is not allowed", + StatusLevel.ERROR ) ), + () -> assertTrue( DeclarationValidatorTest.contains( events, + "Event detection was declared alongside explicit " + + "time pools, which is not allowed", + StatusLevel.ERROR ) ), + () -> assertTrue( DeclarationValidatorTest.contains( events, + "Event detection was declared alongside lead time " + + "pools", + StatusLevel.WARN ) ), + () -> assertTrue( DeclarationValidatorTest.contains( events, + "Event detection was declared alongside reference date " + + "pools", + StatusLevel.WARN ) ) ); + } + + @Test + void testFeatureGroupAndEventDetectionProducesError() + { + EventDetection eventDetection = EventDetectionBuilder.builder() + .datasets( Set.of( EventDetectionDataset.OBSERVED ) ) + .build(); + + FeatureGroups featureGroups = FeatureGroupsBuilder.builder() + .build(); + + EvaluationDeclaration declaration + = EvaluationDeclarationBuilder.builder() + .left( this.defaultDataset ) + .right( this.defaultDataset ) + .eventDetection( eventDetection ) + .featureGroups( featureGroups ) + .build(); + + List events = DeclarationValidator.validate( declaration ); + + assertTrue( DeclarationValidatorTest.contains( events, + "Event detection was declared alongside feature groups, " + + "which is not currently supported", + StatusLevel.ERROR ) ); + } + + @Test + void testFeatureServiceGroupWithPoolingAndEventDetectionProducesError() + { + EventDetection eventDetection = EventDetectionBuilder.builder() + .datasets( Set.of( EventDetectionDataset.OBSERVED ) ) + .build(); + + FeatureServiceGroup group = new FeatureServiceGroup( "a", "b", true ); + FeatureService featureService = new FeatureService( URI.create( "http://foo.bar" ), Set.of( group ) ); + + EvaluationDeclaration declaration + = EvaluationDeclarationBuilder.builder() + .left( this.defaultDataset ) + .right( this.defaultDataset ) + .eventDetection( eventDetection ) + .featureService( featureService ) + .build(); + + List events = DeclarationValidator.validate( declaration ); + + assertTrue( DeclarationValidatorTest.contains( events, + "Event detection was declared alongside feature groups, " + + "which is not currently supported", + StatusLevel.ERROR ) ); + } + @Test void testInconsistentGraphicsOrientationAndPoolingDeclarationProducesError() throws IOException // NOSONAR { @@ -2684,7 +2783,7 @@ void testCombinationOfExplicitAndGeneratedPoolsProducesWarning() .left( this.defaultDataset ) .right( this.defaultDataset ) .leadTimePools( leadTimePools ) - .timeWindows( timeWindows ) + .timePools( timeWindows ) .build(); List events = DeclarationValidator.validate( declaration );