13 Dispatch with a fixed tank
Problem 13 | Hydrogen Production and Storage | Level 1: Guided problem
13.1 Problem Statement
An electrolyzer operates over six four-hour periods, representing one day. Electricity use is constant at 0.05 MWh/kg of hydrogen. Power is limited to 4 MW. Initial hydrogen inventory is 50 kg, and final inventory must also be 50 kg. Storage has no leakage and no charging-rate restriction. Production and withdrawal rates are each uniform within a period. Inventory therefore varies linearly between period boundaries.
| Period | Electricity price (USD/MWh) | Hydrogen demand (kg/period) |
|---|---|---|
| 1 | 30 | 120 |
| 2 | 25 | 150 |
| 3 | 80 | 180 |
| 4 | 100 | 220 |
| 5 | 45 | 180 |
| 6 | 35 | 140 |
Capacity is enforced at every period boundary, including the 50 kg initial stock. Under the uniform-rate assumption, this also bounds inventory throughout each period. If deliveries are discrete or rates vary within a period, an additional inventory profile or shorter time steps is necessary for physical sizing. A daily capacity charge of 0.15 USD/(kg d) is used where investment is optimized. It is an assumed daily equivalent, not a full capital-cost model.
Your task. Fix the tank at 150 kg and minimize daily electricity cost. Meet every period demand without purchases from an external supplier.
13.1.1 Before you code
Which hours are cheapest? Why might the electrolyzer still need to operate in an expensive period despite cheap electricity earlier?
13.1.2 Deliverables
- Define decision variables, units, objective, and constraints. State which data and assumptions are fixed.
- Predict one feature of the solution and explain why it should occur.
- Build and solve a Pyomo model. Check balances and bounds independently.
- Interpret the result and investigate the follow-up question below. For an open-ended challenge, state and defend your chosen policy separately from the reference solution.
13.2 Model Formulation
Let \(P_{st}\) be power in MW, \(I_{st}\) end inventory in kg, and \(K\) capacity in kg. With \(\Delta t=4\) h and \(e=0.05\) MWh/kg, \[I_{st}=I_{s,t-1}+\frac{\Delta t}{e}P_{st}-D_{st},\qquad 0\le P_{st}\le4,\quad0\le I_{st}\le K.\] Use \(I_{s0}=50\) before the first period and \(I_{s6}=50\) at the end. Total production must equal total demand. The electricity cost is \(\sum_t c_t\Delta t P_{st}\) USD/d. Omitting \(\Delta t\) would understate cost by a factor of four.
Fix \(K=150\) kg and minimize electricity cost under the inventory balances. Initial inventory is not free net supply because the terminal condition returns the same amount. The LP does not impose a minimum operating load or a startup charge.
13.3 Pyomo Implementation
The code below is the model-building portion of models/hydrogen.py. The level argument selects this problem’s assumptions. Run the complete module from the source bundle to reproduce the solution, including its independent checks:
~/.venvs/optim/bin/python -m models.hydrogen 1"""Daily hydrogen dispatch, tank design, and scenario-dependent operation."""
import pyomo.environ as pyo
from models.common import value
PRICE = [30, 25, 80, 100, 45, 35] # USD/MWh
DEMAND = [120, 150, 180, 220, 180, 140] # kg per four-hour period
DT = 4 # h
SPECIFIC_ENERGY = 0.05 # MWh/kg
TANK_CHARGE = 0.15 # USD/(kg capacity d)
def build(level):
states = (
{"nominal": (1, 1)} if level < 3 else {"low": (0.9, 0.5), "high": (1.15, 0.5)}
)
m = pyo.ConcreteModel(name="Hydrogen production and storage")
m.S = pyo.Set(initialize=list(states))
m.T = pyo.RangeSet(0, 5)
m.k = pyo.Var(bounds=(50, 500))
if level == 1:
m.k.fix(150)
m.power = pyo.Var(m.S, m.T, bounds=(0, 4))
m.stock = pyo.Var(m.S, m.T, domain=pyo.NonNegativeReals)
m.balance = pyo.Constraint(
m.S,
m.T,
rule=lambda m, s, t: (
m.stock[s, t]
== (50 if t == 0 else m.stock[s, t - 1])
+ DT * m.power[s, t] / SPECIFIC_ENERGY
- DEMAND[t] * states[s][0]
),
)
m.capacity = pyo.Constraint(m.S, m.T, rule=lambda m, s, t: m.stock[s, t] <= m.k)
m.terminal = pyo.Constraint(m.S, rule=lambda m, s: m.stock[s, 5] == 50)
if level == 3:
m.on = pyo.Var(m.S, m.T, domain=pyo.Binary)
m.start = pyo.Var(m.S, m.T, domain=pyo.Binary)
m.upper = pyo.Constraint(
m.S, m.T, rule=lambda m, s, t: m.power[s, t] <= 4 * m.on[s, t]
)
m.lower = pyo.Constraint(
m.S, m.T, rule=lambda m, s, t: m.power[s, t] >= m.on[s, t]
)
m.startup = pyo.Constraint(
m.S,
m.T,
rule=lambda m, s, t: (
m.start[s, t] >= m.on[s, t] - (0 if t == 0 else m.on[s, t - 1])
),
)
fixed = 0 if level == 1 else TANK_CHARGE * m.k
m.obj = pyo.Objective(
expr=fixed
+ sum(
states[s][1]
* sum(
DT * PRICE[t] * m.power[s, t]
+ (10 * m.start[s, t] if level == 3 else 0)
for t in m.T
)
for s in m.S
)
)
m._states = states
return mThe common solve function calls appsi_highs, checks optimal termination before loading a solution, and checks constraint residuals, bounds, and integer domains. After building the model, use:
from models.common import solve
m = solve(build(1))13.4 Optimal Solution
The reference objective is 2,287.5000 USD/d. This value is optimal for the explicitly stated model and data.
| State | Period | Power (MW) | Demand (kg) | End stock (kg) |
|---|---|---|---|---|
| nominal | 1 | 0.8750 | 120 | 0.0000 |
| nominal | 2 | 3.7500 | 150 | 150.0000 |
| nominal | 3 | 2.2500 | 180 | 150.0000 |
| nominal | 4 | 0.8750 | 220 | 0.0000 |
| nominal | 5 | 2.2500 | 180 | 0.0000 |
| nominal | 6 | 2.3750 | 140 | 50.0000 |
| Quantity | Value |
|---|---|
| Tank capacity (kg) | 150.0000 |
| Expected electricity (MWh/d) | 49.5000 |
13.4.1 Check your solution
Check daily production equals 990 kg and electricity use equals 49.5 MWh. Verify nonnegative inventory in all periods and the 50 kg terminal stock.
The automated residual, bound, and integrality audit passed with a maximum violation of 0.00e+00 in model units. Domain-specific checks passed. Model units differ across equations, so this numerical audit does not replace dimensional analysis.
13.5 Brief Discussion
Daily electricity costs 2287.50 USD. Total energy is 49.50 MWh: the storage benefit comes from purchase timing, not reduced energy use.
13.5.1 Extension to investigate
Remove the terminal condition and quantify the apparent savings. Explain why consuming starting stock is not a repeatable daily operating policy.