Storage
nimopt.models.storage dispatches a generator fleet and a set of batteries
against an hourly load. The state_of_charge row references the previous
hour through T.cyclic - 1. The row at the first hour therefore references
the last hour, and every hour has a row. The ramp row references T - 1. The
first hour has no predecessor, and that row is not produced. The model
exercises both lag rules.
minimize Σ_{g,t} cost[g,t] · gen[g,t]
subject to Σ_g gen[g,t] + Σ_s discharge[s,t] − Σ_s charge[s,t] == load[t]
soc[s,t] − soc[s,t−1] − charge_eta[s,t] · charge[s,t]
+ discharge_eta[s,t] · discharge[s,t] == 0 (t−1 wraps)
gen[g,t] − gen[g,t−1] ≤ ramp_limit[g,t] (t=0 dropped)
gen[g,t] ≤ capacity[g,t]
charge[s,t] ≤ power[s,t]
discharge[s,t] ≤ power[s,t]
soc[s,t] ≤ energy[s,t]
Every limit is a constraint and not a bound, and each therefore has a dual value.
from nimopt.models import storage
print(storage.definition().explain())
Output
storage min not built
sets T · G · S
parameters cost (G,T) · capacity (G,T) · ramp_limit (G,T) · load (T) · power (S,T) · energy (S,T) · charge_eta (S,T) · discharge_eta (S,T)
variables gen (G×T) [0.0, inf] · charge (S×T) [0.0, inf] · discharge (S×T) [0.0, inf] · soc (S×T) [0.0, inf]
constraint balance (T) Sum(G, gen[G, T]) + Sum(S, discharge[S, T]) - Sum(S, charge[S, T]) == load[T]
constraint state_of_charge (S,T) soc[S, T] - soc[S, T.cyclic - 1] - charge_eta[S, T] * charge[S, T] + discharge_eta[S, T] * discharge[S, T] == 0
constraint generation_limit (G,T) gen[G, T] <= capacity[G, T]
constraint ramp (G,T) gen[G, T] - gen[G, T - 1] <= ramp_limit[G, T]
constraint charge_limit (S,T) charge[S, T] <= power[S, T]
constraint discharge_limit (S,T) discharge[S, T] <= power[S, T]
constraint energy_limit (S,T) soc[S, T] <= energy[S, T]
objective min Sum(G, T, cost[G, T] * gen[G, T])
The batteries are lossy, with a round-trip efficiency of 0.95 · 0.93, and
the costs of the fleet span 50.0 to 55.0. Shifting energy through the store
costs more than it saves. The store is therefore idle, and the optimum is the
hourly merit order. A store that cycles requires data with a wider cost
spread, and the benchmarks supply it.
from nimopt.models import storage
inputs = storage.data()
solution = storage.definition().build(inputs).solve()
print(solution.objective, storage.reference(inputs))
print("store moved:", abs(solution.primal("charge").to_dense()).max())
Output
145449.3739227024 145449.37392270242
store moved: 0.0
Every hour has a state_of_charge row: that lag wraps. The ramp row at the
first hour is not produced: that lag does not wrap.
from nimopt.models import storage
model = storage.definition().build(storage.data())
print(model.absent("state_of_charge"))
print()
print(model.absent("ramp"))
Output
state_of_charge 24 of 24 rows stated by terms
ramp 69 of 72 rows stated by terms
row absent G='base0', T=0 term-does-not-reach (gen)
row absent G='mid0', T=0 term-does-not-reach (gen)
row absent G='peak0', T=0 term-does-not-reach (gen)