feat(application): integrate measured-load ingestion training and planner source
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user