Optimizer: stop demanding charge once the vehicle is full (#32633)
This commit is contained in:
parent
6a976286d1
commit
29137181f1
2 changed files with 90 additions and 1 deletions
|
|
@ -800,12 +800,35 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati
|
|||
}
|
||||
|
||||
if demand != nil {
|
||||
bat.PDemand = prorate(demand, firstSlotDuration)
|
||||
// after prorate, so the shortened first slot counts with the energy it really carries
|
||||
bat.PDemand = clearDemandWhenFull(prorate(demand, firstSlotDuration), bat.SMax-bat.SInitial)
|
||||
}
|
||||
|
||||
return bat, detail
|
||||
}
|
||||
|
||||
// clearDemandWhenFull zeroes the charge demand from the slot the accumulated energy fills the
|
||||
// vehicle. The optimizer drops the demand at s_max anyway, but pays two binaries per slot to
|
||||
// detect it, so slots that cannot bind are worth not asking about. Losses are accounted for.
|
||||
//
|
||||
// The cut assumes the demand is met every slot. A grid import limit can throttle charging below
|
||||
// it, moving the real fill point later than the estimate - the next request corrects that from
|
||||
// the measured soc, and the near slots are never affected because the cut sits a full charge away.
|
||||
func clearDemandWhenFull(demand []float32, headroom float32) []float32 {
|
||||
res := slices.Clone(demand)
|
||||
|
||||
var acc float32
|
||||
for i, d := range res {
|
||||
if acc >= headroom {
|
||||
res[i] = 0
|
||||
continue
|
||||
}
|
||||
acc += d * eta
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measurement, grid api.Rates, minLen int, firstSlotDuration time.Duration) (optimizer.BatteryConfig, batteryDetail) {
|
||||
bat := optimizer.BatteryConfig{
|
||||
CMax: batteryPower,
|
||||
|
|
|
|||
66
core/site_optimizer_demand_test.go
Normal file
66
core/site_optimizer_demand_test.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClearDemandWhenFull(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
demand []float32
|
||||
headroom float32
|
||||
expected []float32
|
||||
}{
|
||||
{
|
||||
// 345Wh demanded per slot stores 310.5Wh, so 1000Wh of headroom is covered
|
||||
// during the fourth slot- that slot still binds, the ones after it cannot
|
||||
"cut once the accumulated energy fills the vehicle",
|
||||
[]float32{345, 345, 345, 345, 345, 345},
|
||||
1000,
|
||||
[]float32{345, 345, 345, 345, 0, 0},
|
||||
},
|
||||
{
|
||||
"headroom beyond the horizon leaves the demand untouched",
|
||||
[]float32{345, 345, 345},
|
||||
1e6,
|
||||
[]float32{345, 345, 345},
|
||||
},
|
||||
{
|
||||
// already at the soc limit: every slot is relaxed at s_max anyway
|
||||
"no headroom clears the whole demand",
|
||||
[]float32{345, 345, 345},
|
||||
0,
|
||||
[]float32{0, 0, 0},
|
||||
},
|
||||
{
|
||||
// the shortened first slot carries less energy and must not pull the cut in
|
||||
"partial first slot counts with its own energy",
|
||||
[]float32{100, 345, 345, 345},
|
||||
600,
|
||||
[]float32{100, 345, 345, 0},
|
||||
},
|
||||
{
|
||||
"empty demand stays empty",
|
||||
[]float32{},
|
||||
1000,
|
||||
[]float32{},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, clearDemandWhenFull(tc.demand, tc.headroom))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// the request contract requires every series to have the same length
|
||||
func TestClearDemandWhenFullKeepsLength(t *testing.T) {
|
||||
demand := make([]float32, 96)
|
||||
for i := range demand {
|
||||
demand[i] = 345
|
||||
}
|
||||
|
||||
assert.Len(t, clearDemandWhenFull(demand, 1000), len(demand))
|
||||
assert.Len(t, clearDemandWhenFull(demand, 0), len(demand))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue