nidus-sync/time.go
Eli Ribble 1395e3d3ac Remove old FieldSeeker tables, use v2 generated tables.
This requires a bunch of changes since the types on these tables are
much closer to the underlying types of the Fieldseeker data we are
getting back from the API.

I now need to use proper UUID types everywhere, which means I had to
modify the bob gen config to consistently use google UUID, my UUID
library of choice.

I also had to add the organization_id to all the fieldseeker tables
since we rely on them existing for some of our compound queries.

There were some changes to the API type signatures to get things to
build. I may yet regret those.
2025-12-24 17:58:08 -07:00

163 lines
3.9 KiB
Go

package main
import (
"sort"
"time"
//"github.com/rs/zerolog/log"
)
// TreatmentModel represents the calculated model for a year's treatments
type TreatmentModel struct {
Year int
SeasonStart time.Time
SeasonEnd time.Time
Interval time.Duration
ActualDates []time.Time
PredictedDates []time.Time
Errors []time.Duration
}
func modelTreatment(treatments []Treatment) []TreatmentModel {
treatment_times := make([]time.Time, 0)
for _, treatment := range treatments {
if treatment.Date != nil {
treatment_times = append(treatment_times, *treatment.Date)
}
}
models := calculateTreatmentModels(treatment_times)
/*models_by_year := make(map[int]TreatmentModel)
for _, m := range models {
models_by_year[m.Year] = m
}*/
offset := 0
for _, model := range models {
for _, e := range model.Errors {
treatments[offset].CadenceDelta = e
offset = offset + 1
}
}
/*
for i, treatment := range treatments {
model
treatment.CadenceDelta = deltas[i]
treatments[i] = treatment
}
*/
/*cadence, deltas := calculateCadenceVariance(treatment_times)
for i, treatment := range treatments {
if i >= len(deltas) {
break
}
treatment.CadenceDelta = deltas[i]
treatments[i] = treatment
}*/
return models
}
// calculateTreatmentModels segments treatments by year and calculates a model for each year
func calculateTreatmentModels(treatments []time.Time) []TreatmentModel {
// Group treatments by year
yearMap := make(map[int][]time.Time)
for _, t := range treatments {
year := t.Year()
yearMap[year] = append(yearMap[year], t)
}
// Calculate a model for each year
var models []TreatmentModel
for year, dates := range yearMap {
// Sort dates within the year
sort.Slice(dates, func(i, j int) bool {
return dates[i].Before(dates[j])
})
model := calculateYearModel(year, dates)
models = append(models, model)
}
// Sort models by year
sort.Slice(models, func(i, j int) bool {
return models[i].Year < models[j].Year
})
return models
}
// calculateYearModel creates a model for a specific year using linear regression
func calculateYearModel(year int, dates []time.Time) TreatmentModel {
n := len(dates)
if n < 2 {
// Not enough data for a model
return TreatmentModel{
Year: year,
ActualDates: dates,
}
}
// Convert dates to numeric values (seconds since epoch)
var x []float64
var y []float64
for i, date := range dates {
x = append(x, float64(i))
y = append(y, float64(date.Unix()))
}
// Calculate linear regression
slope, intercept := linearRegression(x, y)
// Convert back to time.Time and time.Duration
startTime := time.Unix(int64(intercept), 0)
intervalSeconds := int64(slope)
interval := time.Duration(intervalSeconds) * time.Second
// Calculate end of season
endTime := time.Unix(int64(intercept+slope*float64(n-1)), 0)
// Generate predicted dates and calculate errors
var predictedDates []time.Time
var errors []time.Duration
for i := 0; i < n; i++ {
predicted := time.Unix(int64(intercept+slope*float64(i)), 0)
predictedDates = append(predictedDates, predicted)
// Calculate error
actualTime := dates[i]
error := actualTime.Sub(predicted)
errors = append(errors, error)
}
return TreatmentModel{
Year: year,
SeasonStart: startTime,
SeasonEnd: endTime,
Interval: interval,
ActualDates: dates,
PredictedDates: predictedDates,
Errors: errors,
}
}
// linearRegression calculates the slope and intercept of the best-fit line
func linearRegression(x, y []float64) (float64, float64) {
n := float64(len(x))
if n < 2 {
return 0, 0
}
var sumX, sumY, sumXY, sumX2 float64
for i := 0; i < len(x); i++ {
sumX += x[i]
sumY += y[i]
sumXY += x[i] * y[i]
sumX2 += x[i] * x[i]
}
// Calculate slope
slope := (n*sumXY - sumX*sumY) / (n*sumX2 - sumX*sumX)
// Calculate intercept
intercept := (sumY - slope*sumX) / n
return slope, intercept
}