Your First Battery Optimizer in Python

← Learning Hub Lesson 2 · Battery Optimizer Course · Estimated time: 20 minutes

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.

Tools: Pyomo + HiGHS

We need two things: a modeling language to express the math, and a solver to find the optimal solution.

pip install pyomo highspy

Step 1: Define parameters

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
Note on efficiency: We split η into charge efficiency (√η) and discharge efficiency (√η). This way charge × √η × √η = charge × η gives the correct round-trip loss. Some formulations apply the full η on one side only — both are valid, just be consistent.

Step 2: Build the Pyomo model

# --- 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)

Step 3: Solve it

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

Step 4: Extract and display results

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))
What you'll see: The optimizer charges during the cheapest hours (hours 3–5, ~€19–21/MWh) and discharges during the evening peak (hours 16–18, ~€68–92/MWh). That spread is your revenue.

The complete script

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))

Check your understanding

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?

Exercises (do these before moving on)

  1. Run the script. What revenue does the optimizer find? (Should be around €800–1100 depending on efficiency handling.)
  2. Change E_max from 20 to 40 MWh. Does revenue increase? Why?
  3. Set eta = 1.0 (perfect efficiency). How much more revenue does a lossless battery earn?
  4. Set soc_init = 0.0. Does the optimizer still discharge in the evening? Look at the early hours.

📖 Recommended reading

Pyomo Documentation: Simple Examples — walks through building LP models step by step. Read the "Concrete Model" example.

💬 Questions? If Pyomo's syntax is confusing, or the solver gives unexpected results, or you want to understand what "tee=True" shows you — ask. Getting comfortable with the tools now saves pain later.

Rechtliches: Impressum