From d8a9a2e53d456e2e072308df8afa09b01a2a266d Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 7 Sep 2026 15:00:21 +0200 Subject: [PATCH 1/4] fix(forecast): restart learning per signal across both pipelines --- .changeset/restart-forecast-learning.md | 5 + go/cmd/ftw/forecast_learning.go | 281 ++++++++++++++++++ go/cmd/ftw/forecast_learning_test.go | 182 ++++++++++++ go/cmd/ftw/forecast_rust.go | 70 +++-- go/cmd/ftw/forecast_rust_learning.go | 151 ++++++++++ go/cmd/ftw/forecast_rust_learning_test.go | 183 ++++++++++++ go/cmd/ftw/forecast_tracking.go | 41 ++- go/cmd/ftw/main.go | 2 + go/internal/api/api.go | 11 +- go/internal/api/api_forecast_learning.go | 40 +++ go/internal/api/api_forecast_learning_test.go | 183 ++++++++++++ go/internal/api/api_loadmodel.go | 12 +- .../energyforecast/native_reset_test.go | 121 ++++++++ go/internal/energyforecast/reset.go | 37 +++ go/internal/energyforecast/reset_test.go | 115 +++++++ go/internal/energyforecast/types.go | 14 + go/internal/forecasting/learning.go | 22 ++ go/internal/loadmodel/model.go | 1 + .../loadmodel/restart_learning_test.go | 200 +++++++++++++ go/internal/loadmodel/service.go | 107 ++++++- go/internal/mpc/energyplan_test.go | 2 +- go/internal/pvmodel/model.go | 15 +- go/internal/pvmodel/restart_learning_test.go | 171 +++++++++++ go/internal/pvmodel/service.go | 72 ++++- go/internal/telemetry/forecast.go | 4 +- .../bundle/forecast-v1.response.schema.json | 79 +++++ .../native/bundle/forecast-v1.schema.json | 82 ++++- .../native/bundle/ftw-solver-darwin-arm64 | Bin 825040 -> 858240 bytes .../native/bundle/ftw-solver-linux-amd64 | Bin 1035304 -> 1078376 bytes .../native/bundle/ftw-solver-linux-arm64 | Bin 867432 -> 901960 bytes optimizer/native/bundle/manifest.json | 24 +- web/twins.js | 256 ++++++++++------ web/twins.test.mjs | 213 +++++++++++++ 33 files changed, 2523 insertions(+), 173 deletions(-) create mode 100644 .changeset/restart-forecast-learning.md create mode 100644 go/cmd/ftw/forecast_learning.go create mode 100644 go/cmd/ftw/forecast_learning_test.go create mode 100644 go/cmd/ftw/forecast_rust_learning.go create mode 100644 go/cmd/ftw/forecast_rust_learning_test.go create mode 100644 go/internal/api/api_forecast_learning.go create mode 100644 go/internal/api/api_forecast_learning_test.go create mode 100644 go/internal/energyforecast/native_reset_test.go create mode 100644 go/internal/energyforecast/reset.go create mode 100644 go/internal/energyforecast/reset_test.go create mode 100644 go/internal/forecasting/learning.go create mode 100644 go/internal/loadmodel/restart_learning_test.go create mode 100644 go/internal/pvmodel/restart_learning_test.go create mode 100644 web/twins.test.mjs diff --git a/.changeset/restart-forecast-learning.md b/.changeset/restart-forecast-learning.md new file mode 100644 index 000000000..ca6483cd1 --- /dev/null +++ b/.changeset/restart-forecast-learning.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Restart solar-production or household-consumption learning after a lasting site change. Each action resets the selected primary model, its legacy fallback and error calibration, preserves measured history and the other model, and shows the new learning period. Saved reset intent survives a restart and prevents old observations from restoring the previous model. diff --git a/go/cmd/ftw/forecast_learning.go b/go/cmd/ftw/forecast_learning.go new file mode 100644 index 000000000..d0a3d37cb --- /dev/null +++ b/go/cmd/ftw/forecast_learning.go @@ -0,0 +1,281 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/srcfl/ftw/go/internal/forecasting" +) + +const forecastLearningKey = "forecast/learning_periods_v1" + +func (f *forecastTracker) setReplan(fn func(string)) { + f.learningMu.Lock() + defer f.learningMu.Unlock() + f.requestReplan = fn +} + +type forecastLearningPeriods struct { + ConfigRevision string `json:"config_revision"` + PVMS int64 `json:"pv_ms"` + LoadMS int64 `json:"load_ms"` +} + +func (f *forecastTracker) learningSiteLocked(site forecastSite) forecastSite { + if f.learningPeriods.ConfigRevision == rustConfigRevision(site) { + site.PVLearningStartedMS, site.LoadLearningStartedMS = f.learningPeriods.PVMS, f.learningPeriods.LoadMS + } + return site +} + +func (f *forecastTracker) restoreLearning(ctx context.Context) error { + if data, ok := f.store.LoadConfig(forecastLearningKey); ok { + if err := json.Unmarshal([]byte(data), &f.learningPeriods); err != nil { + return fmt.Errorf("read forecast learning periods: %w", err) + } + if f.learningPeriods.ConfigRevision == "" || f.learningPeriods.PVMS < 0 || f.learningPeriods.LoadMS < 0 { + return errors.New("invalid forecast learning periods") + } + } + if f.configMu != nil { + f.configMu.RLock() + defer f.configMu.RUnlock() + } + f.learningMu.Lock() + defer f.learningMu.Unlock() + for _, signal := range []string{"pv", "load"} { + if err := f.applyLearningLocked(ctx, signal); err != nil { + slog.Warn("forecast learning restart pending", "signal", signal, "err", err) + } + } + return nil +} + +func (f *forecastTracker) applyLearningLocked(ctx context.Context, signal string) error { + site := f.learningSiteLocked(f.site()) + if site.IdentityPending { + return nil + } + var errs []error + if signal == "pv" && site.PVLearningStartedMS > 0 && f.pv != nil { + errs = append(errs, f.pv.RestartLearning(time.UnixMilli(site.PVLearningStartedMS))) + } + if signal == "load" && site.LoadLearningStartedMS > 0 && f.load != nil { + errs = append(errs, f.load.RestartLearning(time.UnixMilli(site.LoadLearningStartedMS))) + } + if r, ok := f.candidate.(*rustForecast); ok { + errs = append(errs, r.RestartLearning(ctx, site, signal)) + } + err := errors.Join(errs...) + if f.learningErrors == nil { + f.learningErrors = make(map[string]error) + } + f.learningErrors[signal] = err + return err +} + +func (f *forecastTracker) learningPendingLocked(site forecastSite, signal string) bool { + if site.IdentityPending || f.learningPeriods.ConfigRevision != rustConfigRevision(site) { + return false + } + cutoff := site.PVLearningStartedMS + if signal == "load" { + cutoff = site.LoadLearningStartedMS + } + if cutoff == 0 { + return false + } + if f.learningErrors[signal] != nil { + return true + } + if signal == "pv" && f.pv != nil && f.pv.LearningStartedMS() < cutoff { + return true + } + if signal == "load" && f.load != nil && f.load.LearningStartedMS() < cutoff { + return true + } + if r, ok := f.candidate.(*rustForecast); ok { + return r.LearningStatus(site, signal).Status == "unavailable" + } + return false +} + +// Both samplers and forecast captures reconcile before using any legacy state. +// In particular, startup may first need to wait for the hardware identity. +func (f *forecastTracker) reconcileLearning(ctx context.Context) { + if f.configMu != nil { + f.configMu.RLock() + defer f.configMu.RUnlock() + } + f.learningMu.Lock() + defer f.learningMu.Unlock() + site := f.learningSiteLocked(f.site()) + if site.IdentityPending { + return + } + for _, signal := range []string{"pv", "load"} { + if !f.learningPendingLocked(site, signal) { + continue + } + workCtx, cancel := context.WithTimeout(ctx, 2*time.Second) + err := f.applyLearningLocked(workCtx, signal) + cancel() + if err == nil && f.requestReplan != nil { + f.requestReplan(signal + "_learning_recovered") + } + } +} + +// RestartLearning saves the intent before changing either pipeline. Startup +// replays it idempotently, including after a crash between the model saves. +func (f *forecastTracker) RestartLearning(ctx context.Context, signal string) error { + if f == nil || (signal != "pv" && signal != "load") { + return errors.New("forecast learning unavailable") + } + if f.refreshIdentity != nil { + f.refreshIdentity() + } + if f.configMu != nil { + f.configMu.RLock() + defer f.configMu.RUnlock() + } + f.learningMu.Lock() + defer f.learningMu.Unlock() + site := f.site() + if site.IdentityPending { + return errors.New("waiting for site identity before restarting learning") + } + if (signal == "pv" && !site.HasLocation && f.pv == nil) || (signal == "load" && f.load == nil) { + return errors.New("forecast model disabled") + } + if r, ok := f.candidate.(*rustForecast); ok { + if !r.resetSupported { + return errors.New("forecast worker does not support restarting learning") + } + if signal == "pv" && !site.HasLocation { + return errors.New("solar forecast needs a site location") + } + } + next := f.learningPeriods + if next.ConfigRevision != rustConfigRevision(site) { + next = forecastLearningPeriods{ConfigRevision: rustConfigRevision(site)} + } + cutoff := f.now().UnixMilli() + previous := next.PVMS + if signal == "load" { + previous = next.LoadMS + } + if cutoff <= 0 || cutoff < previous { + return errors.New("clock precedes the current learning period") + } + // A retry of an incomplete operation retains its original cutoff. + if f.learningErrors[signal] != nil && previous > 0 { + cutoff = previous + } + if signal == "pv" { + next.PVMS = cutoff + } else { + next.LoadMS = cutoff + } + data, err := json.Marshal(next) + if err != nil { + return err + } + // Cancel a solve that captured the old models. Its replacement waits for + // learningMu, so it cannot capture a partially reset pair of pipelines. + if f.requestReplan != nil { + f.requestReplan(signal + "_learning_restarted") + } + if err = f.store.SaveConfig(forecastLearningKey, string(data)); err != nil { + return fmt.Errorf("save forecast learning period: %w", err) + } + f.learningPeriods = next + if err = f.applyLearningLocked(ctx, signal); err != nil { + return &forecasting.LearningRestartPendingError{Err: err} + } + return nil +} + +func (f *forecastTracker) LearningStatus(signal string) forecasting.LearningStatus { + status := forecasting.LearningStatus{Engine: "legacy", Status: "unavailable"} + if f == nil { + return status + } + if f.configMu != nil { + f.configMu.RLock() + defer f.configMu.RUnlock() + } + f.learningMu.RLock() + defer f.learningMu.RUnlock() + site := f.learningSiteLocked(f.site()) + if r, ok := f.candidate.(*rustForecast); ok { + status = r.LearningStatus(site, signal) + } else { + status.Status, status.ResetAvailable = "cold_start", !site.IdentityPending + if signal == "pv" { + status.StartedMS = site.PVLearningStartedMS + if f.pv == nil { + status.Status, status.ResetAvailable = "unavailable", false + } else { + m := f.pv.Model() + status.LatestTrainingMS = m.LastMs + if m.Samples > 0 { + status.Status = "learning" + } + if m.Quality() >= 1 { + status.Status = "ready" + } + } + } else { + status.StartedMS = site.LoadLearningStartedMS + if f.load == nil { + status.Status, status.ResetAvailable = "unavailable", false + } else { + m := f.load.Model() + status.LatestTrainingMS = m.LastMs + if m.Samples > 0 { + status.Status = "learning" + } + if m.Quality() >= 1 { + status.Status = "ready" + } + } + } + } + if site.IdentityPending || (f.learningPeriods.ConfigRevision == rustConfigRevision(site) && f.learningErrors[signal] != nil) { + status.Status = "unavailable" + } + return status +} + +// Calibration is per signal: a PV reset also drops joint net errors, while +// retaining load evidence. Neither the archive nor its measured truth is erased. +func learningEvidence(history []forecasting.ErrorSample, observations []forecasting.Observation, pvMS, loadMS int64) ([]forecasting.ErrorSample, []forecasting.Observation) { + if pvMS == 0 && loadMS == 0 { + return history, observations + } + errors := append([]forecasting.ErrorSample(nil), history...) + for i := range errors { + e := &errors[i] + if e.OriginMS < pvMS || e.StartMS < pvMS { + e.PVKnown = false + } + if e.OriginMS < loadMS || e.StartMS < loadMS { + e.LoadKnown = false + } + } + truth := append([]forecasting.Observation(nil), observations...) + for i := range truth { + if truth[i].StartMS < pvMS { + truth[i].PVKnown = false + } + if truth[i].StartMS < loadMS { + truth[i].LoadKnown = false + } + } + return errors, truth +} diff --git a/go/cmd/ftw/forecast_learning_test.go b/go/cmd/ftw/forecast_learning_test.go new file mode 100644 index 000000000..daf2c3567 --- /dev/null +++ b/go/cmd/ftw/forecast_learning_test.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "sync/atomic" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/energyforecast" + "github.com/srcfl/ftw/go/internal/forecasting" + "github.com/srcfl/ftw/go/internal/loadmodel" + "github.com/srcfl/ftw/go/internal/pvmodel" + "github.com/srcfl/ftw/go/internal/state" + "github.com/srcfl/ftw/go/internal/telemetry" +) + +func learningTracker(st *state.Store, site *forecastSite, at time.Time, r *rustForecast) *forecastTracker { + tel := telemetry.NewStore() + f := &forecastTracker{store: st, tele: tel, site: func() forecastSite { return *site }, clock: func() time.Time { return at }, + pv: pvmodel.NewService(st, tel, func(time.Time) float64 { return 800 }, nil, 8000), load: loadmodel.NewService(st, tel, "site", 4000, 0)} + if r != nil { + f.candidate = r + } + return f +} + +func TestForecastLearningNativePendingExchangeRecoversOnlySelectedSignal(t *testing.T) { + st := hostForecastDB(t) + r := learningNative(t, st) + site := hostForecastSite() + at := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC) + if err := r.Update(context.Background(), site, learningObservation(site, at), nil, false); err != nil { + t.Fatal(err) + } + f := learningTracker(st, &site, at.Add(22*time.Minute), r) + transport := r.transport.(energyforecast.RoundTripper) + var fail atomic.Bool + fail.Store(true) + r.client = energyforecast.NewClient(hostForecastExchange(func(ctx context.Context, p []byte) ([]byte, error) { + var header struct { + Action string `json:"action"` + } + _ = json.Unmarshal(p, &header) + if header.Action == "reset" && fail.Load() { + return nil, errors.New("injected worker exchange failure") + } + return transport.RoundTrip(ctx, p) + })) + if err := f.RestartLearning(context.Background(), "pv"); err == nil { + t.Fatal("failed worker reset reported success") + } + if _, ok := st.LoadConfig(forecastLearningKey); !ok { + t.Fatal("reset intent was not durable before worker call") + } + if s := f.LearningStatus("pv"); s.Status != "unavailable" { + t.Fatalf("pending PV=%+v", s) + } + if s := f.LearningStatus("load"); s.Status != "learning" { + t.Fatalf("pending PV hid load=%+v", s) + } + if f.pv.LearningStartedMS() != at.Add(22*time.Minute).UnixMilli() { + t.Fatal("legacy fallback retained old epoch") + } + fail.Store(false) + f.Snapshot(time.Now(), nil) + if s := f.LearningStatus("pv"); s.Status != "cold_start" { + t.Fatalf("automatic reconciliation left PV pending=%+v", s) + } + if s := f.LearningStatus("load"); s.Status != "learning" { + t.Fatalf("recovery changed load=%+v", s) + } + restored := learningNative(t, st) + if s := restored.LearningStatus(f.learningSiteLocked(site), "pv"); s.Status != "cold_start" { + t.Fatalf("recovered reset not persisted=%+v", s) + } +} + +func TestForecastLearningIdentityReadyAppliesSavedIntentBeforeCapture(t *testing.T) { + st := hostForecastDB(t) + site := hostForecastSite() + site.IdentityPending = true + at := time.Now().Add(-time.Minute) + cutoff := at.UnixMilli() + data, _ := json.Marshal(forecastLearningPeriods{ConfigRevision: rustConfigRevision(site), PVMS: cutoff}) + if err := st.SaveConfig(forecastLearningKey, string(data)); err != nil { + t.Fatal(err) + } + f := learningTracker(st, &site, time.Now(), nil) + if err := f.restoreLearning(context.Background()); err != nil { + t.Fatal(err) + } + if f.pv.LearningStartedMS() != 0 { + t.Fatal("identity-pending startup changed model") + } + site.IdentityPending = false + f.Snapshot(time.Now(), nil) + if f.pv.LearningStartedMS() != cutoff { + t.Fatal("first capture after identity resolution missed durable reset") + } + if f.load.LearningStartedMS() != 0 { + t.Fatal("PV reset changed load epoch") + } + restored := learningTracker(st, &site, time.Now(), nil) + if restored.pv.LearningStartedMS() != cutoff { + t.Fatal("legacy reset did not survive DB reload") + } +} + +func TestForecastLearningNativeDisabledPVDoesNotPersistIntentOrBlockLoad(t *testing.T) { + st := hostForecastDB(t) + r := learningNative(t, st) + site := hostForecastSite() + site.HasLocation = false + at := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC) + f := learningTracker(st, &site, at.Add(22*time.Minute), r) + if err := f.RestartLearning(context.Background(), "pv"); err == nil { + t.Fatal("disabled native PV reset accepted") + } + if _, ok := st.LoadConfig(forecastLearningKey); ok { + t.Fatal("disabled PV reset saved a blocking intent") + } + if s := f.LearningStatus("pv"); s.ResetAvailable { + t.Fatalf("disabled PV reset advertised: %+v", s) + } + // An older saved PV intent must not block updates in a load-only model. + site.PVLearningStartedMS = at.Add(10 * time.Minute).UnixMilli() + if err := r.Update(context.Background(), site, learningObservation(site, at), nil, false); err != nil { + t.Fatal(err) + } + if s := r.LearningStatus(site, "load"); s.Status != "learning" { + t.Fatalf("disabled PV blocked load=%+v", s) + } +} + +func TestForecastLearningCalibrationDropsPVAndNetRetainsLoadAndArchive(t *testing.T) { + base := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli() + var history []forecasting.ErrorSample + var truth []forecasting.Observation + for day := 0; day < 7; day++ { + for hour := 0; hour < 8; hour++ { + start := base + int64(day*24+hour)*3600000 + origin := start - 2*3600000 + p := forecasting.Point{StartMS: start, EndMS: start + 3600000, PVW: 2000, LoadW: 1000, PVKnown: true, LoadKnown: true, PVQuality: "ready", LoadQuality: "ready", PVBand: forecasting.Band{Method: forecasting.BandMethodColdStart}, LoadBand: forecasting.Band{Method: forecasting.BandMethodColdStart}, NetBand: forecasting.Band{Method: forecasting.BandMethodColdStart}} + e := forecasting.ErrorSample{Series: "energyplan", ConfigVersion: "cfg", IssueID: fmt.Sprintf("%d-%d", day, hour), OriginMS: origin, IssuedAtMS: origin, StartMS: start, EndMS: start + 3600000, AvailableAtMS: start + 3600000, Lead: forecasting.LeadBucket(origin, start), PVErrorW: float64(hour + day), LoadErrorW: float64(hour - day), PVKnown: true, LoadKnown: true, Prediction: p} + if err := e.Validate(); err != nil { + t.Fatal(err) + } + history = append(history, e) + truth = append(truth, forecasting.Observation{StartMS: start, EndMS: start + 3600000, PVKnown: true, LoadKnown: true}) + } + } + archived := append([]forecasting.ErrorSample(nil), history...) + rawTruth := append([]forecasting.Observation(nil), truth...) + cutoff := base + 8*24*3600000 + before := forecasting.NewCalibrator(history, "cfg", cutoff) + filtered, observations := learningEvidence(history, truth, cutoff, 0) + after := forecasting.NewCalibrator(filtered, "cfg", cutoff) + for _, signal := range []string{"pv", "net", "load"} { + previous := before.Band("energyplan", signal, cutoff+2*3600000, 1000) + got := after.Band("energyplan", signal, cutoff+2*3600000, 1000) + if previous.Method != forecasting.BandMethodEmpirical { + t.Fatalf("fixture not calibrated for %s: %+v", signal, previous) + } + if signal == "load" { + if got != previous { + t.Fatalf("PV reset changed load calibration: %+v %+v", previous, got) + } + } else if got.Samples != 0 { + t.Fatalf("%s retained pre-reset errors: %+v", signal, got) + } + } + if !reflect.DeepEqual(history, archived) || !reflect.DeepEqual(truth, rawTruth) { + t.Fatal("reset rewrote archived evidence") + } + if observations[0].PVKnown || !observations[0].LoadKnown { + t.Fatal("observation evidence reset crossed signals") + } +} diff --git a/go/cmd/ftw/forecast_rust.go b/go/cmd/ftw/forecast_rust.go index 3289c58c6..1ab339a94 100644 --- a/go/cmd/ftw/forecast_rust.go +++ b/go/cmd/ftw/forecast_rust.go @@ -22,20 +22,27 @@ import ( const forecastRustStateKey = "forecast/energyplan_state_v1" type savedForecastState struct { - SiteID string `json:"site_id"` - ConfigRevision string `json:"config_revision"` - ModelRevision uint64 `json:"model_revision"` - LatestAvailableMS int64 `json:"latest_available_ms"` - State json.RawMessage `json:"state"` + SiteID string `json:"site_id"` + ConfigRevision string `json:"config_revision"` + ModelRevision uint64 `json:"model_revision"` + LatestAvailableMS int64 `json:"latest_available_ms"` + LatestTraining energyforecast.LatestInput `json:"latest_training_ms"` + PVLearningStartedMS int64 `json:"pv_learning_started_ms,omitempty"` + LoadLearningStartedMS int64 `json:"load_learning_started_ms,omitempty"` + State json.RawMessage `json:"state"` } type rustForecast struct { - version string - client *energyforecast.Client - transport interface{ Close() error } - store *state.Store - mu sync.RWMutex - saved savedForecastState + version string + client *energyforecast.Client + transport interface{ Close() error } + store *state.Store + mu sync.RWMutex + updateMu sync.Mutex // serialize complete update/reset exchanges and persistence + saved savedForecastState + resetSupported bool + pvQuality, loadQuality string + predictedTraining energyforecast.LatestInput } func newRustForecast(st *state.Store, binary string) (*rustForecast, error) { @@ -47,9 +54,10 @@ func newRustForecast(st *state.Store, binary string) (*rustForecast, error) { defer cancel() line, err := transport.RoundTrip(ctx, []byte(`{"type":"handshake","protocol_version":1}`)) var info struct { - Name string `json:"name"` - Version string `json:"version"` - ForecastVersion int `json:"forecast_protocol_version"` + Name string `json:"name"` + Version string `json:"version"` + ForecastVersion int `json:"forecast_protocol_version"` + Features []string `json:"features"` } if err == nil { err = json.Unmarshal(line, &info) @@ -59,6 +67,11 @@ func newRustForecast(st *state.Store, binary string) (*rustForecast, error) { return nil, fmt.Errorf("Energyplan forecast v1 unavailable: name=%q version=%d: %v", info.Name, info.ForecastVersion, err) } r := &rustForecast{version: forecastBinaryIdentity(binary), client: energyforecast.NewClient(transport), transport: transport, store: st} + for _, feature := range info.Features { + if feature == "forecast_reset" { + r.resetSupported = true + } + } if data, ok := st.LoadConfig(forecastRustStateKey); ok { if len(data) <= energyforecast.MaxStateBytes+4096 { var saved savedForecastState @@ -106,14 +119,19 @@ func rustFeatures(t time.Time, site forecastSite, home bool, row *state.Forecast } func (r *rustForecast) Update(ctx context.Context, site forecastSite, o forecasting.Observation, weather *state.ForecastPoint, away bool) error { + r.updateMu.Lock() + defer r.updateMu.Unlock() + if err := r.applyLearningPeriodsLocked(ctx, site, max(time.Now().UnixMilli(), o.AvailableAtMS)); err != nil { + return err + } r.mu.RLock() saved := r.saved r.mu.RUnlock() if saved.SiteID != site.SiteID || saved.ConfigRevision != rustConfigRevision(site) { saved = savedForecastState{SiteID: site.SiteID, ConfigRevision: rustConfigRevision(site)} } - origin := o.AvailableAtMS - if weather != nil && weather.FetchedAtMs > origin { + origin := max(o.AvailableAtMS, saved.LatestAvailableMS) + if weather != nil && weather.FetchedAtMs > o.AvailableAtMS { return errors.New("observation weather arrived after update origin") } input := energyforecast.Observation{Interval: energyforecast.Interval{ValidStartMs: o.StartMS, ValidEndMs: o.EndMS}, @@ -133,8 +151,9 @@ func (r *rustForecast) Update(ctx context.Context, site forecastSite, o forecast if err != nil { return err } - next := savedForecastState{SiteID: site.SiteID, ConfigRevision: rustConfigRevision(site), ModelRevision: reply.ModelRevision, - LatestAvailableMS: origin, State: reply.State} + next := saved + next.ModelRevision, next.LatestAvailableMS, next.State = reply.ModelRevision, origin, reply.State + next.LatestTraining = reply.LatestTrainingMs data, err := json.Marshal(next) if err != nil { return err @@ -144,6 +163,10 @@ func (r *rustForecast) Update(ctx context.Context, site forecastSite, o forecast return err } r.mu.Lock() + if r.saved.SiteID != next.SiteID || r.saved.ConfigRevision != next.ConfigRevision { + r.pvQuality, r.loadQuality = "", "" + r.predictedTraining = energyforecast.LatestInput{} + } r.saved = next r.mu.Unlock() return nil @@ -210,6 +233,17 @@ func (r *rustForecast) Predict(ctx context.Context, site forecastSite, issued fo if err != nil { return forecasting.Issue{}, err } + // A durable reset intent can outlive a failed worker exchange. Until that + // signal has restarted, only its freshly reset legacy fallback may serve it. + for i := range reply.Predictions { + if saved.PVLearningStartedMS < site.PVLearningStartedMS { + reply.Predictions[i].PV = &energyforecast.Estimate{Quality: "cold_start", Uncertainty: "unknown"} + } + if saved.LoadLearningStartedMS < site.LoadLearningStartedMS { + reply.Predictions[i].Load = &energyforecast.Estimate{Quality: "cold_start", Uncertainty: "unknown"} + } + } + r.recordLearningQuality(saved, reply) now := time.Now().UnixMilli() out := forecasting.Issue{Schema: forecasting.Schema, ID: uuid.NewString(), DecisionID: issued.DecisionID, OriginMS: issued.OriginMS, IssuedAtMS: now, ConfigVersion: issued.ConfigVersion, Site: forecastSiteContext(site), Weather: issued.Weather, diff --git a/go/cmd/ftw/forecast_rust_learning.go b/go/cmd/ftw/forecast_rust_learning.go new file mode 100644 index 000000000..4685f03d5 --- /dev/null +++ b/go/cmd/ftw/forecast_rust_learning.go @@ -0,0 +1,151 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/google/uuid" + "github.com/srcfl/ftw/go/internal/energyforecast" + "github.com/srcfl/ftw/go/internal/forecasting" +) + +func (r *rustForecast) RestartLearning(ctx context.Context, site forecastSite, signal string) error { + if signal == "pv" { + site.LoadLearningStartedMS = 0 + } else { + site.PVLearningStartedMS = 0 + } + r.updateMu.Lock() + defer r.updateMu.Unlock() + return r.applyLearningPeriodsLocked(ctx, site, time.Now().UnixMilli()) +} + +// Caller holds updateMu across the request, durable save and memory update. +func (r *rustForecast) applyLearningPeriodsLocked(ctx context.Context, site forecastSite, origin int64) error { + for _, signal := range []string{"pv", "load"} { + if signal == "pv" && !site.HasLocation { + continue + } + cutoff := site.PVLearningStartedMS + if signal == "load" { + cutoff = site.LoadLearningStartedMS + } + if cutoff == 0 { + continue + } + r.mu.RLock() + saved := r.saved + r.mu.RUnlock() + if saved.SiteID != site.SiteID || saved.ConfigRevision != rustConfigRevision(site) { + saved = savedForecastState{SiteID: site.SiteID, ConfigRevision: rustConfigRevision(site)} + } + applied := saved.PVLearningStartedMS + if signal == "load" { + applied = saved.LoadLearningStartedMS + } + if applied >= cutoff { + continue + } + if !r.resetSupported { + return errors.New("forecast worker does not support restarting learning") + } + origin = max(origin, saved.LatestAvailableMS, cutoff) + reply, err := r.client.Reset(ctx, energyforecast.ResetRequest{RequestContext: energyforecast.RequestContext{ + RequestID: uuid.NewString(), SiteID: site.SiteID, ConfigRevision: rustConfigRevision(site), OriginMs: origin, + Config: rustForecastConfig(site), State: saved.State}, Signal: signal, LearningStartedMs: cutoff}) + if err != nil { + return err + } + next := saved + next.ModelRevision, next.LatestAvailableMS, next.State = reply.ModelRevision, origin, reply.State + next.LatestTraining = reply.LatestTrainingMs + if signal == "pv" { + next.PVLearningStartedMS = reply.LearningStartedMs + } else { + next.LoadLearningStartedMS = reply.LearningStartedMs + } + data, err := json.Marshal(next) + if err != nil { + return err + } + if err = r.store.SaveConfig(forecastRustStateKey, string(data)); err != nil { + return err + } + r.mu.Lock() + r.saved = next + if signal == "pv" { + r.pvQuality = "cold_start" + r.predictedTraining.PV = nil + } else { + r.loadQuality = "cold_start" + r.predictedTraining.Load = nil + } + r.mu.Unlock() + } + return nil +} + +func (r *rustForecast) LearningStatus(site forecastSite, signal string) forecasting.LearningStatus { + r.mu.RLock() + defer r.mu.RUnlock() + status := forecasting.LearningStatus{Engine: "energyplan", Status: "cold_start", ResetAvailable: r.resetSupported && !site.IdentityPending} + if signal == "pv" && !site.HasLocation { + status.Status, status.ResetAvailable = "unavailable", false + return status + } + cutoff, applied, latest, quality := site.PVLearningStartedMS, r.saved.PVLearningStartedMS, r.saved.LatestTraining.PV, r.pvQuality + if signal == "load" { + cutoff, applied, latest, quality = site.LoadLearningStartedMS, r.saved.LoadLearningStartedMS, r.saved.LatestTraining.Load, r.loadQuality + } + predictedLatest := r.predictedTraining.PV + if signal == "load" { + predictedLatest = r.predictedTraining.Load + } + if predictedLatest != nil && (latest == nil || *predictedLatest > *latest) { + latest = predictedLatest + } + status.StartedMS = cutoff + if site.IdentityPending { + status.Status = "unavailable" + return status + } + if r.saved.SiteID != site.SiteID || r.saved.ConfigRevision != rustConfigRevision(site) { + applied, latest, quality = 0, nil, "cold_start" + } + if applied < cutoff { + status.Status = "unavailable" + return status + } + if latest != nil && *latest > cutoff { + status.LatestTrainingMS, status.Status = *latest, "learning" + if quality == "ready" { + status.Status = "ready" + } + } + return status +} + +func (r *rustForecast) recordLearningQuality(saved savedForecastState, reply energyforecast.PredictReply) { + r.mu.Lock() + defer r.mu.Unlock() + if r.saved.SiteID != saved.SiteID || r.saved.ConfigRevision != saved.ConfigRevision || r.saved.ModelRevision != saved.ModelRevision { + return + } + pv, load := "ready", "ready" + for _, p := range reply.Predictions { + if p.PV == nil || !p.PV.Known { + pv = leastForecastQuality(pv, "cold_start") + } else { + pv = leastForecastQuality(pv, p.PV.Quality) + } + if p.Load == nil || !p.Load.Known { + load = leastForecastQuality(load, "cold_start") + } else { + load = leastForecastQuality(load, p.Load.Quality) + } + } + r.pvQuality, r.loadQuality = pv, load + r.predictedTraining = reply.LatestTrainingMs +} diff --git a/go/cmd/ftw/forecast_rust_learning_test.go b/go/cmd/ftw/forecast_rust_learning_test.go new file mode 100644 index 000000000..ff4085cba --- /dev/null +++ b/go/cmd/ftw/forecast_rust_learning_test.go @@ -0,0 +1,183 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "os" + "sync" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/energyforecast" + "github.com/srcfl/ftw/go/internal/forecasting" + "github.com/srcfl/ftw/go/internal/state" +) + +func learningNative(t *testing.T, st *state.Store) *rustForecast { + t.Helper() + binary := os.Getenv("FTW_FORECAST_WORKER") + if binary == "" { + t.Skip("set FTW_FORECAST_WORKER for native learning integration") + } + r, err := newRustForecast(st, binary) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = r.Close() }) + return r +} + +func learningObservation(site forecastSite, start time.Time) forecasting.Observation { + return forecasting.Observation{StartMS: start.UnixMilli(), EndMS: start.Add(15 * time.Minute).UnixMilli(), AvailableAtMS: start.Add(15 * time.Minute).UnixMilli(), PVW: 2350.125, PVKnown: true, LoadW: 677.375, LoadKnown: true, Quality: "complete", ConfigVersion: site.Revision} +} + +func learningModel(t *testing.T, r *rustForecast, signal string) json.RawMessage { + t.Helper() + var saved savedForecastState + if err := json.Unmarshal(r.Snapshot(), &saved); err != nil { + t.Fatal(err) + } + var models map[string]json.RawMessage + if err := json.Unmarshal(saved.State, &models); err != nil { + t.Fatal(err) + } + return models[signal] +} + +func TestForecastLearningNativeSelectiveResetAndDurableRecovery(t *testing.T) { + for _, signal := range []string{"pv", "load"} { + t.Run(signal, func(t *testing.T) { + st := hostForecastDB(t) + r := learningNative(t, st) + site := hostForecastSite() + start := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC) + if err := r.Update(context.Background(), site, learningObservation(site, start), nil, false); err != nil { + t.Fatal(err) + } + other := "load" + if signal == "load" { + other = "pv" + } + originalOther := learningModel(t, r, other) + cutoff := start.Add(22 * time.Minute).UnixMilli() + if signal == "pv" { + site.PVLearningStartedMS = cutoff + } else { + site.LoadLearningStartedMS = cutoff + } + if err := r.RestartLearning(context.Background(), site, signal); err != nil { + t.Fatal(err) + } + if !bytes.Equal(originalOther, learningModel(t, r, other)) { + t.Fatal("reset changed other model") + } + if s := r.LearningStatus(site, signal); s.Status != "cold_start" || s.LatestTrainingMS != 0 { + t.Fatalf("reset status=%+v", s) + } + cleared := learningModel(t, r, signal) + restored := learningNative(t, st) + if !bytes.Equal(r.Snapshot(), restored.Snapshot()) { + t.Fatal("DB restore changed state") + } + // Replaying an already-consumed pre-reset interval cannot repopulate + // the cleared signal or publish a partial update of the other model. + beforeOld := restored.Snapshot() + if err := restored.Update(context.Background(), site, learningObservation(site, start), nil, false); err == nil { + t.Fatal("duplicate pre-reset interval unexpectedly accepted") + } + if !bytes.Equal(beforeOld, restored.Snapshot()) { + t.Fatal("failed pre-reset replay changed durable state") + } + // A delayed quarter straddles the reset cutoff. Only the other signal learns. + if err := restored.Update(context.Background(), site, learningObservation(site, start.Add(15*time.Minute)), nil, false); err != nil { + t.Fatal(err) + } + if !bytes.Equal(cleared, learningModel(t, restored, signal)) { + t.Fatal("straddling interval repopulated reset model") + } + if bytes.Equal(originalOther, learningModel(t, restored, other)) { + t.Fatal("reset blocked other signal's next interval") + } + eligible := learningObservation(site, start.Add(30*time.Minute)) + futureWeather := &state.ForecastPoint{FetchedAtMs: eligible.AvailableAtMS + 1, SolarWm2: hostForecastPtr(500.0)} + beforeWeather := restored.Snapshot() + if err := restored.Update(context.Background(), site, eligible, futureWeather, false); err == nil { + t.Fatal("newer request origin admitted weather unavailable to the observation") + } + if !bytes.Equal(beforeWeather, restored.Snapshot()) { + t.Fatal("rejected weather changed state") + } + if err := restored.Update(context.Background(), site, learningObservation(site, start.Add(30*time.Minute)), nil, false); err != nil { + t.Fatal(err) + } + if s := restored.LearningStatus(site, signal); s.Status != "learning" || s.LatestTrainingMS != start.Add(45*time.Minute).UnixMilli() { + t.Fatalf("post-reset learning=%+v", s) + } + learned := restored.Snapshot() + if err := restored.RestartLearning(context.Background(), site, signal); err != nil { + t.Fatal(err) + } + if !bytes.Equal(learned, restored.Snapshot()) { + t.Fatal("durable reset replay erased new evidence") + } + }) + } +} + +func TestForecastLearningNativeInFlightUpdateCannotUndoReset(t *testing.T) { + st := hostForecastDB(t) + r := learningNative(t, st) + site := hostForecastSite() + start := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC) + transport := r.transport.(energyforecast.RoundTripper) + entered, release := make(chan struct{}), make(chan struct{}) + var once sync.Once + r.client = energyforecast.NewClient(hostForecastExchange(func(ctx context.Context, p []byte) ([]byte, error) { + reply, err := transport.RoundTrip(ctx, p) + var header struct { + Action string `json:"action"` + } + _ = json.Unmarshal(p, &header) + if header.Action == "update" { + once.Do(func() { + close(entered) + select { + case <-release: + case <-ctx.Done(): + } + }) + } + return reply, err + })) + done := make(chan error, 1) + go func() { done <- r.Update(context.Background(), site, learningObservation(site, start), nil, false) }() + select { + case <-entered: + case <-time.After(5 * time.Second): + t.Fatal("update never reached persistence boundary") + } + site.PVLearningStartedMS = start.Add(20 * time.Minute).UnixMilli() + resetDone := make(chan error, 1) + go func() { resetDone <- r.RestartLearning(context.Background(), site, "pv") }() + select { + case err := <-resetDone: + close(release) + t.Fatalf("reset bypassed in-flight update lock: %v", err) + case <-time.After(30 * time.Millisecond): + } + close(release) + if err := <-done; err != nil { + t.Fatal(err) + } + if err := <-resetDone; err != nil { + t.Fatal(err) + } + reloaded := learningNative(t, st) + if s := reloaded.LearningStatus(site, "pv"); s.Status != "cold_start" || s.LatestTrainingMS != 0 { + t.Fatalf("old update restored PV: %+v", s) + } + if s := reloaded.LearningStatus(site, "load"); s.LatestTrainingMS != start.Add(15*time.Minute).UnixMilli() { + t.Fatalf("load history lost: %+v", s) + } +} diff --git a/go/cmd/ftw/forecast_tracking.go b/go/cmd/ftw/forecast_tracking.go index 47c1a3df8..1e272089d 100644 --- a/go/cmd/ftw/forecast_tracking.go +++ b/go/cmd/ftw/forecast_tracking.go @@ -22,17 +22,19 @@ import ( ) type forecastSite struct { - IdentityPending bool - WeatherSinceMS int64 - LearningRevision string - Revision string - SiteID string - Meter string - Latitude, Longitude float64 - HasLocation bool - HasPVScale bool - Timezone string - Options telemetry.ForecastOptions + IdentityPending bool + WeatherSinceMS int64 + LearningRevision string + Revision string + SiteID string + Meter string + Latitude, Longitude float64 + HasLocation bool + HasPVScale bool + Timezone string + Options telemetry.ForecastOptions + PVLearningStartedMS int64 + LoadLearningStartedMS int64 } // forecastCandidate is a host adapter around the compiled model. State is @@ -70,9 +72,16 @@ type forecastTracker struct { pvAccumulator telemetry.ForecastAccumulator stopped bool clock func() time.Time + learningMu sync.RWMutex + learningPeriods forecastLearningPeriods + learningErrors map[string]error + requestReplan func(string) } func (f *forecastTracker) Start(ctx context.Context) error { + if err := f.restoreLearning(ctx); err != nil { + return err + } initCtx, cancel := context.WithTimeout(ctx, 3*time.Second) err := f.store.InitForecastArchive(initCtx) cancel() @@ -167,6 +176,7 @@ func (f *forecastTracker) observe(ctx context.Context, now time.Time) { if f.refreshIdentity != nil { f.refreshIdentity() } + f.reconcileLearning(ctx) if f.configMu != nil { f.configMu.RLock() } @@ -205,7 +215,10 @@ func (f *forecastTracker) observe(ctx context.Context, now time.Time) { } away := f.away != nil && f.away(time.UnixMilli(o.StartMS)) workCtx, cancel := context.WithTimeout(ctx, 2*time.Second) + f.learningMu.RLock() + site = f.learningSiteLocked(site) err = f.candidate.Update(workCtx, site, o, weather, away) + f.learningMu.RUnlock() cancel() if err != nil { slog.Debug("forecast candidate update unavailable", "err", err) @@ -356,12 +369,15 @@ func (f *forecastTracker) Snapshot(_ time.Time, weather []state.ForecastPoint) m if f.refreshIdentity != nil { f.refreshIdentity() } + f.reconcileLearning(context.Background()) if f.configMu != nil { f.configMu.RLock() defer f.configMu.RUnlock() } + f.learningMu.RLock() + defer f.learningMu.RUnlock() captureAt := f.now() - site := f.site() + site := f.learningSiteLocked(f.site()) if site.IdentityPending { // Keep persisted learning untouched until the running hardware proves its // binding. An explicit empty weather slice prevents fallback to old rows. @@ -374,6 +390,7 @@ func (f *forecastTracker) Snapshot(_ time.Time, weather []state.ForecastPoint) m history := f.errors observations := f.observations f.mu.RUnlock() + history, observations = learningEvidence(history, observations, site.PVLearningStartedMS, site.LoadLearningStartedMS) candidateState := json.RawMessage(nil) if f.candidate != nil { candidateState = append(json.RawMessage(nil), f.candidate.Snapshot()...) diff --git a/go/cmd/ftw/main.go b/go/cmd/ftw/main.go index 667b14f78..5c2ca578d 100644 --- a/go/cmd/ftw/main.go +++ b/go/cmd/ftw/main.go @@ -1793,6 +1793,7 @@ func main() { } if forecastTrackerSvc != nil { mpcSvc.ForecastSnapshot = forecastTrackerSvc.Snapshot + forecastTrackerSvc.setReplan(mpcSvc.RequestReplan) } mpcSvc.Start(ctx) defer mpcSvc.Stop() @@ -2469,6 +2470,7 @@ func main() { PlannerPrefs: plannerPrefs, PVModel: pvSvc, LoadModel: loadSvc, + ForecastLearning: forecastTrackerSvc, Loadpoints: lpMgr, LoadpointCtrl: lpController, OCPPChargers: ocppChargersFn, diff --git a/go/internal/api/api.go b/go/internal/api/api.go index 560223c7a..857bd3b0d 100644 --- a/go/internal/api/api.go +++ b/go/internal/api/api.go @@ -150,7 +150,8 @@ type Deps struct { PVModel *pvmodel.Service // Optional: load digital-twin self-learner. - LoadModel *loadmodel.Service + LoadModel *loadmodel.Service + ForecastLearning ForecastLearning // Optional: EV loadpoint state consumed by the API and MPC. Loadpoints *loadpoint.Manager @@ -2978,16 +2979,12 @@ func (s *Server) handlePVModel(w http.ResponseWriter, r *http.Request) { "pv_residual_mean_w": rd.MeanW, "pv_residual_std_w": rd.StdW, "pv_residual_window_minutes": rd.WindowMinutes, + "learning": s.forecastLearningStatus("pv"), }) } func (s *Server) handlePVModelReset(w http.ResponseWriter, r *http.Request) { - if s.deps.PVModel == nil { - writeJSON(w, 400, map[string]string{"error": "pvmodel disabled"}) - return - } - s.deps.PVModel.Reset() - writeJSON(w, 200, map[string]string{"status": "reset"}) + s.handleForecastLearningReset(w, r, "pv") } // ---- static ---- diff --git a/go/internal/api/api_forecast_learning.go b/go/internal/api/api_forecast_learning.go new file mode 100644 index 000000000..fe6415e23 --- /dev/null +++ b/go/internal/api/api_forecast_learning.go @@ -0,0 +1,40 @@ +package api + +import ( + "context" + "errors" + "net/http" + + "github.com/srcfl/ftw/go/internal/forecasting" +) + +// ForecastLearning restarts the primary, fallback and calibration together. +// It owns the durable learning boundary; the API never edits model state. +type ForecastLearning interface { + LearningStatus(string) forecasting.LearningStatus + RestartLearning(context.Context, string) error +} + +func (s *Server) forecastLearningStatus(signal string) forecasting.LearningStatus { + if s.deps.ForecastLearning == nil { + return forecasting.LearningStatus{Engine: "legacy", Status: "unavailable"} + } + return s.deps.ForecastLearning.LearningStatus(signal) +} + +func (s *Server) handleForecastLearningReset(w http.ResponseWriter, r *http.Request, signal string) { + if s.deps.ForecastLearning == nil { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "forecast learning service unavailable"}) + return + } + if err := s.deps.ForecastLearning.RestartLearning(r.Context(), signal); err != nil { + status := "error" + var pending *forecasting.LearningRestartPendingError + if errors.As(err, &pending) { + status = "pending" + } + writeJSON(w, http.StatusServiceUnavailable, map[string]any{"status": status, "error": err.Error(), "learning": s.forecastLearningStatus(signal)}) + return + } + writeJSON(w, http.StatusOK, map[string]any{"status": "reset", "learning": s.forecastLearningStatus(signal)}) +} diff --git a/go/internal/api/api_forecast_learning_test.go b/go/internal/api/api_forecast_learning_test.go new file mode 100644 index 000000000..a08ece3f5 --- /dev/null +++ b/go/internal/api/api_forecast_learning_test.go @@ -0,0 +1,183 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/forecasting" + "github.com/srcfl/ftw/go/internal/loadmodel" + "github.com/srcfl/ftw/go/internal/pvmodel" + "github.com/srcfl/ftw/go/internal/telemetry" +) + +type forecastLearningAPIMock struct { + status map[string]forecasting.LearningStatus + err error + calls []string +} + +func (m *forecastLearningAPIMock) LearningStatus(signal string) forecasting.LearningStatus { + return m.status[signal] +} + +func (m *forecastLearningAPIMock) RestartLearning(_ context.Context, signal string) error { + m.calls = append(m.calls, signal) + if m.err != nil { + return m.err + } + status := m.status[signal] + status.Status = "learning" + status.StartedMS += 1000 + m.status[signal] = status + return nil +} + +type forecastLearningAPIResponse struct { + Status string `json:"status"` + Error string `json:"error"` + Learning forecasting.LearningStatus `json:"learning"` +} + +func decodeForecastLearningResponse(t *testing.T, rr *httptest.ResponseRecorder) forecastLearningAPIResponse { + t.Helper() + var got forecastLearningAPIResponse + if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil { + t.Fatalf("decode response: %v; body: %s", err, rr.Body.String()) + } + return got +} + +func TestForecastLearningEndpointsDelegateExactSignalAndReportCurrentStatus(t *testing.T) { + learning := &forecastLearningAPIMock{status: map[string]forecasting.LearningStatus{ + "pv": {Engine: "energyplan", Status: "ready", StartedMS: 100, LatestTrainingMS: 150, ResetAvailable: true}, + "load": {Engine: "energyplan", Status: "ready", StartedMS: 200, LatestTrainingMS: 250, ResetAvailable: true}, + }} + srv := New(&Deps{ + PVModel: pvmodel.NewService(nil, nil, nil, nil, 5000), + LoadModel: loadmodel.NewService(nil, telemetry.NewStore(), "site", 4000, 0), + ForecastLearning: learning, + }) + + for _, tc := range []struct { + signal string + path string + }{ + {signal: "pv", path: "/api/pvmodel"}, + {signal: "load", path: "/api/loadmodel"}, + } { + t.Run(tc.signal, func(t *testing.T) { + get := httptest.NewRecorder() + srv.Handler().ServeHTTP(get, httptest.NewRequest(http.MethodGet, tc.path, nil)) + if get.Code != http.StatusOK { + t.Fatalf("GET status = %d, body: %s", get.Code, get.Body.String()) + } + before := decodeForecastLearningResponse(t, get) + if before.Learning != learning.status[tc.signal] { + t.Fatalf("GET learning = %+v, want %+v", before.Learning, learning.status[tc.signal]) + } + + post := httptest.NewRecorder() + srv.Handler().ServeHTTP(post, httptest.NewRequest(http.MethodPost, tc.path+"/reset", nil)) + if post.Code != http.StatusOK { + t.Fatalf("POST status = %d, body: %s", post.Code, post.Body.String()) + } + after := decodeForecastLearningResponse(t, post) + if after.Status != "reset" || after.Learning != learning.status[tc.signal] { + t.Fatalf("POST response = %+v, current status = %+v", after, learning.status[tc.signal]) + } + }) + } + if got := strings.Join(learning.calls, ","); got != "pv,load" { + t.Fatalf("restart signals = %q, want pv,load", got) + } +} + +func TestForecastLearningResetRefusesWithoutCoordinatorAndKeepsProfileActionSeparate(t *testing.T) { + pv := pvmodel.NewService(nil, nil, nil, nil, 5000) + pv.Residuals.Add(timeForForecastLearningTest(), 1000, 1500) + load := loadmodel.NewService(nil, telemetry.NewStore(), "site", 4000, 0) + srv := New(&Deps{PVModel: pv, LoadModel: load}) + profile := httptest.NewRecorder() + profileRequest := httptest.NewRequest(http.MethodPost, "/api/loadmodel/profile", strings.NewReader(`{"profile":"away"}`)) + profileRequest.Header.Set("Content-Type", "application/json") + srv.Handler().ServeHTTP(profile, profileRequest) + if profile.Code != http.StatusOK { + t.Fatalf("profile action status = %d, body: %s", profile.Code, profile.Body.String()) + } + + for _, path := range []string{"/api/pvmodel/reset", "/api/loadmodel/reset"} { + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, httptest.NewRequest(http.MethodPost, path, nil)) + if rr.Code != http.StatusServiceUnavailable { + t.Fatalf("%s status = %d, want 503; body: %s", path, rr.Code, rr.Body.String()) + } + got := decodeForecastLearningResponse(t, rr) + if got.Status == "reset" || got.Error == "" { + t.Fatalf("%s claimed success: %+v", path, got) + } + } + if pv.Residuals.Len() != 1 { + t.Fatal("PV legacy state was reset without the coordinator") + } + if load.Profile() != loadmodel.ProfileAway { + t.Fatal("load learning reset changed the separate profile action") + } +} + +func TestForecastLearningPersistenceFailureReturns503WithoutLegacyReset(t *testing.T) { + pv := pvmodel.NewService(nil, nil, nil, nil, 5000) + pv.Residuals.Add(timeForForecastLearningTest(), 1000, 1500) + wantErr := errors.New("persist learning boundary: disk full") + learning := &forecastLearningAPIMock{ + status: map[string]forecasting.LearningStatus{"pv": {Engine: "energyplan", Status: "error", ResetAvailable: true}}, + err: wantErr, + } + srv := New(&Deps{PVModel: pv, ForecastLearning: learning}) + + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, httptest.NewRequest(http.MethodPost, "/api/pvmodel/reset", nil)) + + if rr.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503; body: %s", rr.Code, rr.Body.String()) + } + got := decodeForecastLearningResponse(t, rr) + if got.Status != "error" || !strings.Contains(got.Error, wantErr.Error()) { + t.Fatalf("failure response = %+v", got) + } + if pv.Residuals.Len() != 1 { + t.Fatal("handler ran a legacy-only reset after coordinator failure") + } + if len(learning.calls) != 1 || learning.calls[0] != "pv" { + t.Fatalf("restart calls = %v", learning.calls) + } +} + +func TestForecastLearningDurablePendingErrorReturns503Pending(t *testing.T) { + cause := errors.New("load model save failed") + learning := &forecastLearningAPIMock{ + status: map[string]forecasting.LearningStatus{"load": {Engine: "energyplan", Status: "pending", StartedMS: 1234, ResetAvailable: true}}, + err: &forecasting.LearningRestartPendingError{Err: cause}, + } + srv := New(&Deps{LoadModel: loadmodel.NewService(nil, telemetry.NewStore(), "site", 4000, 0), ForecastLearning: learning}) + + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, httptest.NewRequest(http.MethodPost, "/api/loadmodel/reset", nil)) + + if rr.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503; body: %s", rr.Code, rr.Body.String()) + } + got := decodeForecastLearningResponse(t, rr) + if got.Status != "pending" || got.Learning != learning.status["load"] || !strings.Contains(got.Error, cause.Error()) { + t.Fatalf("pending response = %+v", got) + } +} + +func timeForForecastLearningTest() time.Time { + return time.Unix(1, 0) +} diff --git a/go/internal/api/api_loadmodel.go b/go/internal/api/api_loadmodel.go index 20766480a..bb412a3f8 100644 --- a/go/internal/api/api_loadmodel.go +++ b/go/internal/api/api_loadmodel.go @@ -44,6 +44,7 @@ func (s *Server) handleLoadModel(w http.ResponseWriter, r *http.Request) { "heating_w_per_degc": stats.HeatingWPerDegC, "buckets_warm": stats.BucketsWarm, "buckets_total": stats.BucketsTotal, + "learning": s.forecastLearningStatus("load"), }) } @@ -80,16 +81,7 @@ func (s *Server) handleLoadModelProfile(w http.ResponseWriter, r *http.Request) } func (s *Server) handleLoadModelReset(w http.ResponseWriter, r *http.Request) { - if s.deps.LoadModel == nil { - writeJSON(w, 400, map[string]string{"error": "loadmodel disabled"}) - return - } - profile := s.deps.LoadModel.Profile() - s.deps.LoadModel.Reset() - if s.deps.MPC != nil { - s.deps.MPC.ReplanWithReason(r.Context(), "load_profile_reset") - } - writeJSON(w, 200, map[string]any{"status": "reset", "profile": profile}) + s.handleForecastLearningReset(w, r, "load") } func loadModelStatsFrom(m loadmodel.Model) loadModelStats { diff --git a/go/internal/energyforecast/native_reset_test.go b/go/internal/energyforecast/native_reset_test.go new file mode 100644 index 000000000..36f9a1beb --- /dev/null +++ b/go/internal/energyforecast/native_reset_test.go @@ -0,0 +1,121 @@ +package energyforecast_test + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/energyforecast" + "github.com/srcfl/ftw/go/internal/mpc" +) + +func TestNativeForecastResetIsSignalScopedAndDurable(t *testing.T) { + binary := os.Getenv("FTW_FORECAST_WORKER") + if binary == "" { + t.Skip("set FTW_FORECAST_WORKER to an Energyplan worker with forecast reset support") + } + newClient := func() (*energyforecast.Client, *mpc.ProcessTransport) { + transport, err := mpc.NewProcessTransport(mpc.ProcessTransportConfig{Command: []string{binary}, ModuleDir: filepath.Dir(binary)}) + if err != nil { + t.Fatal(err) + } + return energyforecast.NewClient(transport), transport + } + start := time.Date(2026, 6, 15, 12, 0, 0, 0, time.UTC) + quarter := 15 * time.Minute + end := start.Add(quarter) + features := func(at time.Time) energyforecast.Features { + ghi := 500.0 + available := start.UnixMilli() + return energyforecast.Features{ + LocalDay: at.Unix() / 86400, LocalWeekday: (int(at.Weekday()) + 6) % 7, + LocalMinute: at.Hour()*60 + at.Minute(), GHIWm2: &ghi, WeatherAvailableAtMs: &available, + } + } + config := energyforecast.Config{PV: &energyforecast.PVConfig{LatitudeDeg: 57, LongitudeDeg: 15}, Load: &energyforecast.LoadConfig{}} + observation := func(at time.Time, pv, load float64) energyforecast.Observation { + return energyforecast.Observation{ + Interval: energyforecast.Interval{ValidStartMs: at.UnixMilli(), ValidEndMs: at.Add(quarter).UnixMilli()}, + Features: features(at), AvailableAtMs: at.Add(quarter).UnixMilli(), + PVAvailableW: &pv, HouseholdLoadW: &load, PVQuality: energyforecast.QualityGood, LoadQuality: energyforecast.QualityGood, + } + } + for _, selected := range []string{"pv", "load"} { + t.Run(selected, func(t *testing.T) { + client, transport := newClient() + meta := energyforecast.RequestContext{RequestID: "native-reset-train-" + selected, SiteID: "native-reset-site", ConfigRevision: "v1", OriginMs: end.UnixMilli(), Config: config} + pv, load := 2300.0, 470.0 + trained, err := client.Update(context.Background(), energyforecast.UpdateRequest{RequestContext: meta, Observations: []energyforecast.Observation{observation(start, pv, load)}}) + if err != nil { + t.Fatal(err) + } + if trained.LatestTrainingMs.PV == nil || trained.LatestTrainingMs.Load == nil { + t.Fatalf("training clocks = %#v", trained.LatestTrainingMs) + } + cutoff := end.UnixMilli() + meta.RequestID = "native-reset-" + selected + meta.OriginMs = cutoff + meta.State = trained.State + reset, err := client.Reset(context.Background(), energyforecast.ResetRequest{RequestContext: meta, Signal: selected, LearningStartedMs: cutoff}) + if err != nil { + t.Fatal(err) + } + if reset.Signal != selected || reset.LearningStartedMs != cutoff || len(reset.State) == 0 { + t.Fatalf("reset response = %#v", reset) + } + if reset.ModelRevision != trained.ModelRevision+1 { + t.Fatalf("reset revision = %d, want %d", reset.ModelRevision, trained.ModelRevision+1) + } + if selected == "pv" { + if reset.LatestTrainingMs.PV != nil || reset.LatestTrainingMs.Load == nil { + t.Fatalf("PV reset clocks = %#v", reset.LatestTrainingMs) + } + } else if reset.LatestTrainingMs.Load != nil || reset.LatestTrainingMs.PV == nil { + t.Fatalf("load reset clocks = %#v", reset.LatestTrainingMs) + } + other := "pv" + if selected == "pv" { + other = "load" + } + meta.RequestID = "native-reset-other-" + other + meta.State = reset.State + both, err := client.Reset(context.Background(), energyforecast.ResetRequest{RequestContext: meta, Signal: other, LearningStartedMs: cutoff}) + if err != nil { + t.Fatal(err) + } + if both.LatestTrainingMs.PV != nil || both.LatestTrainingMs.Load != nil { + t.Fatalf("both reset clocks = %#v", both.LatestTrainingMs) + } + + // State is the saved worker snapshot. A fresh worker must retain the + // two independently applied cutoffs before it receives old history. + if err := transport.Close(); err != nil { + t.Fatal(err) + } + client, transport = newClient() + t.Cleanup(func() { _ = transport.Close() }) + meta.RequestID = "native-reset-old-input-" + selected + meta.OriginMs = cutoff + quarter.Milliseconds() + meta.State = both.State + oldPV, oldLoad := 9000.0, 9000.0 + afterOld, err := client.Update(context.Background(), energyforecast.UpdateRequest{RequestContext: meta, Observations: []energyforecast.Observation{observation(start, oldPV, oldLoad)}}) + if err != nil { + t.Fatal(err) + } + selectedCounts, otherCounts := afterOld.Updates.PV, afterOld.Updates.Load + selectedClock, otherClock := afterOld.LatestTrainingMs.PV, afterOld.LatestTrainingMs.Load + if selected == "load" { + selectedCounts, otherCounts = afterOld.Updates.Load, afterOld.Updates.PV + selectedClock, otherClock = afterOld.LatestTrainingMs.Load, afterOld.LatestTrainingMs.PV + } + if selectedCounts.Applied != 0 || selectedCounts.Skipped != 1 || selectedClock != nil { + t.Fatalf("old %s history resurrected: counts=%#v clock=%v", selected, selectedCounts, selectedClock) + } + if otherCounts.Applied != 0 || otherCounts.Skipped != 1 || otherClock != nil { + t.Fatalf("old %s history resurrected: counts=%#v clock=%v", other, otherCounts, otherClock) + } + }) + } +} diff --git a/go/internal/energyforecast/reset.go b/go/internal/energyforecast/reset.go new file mode 100644 index 000000000..421318373 --- /dev/null +++ b/go/internal/energyforecast/reset.go @@ -0,0 +1,37 @@ +package energyforecast + +import ( + "context" + "errors" +) + +func (c *Client) Reset(ctx context.Context, request ResetRequest) (ResetReply, error) { + var reply ResetReply + if err := validateContext(request.RequestContext); err != nil { + return reply, err + } + if (request.Signal != "pv" && request.Signal != "load") || + (request.Signal == "pv" && request.Config.PV == nil) || + (request.Signal == "load" && request.Config.Load == nil) { + return reply, errors.New("forecast reset needs an enabled pv or load signal") + } + if request.LearningStartedMs <= 0 || request.LearningStartedMs > request.OriginMs { + return reply, errors.New("forecast learning start must be positive and no later than origin") + } + payload := struct { + Op string `json:"op"` + Version int `json:"version"` + Action string `json:"action"` + ResetRequest + }{"forecast", ProtocolVersion, "reset", request} + if err := c.call(ctx, payload, request.RequestContext, "reset", &reply); err != nil { + return ResetReply{}, err + } + if err := validateState(reply.State, true); err != nil { + return ResetReply{}, err + } + if reply.Signal != request.Signal || reply.LearningStartedMs < request.LearningStartedMs || reply.LearningStartedMs > request.OriginMs { + return ResetReply{}, errors.New("forecast reset reply changed signal or learning start") + } + return reply, nil +} diff --git a/go/internal/energyforecast/reset_test.go b/go/internal/energyforecast/reset_test.go new file mode 100644 index 000000000..633410515 --- /dev/null +++ b/go/internal/energyforecast/reset_test.go @@ -0,0 +1,115 @@ +package energyforecast + +import ( + "context" + "encoding/json" + "testing" +) + +func resetRequest() ResetRequest { + return ResetRequest{RequestContext: requestContext(), Signal: "pv", LearningStartedMs: requestContext().OriginMs} +} + +func resetReply(payload []byte) map[string]any { + var request map[string]any + _ = json.Unmarshal(payload, &request) + reply := map[string]any{ + "ok": true, + "model_revision": 2, + "latest_input_ms": map[string]any{"pv": nil, "load": nil}, + "latest_training_ms": map[string]any{"pv": nil, "load": nil}, + "latest_available_at_ms": map[string]any{"pv": nil, "load": nil}, + "state": map[string]any{"opaque": "reset-state"}, + "signal": request["signal"], + "learning_started_ms": request["learning_started_ms"], + } + for _, key := range []string{"op", "version", "action", "request_id", "site_id", "config_revision", "origin_ms"} { + reply[key] = request[key] + } + return reply +} + +func resetReplying(mutate func(map[string]any)) (*Client, *map[string]any) { + var request map[string]any + c := NewClient(exchangeFunc(func(_ context.Context, payload []byte) ([]byte, error) { + if err := json.Unmarshal(payload, &request); err != nil { + return nil, err + } + reply := resetReply(payload) + if mutate != nil { + mutate(reply) + } + return json.Marshal(reply) + })) + return c, &request +} + +func TestClientResetSendsTargetAndAcceptsVersionedState(t *testing.T) { + c, sent := resetReplying(nil) + request := resetRequest() + reply, err := c.Reset(context.Background(), request) + if err != nil { + t.Fatal(err) + } + if (*sent)["op"] != "forecast" || (*sent)["action"] != "reset" || (*sent)["signal"] != "pv" { + t.Fatalf("reset target = %#v", *sent) + } + if got := int64((*sent)["learning_started_ms"].(float64)); got != request.LearningStartedMs { + t.Fatalf("reset cutoff = %d, want %d", got, request.LearningStartedMs) + } + if reply.Signal != request.Signal || reply.LearningStartedMs != request.LearningStartedMs || len(reply.State) == 0 { + t.Fatalf("reset reply = %#v", reply) + } + if reply.Action != "reset" || reply.RequestID != request.RequestID || reply.SiteID != request.SiteID || reply.ConfigRevision != request.ConfigRevision || reply.OriginMs != request.OriginMs { + t.Fatalf("reset metadata = %#v", reply.ReplyContext) + } +} + +func TestClientResetRejectsInvalidReply(t *testing.T) { + request := resetRequest() + for name, mutate := range map[string]func(map[string]any){ + "missing_state": func(r map[string]any) { delete(r, "state") }, + "array_state": func(r map[string]any) { r["state"] = []any{"not opaque"} }, + "wrong_signal": func(r map[string]any) { r["signal"] = "load" }, + "missing_signal": func(r map[string]any) { delete(r, "signal") }, + "earlier_cutoff": func(r map[string]any) { r["learning_started_ms"] = request.LearningStartedMs - 1 }, + "future_cutoff": func(r map[string]any) { r["learning_started_ms"] = request.OriginMs + 1 }, + "future_clock": func(r map[string]any) { + r["latest_training_ms"] = map[string]any{"pv": request.OriginMs + 1, "load": nil} + }, + "wrong_action": func(r map[string]any) { r["action"] = "update" }, + "predictions": func(r map[string]any) { r["predictions"] = []any{} }, + } { + t.Run(name, func(t *testing.T) { + c, _ := resetReplying(mutate) + if _, err := c.Reset(context.Background(), request); err == nil { + t.Fatal("invalid reset reply accepted") + } + }) + } +} + +func TestClientResetRejectsInvalidRequestBeforeIO(t *testing.T) { + calls := 0 + c := NewClient(exchangeFunc(func(context.Context, []byte) ([]byte, error) { + calls++ + return nil, nil + })) + for _, mutate := range []func(*ResetRequest){ + func(r *ResetRequest) { r.Signal = "battery" }, + func(r *ResetRequest) { r.Signal = "pv"; r.Config.PV = nil }, + func(r *ResetRequest) { r.Signal = "load"; r.Config.Load = nil }, + func(r *ResetRequest) { r.LearningStartedMs = 0 }, + func(r *ResetRequest) { r.LearningStartedMs = r.OriginMs + 1 }, + func(r *ResetRequest) { r.State = json.RawMessage("[]") }, + } { + request := resetRequest() + mutate(&request) + if _, err := c.Reset(context.Background(), request); err == nil { + t.Fatal("invalid reset request reached client call") + } + } + if calls != 0 { + t.Fatalf("invalid reset requests reached worker %d times", calls) + } +} diff --git a/go/internal/energyforecast/types.go b/go/internal/energyforecast/types.go index 870e91962..24e8b94a8 100644 --- a/go/internal/energyforecast/types.go +++ b/go/internal/energyforecast/types.go @@ -100,6 +100,14 @@ type PredictRequest struct { Horizon []HorizonSlot `json:"horizon"` } +// ResetRequest starts a new learning period for one signal. OriginMs is the +// current request time; LearningStartedMs also permits replay of a saved intent. +type ResetRequest struct { + RequestContext + Signal string `json:"signal"` + LearningStartedMs int64 `json:"learning_started_ms"` +} + type ReplyContext struct { Op string `json:"op"` Version int `json:"version"` @@ -129,6 +137,12 @@ type UpdateReply struct { State json.RawMessage `json:"state"` Updates UpdateCounts `json:"updates"` } +type ResetReply struct { + ReplyContext + State json.RawMessage `json:"state"` + Signal string `json:"signal"` + LearningStartedMs int64 `json:"learning_started_ms"` +} type LatestInput struct { PV *int64 `json:"pv,omitempty"` Load *int64 `json:"load,omitempty"` diff --git a/go/internal/forecasting/learning.go b/go/internal/forecasting/learning.go new file mode 100644 index 000000000..402cc89f1 --- /dev/null +++ b/go/internal/forecasting/learning.go @@ -0,0 +1,22 @@ +package forecasting + +import "fmt" + +// LearningRestartPendingError means the boundary is durable, but one of the +// model saves still needs retry. Callers must not report the intent as rejected. +type LearningRestartPendingError struct{ Err error } + +func (e *LearningRestartPendingError) Error() string { + return fmt.Sprintf("Learning period saved; model restart pending: %v", e.Err) +} +func (e *LearningRestartPendingError) Unwrap() error { return e.Err } + +// LearningStatus describes the model serving one forecast signal. Historical +// measurements and issued forecasts remain available across learning periods. +type LearningStatus struct { + Engine string `json:"engine"` + Status string `json:"status"` + StartedMS int64 `json:"started_ms"` + LatestTrainingMS int64 `json:"latest_training_ms"` + ResetAvailable bool `json:"reset_available"` +} diff --git a/go/internal/loadmodel/model.go b/go/internal/loadmodel/model.go index b2fe3940f..79c17b489 100644 --- a/go/internal/loadmodel/model.go +++ b/go/internal/loadmodel/model.go @@ -58,6 +58,7 @@ type Bucket struct { type Model struct { ConfigRevision string `json:"config_revision,omitempty"` + LearningStartedMS int64 `json:"learning_started_ms,omitempty"` Timezone string `json:"timezone,omitempty"` LastTemperatureC float64 `json:"last_temperature_c"` HasTemperature bool `json:"has_temperature"` diff --git a/go/internal/loadmodel/restart_learning_test.go b/go/internal/loadmodel/restart_learning_test.go new file mode 100644 index 000000000..4055e554b --- /dev/null +++ b/go/internal/loadmodel/restart_learning_test.go @@ -0,0 +1,200 @@ +package loadmodel + +import ( + "encoding/json" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/modelstate" + "github.com/srcfl/ftw/go/internal/state" + "github.com/srcfl/ftw/go/internal/telemetry" +) + +func TestRestartLearningClearsEveryLoadProfileAndRestoresConfiguredPrior(t *testing.T) { + s := NewService(nil, telemetry.NewStore(), "site", 4000, 0) + if err := s.Reconfigure("site", telemetry.ForecastOptions{}, "Europe/Stockholm", "site-a"); err != nil { + t.Fatal(err) + } + s.SeedHeatingCoef(275) + for _, profile := range Profiles() { + m := s.models[profile] + m.Update(time.Now(), 2000, 5) + m.HeatingW_per_degC = 900 + } + if err := s.SetProfile(ProfileAway); err != nil { + t.Fatal(err) + } + cutoff := time.Date(2026, 9, 7, 12, 0, 0, 0, time.UTC) + + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + + snap := s.Snapshot() + if snap.ActiveProfile != ProfileAway { + t.Fatalf("active profile changed to %q", snap.ActiveProfile) + } + for _, profile := range Profiles() { + got := snap.Profiles[profile] + if got.Samples != 0 || got.MAE != 0 || got.LastMs != 0 || got.HasTemperature { + t.Fatalf("learned %s state survived: %+v", profile, got) + } + if got.HeatingW_per_degC != 275 { + t.Fatalf("%s heating prior = %.0f, want configured 275", profile, got.HeatingW_per_degC) + } + if got.PeakW != 4000 || got.Timezone != "Europe/Stockholm" || got.ConfigRevision != "site-a" { + t.Fatalf("configured %s state changed: %+v", profile, got) + } + if got.LearningStartedMS != cutoff.UnixMilli() { + t.Fatalf("%s cutoff = %d", profile, got.LearningStartedMS) + } + } +} + +func TestRestartLearningRejectsLoadSampleCapturedBeforeCutoff(t *testing.T) { + tel := telemetry.NewStore() + tel.Update("site", telemetry.DerMeter, 1000, nil, nil) + tel.RecordDriverSuccess("site") + s := NewService(nil, tel, "site", 4000, 0) + cutoff := time.Now().Add(time.Millisecond) + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + + s.sampleAt(cutoff.Add(10 * time.Millisecond)) + + if got := s.Model().Samples; got != 0 { + t.Fatalf("pre-cutoff telemetry trained load model: %d samples", got) + } +} + +func TestRestartLearningRejectsMixedLoadBalanceWithOldInput(t *testing.T) { + tel := telemetry.NewStore() + now := time.Now() + cutoff := now.Add(-time.Second) + powerData := func(watts float64, measured time.Time) []byte { + data, err := json.Marshal(map[string]telemetry.ForecastPowerSample{ + "forecast_power": { + Version: 1, + Known: true, + Watts: watts, + MeasuredAtMS: measured.UnixMilli(), + ReceivedAtMS: now.UnixMilli(), + }, + }) + if err != nil { + t.Fatal(err) + } + return data + } + tel.Update("site", telemetry.DerMeter, 1000, nil, powerData(1000, cutoff.Add(-time.Second))) + tel.RecordDriverSuccess("site") + tel.Update("battery", telemetry.DerBattery, 500, nil, powerData(500, cutoff.Add(time.Second))) + tel.RecordDriverSuccess("battery") + s := NewService(nil, tel, "site", 4000, 0) + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + + s.sampleAt(now.Add(time.Second)) + + if got := s.Model().Samples; got != 0 { + t.Fatalf("balance with pre-cutoff meter trained load model: %d samples", got) + } +} + +func TestRestartLearningInvalidatesLoadSampleInFlight(t *testing.T) { + tel := telemetry.NewStore() + tel.Update("site", telemetry.DerMeter, 1000, nil, nil) + tel.RecordDriverSuccess("site") + s := NewService(nil, tel, "site", 4000, 0) + entered := make(chan struct{}) + release := make(chan struct{}) + s.Temp = func(time.Time) (float64, bool) { + close(entered) + <-release + return 5, true + } + done := make(chan struct{}) + go func() { + defer close(done) + s.sampleAt(time.Now()) + }() + <-entered + if err := s.RestartLearning(time.Now()); err != nil { + t.Fatal(err) + } + close(release) + <-done + + if got := s.Model().Samples; got != 0 { + t.Fatalf("in-flight sample survived restart: %d", got) + } +} + +func TestRestartLearningLoadRetryPersistsAllProfilesWithoutErasingNewSamples(t *testing.T) { + bad := openTestDB(t) + s := NewService(bad, telemetry.NewStore(), "site", 4000, 0) + s.SeedHeatingCoef(275) + if err := bad.Close(); err != nil { + t.Fatal(err) + } + cutoff := time.Date(2026, 9, 7, 13, 0, 0, 0, time.UTC) + if err := s.RestartLearning(cutoff); err == nil { + t.Fatal("closed store did not surface persistence failure") + } + for _, profile := range Profiles() { + if !s.models[profile].Update(cutoff.Add(time.Minute), 1000, 10) { + t.Fatalf("post-reset %s sample rejected", profile) + } + } + + good, err := state.Open(t.TempDir() + "/retry.db") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { good.Close() }) + s.Store = good + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + restored := NewService(good, telemetry.NewStore(), "site", 4000, 0) + if restored.LearningStartedMS() != cutoff.UnixMilli() { + t.Fatalf("restored cutoff = %d", restored.LearningStartedMS()) + } + for _, profile := range Profiles() { + if got := restored.models[profile]; got.Samples != 1 || got.LearningStartedMS != cutoff.UnixMilli() { + t.Fatalf("same-cutoff retry lost %s learning: %+v", profile, got) + } + } +} + +func TestLoadRestoreReconcilesPartiallyPersistedEpoch(t *testing.T) { + st := openTestDB(t) + s := NewService(st, telemetry.NewStore(), "site", 4000, 0) + at := time.Date(2026, 9, 7, 14, 0, 0, 0, time.UTC) + for _, profile := range Profiles() { + s.models[profile].Update(at, 1000, 10) + } + if err := s.persist(); err != nil { + t.Fatal(err) + } + + cutoff := at.Add(time.Hour).UnixMilli() + home := freshProfile(s.models[ProfileHome], ProfileHome, "UTC", cutoff, nil) + js, err := modelstate.Wrap(FeatureHash(), home) + if err != nil { + t.Fatal(err) + } + if err := st.SaveConfig(stateKey(ProfileHome), js); err != nil { + t.Fatal(err) + } + + restored := NewService(st, telemetry.NewStore(), "site", 4000, 0) + for _, profile := range Profiles() { + got := restored.models[profile] + if got.Samples != 0 || got.LearningStartedMS != cutoff { + t.Fatalf("partial epoch left stale %s state: %+v", profile, got) + } + } +} diff --git a/go/internal/loadmodel/service.go b/go/internal/loadmodel/service.go index 2e9a2dd56..8add3c562 100644 --- a/go/internal/loadmodel/service.go +++ b/go/internal/loadmodel/service.go @@ -74,6 +74,7 @@ type Service struct { forecastOptions telemetry.ForecastOptions timezone string lastForecastInput time.Time + configuredHeating *float64 stop chan struct{} done chan struct{} @@ -141,12 +142,25 @@ func NewService(st *state.Store, tel *telemetry.Store, siteMeter string, peakW, zone = "UTC" } if old.Samples > 0 && zone != s.timezone { - m := newProfileModel(old.PeakW, profile) - m.HeatingW_per_degC = old.HeatingW_per_degC - s.models[profile] = m + heating := old.HeatingW_per_degC + s.models[profile] = freshProfile(old, profile, s.timezone, old.LearningStartedMS, &heating) } s.models[profile].Timezone = s.timezone } + var latestEpoch int64 + for _, profile := range Profiles() { + if m := s.models[profile]; m != nil && m.LearningStartedMS > latestEpoch { + latestEpoch = m.LearningStartedMS + } + } + if latestEpoch > 0 { + for _, profile := range Profiles() { + old := s.models[profile] + if old != nil && old.LearningStartedMS < latestEpoch { + s.models[profile] = freshProfile(old, profile, s.timezone, latestEpoch, nil) + } + } + } } return s } @@ -387,9 +401,14 @@ func (s *Service) Reconfigure(siteMeter string, opts telemetry.ForecastOptions, s.lastForecastInput = time.Time{} if changed { peak := s.activeModelLocked().PeakW + epoch := s.learningStartedMSLocked() for _, p := range Profiles() { m := newProfileModel(peak, p) m.Timezone, m.ConfigRevision = zone, revision + m.LearningStartedMS = epoch + if s.configuredHeating != nil { + m.HeatingW_per_degC = *s.configuredHeating + } s.models[p] = m } } @@ -412,6 +431,9 @@ func (s *Service) SetTimezone(zone string) error { m := newProfileModel(old.PeakW, p) m.Timezone = zone m.HeatingW_per_degC = old.HeatingW_per_degC + m.LearningStartedMS = old.LearningStartedMS + m.ConfigRevision = old.ConfigRevision + m.MaxPlausibleW = old.MaxPlausibleW s.models[p] = m } } @@ -427,7 +449,7 @@ func (s *Service) sampleAt(now time.Time) { return } s.mu.RLock() - site, opts, profile, generation := s.SiteMeter, s.forecastOptions, s.active, s.generation + site, opts, profile, generation, learningStartedMS := s.SiteMeter, s.forecastOptions, s.active, s.generation, s.learningStartedMSLocked() s.mu.RUnlock() reading := s.Tele.ForecastMeasurement(now, site, opts) temp := math.NaN() @@ -443,7 +465,7 @@ func (s *Service) sampleAt(now time.Time) { } model := s.activeModelLocked() updated := false - if reading.Valid && reading.Latest.After(s.lastForecastInput) { + if reading.Valid && reading.Earliest.UnixMilli() >= learningStartedMS && reading.Latest.After(s.lastForecastInput) { s.lastForecastInput = reading.Latest updated = model.Update(now, reading.HouseholdW, temp) } @@ -466,6 +488,10 @@ func (s *Service) persistProfile(profile Profile) error { func (s *Service) persist() error { s.persistMu.Lock() defer s.persistMu.Unlock() + return s.persistLocked() +} + +func (s *Service) persistLocked() error { if s.Store == nil { return nil } @@ -510,6 +536,8 @@ func (s *Service) SetHeatingCoef(w float64) { return } s.mu.Lock() + prior := w + s.configuredHeating = &prior for _, model := range s.models { model.HeatingW_per_degC = w } @@ -533,6 +561,8 @@ func (s *Service) SeedHeatingCoef(w float64) { return } s.mu.Lock() + prior := w + s.configuredHeating = &prior for _, model := range s.models { if model.Samples > 0 { continue @@ -557,6 +587,8 @@ func (s *Service) Reset() { s.models[profile].HeatingW_per_degC = heating s.models[profile].Timezone = s.timezone s.models[profile].ConfigRevision = old.ConfigRevision + s.models[profile].LearningStartedMS = old.LearningStartedMS + s.models[profile].MaxPlausibleW = old.MaxPlausibleW s.generation++ s.lastForecastInput = time.Time{} s.mu.Unlock() @@ -564,3 +596,68 @@ func (s *Service) Reset() { slog.Warn("loadmodel persist", "err", err) } } + +// RestartLearning clears every profile and starts one shared learned-model +// epoch. Replaying the same or an older cutoff persists the current models but +// does not erase samples learned after the first reset. +func (s *Service) RestartLearning(at time.Time) error { + if s == nil { + return nil + } + startedMS := at.UnixMilli() + if at.IsZero() || startedMS <= 0 { + return fmt.Errorf("loadmodel learning cutoff must be after the Unix epoch") + } + s.persistMu.Lock() + defer s.persistMu.Unlock() + s.mu.Lock() + if startedMS > s.learningStartedMSLocked() { + for _, profile := range Profiles() { + s.models[profile] = freshProfile(s.models[profile], profile, s.timezone, startedMS, s.configuredHeating) + } + s.generation++ + s.lastForecastInput = time.Time{} + } + s.mu.Unlock() + return s.persistLocked() +} + +// LearningStartedMS returns the cutoff shared by all load profiles. +func (s *Service) LearningStartedMS() int64 { + if s == nil { + return 0 + } + s.mu.RLock() + defer s.mu.RUnlock() + return s.learningStartedMSLocked() +} + +func (s *Service) learningStartedMSLocked() int64 { + var startedMS int64 + for _, profile := range Profiles() { + if m := s.models[profile]; m != nil && m.LearningStartedMS > startedMS { + startedMS = m.LearningStartedMS + } + } + return startedMS +} + +func freshProfile(old *Model, profile Profile, zone string, startedMS int64, heating *float64) *Model { + peak := 0.0 + revision := "" + maxPlausibleW := 0.0 + if old != nil { + peak = old.PeakW + revision = old.ConfigRevision + maxPlausibleW = old.MaxPlausibleW + } + m := newProfileModel(peak, profile) + m.Timezone = zone + m.ConfigRevision = revision + m.MaxPlausibleW = maxPlausibleW + m.LearningStartedMS = startedMS + if heating != nil { + m.HeatingW_per_degC = *heating + } + return m +} diff --git a/go/internal/mpc/energyplan_test.go b/go/internal/mpc/energyplan_test.go index e6844cfeb..cd5aa599a 100644 --- a/go/internal/mpc/energyplan_test.go +++ b/go/internal/mpc/energyplan_test.go @@ -60,7 +60,7 @@ func TestNativeEnergyplanDownsideAndAsyncShadow(t *testing.T) { svc := shadowTestService(t) svc.Optimizer = &EnergyplanOptimizer{ExternalOptimizer: o} info, err := svc.Optimizer.(*EnergyplanOptimizer).Health(context.Background()) - if err != nil || info.Name != "ftw-solver" || info.Version != "0.2.1" { + if err != nil || info.Name != "ftw-solver" || info.Version != "0.2.2" { t.Fatalf("bundled worker health: %+v %v", info, err) } start := time.Now().UTC().Truncate(time.Hour) diff --git a/go/internal/pvmodel/model.go b/go/internal/pvmodel/model.go index 3c89c08aa..f2e4a8567 100644 --- a/go/internal/pvmodel/model.go +++ b/go/internal/pvmodel/model.go @@ -51,13 +51,14 @@ func finite(v float64) bool { return !math.IsNaN(v) && !math.IsInf(v, 0) } // Model is the learned PV predictor. type Model struct { - ConfigRevision string `json:"config_revision,omitempty"` - Beta [NFeat]float64 `json:"beta"` - P [NFeat][NFeat]float64 `json:"p"` // covariance - Forgetting float64 `json:"forgetting"` - Samples int64 `json:"samples"` - LastMs int64 `json:"last_ms"` - MAE float64 `json:"mae"` // EMA of |err| (W) + ConfigRevision string `json:"config_revision,omitempty"` + LearningStartedMS int64 `json:"learning_started_ms,omitempty"` + Beta [NFeat]float64 `json:"beta"` + P [NFeat][NFeat]float64 `json:"p"` // covariance + Forgetting float64 `json:"forgetting"` + Samples int64 `json:"samples"` + LastMs int64 `json:"last_ms"` + MAE float64 `json:"mae"` // EMA of |err| (W) // RelMAE is MAE expressed as a share of the prediction it belongs to // (0..1), over the same EMA window. The planner sizes each slot's PV // downside against that slot's own expected generation, which a watt diff --git a/go/internal/pvmodel/restart_learning_test.go b/go/internal/pvmodel/restart_learning_test.go new file mode 100644 index 000000000..82946453d --- /dev/null +++ b/go/internal/pvmodel/restart_learning_test.go @@ -0,0 +1,171 @@ +package pvmodel + +import ( + "encoding/json" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/state" + "github.com/srcfl/ftw/go/internal/telemetry" +) + +func TestRestartLearningClearsLearnedPVState(t *testing.T) { + s := NewService(nil, nil, func(time.Time) float64 { return 800 }, nil, 5000) + s.SetACLimit(4200) + s.model.ConfigRevision = "site-a" + at := time.Date(2026, 9, 7, 10, 0, 0, 0, time.UTC) + for i := 0; i < 20; i++ { + s.model.Update(800, 20, at.Add(time.Duration(i)*time.Minute), 3000) + s.Residuals.Add(at.Add(time.Duration(i)*time.Minute), 2500, 3000) + } + + if err := s.RestartLearning(at.Add(time.Hour)); err != nil { + t.Fatal(err) + } + + got := s.Model() + if got.Samples != 0 || got.MAE != 0 || got.RelMAE != 0 || got.InferredScaleKnown || got.ScaleSamples != 0 { + t.Fatalf("learned state survived: %+v", got) + } + if s.Residuals.Len() != 0 { + t.Fatal("PV residuals survived") + } + if got.RatedW != 5000 || got.ACLimitW != 4200 || got.ConfigRevision != "site-a" { + t.Fatalf("configured PV state changed: %+v", got) + } + if got.LearningStartedMS != at.Add(time.Hour).UnixMilli() || s.LearningStartedMS() != got.LearningStartedMS { + t.Fatalf("learning cutoff = %d", got.LearningStartedMS) + } +} + +func TestRestartLearningRejectsSampleCapturedBeforeCutoff(t *testing.T) { + tel := telemetry.NewStore() + tel.Update("pv", telemetry.DerPV, -3000, nil, nil) + tel.RecordDriverSuccess("pv") + s := NewService(nil, tel, func(time.Time) float64 { return 800 }, func(time.Time) (float64, bool) { return 20, true }, 5000) + cutoff := time.Now().Add(time.Millisecond) + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + + s.sampleAt(cutoff.Add(10 * time.Millisecond)) + + if got := s.Model().Samples; got != 0 { + t.Fatalf("pre-cutoff telemetry trained PV model: %d samples", got) + } +} + +func TestRestartLearningRejectsMixedPVWithOldInput(t *testing.T) { + tel := telemetry.NewStore() + now := time.Now() + cutoff := now.Add(-time.Second) + tel.Update("old-pv", telemetry.DerPV, -1000, nil, forecastPowerData(t, -1000, cutoff.Add(-time.Second), now)) + tel.RecordDriverSuccess("old-pv") + tel.Update("new-pv", telemetry.DerPV, -2000, nil, forecastPowerData(t, -2000, cutoff.Add(time.Second), now)) + tel.RecordDriverSuccess("new-pv") + s := NewService(nil, tel, func(time.Time) float64 { return 800 }, func(time.Time) (float64, bool) { return 20, true }, 5000) + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + + s.sampleAt(now.Add(time.Second)) + + if got := s.Model().Samples; got != 0 { + t.Fatalf("PV sum with pre-cutoff source trained model: %d samples", got) + } +} + +func TestRestartLearningIgnoresOldNonPVInputForPVTraining(t *testing.T) { + tel := telemetry.NewStore() + now := time.Now() + cutoff := now.Add(-time.Second) + tel.Update("pv", telemetry.DerPV, -3000, nil, forecastPowerData(t, -3000, cutoff.Add(time.Second), now)) + tel.RecordDriverSuccess("pv") + tel.Update("battery", telemetry.DerBattery, 500, nil, forecastPowerData(t, 500, cutoff.Add(-time.Second), now)) + tel.RecordDriverSuccess("battery") + s := NewService(nil, tel, func(time.Time) float64 { return 800 }, func(time.Time) (float64, bool) { return 20, true }, 5000) + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + + s.sampleAt(now.Add(time.Second)) + + if got := s.Model().Samples; got != 1 { + t.Fatalf("old non-PV input blocked fresh PV training: %d samples", got) + } +} + +func forecastPowerData(t *testing.T, watts float64, measured, received time.Time) []byte { + t.Helper() + data, err := json.Marshal(map[string]telemetry.ForecastPowerSample{ + "forecast_power": { + Version: 1, + Known: true, + Watts: watts, + MeasuredAtMS: measured.UnixMilli(), + ReceivedAtMS: received.UnixMilli(), + }, + }) + if err != nil { + t.Fatal(err) + } + return data +} + +func TestRestartLearningInvalidatesPVSampleInFlight(t *testing.T) { + tel := telemetry.NewStore() + tel.Update("pv", telemetry.DerPV, -3000, nil, nil) + tel.RecordDriverSuccess("pv") + entered := make(chan struct{}) + release := make(chan struct{}) + s := NewService(nil, tel, func(time.Time) float64 { + close(entered) + <-release + return 800 + }, func(time.Time) (float64, bool) { return 20, true }, 5000) + done := make(chan struct{}) + go func() { + defer close(done) + s.sampleAt(time.Now()) + }() + <-entered + cutoff := time.Now() + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + close(release) + <-done + + if got := s.Model().Samples; got != 0 || s.Residuals.Len() != 0 { + t.Fatalf("in-flight sample survived restart: samples=%d residuals=%d", got, s.Residuals.Len()) + } +} + +func TestRestartLearningPVRetryPersistsWithoutErasingNewSamples(t *testing.T) { + bad := openTestDB(t) + s := NewService(bad, nil, func(time.Time) float64 { return 800 }, nil, 5000) + if err := bad.Close(); err != nil { + t.Fatal(err) + } + cutoff := time.Date(2026, 9, 7, 11, 0, 0, 0, time.UTC) + if err := s.RestartLearning(cutoff); err == nil { + t.Fatal("closed store did not surface persistence failure") + } + if !s.model.Update(800, 20, cutoff.Add(time.Minute), 3000) { + t.Fatal("post-reset fixture sample rejected") + } + + good, err := state.Open(t.TempDir() + "/retry.db") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { good.Close() }) + s.Store = good + if err := s.RestartLearning(cutoff); err != nil { + t.Fatal(err) + } + restored := NewService(good, nil, func(time.Time) float64 { return 800 }, nil, 5000) + if got := restored.Model(); got.Samples != 1 || got.LearningStartedMS != cutoff.UnixMilli() { + t.Fatalf("same-cutoff retry erased or failed to persist new learning: %+v", got) + } +} diff --git a/go/internal/pvmodel/service.go b/go/internal/pvmodel/service.go index 01b216cc4..6a5ce5840 100644 --- a/go/internal/pvmodel/service.go +++ b/go/internal/pvmodel/service.go @@ -2,6 +2,7 @@ package pvmodel import ( "context" + "fmt" "log/slog" "sync" "time" @@ -344,14 +345,19 @@ func (s *Service) liveActualPV() (float64, bool) { } func (s *Service) liveActualPVAt(now time.Time) (float64, bool) { + pvW, _, valid := s.livePVMeasurementAt(now) + return pvW, valid +} + +func (s *Service) livePVMeasurementAt(now time.Time) (float64, time.Time, bool) { if s.Tele == nil || (s.CurtailmentActive != nil && s.CurtailmentActive()) { - return 0, false + return 0, time.Time{}, false } s.mu.RLock() options := s.forecastOptions s.mu.RUnlock() m := s.Tele.ForecastMeasurement(now, "", options) - return -m.PVW, m.PVValid + return -m.PVW, m.PVEarliest, m.PVValid } // PredictNow returns the twin's prediction for right now using the @@ -416,7 +422,7 @@ func (s *Service) sample() { func (s *Service) sampleAt(now time.Time) { s.mu.RLock() - clearSky, generation := s.ClearSky, s.generation + clearSky, generation, learningStartedMS := s.ClearSky, s.generation, s.model.LearningStartedMS s.mu.RUnlock() if clearSky == nil { return @@ -434,10 +440,13 @@ func (s *Service) sampleAt(now time.Time) { } // Aggregate PV across all drivers. PV telemetry is stored as // site-sign (negative = generating), so flip to positive. - pvW, valid := s.liveActualPVAt(now) + pvW, earliest, valid := s.livePVMeasurementAt(now) if !valid { return } + if learningStartedMS > 0 && earliest.UnixMilli() < learningStartedMS { + return + } // Capture (predicted_at_now, actual_at_now) for the residual buffer // BEFORE running the RLS update — otherwise the model's prediction @@ -470,24 +479,31 @@ func (s *Service) sampleAt(now time.Time) { } } -func (s *Service) persist() { - if s.Store == nil { - return - } +func (s *Service) persist() error { // Serialise the entire marshal+save so a sample-loop persist that // started before a Reset cannot finish after Reset's persist and // clobber the clean state with stale coefficients. s.persistMu.Lock() defer s.persistMu.Unlock() + return s.persistLocked() +} + +// persistLocked writes one snapshot while persistMu is held. +func (s *Service) persistLocked() error { + if s.Store == nil { + return nil + } s.mu.RLock() js, err := modelstate.Wrap(FeatureHash(), s.model) s.mu.RUnlock() if err != nil { - return + return err } if err := s.Store.SaveConfig(stateKey, string(js)); err != nil { slog.Warn("pvmodel persist", "err", err) + return err } + return nil } // Reset clears the model to a fresh prior (useful after a system change @@ -501,20 +517,56 @@ func (s *Service) Reset() { s.mu.Lock() s.resetLocked() s.mu.Unlock() - s.persist() + if err := s.persist(); err != nil { + slog.Warn("pvmodel persist", "err", err) + } } func (s *Service) resetLocked() { rated := s.model.RatedW ac := s.model.ACLimitW revision := s.model.ConfigRevision + learningStartedMS := s.model.LearningStartedMS s.model = NewModel(rated) s.model.ACLimitW = ac s.model.ConfigRevision = revision + s.model.LearningStartedMS = learningStartedMS s.Residuals = NewResidualBuffer() s.generation++ } +// RestartLearning starts a new learned-model epoch. A replay of the same or an +// older cutoff only persists the current state, so a boot-time retry repairs a +// failed write without erasing samples learned after the reset. +func (s *Service) RestartLearning(at time.Time) error { + if s == nil { + return nil + } + startedMS := at.UnixMilli() + if at.IsZero() || startedMS <= 0 { + return fmt.Errorf("pvmodel learning cutoff must be after the Unix epoch") + } + s.persistMu.Lock() + defer s.persistMu.Unlock() + s.mu.Lock() + if startedMS > s.model.LearningStartedMS { + s.resetLocked() + s.model.LearningStartedMS = startedMS + } + s.mu.Unlock() + return s.persistLocked() +} + +// LearningStartedMS returns the current learned-model epoch cutoff. +func (s *Service) LearningStartedMS() int64 { + if s == nil { + return 0 + } + s.mu.RLock() + defer s.mu.RUnlock() + return s.model.LearningStartedMS +} + // ResidualCorrect is the integration point for the MPC. Returns the // additive correction (W) to apply to a base prediction targeting // tTarget, computed at wall time `now`. Returns 0 when residuals are diff --git a/go/internal/telemetry/forecast.go b/go/internal/telemetry/forecast.go index d2344cc39..8ed431ad5 100644 --- a/go/internal/telemetry/forecast.go +++ b/go/internal/telemetry/forecast.go @@ -39,7 +39,7 @@ type ForecastOptions struct { // Valid describes the household balance. PVValid only describes the PV sources. // Neither flag claims that the instantaneous sample covers a time interval. type ForecastReading struct { - At, Earliest, Latest, PVLatest time.Time + At, Earliest, Latest, PVEarliest, PVLatest time.Time GridW, PVW, BatteryW, EVW, V2XW, HouseholdW float64 Valid, PVValid bool Reason, PVReason string @@ -208,7 +208,7 @@ func (s *Store) forecastMeasurementLocked(now time.Time, siteMeter string, opts if out.Latest.Sub(out.Earliest) > opts.MaxSkew { fail("time_skew", false) } - out.PVLatest = pvLast + out.PVEarliest, out.PVLatest = pvFirst, pvLast if pvLast.Sub(pvFirst) > opts.MaxSkew { fail("pv_time_skew", true) } diff --git a/optimizer/native/bundle/forecast-v1.response.schema.json b/optimizer/native/bundle/forecast-v1.response.schema.json index a83703574..31f920b7c 100644 --- a/optimizer/native/bundle/forecast-v1.response.schema.json +++ b/optimizer/native/bundle/forecast-v1.response.schema.json @@ -175,6 +175,85 @@ "predictions" ] }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "op": { + "const": "forecast" + }, + "version": { + "const": 1 + }, + "action": { + "const": "reset" + }, + "request_id": { + "type": "string" + }, + "site_id": { + "type": "string" + }, + "config_revision": { + "type": "string" + }, + "origin_ms": { + "type": "integer", + "minimum": 0 + }, + "ok": { + "const": true + }, + "model_revision": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615, + "description": "Increments when reset applies a newer cutoff. Replaying an already-applied cutoff keeps the revision." + }, + "latest_input_ms": { + "$ref": "#/$defs/watermarks" + }, + "latest_training_ms": { + "$ref": "#/$defs/watermarks" + }, + "latest_available_at_ms": { + "$ref": "#/$defs/watermarks" + }, + "state": { + "type": "object", + "description": "Opaque snapshot after the selected signal reset. Persist unchanged." + }, + "signal": { + "enum": [ + "pv", + "load" + ], + "description": "The selected model signal." + }, + "learning_started_ms": { + "type": "integer", + "minimum": 0, + "description": "Applied learning cutoff for signal. It is no later than origin_ms and can be later than a replayed request cutoff." + } + }, + "required": [ + "op", + "version", + "action", + "request_id", + "site_id", + "config_revision", + "origin_ms", + "ok", + "model_revision", + "latest_input_ms", + "latest_training_ms", + "latest_available_at_ms", + "state", + "signal", + "learning_started_ms" + ] + }, { "type": "object", "required": [ diff --git a/optimizer/native/bundle/forecast-v1.schema.json b/optimizer/native/bundle/forecast-v1.schema.json index 2487b9dce..e5df5e978 100644 --- a/optimizer/native/bundle/forecast-v1.schema.json +++ b/optimizer/native/bundle/forecast-v1.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "urn:sourceful:energyplan:forecast:1", "title": "Energyplan forecasting request v1", - "description": "One JSON object per stdin line, at most 2 MiB including the newline. The caller must split update batches by encoded byte size, not just count; a larger line terminates the worker. Update consumes complete UTC-aligned 15-minute means available by origin_ms and returns an opaque model snapshot. Predict uses an immutable snapshot. Each requested future segment lasts 1..900000 ms and stays within one UTC quarter-hour; full aligned quarters remain valid. Returned bounds are the actual requested segment. For prediction, site-local clock, weather and occupancy fields refer to the containing quarter-hour start. The caller splits longer spans at each UTC quarter boundary and handles timezone or DST changes. Site/config identity, interval order, state bounds and causal availability are checked at runtime. Household load excludes EV, battery and V2X. PV denotes nonnegative available AC generation, distinct from signed planner pv_w. The caller records actual completion/issue time and persists the snapshot. See forecast-v1.response.schema.json for responses.", + "description": "One JSON object per stdin line, at most 2 MiB including the newline. The caller must split update batches by encoded byte size, not just count; a larger line terminates the worker. Update consumes complete UTC-aligned 15-minute means available by origin_ms and returns an opaque model snapshot. Predict uses an immutable snapshot. Reset starts a new learning period for exactly one enabled signal while retaining the other signal's state. Each requested future segment lasts 1..900000 ms and stays within one UTC quarter-hour; full aligned quarters remain valid. Returned bounds are the actual requested segment. For prediction, site-local clock, weather and occupancy fields refer to the containing quarter-hour start. The caller splits longer spans at each UTC quarter boundary and handles timezone or DST changes. Site/config identity, interval order, state bounds and causal availability are checked at runtime. Household load excludes EV, battery and V2X. PV denotes nonnegative available AC generation, distinct from signed planner pv_w. The caller records actual completion/issue time and persists the snapshot. See forecast-v1.response.schema.json for responses.", "type": "object", "additionalProperties": false, "properties": { @@ -15,7 +15,8 @@ "action": { "enum": [ "update", - "predict" + "predict", + "reset" ] }, "request_id": { @@ -175,6 +176,18 @@ "items": { "$ref": "#/$defs/horizon" } + }, + "signal": { + "enum": [ + "pv", + "load" + ], + "description": "The one enabled model whose learned state reset replaces. Only valid for action reset." + }, + "learning_started_ms": { + "type": "integer", + "minimum": 0, + "description": "Durable start cutoff for the selected signal. It must not be later than origin_ms; runtime validates that relation. A replay may return a later already-applied cutoff. Only valid for action reset." } }, "required": [ @@ -201,8 +214,22 @@ "observations" ], "not": { - "required": [ - "horizon" + "anyOf": [ + { + "required": [ + "horizon" + ] + }, + { + "required": [ + "signal" + ] + }, + { + "required": [ + "learning_started_ms" + ] + } ] } } @@ -220,8 +247,51 @@ "horizon" ], "not": { - "required": [ - "observations" + "anyOf": [ + { + "required": [ + "observations" + ] + }, + { + "required": [ + "signal" + ] + }, + { + "required": [ + "learning_started_ms" + ] + } + ] + } + } + }, + { + "if": { + "properties": { + "action": { + "const": "reset" + } + } + }, + "then": { + "required": [ + "signal", + "learning_started_ms" + ], + "not": { + "anyOf": [ + { + "required": [ + "observations" + ] + }, + { + "required": [ + "horizon" + ] + } ] } } diff --git a/optimizer/native/bundle/ftw-solver-darwin-arm64 b/optimizer/native/bundle/ftw-solver-darwin-arm64 index e657e916175f213b5c0f7a332843bfb3c3a7b779..c3c9028460d713a91b6db50a6a44163caed75ef2 100755 GIT binary patch delta 243484 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�#%9TwG#Q&IWAbEd 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