Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/energyplan-ev-budget.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions go/internal/mpc/energyplan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
26 changes: 24 additions & 2 deletions go/internal/mpc/energyplan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
Loading