Skip to content

Commit f654400

Browse files
committed
fix: mpc calculations based on timeslot duration
1 parent e0f8d94 commit f654400

6 files changed

Lines changed: 629 additions & 188 deletions

File tree

‎mpc/example/main.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func main() {
2222
BatteryDegradationCost: 0.05, // $0.05 per kWh cycled
2323
MaxGridImport: 10.0, // 10 kW
2424
MaxGridExport: 10.0, // 10 kW
25+
TimeSlotDuration: 1.0, // 1 hour per time slot (use 0.25 for 15-minute intervals)
2526
}
2627

2728
// Create MPC controller with 24-hour horizon using hourly time slots

‎mpc/mpc.go‎

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ type SystemConfig struct {
1818
MaxGridExport float64 // kW
1919
BatteryPreHeatPower float64 // kW - power consumption of battery preheating when active
2020
BatteryPreHeatTempThreshold float64 // °C - temperature threshold below which battery preheating activates
21-
BatteryThermalTimeConstant float64 // fraction per time slot - rate at which battery temperature approaches air temperature (0-1)
21+
BatteryThermalTimeConstant float64 // fraction per hour - rate at which battery temperature approaches air temperature (0-1). This is automatically scaled based on TimeSlotDuration.
22+
TimeSlotDuration float64 // hours - duration of each time slot (e.g., 0.25 for 15 minutes, 1.0 for 1 hour). MUST match CheckPriceInterval configuration.
2223
}
2324

2425
// TimeSlot represents one time period of operation (typically 15 minutes, configurable via check_price_interval)
@@ -60,10 +61,10 @@ type ControlDecision struct {
6061

6162
// Controller implements Model Predictive Control
6263
type Controller struct {
63-
Config SystemConfig
64-
Horizon int // number of time periods to look ahead
65-
CurrentSOC float64
66-
CurrentBatteryTemp float64 // °C current battery temperature
64+
Config SystemConfig
65+
Horizon int // number of time periods to look ahead
66+
CurrentSOC float64
67+
CurrentBatteryTemp float64 // °C current battery temperature
6768
}
6869

6970
// NewController creates a new MPC controller
@@ -220,12 +221,31 @@ func (mpc *Controller) calculateNextBatteryTemp(currentTemp, airTemp float64, is
220221
// When charging with preheat, battery maintains temperature at threshold
221222
return math.Max(currentTemp, mpc.Config.BatteryPreHeatTempThreshold)
222223
}
223-
224+
225+
// Get time slot duration (default to 1 hour if not specified for backward compatibility)
226+
timeSlotDuration := mpc.Config.TimeSlotDuration
227+
if timeSlotDuration == 0 {
228+
timeSlotDuration = 1.0
229+
}
230+
231+
// Scale thermal time constant by time slot duration
232+
// BatteryThermalTimeConstant is defined per hour, so we scale it for the actual slot duration
233+
// For small k: k_slot ≈ k_hour * slot_duration_hours
234+
// For larger k, use exponential formula: k_slot = 1 - (1 - k_hour)^slot_duration_hours
235+
thermalConstant := mpc.Config.BatteryThermalTimeConstant
236+
if thermalConstant < 0.2 {
237+
// Use linear approximation for small values (more efficient)
238+
thermalConstant = thermalConstant * timeSlotDuration
239+
} else {
240+
// Use exponential formula for larger values (more accurate)
241+
thermalConstant = 1.0 - math.Pow(1.0-thermalConstant, timeSlotDuration)
242+
}
243+
224244
// When not charging or warm enough, battery temperature moves toward air temperature
225245
// T(t+1) = T(t) + k * (T_air - T(t))
226246
// This models natural cooling/heating toward ambient air temperature
227247
tempDiff := airTemp - currentTemp
228-
return currentTemp + mpc.Config.BatteryThermalTimeConstant*tempDiff
248+
return currentTemp + thermalConstant*tempDiff
229249
}
230250

231251
// generateFeasibleDecisions creates a set of feasible control decisions
@@ -281,7 +301,7 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, currentBatt
281301
for _, action := range batteryActions {
282302
// Battery preheating is only active when we're actually charging and temp is below threshold
283303
preHeatActive := needsPreHeat && action.charge > 0
284-
304+
285305
dec := ControlDecision{
286306
Hour: slot.Hour,
287307
Timestamp: slot.Timestamp,
@@ -294,12 +314,12 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, currentBatt
294314
// When battery preheating is active (battery is charging at low temp), it consumes extra power from the grid
295315
netSolar := slot.SolarForecast
296316
extraLoad := 0.0
297-
317+
298318
// Battery preheating only consumes power when battery is charging
299319
if preHeatActive {
300320
extraLoad = preHeatPower
301321
}
302-
322+
303323
netLoad := slot.LoadForecast + action.charge/mpc.Config.BatteryEfficiency + extraLoad
304324
netSupply := netSolar + action.discharge*mpc.Config.BatteryEfficiency
305325

@@ -329,15 +349,25 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, currentBatt
329349
// Therefore, GridImport and GridExport already reflect the effect of battery operations and battery preheating.
330350
// Profit is simply: revenue from exports - cost of imports - degradation cost
331351
// Note: The battery preheating cost is already included in GridImport when battery is charging at low temperatures
352+
// All power values (kW) are multiplied by time slot duration (hours) to get energy (kWh)
332353
func (mpc *Controller) calculateProfit(dec ControlDecision, slot TimeSlot) float64 {
354+
// Get time slot duration (default to 1 hour if not specified for backward compatibility)
355+
timeSlotDuration := mpc.Config.TimeSlotDuration
356+
if timeSlotDuration == 0 {
357+
timeSlotDuration = 1.0
358+
}
359+
333360
// Revenue from exporting to grid
334-
revenue := dec.GridExport * slot.ExportPrice
361+
// GridExport is in kW, multiply by time slot duration to get kWh
362+
revenue := dec.GridExport * slot.ExportPrice * timeSlotDuration
335363

336364
// Cost of importing from grid (already includes battery preheating consumption when active)
337-
importCost := dec.GridImport * slot.ImportPrice
365+
// GridImport is in kW, multiply by time slot duration to get kWh
366+
importCost := dec.GridImport * slot.ImportPrice * timeSlotDuration
338367

339368
// Battery degradation cost (wear and tear from cycling)
340-
batteryThroughput := dec.BatteryCharge + dec.BatteryDischarge
369+
// Throughput is in kW, multiply by time slot duration to get kWh cycled
370+
batteryThroughput := (dec.BatteryCharge + dec.BatteryDischarge) * timeSlotDuration
341371
degradationCost := batteryThroughput * mpc.Config.BatteryDegradationCost
342372

343373
// Net profit:
@@ -358,18 +388,42 @@ func (mpc *Controller) calculateProfit(dec ControlDecision, slot TimeSlot) float
358388

359389
// Helper functions
360390
func (mpc *Controller) canCharge(soc, charge float64) bool {
361-
newSOC := soc + (charge / mpc.Config.BatteryCapacity)
391+
// Get time slot duration (default to 1 hour if not specified for backward compatibility)
392+
timeSlotDuration := mpc.Config.TimeSlotDuration
393+
if timeSlotDuration == 0 {
394+
timeSlotDuration = 1.0
395+
}
396+
397+
// Convert power (kW) to energy (kWh) by multiplying by time slot duration
398+
chargeEnergy := charge * timeSlotDuration
399+
newSOC := soc + (chargeEnergy / mpc.Config.BatteryCapacity)
362400
return newSOC <= mpc.Config.BatteryMaxSOC
363401
}
364402

365403
func (mpc *Controller) canDischarge(soc, discharge float64) bool {
366-
newSOC := soc - (discharge / mpc.Config.BatteryCapacity)
404+
// Get time slot duration (default to 1 hour if not specified for backward compatibility)
405+
timeSlotDuration := mpc.Config.TimeSlotDuration
406+
if timeSlotDuration == 0 {
407+
timeSlotDuration = 1.0
408+
}
409+
410+
// Convert power (kW) to energy (kWh) by multiplying by time slot duration
411+
dischargeEnergy := discharge * timeSlotDuration
412+
newSOC := soc - (dischargeEnergy / mpc.Config.BatteryCapacity)
367413
return newSOC >= mpc.Config.BatteryMinSOC
368414
}
369415

370416
func (mpc *Controller) calculateNewSOC(currentSOC, charge, discharge float64) float64 {
371-
chargeEnergy := charge * mpc.Config.BatteryEfficiency
372-
socChange := (chargeEnergy - discharge) / mpc.Config.BatteryCapacity
417+
// Get time slot duration (default to 1 hour if not specified for backward compatibility)
418+
timeSlotDuration := mpc.Config.TimeSlotDuration
419+
if timeSlotDuration == 0 {
420+
timeSlotDuration = 1.0
421+
}
422+
423+
// Convert power (kW) to energy (kWh) by multiplying by time slot duration
424+
chargeEnergy := charge * timeSlotDuration * mpc.Config.BatteryEfficiency
425+
dischargeEnergy := discharge * timeSlotDuration
426+
socChange := (chargeEnergy - dischargeEnergy) / mpc.Config.BatteryCapacity
373427
newSOC := currentSOC + socChange
374428
return math.Max(mpc.Config.BatteryMinSOC, math.Min(mpc.Config.BatteryMaxSOC, newSOC))
375429
}

0 commit comments

Comments
 (0)