"""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}