# HG changeset patch # User Sascha L. Teichmann # Date 1555865940 -7200 # Node ID 09d1ffce3d005a46b68b8932c793fb532571003c # Parent 014be2194bd1aa3fa43fe1c2a4f8db6568357bd2 Reworked Nash-Sutcliffe calculation. diff -r 014be2194bd1 -r 09d1ffce3d00 pkg/common/nashsutcliffe.go --- 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 diff -r 014be2194bd1 -r 09d1ffce3d00 pkg/controllers/gauges.go --- 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{