Quantum Stochastic Programming¶
Quantum computing algorithms for two-stage stochastic optimization, with a focus on the Unit Commitment (UC) problem in power systems. The algorithms combine Discrete Quantum Annealing (DQA) with Quantum Amplitude Estimation (QAE) to compute expected-value objective functions over a probability distribution of wind-power scenarios.
Based on: arXiv 2402.15029 — "Quantum algorithms for the two-stage stochastic unit commitment problem"
Problem Description¶
The Unit Commitment problem decides which gas generators to commit before uncertain wind power is realised (first-stage decision \(x\)), then dispatches all generators optimally for each realised scenario (second-stage recourse \(y\)).
The quantum algorithm evaluates \(\mathbb{E}[C(x,\xi)]\) via QAE in \(O(1/\epsilon)\) circuit evaluations rather than the \(O(1/\epsilon^2)\) shots needed classically.
Repository Structure¶
Two implementations exist:
qiskit_impl/ |
QuantumExpectedValueFunctionProject/ |
|
|---|---|---|
| API style | Functional primitives + args dicts |
Class-based OOP |
| Encoding | Binary only | Binary or Unary |
| Qiskit version | 2.x (current) | Originally 0.x/1.x (needs import fixes) |
| Status | Active development | Legacy / reference |
Quick Start¶
from qiskit_impl.binary_optimizer import BinaryNestedOptimizer
import numpy as np
# Problem: 2 wind turbines, total demand = 2 MW (both turbines needed).
# Each turbine produces exactly 1 MW when ON and wind is available.
# The algorithm decides: which turbines to commit (y) given a gas backup (x)?
# Probability distribution over wind scenarios: keys are (ξ_0, ξ_1) binary tuples.
# ξ_j = 1: wind is blowing for turbine j (it can generate); ξ_j = 0: no wind.
pdf = {(0,0): 0.25, # no wind for either turbine
(0,1): 0.25, # wind only for turbine 1
(1,0): 0.25, # wind only for turbine 0
(1,1): 0.25} # wind available for both turbines
bno = BinaryNestedOptimizer(
[0.4], # gas_costs: cost of committing the gas generator ($/MW)
# 1 gas generator here; it can cover 0, 1, or 2 MW.
[0.05, 0.10], # wind_costs: dispatch cost per turbine when wind is available ($/MWh)
# turbine 0 = $0.05/MWh (cheaper), turbine 1 = $0.10/MWh
1.0, # recourse_cost: penalty per MW of unmet demand (expensive backup) ($/MWh)
pdf, # pdf: scenario probability distribution (must sum to 1.0)
demand=2, # demand: total MW that must be covered each period
is_uniform=True, # is_uniform: if True, override pdf weights with uniform 1/N
)
# With demand=2: each scenario dispatches both turbines (y=[1,1]).
# If wind fails for one (ξ_j=0), that turbine produces 0 MW → recourse covers the gap.
# Tradeoff: commit gas (certain but costly at $0.40) vs rely on wind (cheap but uncertain).
# --- Step 1: Classical brute-force reference ---
# Computes φ(x) = E_ξ[min_y q(y,ξ)] for ALL values of x (0, 1, 2).
# Returns a dict/array indexed by x: ev_true[x] = expected second-stage cost when gas covers x MW.
ev_true = bno.brute_force_wind_demand_expectation_values()
print("Classical φ(x) for all x:", ev_true)
# Total cost for each x: c_x * x + φ(x) = 0.4*x + ev_true[x] → pick x with minimum total cost.
# x=0: $0.00 + $1.075 = $1.075
# x=1: $0.40 + $0.300 = $0.700 ← optimal
# x=2: $0.80 + $0.000 = $0.800
# --- Step 2: DQA quantum estimate of φ(x) for one specific x ---
#
# 'wind_demand' is the MW the wind turbines must cover = demand - x.
# It encodes the first-stage decision x implicitly:
# wind_demand=2 → x=0 (gas off, wind covers all 2 MW)
# wind_demand=1 → x=1 (gas covers 1 MW, wind covers the remaining 1 MW) ← optimal
# wind_demand=0 → x=2 (gas covers everything, no wind needed)
#
# We evaluate x=1 (wind_demand=1) — the optimal decision.
# With wind_demand=1, the Dicke state is a superposition of two turbine choices:
# (1/√2)(|10⟩ + |01⟩) — turbine 0 on OR turbine 1 on
# DQA annealing shifts amplitude toward turbine 0 (cheaper at $0.05 vs $0.10).
qc = bno.adiabatic_evolution_circuit(
wind_demand=1, # encodes x=1: gas covers 1 MW, wind turbines cover remaining 1 MW
time=100, # total annealing time T (longer → closer to ground state)
time_steps=4, # number of Trotter steps p (circuit depth ∝ p)
norm=10, # normalization factor for Hamiltonian coefficients
)
counts = bno.execute_optimizer(qc, num_meas=4096)
ev_dqa = bno.process_expectation_value_optimizer(wind_demand=1, counts=counts)
print(f"DQA estimate of φ(x=1): {ev_dqa:.4f} (classical: {ev_true[1]:.4f})")
Installation¶
NREL HPC (GPU-enabled)¶
Local¶
Running the Code¶
Start here (active implementation)¶
qiskit_impl/qae_example.ipynb — the main end-to-end notebook. Walks through the full algorithm (DQA + QAE) using the current qiskit_impl/ codebase. Run this first.
Legacy / reference notebooks (original paper)¶
These are in QuantumExpectedValueFunctionProject/ and use an older Qiskit API (may need import fixes):
| Notebook | Purpose |
|---|---|
four_binary_turbines_uniform_distribution_tutorial.ipynb |
Step-by-step tutorial: 4 turbines, uniform wind distribution |
ExpValFun_QAOA_QAE_computations.ipynb |
Reproduces the numerical results from the paper |
ExpValFun_QAOA_QAE_figures.ipynb |
Generates the paper's figures from saved computation data |
simple_UC_model.ipynb |
Minimal classical unit commitment reference |
SED_classical_solution.ipynb |
Classical SED (stochastic economic dispatch) baseline |
Convergence experiment (script)¶
Runs DQA convergence sweeps over annealing depth and time steps, producing the data behind Fig. 3 of the paper.Navigation¶
- Algorithm Overview — How DQA and QAE work step by step
- API Reference — Full class and function documentation
- GPU Porting Guide — Three-tier strategy for H100 acceleration
- References — Papers and further reading