Robust Optimization#
Real-world optimization problems rarely have perfectly known parameters. Demand forecasts carry error, material costs fluctuate, and physical measurements are imprecise. Robust optimization provides a principled framework for making decisions that remain feasible — and near-optimal — for every realization of uncertain parameters within a prescribed set [Ben-Tal et al., 2009, Ben-Tal and Nemirovski, 1999].
What is Robust Optimization?#
Given a nominal problem
where \(p\) is an uncertain parameter taking values in an uncertainty set \(\mathcal{U}\), the robust counterpart is:
A key insight (Ben-Tal & Nemirovski Ben-Tal and Nemirovski [1999]) is that for standard uncertainty set shapes, this minimax problem can be reformulated as a deterministic program without nested optimization.
Uncertainty Sets#
discopt supports three standard uncertainty set families:
Set |
Definition |
Reformulation |
|---|---|---|
Box |
\(\lvert p_j - \bar{p}_j \rvert \le \delta_j\) |
Substitute worst-case bound per component |
Ellipsoidal |
\(\lVert \Sigma^{-1/2}(p-\bar{p}) \rVert_2 \le \rho\) |
Add SOC norm penalty [Ben-Tal and Nemirovski, 1999] |
Polyhedral |
\(A\xi \le b,\; \xi = p - \bar{p}\) |
LP dual auxiliary variables [Bertsimas and Sim, 2004] |
import os
os.environ["JAX_PLATFORMS"] = "cpu"
os.environ["JAX_ENABLE_X64"] = "1"
import discopt.modeling as dm
import numpy as np
from discopt.ro import BoxUncertaintySet, RobustCounterpart
Example 1 — Production Planning with Box Uncertainty#
A manufacturer produces three products with unit costs \(c\) and must meet a total demand \(d\):
Both costs \(c\) and demand \(d\) are uncertain: \(c \in [\bar{c} - \delta_c, \bar{c} + \delta_c]\) and \(d \in [\bar{d} - \delta_d, \bar{d} + \delta_d]\). We solve the nominal problem first, then compare with the robust counterpart.
c_bar = np.array([10.0, 15.0, 8.0]) # nominal unit costs
d_bar = 100.0 # nominal demand
# --- Nominal model (solve with nominal parameter values) ---
m_nom = dm.Model("nominal")
x_nom = m_nom.continuous("x", shape=(3,), lb=0, ub=200)
c_nom = m_nom.parameter("c", value=c_bar)
d_nom = m_nom.parameter("d", value=d_bar)
m_nom.minimize(dm.sum(c_nom * x_nom))
m_nom.subject_to(dm.sum(x_nom) >= d_nom, name="demand")
r_nom = m_nom.solve()
x_n = list(r_nom.x.values())[0]
print(f"Nominal: obj = {r_nom.objective:.2f}, x = {np.round(x_n, 1)}")
print(" (All demand met by cheapest product: 8.0 * 100 = 800)")
# --- Robust model (costs +/- 10%, demand +/- 5%) ---
m_rob = dm.Model("robust")
x_rob = m_rob.continuous("x", shape=(3,), lb=0, ub=200)
c_rob = m_rob.parameter("c", value=c_bar)
d_rob = m_rob.parameter("d", value=d_bar)
m_rob.minimize(dm.sum(c_rob * x_rob))
m_rob.subject_to(dm.sum(x_rob) >= d_rob, name="demand")
unc_c = BoxUncertaintySet(c_rob, delta=0.10 * c_bar)
unc_d = BoxUncertaintySet(d_rob, delta=0.05 * d_bar)
rc = RobustCounterpart(m_rob, [unc_c, unc_d])
rc.formulate()
r_rob = m_rob.solve()
x_r = list(r_rob.x.values())[0]
print(f"\nRobust: obj = {r_rob.objective:.2f}, x = {np.round(x_r, 1)}")
print(" (Worst-case cost 8.8, worst-case demand 105: 8.8 * 105 = 924)")
print(
f"\nPrice of robustness: {r_rob.objective - r_nom.objective:.2f} "
f"({(r_rob.objective / r_nom.objective - 1) * 100:.1f}% increase)"
)
Nominal: obj = 800.00, x = [ 0. 0. 100.]
(All demand met by cheapest product: 8.0 * 100 = 800)
Robust: obj = 924.00, x = [ 0. 0. 105.]
(Worst-case cost 8.8, worst-case demand 105: 8.8 * 105 = 924)
Price of robustness: 124.00 (15.5% increase)
Example 2 — Robust Demand with Capacity Constraints#
When capacity constraints force diversification, the robust counterpart changes the production plan. Here product \(x\) is cheap but capacity-limited, and product \(y\) is the expensive backup:
With demand \(d\) uncertain within \([40, 60]\) (nominal \(\bar{d} = 50\), \(\delta = 10\)), the robust counterpart must ensure feasibility at \(d = 60\).
# --- Nominal ---
m_nom = dm.Model("nominal")
x_n = m_nom.continuous("x", lb=0, ub=100)
y_n = m_nom.continuous("y", lb=0, ub=100)
d_n = m_nom.parameter("d", value=50.0)
m_nom.minimize(3.0 * x_n + 5.0 * y_n)
m_nom.subject_to(x_n + y_n >= d_n, name="demand")
m_nom.subject_to(x_n <= 40.0, name="x_cap")
r_nom = m_nom.solve()
print(f"Nominal (d=50): obj = {r_nom.objective:.2f}, x = {r_nom.x}")
# --- Robust (demand uncertain +/- 10) ---
m_rob = dm.Model("robust")
x_r = m_rob.continuous("x", lb=0, ub=100)
y_r = m_rob.continuous("y", lb=0, ub=100)
d_r = m_rob.parameter("d", value=50.0)
m_rob.minimize(3.0 * x_r + 5.0 * y_r)
m_rob.subject_to(x_r + y_r >= d_r, name="demand")
m_rob.subject_to(x_r <= 40.0, name="x_cap")
RobustCounterpart(m_rob, BoxUncertaintySet(d_r, delta=10.0)).formulate()
r_rob = m_rob.solve()
print(f"Robust (d<=60): obj = {r_rob.objective:.2f}, x = {r_rob.x}")
print("\nThe robust solution orders 10 more units of the expensive product")
print("to guarantee feasibility if demand reaches 60.")
Nominal (d=50): obj = 170.00, x = {'x': array(40.), 'y': array(10.)}
Robust (d<=60): obj = 220.00, x = {'x': array(40.), 'y': array(20.)}
The robust solution orders 10 more units of the expensive product
to guarantee feasibility if demand reaches 60.
Example 3 — Portfolio with Ellipsoidal Uncertainty#
Portfolio optimization with uncertain expected returns [Ben-Tal and Nemirovski, 1999]. The nominal investor concentrates in the highest-return asset, while the robust investor diversifies to hedge against return uncertainty:
where returns \(\mu\) lie in an ellipsoidal set \(\{\mu: \lVert \mu - \bar{\mu} \rVert_2 \le \rho\}\). The robust counterpart adds a norm penalty: \(\max_x\; \bar{\mu}^\top x - \rho \lVert x \rVert_2\).
from discopt.ro import EllipsoidalUncertaintySet
rng = np.random.RandomState(42)
n_assets = 5
mu_bar = rng.uniform(0.05, 0.15, n_assets)
# --- Nominal portfolio ---
m_nom = dm.Model("nominal_portfolio")
x_nom = m_nom.continuous("x", shape=(n_assets,), lb=0, ub=1)
mu_nom = m_nom.parameter("mu", value=mu_bar)
m_nom.minimize(-(mu_nom @ x_nom))
m_nom.subject_to(dm.sum(x_nom) == 1.0, name="budget")
r_nom = m_nom.solve()
x_n = list(r_nom.x.values())[0]
print(f"Nominal: return = {-r_nom.objective:.4f}")
print(f" x = {np.round(x_n, 4)}")
print(" (All capital in asset with highest return)")
# --- Robust portfolio (rho=0.02) ---
m_rob = dm.Model("robust_portfolio")
x_rob = m_rob.continuous("x", shape=(n_assets,), lb=0, ub=1)
mu_rob = m_rob.parameter("mu", value=mu_bar)
m_rob.minimize(-(mu_rob @ x_rob))
m_rob.subject_to(dm.sum(x_rob) == 1.0, name="budget")
RobustCounterpart(m_rob, EllipsoidalUncertaintySet(mu_rob, rho=0.02)).formulate()
r_rob = m_rob.solve()
x_r = list(r_rob.x.values())[0]
print(f"\nRobust: return = {-r_rob.objective:.4f}")
print(f" x = {np.round(x_r, 4)}")
print(" (Diversified to hedge against return uncertainty)")
Nominal: return = 0.1451
x = [0. 1. 0. 0. 0.]
(All capital in asset with highest return)
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit https://github.com/coin-or/Ipopt
******************************************************************************
Robust: return = 0.1251
x = [0. 1. 0. 0. 0.]
(Diversified to hedge against return uncertainty)
/Users/jkitchin/Dropbox/projects/discopt/python/discopt/_jax/convexity/interval_ad.py:168: RuntimeWarning: invalid value encountered in multiply
p1 = s_lo * arr.lo
/Users/jkitchin/Dropbox/projects/discopt/python/discopt/_jax/convexity/interval_ad.py:169: RuntimeWarning: invalid value encountered in multiply
p2 = s_lo * arr.hi
/Users/jkitchin/Dropbox/projects/discopt/python/discopt/_jax/convexity/interval_ad.py:170: RuntimeWarning: invalid value encountered in multiply
p3 = s_hi * arr.lo
/Users/jkitchin/Dropbox/projects/discopt/python/discopt/_jax/convexity/interval_ad.py:171: RuntimeWarning: invalid value encountered in multiply
p4 = s_hi * arr.hi
Multiple Uncertain Parameters#
Multiple parameters from the same uncertainty family can be passed as a list:
m_multi = dm.Model("multi_uncertain")
x = m_multi.continuous("x", shape=(3,), lb=0, ub=100)
c = m_multi.parameter("c", value=[10.0, 15.0, 8.0])
d = m_multi.parameter("d", value=50.0)
m_multi.minimize(dm.sum(c * x))
m_multi.subject_to(dm.sum(x) >= d, name="demand")
m_multi.subject_to(x[0] + 2 * x[1] <= 80.0, name="resource")
rc_multi = RobustCounterpart(
m_multi,
[
BoxUncertaintySet(c, delta=[1.0, 1.5, 0.8]),
BoxUncertaintySet(d, delta=5.0),
],
)
rc_multi.formulate()
r_multi = m_multi.solve()
xv = list(r_multi.x.values())[0]
print(f"Robust obj = {r_multi.objective:.2f}, x = {np.round(xv, 2)}")
print("\nRobust constraints (worst-case constants substituted):")
for con in m_multi._constraints:
print(f" [{con.name}] {con.body} {con.sense} {con.rhs}")
Robust obj = 484.00, x = [ 0. 0. 55.]
Robust constraints (worst-case constants substituted):
[demand] (55 - sum(x[3])) <= 0.0
[resource] ((x[3][0] + (2 * x[3][1])) - 80) <= 0.0
Example 4 — Drug Production (Ben-Tal et al. 2009, Example 1.1.1)#
This is the opening example from the textbook Robust Optimization [Ben-Tal et al., 2009]. A company produces two drugs from two raw materials. The active agent concentrations in the raw materials are uncertain, and the robust counterpart protects against worst-case yields.
The uncertain concentrations are \(a_1 = 0.01 \pm 0.5\%\) and \(a_2 = 0.02 \pm 2\%\). The published solutions are: nominal profit = $8,819.66 (using only RawII), robust profit = $8,294.57 (switching to the more reliable RawI), a 5.95% price of robustness.
def drug_model(name):
"""Build the Ben-Tal Example 1.1.1 drug production model."""
m = dm.Model(name)
d1 = m.continuous("DrugI", lb=0, ub=1000)
d2 = m.continuous("DrugII", lb=0, ub=1000)
r1 = m.continuous("RawI", lb=0, ub=1000)
r2 = m.continuous("RawII", lb=0, ub=1000)
aI = m.parameter("aI", value=0.01)
aII = m.parameter("aII", value=0.02)
# Maximize profit (net revenue minus raw material cost)
m.minimize(-(5500 * d1 + 6100 * d2 - 100 * r1 - 199.90 * r2))
m.subject_to(aI * r1 + aII * r2 >= 0.5 * d1 + 0.6 * d2, name="balance")
m.subject_to(r1 + r2 <= 1000, name="storage")
m.subject_to(90 * d1 + 100 * d2 <= 2000, name="manpower")
m.subject_to(40 * d1 + 50 * d2 <= 800, name="equipment")
m.subject_to(100 * r1 + 199.90 * r2 + 700 * d1 + 800 * d2 <= 100000, name="budget")
return m, aI, aII
# --- Nominal ---
m_nom, _, _ = drug_model("nominal")
r_nom = m_nom.solve()
print("Nominal solution:")
print(f" Profit = ${-r_nom.objective:,.2f}")
for name, val in r_nom.x.items():
if abs(val) > 0.01:
print(f" {name} = {val:.4f}")
# --- Robust (box uncertainty on active agent concentrations) ---
m_rob, aI, aII = drug_model("robust")
# aI = 0.01 +/- 0.5% = 0.01 +/- 0.00005
# aII = 0.02 +/- 2.0% = 0.02 +/- 0.0004
RobustCounterpart(
m_rob, [BoxUncertaintySet(aI, delta=0.00005), BoxUncertaintySet(aII, delta=0.0004)]
).formulate()
r_rob = m_rob.solve()
print("\nRobust solution:")
print(f" Profit = ${-r_rob.objective:,.2f}")
for name, val in r_rob.x.items():
if abs(val) > 0.01:
print(f" {name} = {val:.4f}")
nom_profit = -r_nom.objective
rob_profit = -r_rob.objective
print(f"\nPrice of robustness: {(nom_profit - rob_profit) / nom_profit * 100:.2f}%")
print(
"\nThe robust solution switches from RawII to RawI because RawI has"
"\ntighter uncertainty (0.5% vs 2%), making it more reliable despite"
"\nhigher cost per gram."
)
Nominal solution:
Profit = $8,819.66
DrugI = 17.5516
RawII = 438.7889
Robust solution:
Profit = $8,294.57
DrugI = 17.4669
RawI = 877.7319
Price of robustness: 5.95%
The robust solution switches from RawII to RawI because RawI has
tighter uncertainty (0.5% vs 2%), making it more reliable despite
higher cost per gram.
API Summary#
from discopt.ro import (
AffineDecisionRule, # adjustable robust (affine policy)
BoxUncertaintySet, # per-component interval
EllipsoidalUncertaintySet, # ellipsoidal ball
PolyhedralUncertaintySet, # general polytope
RobustCounterpart, # static robust counterpart
budget_uncertainty_set, # Bertsimas-Sim convenience constructor
)
# -- Static robust optimization -----------------------------------------------
m = dm.Model()
p = m.parameter("p", value=nominal_value)
# ... build objective and constraints using p ...
rc = RobustCounterpart(m, BoxUncertaintySet(p, delta=0.1 * nominal_value))
rc.formulate() # modifies m in-place; p replaced by worst-case constant
result = m.solve()
# -- Adjustable robust optimization (two-stage) -------------------------------
m = dm.Model()
x = m.continuous("x", ...) # here-and-now decisions
y = m.continuous("y", ...) # wait-and-see recourse
p = m.parameter("p", value=nominal_value)
# ... build objective and constraints using x, y, p ...
# Stage 1: affine rule y(xi) = y0 + Y0*(p - p_bar)
adr = AffineDecisionRule(y, uncertain_params=p)
adr.apply() # retires y; adds y0, Y0 as decision variables
# Stage 2: handle remaining xi terms
rc = RobustCounterpart(m, BoxUncertaintySet(p, delta=delta))
rc.formulate() # fully deterministic model
result = m.solve() # optimises over (x, y0, Y0)
Limitations#
Coefficient uncertainty (bilinear
p * x) requires LP dual auxiliary variables; planned for a future release.Recourse shape:
AffineDecisionRulesupports scalar and 1-D recourse variables; 2-D support is planned.Non-affine recourse (polynomial, piecewise-linear) is not supported.
Distributionally robust optimization (moment-based or Wasserstein ambiguity sets) is not yet supported.
For a full adjustable RO tutorial including multi-parameter recourse and vector recourse variables, see Adjustable Robust Optimization.