In Lesson 1 you learned the three parts of any optimization problem. Now we'll turn that formulation into running Python code. By the end of this lesson, you'll have a working optimizer that produces a 24-hour dispatch schedule from real price data.
We need two things: a modeling language to express the math, and a solver to find the optimal solution.
pip install pyomo highspy
Let's use realistic day-ahead prices (€/MWh) for a typical volatile day. In production you'd fetch these from EPEX SPOT or your data provider.
import pyomo.environ as pyo
import numpy as np
# --- Parameters ---
T = 24 # hours
prices = [
28, 25, 22, 20, 19, 21, 35, 55, # hours 0-7 (night cheap, morning ramp)
62, 58, 52, 48, 45, 44, 47, 53, # hours 8-15 (midday solar dip)
68, 85, 92, 78, 60, 45, 35, 30 # hours 16-23 (evening peak then drop)
]
P_max = 10.0 # MW — max charge/discharge power
E_max = 20.0 # MWh — battery energy capacity
eta = 0.90 # round-trip efficiency (sqrt applied to each direction)
soc_init = 10.0 # MWh — start at 50% state of charge
charge × √η × √η = charge × η gives the correct round-trip loss.
Some formulations apply the full η on one side only — both are valid, just be consistent.
# --- Model ---
model = pyo.ConcreteModel()
# Sets
model.T = pyo.RangeSet(0, T - 1)
# Decision variables
model.p_charge = pyo.Var(model.T, bounds=(0, P_max))
model.p_discharge = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0, E_max))
# Objective: maximize revenue
eta_sqrt = np.sqrt(eta)
def revenue_rule(m):
return sum(
prices[t] * (m.p_discharge[t] - m.p_charge[t])
for t in m.T
)
model.objective = pyo.Objective(rule=revenue_rule, sense=pyo.maximize)
# Constraint: energy balance (SoC dynamics)
def soc_rule(m, t):
if t == 0:
return m.soc[t] == soc_init + eta_sqrt * m.p_charge[t] - m.p_discharge[t] / eta_sqrt
return m.soc[t] == m.soc[t-1] + eta_sqrt * m.p_charge[t] - m.p_discharge[t] / eta_sqrt
model.soc_balance = pyo.Constraint(model.T, rule=soc_rule)
# --- Solve ---
solver = pyo.SolverFactory('highs')
result = solver.solve(model, tee=False)
# Check solver status
print(f"Solver status: {result.solver.status}")
print(f"Optimal revenue: €{pyo.value(model.objective):.2f}")
import pandas as pd
# Extract schedule
schedule = pd.DataFrame({
'hour': range(T),
'price_eur_mwh': prices,
'charge_mw': [pyo.value(model.p_charge[t]) for t in range(T)],
'discharge_mw': [pyo.value(model.p_discharge[t]) for t in range(T)],
'soc_mwh': [pyo.value(model.soc[t]) for t in range(T)],
})
schedule['net_mw'] = schedule['discharge_mw'] - schedule['charge_mw']
print(schedule.to_string(index=False))
Here's everything in one copy-pasteable block:
"""
battery_optimizer.py — Lesson 2: Day-ahead dispatch optimizer
"""
import pyomo.environ as pyo
import numpy as np
import pandas as pd
# --- Parameters ---
T = 24
prices = [28, 25, 22, 20, 19, 21, 35, 55,
62, 58, 52, 48, 45, 44, 47, 53,
68, 85, 92, 78, 60, 45, 35, 30]
P_max = 10.0 # MW
E_max = 20.0 # MWh
eta = 0.90 # round-trip efficiency
soc_init = 10.0 # MWh
# --- Model ---
model = pyo.ConcreteModel()
model.T = pyo.RangeSet(0, T - 1)
model.p_charge = pyo.Var(model.T, bounds=(0, P_max))
model.p_discharge = pyo.Var(model.T, bounds=(0, P_max))
model.soc = pyo.Var(model.T, bounds=(0, E_max))
eta_sqrt = np.sqrt(eta)
def revenue_rule(m):
return sum(prices[t] * (m.p_discharge[t] - m.p_charge[t]) for t in m.T)
model.objective = pyo.Objective(rule=revenue_rule, sense=pyo.maximize)
def soc_rule(m, t):
if t == 0:
return m.soc[t] == soc_init + eta_sqrt * m.p_charge[t] - m.p_discharge[t] / eta_sqrt
return m.soc[t] == m.soc[t-1] + eta_sqrt * m.p_charge[t] - m.p_discharge[t] / eta_sqrt
model.soc_balance = pyo.Constraint(model.T, rule=soc_rule)
# --- Solve ---
solver = pyo.SolverFactory('highs')
result = solver.solve(model, tee=False)
print(f"Solver status: {result.solver.status}")
print(f"Optimal revenue: €{pyo.value(model.objective):.2f}\n")
# --- Results ---
schedule = pd.DataFrame({
'hour': range(T),
'price': prices,
'charge_mw': [round(pyo.value(model.p_charge[t]), 2) for t in range(T)],
'discharge_mw': [round(pyo.value(model.p_discharge[t]), 2) for t in range(T)],
'soc_mwh': [round(pyo.value(model.soc[t]), 2) for t in range(T)],
})
print(schedule.to_string(index=False))
What does pyo.Var(model.T, bounds=(0, P_max)) create?
If the solver returns p_charge[4] = 10.0 and p_discharge[4] = 0.0, what's happening in hour 4?
Why do we use pyo.SolverFactory('highs') instead of writing our own search algorithm?
E_max from 20 to 40 MWh. Does revenue increase? Why?eta = 1.0 (perfect efficiency). How much more revenue does a lossless battery earn?soc_init = 0.0. Does the optimizer still discharge in the evening? Look at the early hours.Pyomo Documentation: Simple Examples — walks through building LP models step by step. Read the "Concrete Model" example.
Rechtliches: Impressum