From f594f1ff5ed549291faf719ae0513515c24c7b95 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 4 Oct 2026 05:13:32 +0200 Subject: [PATCH] fix(mpc): give Energyplan 1.5 s when a car is plugged in On the home Pi 4, two plans with the car came back 11% above the proven optimum at Core's 0.5 s budget. On the same requests the planner reaches the optimum with about 0.6-0.7 s of Pi time, so 0.5 s sat just below the point where it can improve its first plan. Requests with an active loadpoint now get 1.5 s; battery-only requests keep 0.5 s, and fleet, demand-charge and curtailment requests keep 5 s. The remaining time of the first slot still caps every budget. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Fredrik Ahlgren --- .changeset/energyplan-ev-budget.md | 5 +++++ go/internal/mpc/energyplan.go | 7 +++++++ go/internal/mpc/energyplan_test.go | 26 ++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 .changeset/energyplan-ev-budget.md diff --git a/.changeset/energyplan-ev-budget.md b/.changeset/energyplan-ev-budget.md new file mode 100644 index 000000000..82c6cc595 --- /dev/null +++ b/.changeset/energyplan-ev-budget.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Give the Energyplan planner 1.5 s instead of 0.5 s when a car is plugged in. On a Raspberry Pi 4, 0.5 s stopped just before the planner could improve on its first plan for the car, and the published plan could cost about 11% more than the best one. Battery-only plans keep their 0.5 s budget, and the first slot's remaining time still caps every budget. diff --git a/go/internal/mpc/energyplan.go b/go/internal/mpc/energyplan.go index 1f29604d3..fb30fcce8 100644 --- a/go/internal/mpc/energyplan.go +++ b/go/internal/mpc/energyplan.go @@ -28,6 +28,10 @@ func NewEnergyplanOptimizer(binary string) (*EnergyplanOptimizer, error) { const ( energyplanSmallBudget = 500 * time.Millisecond + // With a car plugged in, a Raspberry Pi 4 needs about 0.6-0.7 s before + // the planner can improve on its first plan; at 0.5 s it stopped just + // short and published plans about 11% above the proven optimum. + energyplanEVBudget = 1500 * time.Millisecond energyplanFleetBudget = 5 * time.Second // Leave time for ValidatePlan and publication before firstSlotExpired. energyplanPublishMargin = 50 * time.Millisecond @@ -42,6 +46,9 @@ func energyplanTimeBudget(slots []Slot, p Params) time.Duration { // Core already adjusts PV to one downside horizon. That margin does not // add worker scenarios or change this model's size. budget := energyplanSmallBudget + if len(p.activeLoadpoints()) > 0 { + budget = energyplanEVBudget + } // 1 battery + 1 EV is 5 "assets". A 193-slot villa with demand charges // routes to fleet MILP; 193*6 would leave it on the 500 ms budget. if len(slots)*assets >= 193*5 || p.PVCurtailment.MinW > 0 || len(p.DemandCharges) > 0 { diff --git a/go/internal/mpc/energyplan_test.go b/go/internal/mpc/energyplan_test.go index 841f1e6db..73a8ef44e 100644 --- a/go/internal/mpc/energyplan_test.go +++ b/go/internal/mpc/energyplan_test.go @@ -309,8 +309,8 @@ func TestBatterylessEVBudgetDoesNotInventStorage(t *testing.T) { for i := range horizon { horizon[i] = slots[0] } - if got := energyplanTimeBudget(horizon, p); got != 500*time.Millisecond { - t.Fatalf("batteryless EV budget=%v", got) + if got := energyplanTimeBudget(horizon, p); got != energyplanEVBudget { + t.Fatalf("batteryless EV budget=%v, want %v", got, energyplanEVBudget) } p.CapacityWh = 20000 if got := energyplanTimeBudget(horizon, p); got != 5*time.Second { @@ -488,3 +488,25 @@ func TestEnergyplanLateFleetReplanDoesNotStartDoomedSolve(t *testing.T) { t.Fatalf("wanted previous plan, got %+v", plan) } } + +// A car on a short published-price horizon gets more time than a battery +// alone. A Pi 4 at 0.5 s stopped just before the planner could improve on +// its first plan for the car. +func TestEVGetsLongerBudgetThanBatteryAlone(t *testing.T) { + slots, p := topologyFixture(1, 1) + horizon := make([]Slot, 112) + for i := range horizon { + horizon[i] = slots[0] + } + if got := energyplanTimeBudget(horizon, p); got != energyplanEVBudget { + t.Fatalf("1b+1EV 112-slot budget=%v, want %v", got, energyplanEVBudget) + } + battery, pb := topologyFixture(1, 0) + short := make([]Slot, 112) + for i := range short { + short[i] = battery[0] + } + if got := energyplanTimeBudget(short, pb); got != energyplanSmallBudget { + t.Fatalf("battery-only 112-slot budget=%v, want %v", got, energyplanSmallBudget) + } +}