Skip to content
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

Issue385 #391

Merged
merged 2 commits into from
Jan 28, 2025
Merged
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
108 changes: 101 additions & 7 deletions src/wres/pipeline/pooling/EventsGenerator.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package wres.pipeline.pooling;

import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
Expand All @@ -19,6 +21,8 @@
import wres.config.yaml.components.EventDetection;
import wres.config.yaml.components.EventDetectionCombination;
import wres.config.yaml.components.EventDetectionDataset;
import wres.config.yaml.components.LeadTimeInterval;
import wres.config.yaml.components.TimeInterval;
import wres.config.yaml.components.TimeWindowAggregation;
import wres.datamodel.scale.TimeScaleOuter;
import wres.datamodel.space.Feature;
Expand All @@ -35,9 +39,11 @@
import wres.io.retrieving.RetrieverFactory;
import wres.statistics.MessageUtilities;
import wres.statistics.generated.TimeScale;
import wres.statistics.generated.TimeWindow;

/**
* Generates {@link TimeWindowOuter} corresponding to events from {@link TimeSeries}.
* Generates {@link TimeWindowOuter} corresponding to events from {@link TimeSeries} and combines them with any declared
* time windows and/or time constraints.
*
* @param leftUpscaler the upscaler for single-valued time-series with a left orientation
* @param rightUpscaler the upscaler for single-valued time-series with a right orientation
Expand Down Expand Up @@ -82,7 +88,9 @@ record EventsGenerator( TimeSeriesUpscaler<Double> leftUpscaler,
}

/**
* Performs event detection for one or more declared time-series datasets.
* Performs event detection for one or more declared time-series datasets, combining the detected events with any
* declared time windows.
*
* @param project the project whose time-series data should be used
* @param featureGroup the feature group to use for event detection
* @param eventRetriever the retriever for time-series data
Expand Down Expand Up @@ -242,10 +250,15 @@ Set<TimeWindowOuter> doEventDetection( Project project,
featureGroup.getName(),
combination );

return this.aggregateEvents( events,
detection.parameters()
.aggregation(),
featureGroup );
// Aggregate any intersecting events, as needed
Set<TimeWindowOuter> aggregated = this.aggregateEvents( events,
detection.parameters()
.aggregation(),
featureGroup );

// Adjust the time windows to account for existing time windows and other explicit time constraints
Set<TimeWindowOuter> declared = TimeWindowSlicer.getTimeWindows( declaration );
return this.adjustTimeWindows( aggregated, declared, declaration.referenceDates(), declaration.leadTimes() );
}

/**
Expand Down Expand Up @@ -354,6 +367,88 @@ private Set<TimeWindowOuter> doEventDetection( EventDetectionDetails details )
return Collections.unmodifiableSet( events );
}

/**
* Adjusts the time windows generated using event detection to reflect any declared time windows or associated
* time constraints.
*
* @param events the detected events
* @param declared the declared time windows, if any
* @param referenceTimes the declared reference time constraints, if any
* @param leadTimes the declared lead time constraints, if any
* @return the adjust time windows
*/

private Set<TimeWindowOuter> adjustTimeWindows( Set<TimeWindowOuter> events,
Set<TimeWindowOuter> declared,
TimeInterval referenceTimes,
LeadTimeInterval leadTimes )
{
Set<TimeWindowOuter> returnMe = new HashSet<>();

// Add any overall reference time or lead time boundaries into the events
for ( TimeWindowOuter nextEvent : events )
{
TimeWindowOuter nextAdjusted = nextEvent;

// Add the reference times, if required
if ( Objects.nonNull( referenceTimes ) )
{
Instant earliest = referenceTimes.minimum();
Instant latest = referenceTimes.maximum();
TimeWindow adjusted = nextEvent.getTimeWindow()
.toBuilder()
.setEarliestReferenceTime( MessageUtilities.getTimestamp( earliest ) )
.setLatestReferenceTime( MessageUtilities.getTimestamp( latest ) )
.build();
nextAdjusted = TimeWindowOuter.of( adjusted );
}

// Add the lead times, if required
if ( Objects.nonNull( leadTimes ) )
{
Duration earliest = leadTimes.minimum();
Duration latest = leadTimes.maximum();
TimeWindow adjusted = nextAdjusted.getTimeWindow()
.toBuilder()
.setEarliestLeadDuration( MessageUtilities.getDuration( earliest ) )
.setLatestLeadDuration( MessageUtilities.getDuration( latest ) )
.build();
nextAdjusted = TimeWindowOuter.of( adjusted );
}

returnMe.add( nextAdjusted );
}

// Merge any declared time window constraints into the event time windows, which will override any basic
// constraints imposed above
if ( !declared.isEmpty() )
{
Set<TimeWindowOuter> adjustedEvents = new HashSet<>();
for ( TimeWindowOuter next : returnMe )
{
for ( TimeWindowOuter adjust : declared )
{
TimeWindow adjusted = next.getTimeWindow()
.toBuilder()
.setEarliestReferenceTime( adjust.getTimeWindow()
.getEarliestReferenceTime() )
.setLatestReferenceTime( adjust.getTimeWindow()
.getLatestReferenceTime() )
.setEarliestLeadDuration( adjust.getTimeWindow()
.getEarliestLeadDuration() )
.setLatestLeadDuration( adjust.getTimeWindow()
.getLatestLeadDuration() )
.build();
adjustedEvents.add( TimeWindowOuter.of( adjusted ) );
}
}

returnMe = adjustedEvents;
}

return Collections.unmodifiableSet( returnMe );
}

/**
* Combines detected events using the prescribed {@link EventDetectionCombination} strategy, modifying the existing
* set of events in place.
Expand Down Expand Up @@ -491,7 +586,6 @@ private Set<TimeWindowOuter> doEventDetection( TimeSeries<Double> timeSeries,
+ "metadata: {}.", timeSeries.getEvents()
.size(), timeSeries.getMetadata() );

// Unbounded time window, placeholder
return this.eventDetector()
.detect( timeSeries );
}
Expand Down
33 changes: 3 additions & 30 deletions src/wres/pipeline/pooling/PoolFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@
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.
Expand Down Expand Up @@ -498,12 +497,12 @@ private Map<FeatureGroup, Set<TimeWindowOuter>> getTimeWindows( EvaluationDeclar
Set<FeatureGroup> featureGroups,
RetrieverFactory<Double, Double, Double> eventRetriever )
{
// Declared time windows
Set<TimeWindowOuter> timeWindows = TimeWindowSlicer.getTimeWindows( declaration );

// Time windows without event detection
if ( Objects.isNull( declaration.eventDetection() ) )
{
// Declared time windows
Set<TimeWindowOuter> timeWindows = TimeWindowSlicer.getTimeWindows( declaration );

return featureGroups.stream()
.collect( Collectors.toMap( Function.identity(),
g -> timeWindows ) );
Expand All @@ -516,32 +515,6 @@ private Map<FeatureGroup, Set<TimeWindowOuter>> getTimeWindows( EvaluationDeclar
Set<TimeWindowOuter> events = this.getEventsGenerator()
.doEventDetection( project, nextGroup, eventRetriever );

// Add the lead time and reference date constraints to each event, if defined
if ( !timeWindows.isEmpty() )
{
Set<TimeWindowOuter> adjustedEvents = new HashSet<>();
for ( TimeWindowOuter next : events )
{
for ( TimeWindowOuter adjust : timeWindows )
{
TimeWindow adjusted = next.getTimeWindow()
.toBuilder()
.setEarliestReferenceTime( adjust.getTimeWindow()
.getEarliestReferenceTime() )
.setLatestReferenceTime( adjust.getTimeWindow()
.getLatestReferenceTime() )
.setEarliestLeadDuration( adjust.getTimeWindow()
.getEarliestLeadDuration() )
.setLatestLeadDuration( adjust.getTimeWindow()
.getLatestLeadDuration() )
.build();
adjustedEvents.add( TimeWindowOuter.of( adjusted ) );
}
}

events = Collections.unmodifiableSet( adjustedEvents );
}

featurefulWindows.put( nextGroup, events );
}

Expand Down
95 changes: 95 additions & 0 deletions test/wres/pipeline/pooling/EventsGeneratorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
import wres.config.yaml.components.EventDetectionMethod;
import wres.config.yaml.components.EventDetectionParameters;
import wres.config.yaml.components.EventDetectionParametersBuilder;
import wres.config.yaml.components.LeadTimeInterval;
import wres.config.yaml.components.Source;
import wres.config.yaml.components.SourceBuilder;
import wres.config.yaml.components.TimeInterval;
import wres.config.yaml.components.TimeWindowAggregation;
import wres.config.yaml.components.Variable;
import wres.datamodel.scale.TimeScaleOuter;
Expand Down Expand Up @@ -633,6 +635,99 @@ void testEventDetectionWithUnionSelectsFourJointEventsFromFourMarginalEvents()
assertEquals( expected, actual );
}

@Test
void testEventDetectionAddsDeclaredTimeConstraintsToDetectedEvent()
{
TimeSeriesUpscaler<Double> upscaler = TimeSeriesOfDoubleUpscaler.of();
EventDetectionParameters parameters = EventDetectionParametersBuilder.builder()
.build();
EventDetector detector = EventDetectorFactory.getEventDetector( EventDetectionMethod.REGINA_OGDEN,
parameters );
String measurementUnit = "foo";
EventsGenerator generator = new EventsGenerator( upscaler,
upscaler,
upscaler,
upscaler,
measurementUnit,
detector );

TimeSeries<Double> timeSeriesOne = this.getTestTimeSeriesWithOffset( Duration.ZERO );

// Mock a retriever factory
Mockito.when( this.leftRetriever.get() )
.thenReturn( Stream.of( timeSeriesOne ) );
Mockito.when( this.retrieverFactory.getLeftRetriever( Mockito.anySet() ) )
.thenReturn( this.leftRetriever );

// Mock the sufficient elements of a project with two separate datasets for event detection
EventDetection eventDeclaration = EventDetectionBuilder.builder()
.method( EventDetectionMethod.REGINA_OGDEN )
.parameters( parameters )
.datasets( Set.of( EventDetectionDataset.OBSERVED ) )
.build();
// Reference time constraints
Instant earliestReference = Instant.parse( "2099-10-21T00:00:00Z" );
Instant latestReference = Instant.parse( "2101-10-21T00:00:00Z" );
TimeInterval referenceTimes = new TimeInterval( earliestReference, latestReference );

// Lead time constraints
Duration earliestLead = Duration.ofHours( 23 );
Duration latestLead = Duration.ofHours( 79 );
LeadTimeInterval leadTimes = new LeadTimeInterval( earliestLead, latestLead );

EvaluationDeclaration declaration = EvaluationDeclarationBuilder.builder()
.eventDetection( eventDeclaration )
.referenceDates( referenceTimes )
.leadTimes( leadTimes )
.build();

Geometry geometry = MessageUtilities.getGeometry( "foo" );
GeometryTuple geoTuple = MessageUtilities.getGeometryTuple( geometry, geometry, geometry );
GeometryGroup geoGroup = MessageUtilities.getGeometryGroup( null, geoTuple );
FeatureGroup groupOne = FeatureGroup.of( geoGroup );

Project project = Mockito.mock( Project.class );
Mockito.when( project.getFeatureGroups() )
.thenReturn( Set.of( groupOne ) );
Mockito.when( project.getDeclaration() )
.thenReturn( declaration );

Set<TimeWindowOuter> actual = generator.doEventDetection( project, groupOne, this.retrieverFactory );

Instant startOne = Instant.parse( "2079-12-03T03:00:00Z" );
Instant endOne = Instant.parse( "2079-12-03T05:00:00Z" );

TimeWindow expectedOne =
MessageUtilities.getTimeWindow()
.toBuilder()
.setEarliestValidTime( MessageUtilities.getTimestamp( startOne ) )
.setLatestValidTime( MessageUtilities.getTimestamp( endOne ) )
.setEarliestReferenceTime( MessageUtilities.getTimestamp( earliestReference ) )
.setLatestReferenceTime( MessageUtilities.getTimestamp( latestReference ) )
.setEarliestLeadDuration( MessageUtilities.getDuration( earliestLead ) )
.setLatestLeadDuration( MessageUtilities.getDuration( latestLead ) )
.build();

Instant startTwo = Instant.parse( "2079-12-03T09:00:00Z" );
Instant endTwo = Instant.parse( "2079-12-03T10:00:00Z" );

TimeWindow expectedTwo =
MessageUtilities.getTimeWindow()
.toBuilder()
.setEarliestValidTime( MessageUtilities.getTimestamp( startTwo ) )
.setLatestValidTime( MessageUtilities.getTimestamp( endTwo ) )
.setEarliestReferenceTime( MessageUtilities.getTimestamp( earliestReference ) )
.setLatestReferenceTime( MessageUtilities.getTimestamp( latestReference ) )
.setEarliestLeadDuration( MessageUtilities.getDuration( earliestLead ) )
.setLatestLeadDuration( MessageUtilities.getDuration( latestLead ) )
.build();

Set<TimeWindowOuter> expected = Set.of( TimeWindowOuter.of( expectedOne ),
TimeWindowOuter.of( expectedTwo ) );

assertEquals( expected, actual );
}

/**
* Generates a test time-series.
* @param offset the offset to apply
Expand Down
Loading