Solve unit commitment in Python (MIQP)

Unit commitment is the classic power-systems planning problem: given several generating units, decide which to switch on and how much each should produce to meet demand at minimum cost. It mixes binary on/off decisions with a quadratic cost curve, which makes it a mixed-integer quadratic program (MIQP). With quicopt it is a short Pyomo model.

Why it's hard

An off unit costs nothing and produces nothing; an on unit pays a fixed start cost plus a fuel cost that grows with the square of its output. The on/off choices are discrete and the cost curves, so neither an LP nor a plain nonlinear solver fits. You need a solver that takes the integrality and the curvature together, which is what MIQP means. MIQP is the quadratic case of a mixed-integer nonlinear program: quadratic objective, linear constraints, some variables binary. Keeping the constraints linear is what lets the answer be proved rather than approached.

Model it

Binary on[i] switches unit i; continuous p[i] is its output, capped at zero unless the unit is on. Meet demand, minimize fixed + fuel cost. Modeled in Pyomo (pip install "quicopt[pyomo]"):

unit_commitment.py
import pyomo.environ as pyo
from quicopt import Client
caps, fixed, a = [50.0, 60.0, 40.0], [100.0, 120.0, 80.0], [0.010, 0.008, 0.012]
demand = 100.0
m = pyo.ConcreteModel()
m.on = pyo.Var(range(3), domain=pyo.Binary)
m.p = pyo.Var(range(3), bounds=(0, None))
m.cap = pyo.Constraint(range(3), rule=lambda m, i: m.p[i] <= caps[i] * m.on[i])
m.dem = pyo.Constraint(expr=sum(m.p[i] for i in range(3)) >= demand)
m.obj = pyo.Objective(expr=sum(fixed[i]*m.on[i] + a[i]*m.p[i]**2 for i in range(3)), sense=pyo.minimize)
print(Client().solve(m).display)
$ python unit_commitment.py
├── shots
│   ├── 1 · primal-dual · global     248   0.14s  ◀ best
│   ├── 2 · primal                 248.0    2.0s
│   └── 3 · primal                 248.0    2.0s
├── status:     optimal
├── feasible:   true
├── objective:  248.0
├── x:          x1=0, x2=1, x3=1, x4=0, x5=60, x6=40  (6 variables)
└── solve_time: 2.0008 s

What you get

The least-cost plan switches on units 2 and 3 (x2=1, x3=1) at outputs 60 and 40 to cover demand 100, for a total cost of 248. Unit 1 stays off — its fixed cost plus fuel would beat neither option. quicopt returns this from binary on/off decisions and a nonlinear cost in one solve.

status: optimal means quicopt proved this is the least-cost commitment — no mix of on/off decisions and outputs does better. The quadratic fuel cost is convex on its own; what makes the problem hard is the binary on/off switching, which is why it needs a mixed-integer solver rather than a plain nonlinear one.

The shots block shows how the proof arrived. quicopt runs several solvers and reports what each answer is worth. All three land on 248, but only the first came back primal-dual · global, carrying the dual bound that settles the question. The other two return a primal point: the same plan, with nothing to say about whether anything beats it. Three solvers agreeing is reassuring, and the certificate is what turns it into a proof. solve_time is the wall clock for the whole set, so it keeps running long after the winning shot finished at 0.14 s.

The same model extends to more units, multiple time periods, and ramp limits by adding constraints.

Next

Reference: A. J. Wood, B. F. Wollenberg, G. B. Sheblé, Power Generation, Operation, and Control, Wiley (3rd ed., 2013).

A bigger fleet — or a different problem?

Tell us what you're optimizing. We'll help you model it and point you at the right approach.

Talk to us →

Frequently asked questions

Why isn't this just a linear program?

Two things break linearity: the on/off decisions are binary, and the fuel cost grows with the square of the output. Together that is a mixed-integer quadratic program (MIQP) — the quadratic case of a mixed-integer nonlinear program, and quicopt handles both parts in one model.

Can I use integer (not just binary) unit counts?

On the free tier, nonlinear models accept binary integers only; general integers return a 422. Model each unit as its own binary on/off, which is the standard formulation anyway.

Is quicopt free to use?

Yes — pip install quicopt and your first call sets up a free key, no license.