changeset 3027:84e6577a474b

Fairway availability: More robust time and value interpolations including corner cases. Still TODO: Distribute to classes.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Fri, 12 Apr 2019 12:11:40 +0200
parents 619f03a0062a
children 188fb0133e50
files pkg/common/time.go pkg/controllers/bottlenecks.go
diffstat 2 files changed, 155 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/pkg/common/time.go	Fri Apr 12 11:49:08 2019 +0200
+++ b/pkg/common/time.go	Fri Apr 12 12:11:40 2019 +0200
@@ -14,7 +14,10 @@
 
 package common
 
-import "time"
+import (
+	"math"
+	"time"
+)
 
 const (
 	// time.RFC3339 equals "simplified ISO format as defined by ECMA-262"
@@ -23,3 +26,94 @@
 	TimeFormat = time.RFC3339
 	DateFormat = "2006-01-02"
 )
+
+type ValueRangeKind int
+
+const (
+	ValueBelow  ValueRangeKind = -1
+	ValueInside ValueRangeKind = 0
+	ValueAbove  ValueRangeKind = +1
+)
+
+func InterpolateValueByTime(t1 time.Time, m1 float64, t2 time.Time, m2 float64) func(time.Time) (float64, ValueRangeKind) {
+
+	// f(t1) = m1
+	// f(t2) = m2
+	// m1 = t1*a + b <=> b = m1 - t1*a
+	// m2 = t2*a + b
+	// m1 - m2 = a*(t1 - t2)
+	// a = (m1 - m2)/(t1 - t2) for t1 != t2
+
+	if t1 == t2 {
+		return func(t time.Time) (float64, ValueRangeKind) {
+			switch {
+			case t.Before(t1):
+				return 0, ValueBelow
+			case t.After(t1):
+				return 0, ValueAbove
+			default:
+				return m1 + (m2-m1)/2, ValueInside
+			}
+		}
+	}
+	var min, max time.Time
+	if t1.Before(t2) {
+		min, max = t1, t2
+	} else {
+		min, max = t2, t1
+	}
+
+	a := (m1 - m2) / t1.Sub(t2).Seconds()
+	b := m1 - a*float64(t1.Unix())
+
+	return func(t time.Time) (float64, ValueRangeKind) {
+		switch {
+		case t.Before(min):
+			return 0, ValueBelow
+		case t.After(max):
+			return 0, ValueAbove
+		default:
+			return a*float64(t.Unix()) + b, ValueInside
+		}
+	}
+}
+
+func InterpolateTimeByValue(t1 time.Time, m1 float64, t2 time.Time, m2 float64) func(float64) (time.Time, ValueRangeKind) {
+
+	// f(m1) = t1
+	// f(m2) = t2
+	// t1 = m1*a + b <=> b = t1 - m1*a
+	// t2 = m2*a + b
+
+	// t1 - t2 = a*(m1 - m2)
+	// a = (t1-t2)/(m1 - m2) for m1 != m2
+
+	if m1 == m2 {
+		return func(m float64) (time.Time, ValueRangeKind) {
+			switch {
+			case m < m1:
+				return time.Time{}, ValueBelow
+			case m > m1:
+				return time.Time{}, ValueAbove
+			default:
+				return t1.Add(t2.Sub(t1) / 2), ValueInside
+			}
+		}
+	}
+
+	min, max := math.Min(m1, m2), math.Max(m1, m2)
+
+	a := t1.Sub(t2).Seconds() / (m1 - m2)
+	b := float64(t1.Unix()) - m1*a
+
+	return func(m float64) (time.Time, ValueRangeKind) {
+		switch {
+		case m < min:
+			return time.Time{}, ValueBelow
+		case m > max:
+			return time.Time{}, ValueAbove
+		default:
+			return time.Unix(int64(math.Ceil(m*a+b)), 0), ValueInside
+		}
+	}
+}
--- a/pkg/controllers/bottlenecks.go	Fri Apr 12 11:49:08 2019 +0200
+++ b/pkg/controllers/bottlenecks.go	Fri Apr 12 12:11:40 2019 +0200
@@ -107,79 +107,86 @@
 	return b
 }
 
-func interpolate(
-	m1, m2 *availMeasurement,
-	diff time.Duration,
-) int {
-	tdiff := m2.when.Sub(m1.when)
-
-	// f(0)     = m1.value
-	// f(tdiff) = m2.value
-	// f(x) = m*x + b
-	// m1.value = m*0 + b     <=> b = m1.value
-	// m2.value = m*tdiff + b <=> m = (m2.value - b)/tdiff
-	// f(diff) = diff*(m2.value - m1.value)/tdiff + m1.value
-
-	return int(diff*time.Duration(m2.value-m1.value)/tdiff) + m1.value
-}
-
 func classifyAvailMeasurements(
 	from, to time.Time,
 	measurements []availMeasurement,
 	classes []availReferenceValue,
 ) []time.Duration {
 
-	results := make([]time.Duration, len(classes)+2)
-
-	if from.Before(measurements[0].when) {
-		results[len(results)-1] = measurements[0].when.Sub(from)
-		from = measurements[0].when
+	type classValues struct {
+		when time.Time
+		kind common.ValueRangeKind
 	}
 
-	if to.After(measurements[len(measurements)-1].when) {
-		results[len(results)-1] += to.Sub(measurements[len(measurements)-1].when)
-		to = measurements[len(measurements)-1].when
+	var invalid time.Duration
+
+	cvs := make([]classValues, len(classes))
+
+	classify := func(v func(float64) (time.Time, common.ValueRangeKind)) {
+		for j := range classes {
+			cvs[j].when, cvs[j].kind = v(float64(classes[j].value))
+		}
 	}
 
+	valInt := func(p1, p2 *availMeasurement) func(time.Time) (float64, common.ValueRangeKind) {
+		return common.InterpolateValueByTime(
+			p1.when, float64(p1.value),
+			p2.when, float64(p2.value),
+		)
+	}
+
+pairs:
 	for i := 0; i < len(measurements)-1; i++ {
 		p1 := &measurements[i]
 		p2 := &measurements[i+1]
-		tdiff := p2.when.Sub(p1.when)
-		if tdiff <= 0 {
-			continue
-		}
+
+		switch {
+		case !p2.when.After(p2.when):
+			// Segment invalid
+			continue pairs
 
-		if from.After(p2.when) || to.Before(p1.when) {
-			continue
-		}
+		case p1.when.After(to) || p2.when.Before(from):
+			// Segment complete outside.
+			continue pairs
 
-		if from.After(p1.when) {
-			tdiff2 := from.Sub(p1.when)
-			vf := interpolate(p1, p2, tdiff2)
-			p1 = &availMeasurement{when: from, value: vf}
-			tdiff = p2.when.Sub(from)
-		}
+		case p1.when.After(from) && p2.when.Before(to):
+			// (from-to) is complete inside segment.
+			invalid += p1.when.Sub(from)
+			invalid += to.Sub(p2.when)
+			v := valInt(p1, p2)
+			f, _ := v(from)
+			t, _ := v(to)
+			classify(common.InterpolateTimeByValue(from, f, to, t))
 
-		if to.Before(p2.when) {
-			tdiff2 := p2.when.Sub(to)
-			vt := interpolate(p1, p2, tdiff2)
-			p2 = &availMeasurement{when: to, value: vt}
-			tdiff = p2.when.Sub(p1.when)
-		}
+		case p1.when.After(from):
+			// from is inside segment
+			invalid += p1.when.Sub(from)
+			f, _ := valInt(p1, p2)(from)
+			classify(common.InterpolateTimeByValue(
+				from, f,
+				p2.when, float64(p2.value),
+			))
 
-		if max(p1.value, p2.value) <= classes[0].value {
-			results[0] += tdiff
-			continue
+		case p2.when.Before(to):
+			// to is inside segment
+			invalid += to.Sub(p2.when)
+			t, _ := valInt(p1, p2)(to)
+			classify(common.InterpolateTimeByValue(
+				p1.when, float64(p1.value),
+				to, t,
+			))
+
+		case !p1.when.Before(from) && !to.After(p2.when):
+			// Segment complete inside.
+			classify(common.InterpolateTimeByValue(
+				p1.when, float64(p1.value),
+				p2.when, float64(p2.value),
+			))
 		}
-		if min(p1.value, p2.value) > classes[len(classes)-1].value {
-			results[len(results)-2] += tdiff
-			continue
-		}
-
-		// TODO: Do the real classes.
+		// TODO: Distribute to classes.
 	}
 
-	return results
+	return nil
 }
 
 func bottleneckAvailabilty(