45 lines
2.6 KiB
Python
45 lines
2.6 KiB
Python
"""SOC-aware battery model, including reserve recovery and hysteresis rearming."""
|
|
from math import sqrt
|
|
|
|
|
|
class BatteryModel:
|
|
@staticmethod
|
|
def build(model,battery,steps,direction):
|
|
cap=battery.capacity_kwh;eta=sqrt(battery.roundtrip_efficiency)
|
|
reserve=cap*battery.min_soc_percent/100
|
|
initial=cap*battery.soc_percent/100
|
|
lower=min(initial,reserve) if battery.recovery_allowed else reserve
|
|
upper=cap*battery.max_soc_percent/100
|
|
energies=[model.variable(lower,upper) for _ in range(len(steps)+1)]
|
|
model.constraint({energies[0]:1},initial,initial)
|
|
terminal=battery.terminal_soc_min_percent
|
|
if terminal is None:terminal=max(battery.soc_percent,battery.min_soc_percent)
|
|
model.constraint({energies[-1]:1},cap*terminal/100)
|
|
model.cost[energies[-1]]=-battery.terminal_value_chf_kwh
|
|
rearm=cap*(battery.rearm_soc_percent if battery.rearm_soc_percent is not None else battery.min_soc_percent)/100
|
|
enabled=[model.variable(0,1,integer=True) for _ in steps]
|
|
# Only a numerical threshold, not an undisclosed extra operating reserve.
|
|
epsilon=min(1e-5,max(0.,upper-reserve)/1000)
|
|
initially_enabled=not battery.discharge_blocked and initial>=reserve+max(epsilon,1e-8)
|
|
model.constraint({enabled[0]:1},int(initially_enabled),int(initially_enabled))
|
|
big=max(upper-lower+epsilon,1.)
|
|
charge,discharge=[],[]
|
|
for i,step in enumerate(steps):
|
|
dt=step.seconds/3600;cmax=battery.max_charge_w/1000;dmax=battery.max_discharge_w/1000
|
|
c=model.variable(0,cmax,battery.throughput_chf_kwh*dt)
|
|
d=model.variable(0,dmax,battery.throughput_chf_kwh*dt)
|
|
charge.append(c);discharge.append(d)
|
|
model.constraint({c:1,direction[i]:-cmax},upper=0)
|
|
model.constraint({d:1,direction[i]:dmax},upper=dmax)
|
|
model.constraint({d:1,enabled[i]:-dmax},upper=0)
|
|
model.constraint({energies[i]:1,enabled[i]:-big},lower=reserve+epsilon-big)
|
|
# Once enabled, discharge may consume only energy above reserve.
|
|
model.constraint({energies[i+1]:1,enabled[i]:-big},lower=reserve-big)
|
|
if i:
|
|
# A disabled battery can rearm only AFTER prior charging has
|
|
# actually reached the configured hysteresis threshold.
|
|
model.constraint({energies[i]:1,enabled[i]:-big,enabled[i-1]:big},lower=rearm-big)
|
|
model.constraint({energies[i+1]:1,energies[i]:-1,c:-eta*dt,d:dt/eta},0,0)
|
|
return {'charge':charge,'discharge':discharge,'energy':energies,
|
|
'terminal_min_percent':terminal,'discharge_enabled':enabled}
|