Files
Enelix-EMS/services/netplan-v4/netplan_v4/battery_model.py
T

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}