Files
aicyclingcoach-go/internal/types/downsample.go
2025-09-17 17:30:18 -07:00

85 lines
1.9 KiB
Go

package types
import "time"
type DownsampledPoint struct {
Timestamp time.Time
Value float64
}
// DownsampleLTTB implements the Largest Triangle Three Buckets algorithm
// for time series downsampling. This is a corrected version that properly
// uses the current module path.
func DownsampleLTTB(data []float64, timestamps []time.Time, threshold int) []DownsampledPoint {
if len(data) != len(timestamps) {
panic("data and timestamps must be same length")
}
if threshold >= len(data) || threshold <= 0 {
result := make([]DownsampledPoint, len(data))
for i := range data {
result[i] = DownsampledPoint{
Timestamp: timestamps[i],
Value: data[i],
}
}
return result
}
sampled := make([]DownsampledPoint, threshold)
sampled[0] = DownsampledPoint{Timestamp: timestamps[0], Value: data[0]}
bucketSize := float64(len(data)-2) / float64(threshold-2)
a := 0
for i := 0; i < threshold-2; i++ {
avgRangeStart := int(float64(i+1)*bucketSize) + 1
avgRangeEnd := int(float64(i+2)*bucketSize) + 1
if avgRangeEnd > len(data) {
avgRangeEnd = len(data)
}
var avgRange float64
for j := avgRangeStart; j < avgRangeEnd; j++ {
avgRange += data[j]
}
avgRange /= float64(avgRangeEnd - avgRangeStart)
rangeOffs := int(float64(i)*bucketSize) + 1
rangeTo := int(float64(i+1)*bucketSize) + 1
if rangeTo > len(data) {
rangeTo = len(data)
}
maxArea := -1.0
nextAAt := 0
for j := rangeOffs; j < rangeTo; j++ {
area := areaSize(
data[a],
data[j],
avgRange,
)
if area > maxArea {
maxArea = area
nextAAt = j
}
}
sampled[i+1] = DownsampledPoint{
Timestamp: timestamps[nextAAt],
Value: data[nextAAt],
}
a = nextAAt
}
sampled[threshold-1] = DownsampledPoint{
Timestamp: timestamps[len(timestamps)-1],
Value: data[len(data)-1],
}
return sampled
}
func areaSize(a, b, avg float64) float64 {
return (a-avg)*(a-avg) + (b-avg)*(b-avg)
}