changeset 3088:09d1ffce3d00

Reworked Nash-Sutcliffe calculation.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Sun, 21 Apr 2019 18:59:00 +0200
parents 014be2194bd1
children 813309225e35
files pkg/common/nashsutcliffe.go pkg/controllers/gauges.go
diffstat 2 files changed, 127 insertions(+), 96 deletions(-) [+]
line wrap: on
line diff
--- a/pkg/common/nashsutcliffe.go	Sun Apr 21 11:14:20 2019 +0200
+++ b/pkg/common/nashsutcliffe.go	Sun Apr 21 18:59:00 2019 +0200
@@ -14,7 +14,6 @@
 package common
 
 import (
-	"math"
 	"sort"
 	"time"
 )
@@ -85,43 +84,7 @@
 	Observed  float64
 }
 
-func (m NSMeasurement) Valid() bool {
-	return !m.When.IsZero() && !math.IsNaN(m.Predicted) && !math.IsNaN(m.Observed)
-}
-
-func NashSutcliffeSort(measurements []NSMeasurement) {
-	sort.Slice(measurements, func(i, j int) bool {
-		return measurements[i].When.Before(measurements[j].When)
-	})
-}
-
-func NashSutcliffe(measurements []NSMeasurement, from, to time.Time) (float64, int) {
-
-	if len(measurements) == 0 {
-		return 0, 0
-	}
-
-	if to.Before(from) {
-		from, to = to, from
-	}
-
-	begin := sort.Search(len(measurements), func(i int) bool {
-		return !measurements[i].When.Before(from)
-	})
-	if begin >= len(measurements) {
-		return 0, 0
-	}
-
-	end := sort.Search(len(measurements), func(i int) bool {
-		return measurements[i].When.After(to)
-	})
-	if end >= len(measurements) {
-		end = len(measurements) - 1
-	}
-	if end <= begin {
-		return 0, 0
-	}
-	sample := measurements[begin:end]
+func NashSutcliffe(sample []NSMeasurement, from, to time.Time) (float64, int) {
 
 	if len(sample) == 0 {
 		return 0, 0
--- a/pkg/controllers/gauges.go	Sun Apr 21 11:14:20 2019 +0200
+++ b/pkg/controllers/gauges.go	Sun Apr 21 18:59:00 2019 +0200
@@ -14,6 +14,7 @@
 package controllers
 
 import (
+	"context"
 	"database/sql"
 	"encoding/csv"
 	"fmt"
@@ -38,6 +39,7 @@
 	selectPredictedObserveredSQL = `
 SELECT
   measure_date,
+  date_issue,
   predicted,
   water_level
 FROM waterway.gauge_measurements
@@ -406,6 +408,109 @@
 	}
 }
 
+func parseISRS(code string) (*models.Isrs, error) {
+	isrs, err := models.IsrsFromString(code)
+	if err != nil {
+		return nil, JSONError{
+			Code:    http.StatusBadRequest,
+			Message: fmt.Sprintf("error: Invalid ISRS code: %v", err),
+		}
+	}
+	return isrs, nil
+}
+
+type observedPredictedValues struct {
+	when      time.Time
+	observed  float64
+	predicted common.TimedValues
+}
+
+func loadNashSutcliffeData(
+	ctx context.Context,
+	conn *sql.Conn,
+	gauge *models.Isrs,
+	when time.Time,
+) ([]observedPredictedValues, error) {
+
+	var rows *sql.Rows
+	var err error
+	if rows, err = conn.QueryContext(
+		ctx,
+		selectPredictedObserveredSQL,
+		gauge.CountryCode,
+		gauge.LoCode,
+		gauge.FairwaySection,
+		gauge.Orc,
+		gauge.Hectometre,
+		when,
+	); err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var (
+		hasCurrent bool
+		current    observedPredictedValues
+		values     []observedPredictedValues
+	)
+
+	for rows.Next() {
+		var (
+			measureDate time.Time
+			issueDate   time.Time
+			predicted   bool
+			value       float64
+		)
+		if err := rows.Scan(
+			&measureDate,
+			&issueDate,
+			&predicted,
+			&value,
+		); err != nil {
+			return nil, err
+		}
+		measureDate = measureDate.UTC()
+		issueDate = issueDate.UTC()
+
+		if hasCurrent {
+			if !current.when.Equal(measureDate) {
+				if !math.IsNaN(current.observed) {
+					values = append(values, current)
+				}
+				current = observedPredictedValues{
+					observed: math.NaN(),
+					when:     measureDate,
+				}
+			}
+		} else {
+			hasCurrent = true
+			current = observedPredictedValues{
+				observed: math.NaN(),
+				when:     measureDate,
+			}
+		}
+
+		if predicted {
+			current.predicted = append(
+				current.predicted,
+				common.TimedValue{When: issueDate, Value: value},
+			)
+		} else {
+			current.observed = value
+		}
+	}
+
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	if hasCurrent && !math.IsNaN(current.observed) {
+		values = append(values, current)
+	}
+
+	return values, nil
+}
+
 func nashSutcliffe(
 	_ interface{},
 	req *http.Request,
@@ -414,11 +519,7 @@
 	gauge := mux.Vars(req)["gauge"]
 
 	var isrs *models.Isrs
-	if isrs, err = models.IsrsFromString(gauge); err != nil {
-		err = JSONError{
-			Code:    http.StatusBadRequest,
-			Message: fmt.Sprintf("error: Invalid ISRS code: %v", err),
-		}
+	if isrs, err = parseISRS(gauge); err != nil {
 		return
 	}
 
@@ -434,65 +535,17 @@
 	} else {
 		when = time.Now()
 	}
+	when = when.UTC()
 
 	ctx := req.Context()
 
-	var rows *sql.Rows
-	if rows, err = conn.QueryContext(
-		ctx,
-		selectPredictedObserveredSQL,
-		isrs.CountryCode,
-		isrs.LoCode,
-		isrs.FairwaySection,
-		isrs.Orc,
-		isrs.Hectometre,
-		when,
-	); err != nil {
-		return
-	}
-	defer rows.Close()
-
-	var measurements []common.NSMeasurement
-
-	invalid := common.NSMeasurement{
-		Predicted: math.NaN(),
-		Observed:  math.NaN(),
-	}
-	current := invalid
+	var values []observedPredictedValues
 
-	for rows.Next() {
-		var (
-			when      time.Time
-			predicted bool
-			value     float64
-		)
-		if err = rows.Scan(
-			&when,
-			&predicted,
-			&value,
-		); err != nil {
-			return
-		}
-		if !when.Equal(current.When) {
-			if current.Valid() {
-				measurements = append(measurements, current)
-			}
-			current = invalid
-		}
-		if predicted {
-			current.Predicted = value
-		} else {
-			current.Observed = value
-		}
-	}
-
-	if err = rows.Err(); err != nil {
+	if values, err = loadNashSutcliffeData(ctx, conn, isrs, when); err != nil {
 		return
 	}
 
-	if current.Valid() {
-		measurements = append(measurements, current)
-	}
+	log.Printf("info: found %d value(s) for Nash Sutcliffe.\n", len(values))
 
 	type coeff struct {
 		Value   float64 `json:"value"`
@@ -505,14 +558,29 @@
 		Coeffs []coeff           `json:"coeffs"`
 	}
 
+	var measurements []common.NSMeasurement
+
 	cs := make([]coeff, 3)
 	for i := range cs {
 		cs[i].Hours = (i + 1) * 24
+		back := when.Add(time.Duration(-cs[i].Hours) * time.Hour)
+
+		for j := range values {
+			if predicted, ok := values[j].predicted.Interpolate(back); ok {
+				measurements = append(measurements, common.NSMeasurement{
+					Observed:  values[i].observed,
+					Predicted: predicted,
+				})
+			}
+		}
+
 		cs[i].Value, cs[i].Samples = common.NashSutcliffe(
 			measurements,
 			when,
-			when.Add(time.Duration(-cs[i].Hours)*time.Hour),
+			back,
 		)
+
+		measurements = measurements[:0]
 	}
 
 	jr = JSONResult{