This notebook was prepared by Jialu Wang and revised by Maddie Watson at the University of Notre Dame.
# Imports
import sys
if "google.colab" in sys.modules:
!wget "https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py"
import helper
helper.easy_install()
else:
sys.path.insert(0, "../")
import helper
helper.set_plotting_style()Matplotlib is building the font cache; this may take a moment.
Farmer’s example: one model, reused¶
Read the physical balances, connect them to Pyomo Blocks, and analyze complete solution tables. The SAA, CVaR, and zero-VSS pages reuse this implementation. Further examples retain the optional uniform-yield, dependence, and profit-floor studies.
Textbook data: Birge and Louveaux (2011), 2nd ed., §1.1a–c, pp. 4–11.
The textbook farmer example in Birge and Louveaux (2011), 2nd ed., §1.1a–c, pp. 4–11, uses 500 acres and three crops. Wheat and corn must supply 200 and 240 tons of feed; shortages can be purchased and surplus sold. Sugar beets have a premium-price sales cap of 6000 tons.
| Wheat | Corn | Sugar Beets | |
|---|---|---|---|
| Yield (T/acre) | 2.5 | 3 | 20 |
| Planting cost ($/acre) | 150 | 230 | 260 |
| Selling price ($/T) | 170 | 150 | 36 under 6000 T, 10 above 6000 T |
| Purchase price ($/T) | 238 | 210 | – |
| Minimum requirement (T) | 200 | 240 | – |
Total available land: 500 acres
To help the farmer make up his mind, we can set up the following model. Let
= acres of land devoted to wheat,
= acres of land devoted to corn,
= acres of land devoted to sugar beets,
= tons of wheat sold,
= tons of wheat purchased,
= tons of corn sold,
= tons of corn purchased,
= tons of sugar beets sold at the favorable price,
= tons of sugar beets sold at the unfavorable price.

Deterministic mean-yield model¶
First solve at the specified mean yields. Later, the same model is solved separately at each realized yield to compute the perfect-information benchmark:

With tons available to sell, optimal beet revenue is . The separate sales variables and premium bound represent this concave piecewise-linear revenue within an LP; binary variables are unnecessary for these prices.
Declare shared decisions and scenario Blocks¶
A Block groups variables, constraints, and expressions. The root model owns
acreage; each scenario Block owns purchases and sales and refers to the same
acreage variables. One scenario gives the deterministic LP. More scenarios reuse
the model without copying planting decisions.
The builder and its helpers below are defined before the first solve. Monetary objectives use profit maximization; this is equivalent to minimizing net cost in the mathematical formulation above. Units are checked on the small models.

import numpy as np
import pyomo.environ as pyo
from pyomo.environ import units as u
from pyomo.util.check_units import assert_units_consistent
u.load_definitions_from_strings(["USD = [currency]"])
USD_PER_ACRE = u.USD / u.acre
USD_PER_TON = u.USD / u.metric_ton
TON_PER_ACRE = u.metric_ton / u.acre
CROPS = ["WHEAT", "CORN", "BEETS"]
FEED_CROPS = ["WHEAT", "CORN"]
SALE_TYPES = ["WHEAT", "CORN", "BEETS_FAVORABLE", "BEETS_UNFAVORABLE"]
MEAN_YIELD = dict(zip(CROPS, [2.5, 3.0, 20.0])) # T/acre
PLANT_COST = dict(zip(CROPS, [150.0, 230.0, 260.0])) # $/acre
BUY_PRICE = dict(zip(FEED_CROPS, [238.0, 210.0])) # $/T
SELL_PRICE = dict(zip(SALE_TYPES, [170.0, 150.0, 36.0, 10.0])) # $/T
FEED_NEED = dict(zip(FEED_CROPS, [200.0, 240.0])) # T
TOTAL_LAND, BEET_QUOTA = 500.0, 6000.0 # acres, Tdef build_farmer(
scenarios, weights=None, plant_cost=None, risk=None, check_units=False
):
"""Return a fresh farmer model; scenarios contain yields and optional prices.
scenarios: nonempty list of dictionaries; 'yield' maps crops to T/acre.
Optional 'buy' and 'sell' mappings give $/T. weights sum to one.
plant_cost maps crops to $/acre. risk selects an optional risk policy.
"""
n = len(scenarios)
if n == 0:
raise ValueError("At least one scenario is required")
w = np.full(n, 1 / n) if weights is None else np.asarray(weights, float)
if w.shape != (n,) or not np.all(np.isfinite(w)) or np.any(w <= 0):
raise ValueError("Retained scenarios need positive finite probabilities")
if not np.isclose(w.sum(), 1.0):
raise ValueError("Scenario probabilities must sum to one")
costs = PLANT_COST if plant_cost is None else plant_cost
m = pyo.ConcreteModel()
m.CROPS = pyo.Set(initialize=CROPS)
m.FEED_CROPS = pyo.Set(initialize=FEED_CROPS)
m.SALE_TYPES = pyo.Set(initialize=SALE_TYPES)
m.SCENARIOS = pyo.RangeSet(0, n - 1)
m.weight = pyo.Param(m.SCENARIOS, initialize=dict(enumerate(w)))
# Plant once, before learning the yields: no scenario index on acreage.
m.acreage = pyo.Var(m.CROPS, domain=pyo.NonNegativeReals, units=u.acre)
m.land_limit = pyo.Constraint(expr=sum(m.acreage.values()) <= TOTAL_LAND * u.acre)
m.planting_cost = pyo.Expression(
expr=sum(costs[c] * USD_PER_ACRE * m.acreage[c] for c in CROPS)
)
# The helper below attaches recourse to each Block, sharing root acreage.
m.scenarios = pyo.Block(
m.SCENARIOS, rule=lambda b, s: add_farmer_scenario(b, m, scenarios[s])
)
m.expected_profit = pyo.Expression(
expr=sum(m.weight[s] * m.scenarios[s].profit for s in m.SCENARIOS)
)
m.objective = pyo.Objective(expr=m.expected_profit, sense=pyo.maximize)
if risk:
add_farmer_risk(m, risk) # Optional tail-risk and downside policies.
if check_units:
assert_units_consistent(m)
return mdef add_farmer_scenario(b, m, scenario):
"""Attach one scenario's recourse, using the root model's acreage."""
crop_yield = scenario["yield"]
buy = scenario.get("buy", BUY_PRICE)
sell = scenario.get("sell", SELL_PRICE)
b.purchases = pyo.Var(m.FEED_CROPS, domain=pyo.NonNegativeReals, units=u.metric_ton)
b.sales = pyo.Var(m.SALE_TYPES, domain=pyo.NonNegativeReals, units=u.metric_ton)
b.sales["BEETS_FAVORABLE"].setub(BEET_QUOTA)
# Harvest + purchases - sales must cover animal feed in this scenario.
@b.Constraint(m.FEED_CROPS)
def feed_balance(block, c):
return (
crop_yield[c] * TON_PER_ACRE * m.acreage[c]
+ block.purchases[c]
- block.sales[c]
>= FEED_NEED[c] * u.metric_ton
)
b.beets_constraint = pyo.Constraint(
expr=b.sales["BEETS_FAVORABLE"] + b.sales["BEETS_UNFAVORABLE"]
<= crop_yield["BEETS"] * TON_PER_ACRE * m.acreage["BEETS"]
)
# Profit includes planting cost; weights summing to one count it once.
b.profit = pyo.Expression(
expr=sum(sell[k] * USD_PER_TON * b.sales[k] for k in SALE_TYPES)
- sum(buy[c] * USD_PER_TON * b.purchases[c] for c in FEED_CROPS)
- m.planting_cost
)farmer_solver = pyo.SolverFactory("appsi_highs")
assert farmer_solver.available(), "HiGHS is required for the rest of this notebook"
def solve_farmer(m):
"""Solve, refusing anything but a proven optimum."""
results = farmer_solver.solve(m)
assert pyo.check_optimal_termination(
results
), f"Solve failed: termination={results.solver.termination_condition}"
return m
def plan_of(m):
"""Stage 1 acreage as a plain array, in the order of CROPS."""
return np.array([pyo.value(m.acreage[c]) for c in CROPS])
def grade_plan(plan, scenarios, weights=None, plant_cost=None):
"""Fix acreage in CROPS order; optimize trades for the supplied scenarios.
Return expected profit and an array of scenario profits, both in dollars.
"""
m = build_farmer(scenarios, weights, plant_cost)
for crop, acres in zip(CROPS, plan):
m.acreage[crop].fix(acres)
solve_farmer(m)
return (
pyo.value(m.expected_profit),
np.array([pyo.value(m.scenarios[i].profit) for i in m.SCENARIOS]),
)
def correlated_scenario(multiplier):
"""One scenario of Section 1.1b: every crop scaled by the same factor."""
return {"yield": {c: MEAN_YIELD[c] * multiplier for c in CROPS}}
# The three equally likely scenarios of Section 1.1b
THREE = [correlated_scenario(f) for f in (1.2, 1.0, 0.8)]Shared helpers for the companion pages¶
The optional risk interface and complete-solution utilities below are also exported from this notebook. The introductory stochastic-programming model uses the default expected-profit objective; CVaR explains the risk formulation.
def add_farmer_cvar(m, risk):
"""Lower-tail profit CVaR: alpha is the confidence level."""
w = m.weight
n = len(m.SCENARIOS)
# Rockafellar-Uryasev epigraph, B&L (9.8)-(9.10), p. 85, written for the
# LOWER tail of profit: CVaR_a = max_nu { nu - E[(nu - profit)_+]/(1-a) }
alpha = (risk.get("cvar") or risk.get("cvar_floor"))[0]
m.nu = pyo.Var(domain=pyo.Reals, units=u.USD)
m.shortfall = pyo.Var(m.SCENARIOS, domain=pyo.NonNegativeReals, units=u.USD)
m.shortfall_con = pyo.Constraint(
m.SCENARIOS,
rule=lambda m, i: m.shortfall[i] >= m.nu - m.scenarios[i].profit,
)
m.cvar = m.nu - (1.0 / (1.0 - alpha)) * sum(w[i] * m.shortfall[i] for i in range(n))
if "cvar_floor" in risk:
m.cvar_con = pyo.Constraint(expr=m.cvar >= risk["cvar_floor"][1] * u.USD)def add_farmer_risk(m, risk):
"""Attach the requested risk policy to a fresh farmer model."""
w = m.weight
n = len(m.SCENARIOS)
m.del_component(m.objective)
if "floor" in risk:
# Exercise 7(b): a profit floor imposed in every scenario
m.profit_floor = pyo.Constraint(
m.SCENARIOS,
rule=lambda m, i: m.scenarios[i].profit >= risk["floor"] * u.USD,
)
if "minimax" in risk:
# B&L (9.15), p. 86, in profit form: maximize the worst scenario
m.worst = pyo.Var(domain=pyo.Reals, units=u.USD)
m.worst_con = pyo.Constraint(
m.SCENARIOS, rule=lambda m, i: m.worst <= m.scenarios[i].profit
)
if "cvar" in risk or "cvar_floor" in risk:
add_farmer_cvar(m, risk)
if "downside" in risk:
# Expected downside risk, B&L (5.1)-(5.3), p. 68
target, level = risk["downside"]
m.shortfall_below_target = pyo.Var(
m.SCENARIOS, domain=pyo.NonNegativeReals, units=u.USD
)
m.downside_def = pyo.Constraint(
m.SCENARIOS,
rule=lambda m, i: m.shortfall_below_target[i]
>= target * u.USD - m.scenarios[i].profit,
)
m.downside_limit = pyo.Constraint(
expr=sum(w[i] * m.shortfall_below_target[i] for i in range(n))
<= level * u.USD
)
if "minimax" in risk:
m.objective = pyo.Objective(expr=m.worst, sense=pyo.maximize)
elif "cvar" in risk:
lam = risk["cvar"][1]
m.objective = pyo.Objective(
expr=(1 - lam) * m.expected_profit + lam * m.cvar, sense=pyo.maximize
)
else:
m.objective = pyo.Objective(expr=m.expected_profit, sense=pyo.maximize)def synthetic_farmer_yields(n, seed):
"""Draw synthetic joint yields, not observed agricultural data.
Weather multiplier = 0.65 + 0.70 * Beta(2, 2), shared by all crops.
Independent crop deviations are Uniform(-0.08, 0.08).
All crop multipliers lie in [0.57, 1.43], with population mean 1.
Dependence comes from shared weather; draws across scenarios are iid.
"""
rng = np.random.default_rng(seed)
weather = 0.65 + 0.70 * rng.beta(2, 2, size=n)
multipliers = weather[:, None] + rng.uniform(-0.08, 0.08, size=(n, 3))
return [{"yield": dict(zip(CROPS, row * np.array(list(MEAN_YIELD.values()))))}
for row in multipliers]
SYNTHETIC_SEED = 20260927
TWENTY = synthetic_farmer_yields(20, SYNTHETIC_SEED)
def solution_record(m):
"""Full solved policy and recourse, with units explicit in field names."""
return {
"acreage": {c: float(pyo.value(m.acreage[c])) for c in CROPS},
"expected_profit": float(pyo.value(m.expected_profit)),
"scenarios": [
{"id": int(i) + 1,
"probability": float(pyo.value(m.weight[i])),
"purchases_tons": {c: float(pyo.value(m.scenarios[i].purchases[c]))
for c in FEED_CROPS},
"sales_tons": {c: float(pyo.value(m.scenarios[i].sales[c]))
for c in SALE_TYPES},
"profit": float(pyo.value(m.scenarios[i].profit))}
for i in m.SCENARIOS],
}
def evaluate_solution(plan, scenarios, weights=None, plant_cost=None):
"""Return all efficient trades for fixed acreage, not arbitrary risk-solve trades."""
m = build_farmer(scenarios, weights=weights, plant_cost=plant_cost)
for c, acres in zip(CROPS, plan):
m.acreage[c].fix(float(acres))
return solution_record(solve_farmer(m))
def analytic_profits(plan, scenarios, plant_cost=None):
"""Independent closed-form recourse for these farmer data (dollars).
Valid when sales prices are nonnegative, purchase prices exceed selling
prices, and favorable beet revenue exceeds over-quota revenue. Feed
shortages are bought, surpluses sold; beet sales fill the premium quota first.
"""
costs = PLANT_COST if plant_cost is None else plant_cost
acreage = dict(zip(CROPS, plan))
planting = sum(costs[c] * acreage[c] for c in CROPS)
profits = []
for s in scenarios:
sell, buy = s.get("sell", SELL_PRICE), s.get("buy", BUY_PRICE)
assert all(buy[c] > sell[c] >= 0 for c in FEED_CROPS)
assert sell["BEETS_FAVORABLE"] >= sell["BEETS_UNFAVORABLE"] >= 0
profit = -planting
for c in FEED_CROPS:
surplus = s["yield"][c] * acreage[c] - FEED_NEED[c]
profit += sell[c] * max(surplus, 0) - buy[c] * max(-surplus, 0)
beets = s["yield"]["BEETS"] * acreage["BEETS"]
profit += sell["BEETS_FAVORABLE"] * min(beets, BEET_QUOTA)
profit += sell["BEETS_UNFAVORABLE"] * max(beets - BEET_QUOTA, 0)
profits.append(profit)
return np.asarray(profits)
def lower_tail_profit(profits, alpha, weights=None):
"""Average the worst mass 1-alpha, including fractional mass at ties."""
values = np.asarray(profits, dtype=float)
if not 0 <= alpha < 1:
raise ValueError("alpha must be in [0, 1)")
w = np.full(len(values), 1 / len(values)) if weights is None else np.asarray(weights)
if len(values) == 0 or w.shape != values.shape or np.any(w < 0) or not np.isclose(w.sum(), 1):
raise ValueError("Use nonempty outcomes with probabilities summing to one")
order = np.argsort(values)
remaining, total = 1 - alpha, 0.0
for i in order:
mass = min(float(w[i]), remaining)
total += mass * values[i]
remaining -= mass
if remaining <= 1e-14:
break
return total / (1 - alpha)
Known yields: solve, then interpret¶
build_farmer([correlated_scenario(1.0)]) creates the mean-yield LP. The same
function receives a singleton list for each known-yield benchmark below.
Discuss: classify the problem. Why does the solution grow 300 acres of beets? Which resource limits the profit? The dual values report the local change in optimal profit per extra unit of the corresponding constraint bound.
mean_model = build_farmer([correlated_scenario(1.0)], check_units=True)
mean_model.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT)
solve_farmer(mean_model)
mean_plan = plan_of(mean_model)
print("Mean-yield acreage [wheat, corn, beets]:", mean_plan)
print(f"Profit: ${pyo.value(mean_model.expected_profit):,.2f}")
print(f"Land multiplier: ${mean_model.dual[mean_model.land_limit]:.2f}/acre")
for c in FEED_CROPS:
con = mean_model.scenarios[0].feed_balance[c]
print(f"Feed minimum multiplier, {c}: ${mean_model.dual[con]:.2f}/T")
assert np.allclose(mean_plan, [120, 80, 300])
assert np.isclose(pyo.value(mean_model.expected_profit), 118600)Mean-yield acreage [wheat, corn, beets]: [120. 80. 300.]
Profit: $118,600.00
Land multiplier: $275.00/acre
Feed minimum multiplier, WHEAT: $-170.00/T
Feed minimum multiplier, CORN: $-168.33/T
The mean-yield model is an LP. Premium beet acreage reaches the sales quota; corn meets feed needs, and wheat takes the remaining land. The land multiplier is a local sensitivity, not a universal crop ranking. Crop rotation and shared labor constraints can change the allocation.
What if yields are known before planting?¶
Solve three separate problems: all crops at 120%, 100%, or 80% of mean yield. This assumes perfect information before planting.

perfect_models = [solve_farmer(build_farmer([scenario])) for scenario in THREE]
print("Known yield wheat corn beets profit ($)")
for label, model in zip(["above", "average", "below"], perfect_models):
a = plan_of(model)
print(
f"{label:12s} {a[0]:8.1f} {a[1]:8.1f} {a[2]:8.1f}"
f" {pyo.value(model.expected_profit):16,.2f}"
)Known yield wheat corn beets profit ($)
above 183.3 66.7 250.0 167,666.67
average 120.0 80.0 300.0 118,600.00
below 100.0 25.0 375.0 59,950.00
Analyze the results: why does lower beet yield lead to more beet acreage? Why does the bad-year solution buy corn? Which assumption makes these three planting plans unavailable to a farmer who must plant before observing yields?
One planting plan, three possible harvests¶
Stage 1: choose acreage before yield information.
Stage 2: buy and sell after observing the harvest.
Nonanticipativity: decisions use only information available at that time. One shared
m.acreageenforces this structurally.
Each scenario has probability 1/3. The same build_farmer(THREE) function now
creates three Blocks and maximizes expected profit.
Birge and Louveaux, §1.1b–c, pp. 6–11; §1.2, pp. 21, 25–26.
Stages describe information¶
A stage groups decisions that have the same information available. It need not be a fixed number of days. In this two-stage model the order is
The first-stage vector is chosen before the uncertain yields are observed. Recourse is chosen afterward, subject to the acreage already planted. Planning for recourse means accounting for those future feasible actions when choosing today. It does not mean waiting to choose every variable until the harvest is known.

Nonanticipativity requires identical decisions whenever scenarios have identical information histories at the decision time. All three farmer scenarios have the same history at planting, so they share acreage. After the harvest is observed, their trading decisions may differ. In a multistage tree, decisions remain shared at a common node and can separate only after the histories branch. See Birge and Louveaux, §1.2, pp. 21, 25–26, and §§2.3–2.4, pp. 57–60.
Random vectors, realizations, and scenarios¶
denotes a random vector of uncertain data; is one realized value. The joint law specifies which combinations occur and with what probabilities. Its support is the smallest closed set with probability one. The book writes for this support; here uppercase bold identifies the random vector. We use for the finite retained scenario indices below, whereas the book also uses for an underlying sample space.
For a finite distribution, scenario has realization and probability , with . Zero-probability scenarios may be removed. A fair die has six outcomes, each of probability . For an event ,
A probability density instead satisfies
A density value is not a probability at a point and can exceed one. For example, has density 2 on that interval, total area , and ; each individual point has probability zero.

For an integrable function ,
The weighted sum is exact when it includes the full finite law. A retained sample of a continuous law gives a discrete approximation; independent identically distributed sampling uses empirical weights . The mean and covariance summarize a law but do not specify it. Likewise, independent draws across scenarios do not imply independent crops within a scenario: shared weather can correlate all three crop yields. The SAA guide supplies a declared joint generator and its complete 20-row sample. See Birge and Louveaux, §§2.1–2.2, pp. 55–57, and §1.1d, pp. 11–15.
General two-stage model and extensive form¶
A general finite-scenario net-cost minimization model is
Here is shared, is scenario-specific, and include variable domains. Every positive-probability scenario has its own feasibility constraints; the probability weights do not relax those constraints. This form allows nonlinear functions or integer domains. The expanded farmer model below is an LP because its coefficients are fixed once a scenario is specified and every variable is continuous.
The book’s classical linear fixed-recourse form makes the nested optimization explicit:
is the recourse matrix, common to scenarios; “fixed recourse” does not require fixed recourse decisions. The data may depend on the realization. For a continuous law, recourse constraints hold almost surely, meaning outside a set of probability zero. For a retained finite law, each positive-probability row must be feasible. For a given , infeasible recourse has value in the net-cost convention. An expected-cost model also needs a well-defined finite expectation at the policies being compared.
Substitute the full finite law into the expectation and include each scenario’s recourse variables:
This extensive form optimizes one planting decision and all contingent trading decisions together. It is equivalent to the nested recourse formulation when the finite scenario law is exact and the recourse optima exist. If a first-stage cost is included inside every , then ; it is counted once because weights sum to one.

An alternative implementation creates a copy per scenario and adds
Any feasible shared-vector solution gives feasible copies by setting each copy to . Conversely, these equalities force every feasible copied solution to use that same vector, giving the same scenario constraints and objective. Thus the formulations are equivalent. Removing the equalities instead permits perfect-information planting and changes the information structure. build_farmer avoids the extra copies by referring to the root acreage in every Block. See Birge and Louveaux, §1.2, model (2.2), pp. 25–26; §2.4, pp. 59–60; and §3.1a, pp. 103–104.
Preserve dependence within a scenario¶

Both constructed laws give each crop a normalized yield of 0.8 or 1.2 with equal probability and mean one. Under common weather, both low yields occur with probability one half; under independence, with probability one quarter. Thus the means and marginals do not determine the joint law. The joint profit distribution and its tail can change. For this farmer’s additive, crop-separable expected recourse, holding all crop marginals fixed leaves the population risk-neutral objective unchanged; the full separability argument explains the required structure. Finite sampled marginals may still differ across runs.
# Validation first: reproduce Birge & Louveaux Table 5, p. 8.
RP = solve_farmer(build_farmer(THREE))
rp_plan = plan_of(RP)
rp_expected = pyo.value(RP.expected_profit)
rp_profits = np.array([pyo.value(RP.scenarios[i].profit) for i in RP.SCENARIOS])
print(
f"acres wheat {rp_plan[0]:.1f} corn {rp_plan[1]:.1f} beets {rp_plan[2]:.1f}"
" (book: 170, 80, 250)"
)
print(f"expected profit = ${rp_expected:,.0f} (book: $108,390)")
print(
"scenario profits above ${:,.0f} | average ${:,.0f} | below ${:,.0f}".format(
*rp_profits
)
)
assert np.allclose(rp_plan, [170.0, 80.0, 250.0], atol=1e-6)
assert abs(rp_expected - 108390.0) < 1.0
# Exercise the unit declarations once, on this small model.
assert_units_consistent(build_farmer(THREE, check_units=True))
print("units consistent")acres wheat 170.0 corn 80.0 beets 250.0 (book: 170, 80, 250)
expected profit = $108,390 (book: $108,390)
scenario profits above $167,000 | average $109,350 | below $48,820
units consistent
print("Scenario wheat sold corn bought corn sold premium beets profit ($)")
for label, s in zip(["above", "average", "below"], RP.SCENARIOS):
b = RP.scenarios[s]
print(
f"{label:8s} {pyo.value(b.sales['WHEAT']):13.1f}"
f" {pyo.value(b.purchases['CORN']):13.1f} {pyo.value(b.sales['CORN']):11.1f}"
f" {pyo.value(b.sales['BEETS_FAVORABLE']):15.1f} {pyo.value(b.profit):12,.0f}"
)Scenario wheat sold corn bought corn sold premium beets profit ($)
above 310.0 0.0 48.0 6000.0 167,000
average 225.0 0.0 0.0 5000.0 109,350
below 140.0 48.0 0.0 4000.0 48,820
Analyze the results: compare acreage with the three perfect-information plans. Explain the switch between buying and selling corn. Does maximizing expected profit protect the worst-year outcome?
The extensive form has 3 shared acreage variables and 6 trading variables per scenario: 21 variables total. With the quota as a bound, there are 10 explicit inequalities (one land limit and three balances per scenario), plus variable bounds. None is an equality; subtracting inequality counts is not a degrees-of- freedom calculation.

In the figure, each is the scenario coefficient block multiplying shared acreage and multiplies that scenario’s trades. Quota bounds are omitted from the block sketch. Shaded blocks may include zero coefficients; the empty off-diagonal blocks are all zero.
Compare information values consistently¶
Use profit maximization throughout this comparison. Let include optimal trading after yields are observed.
| Policy | Low-yield profit [$] | Mean-yield profit [$] | High-yield profit [$] | Expected profit [$] |
|---|---|---|---|---|
| Perfect-information plans | 59,950 | 118,600 | 167,666.67 | 115,405.56 |
| Shared stochastic plan | 48,820 | 109,350 | 167,000 | 108,390 |
Values are rounded after averaging; Birge and Louveaux, §4.1, pp. 163–164.
Expected value of perfect information¶
For profit maximization, . It is the model’s increase in optimal expected profit if yields are known before planting. It bounds a risk-neutral decision maker’s willingness to pay for perfect yield information within this model, not the price of an arbitrary forecast. See Birge and Louveaux, §4.1, pp. 163–164.
Expected-value plan evaluated under uncertainty¶
Choose and then compute . Only acreage is fixed; trading is optimized per scenario.
For the mean-yield plan (120,80,300), . See Birge and Louveaux, §4.2, p. 165.
Value of the stochastic solution¶
With the profit convention, . The general ordering assumes a feasible EV plan and consistently optimized recourse. See Birge and Louveaux, §§4.2–4.3, pp. 165–166.
Why the information values have this order¶
Assume the optima below exist, their expected profits are finite, and the expected-value acreage admits feasible recourse in every retained scenario. Let impose the first-stage restrictions and let be the maximum scenario profit after optimizing trades at that fixed acreage. Acreage infeasible for a scenario can be excluded from that scenario’s maximization. Write for an optimal shared plan and for a scenario’s perfect-information plan.

For each scenario, is an available candidate when planting may use perfect information. Therefore
Multiply by and sum: .
The shared stochastic optimum can choose the feasible candidate , so
Consequently , , and . The first inequality relaxes a restriction on information; the second compares an optimized plan with one feasible candidate. Neither proof requires convexity. For net-cost minimization every profit changes sign, giving , , and . This is the profit-form derivation of Birge and Louveaux, §4.3, Proposition 1, p. 166.
Scope. If the mean-data plan is infeasible in a retained scenario, a finite EEV cannot be assumed; one must report infeasibility or explicitly introduce and price emergency recourse. A feasible plan’s trades must be reoptimized for each harvest. Reusing mean-harvest trades is a different, more restricted policy. The mean-data optimum is also a different quantity from the expected realized profit : here , while .
There is no general ordering between EVPI and VSS. The alternative-price farmer gives a fully solved case with zero VSS and positive EVPI; Birge and Louveaux, §4.4, pp. 167–170, also treats the reverse case. EVPI concerns perfect information before the decision. VSS concerns optimizing the given probability law rather than its mean-data replacement. It is not the value of learning that law.
For risk-neutral expected profit, a forecast that only supplies partial yield information and leaves this model’s actions, prices, and uncertainty law unchanged cannot improve expected profit by more than perfect information does. An $8,000 forecast therefore cannot cover its cost when . This comparison does not price additional benefits outside the stated model.

The square, diagonal, and fixed point illustrate the feasible-set argument. The drawing normalizes one decision to and omits other constraints; it does not assert that the full farmer feasible set is a square.
WS = np.mean([pyo.value(m.expected_profit) for m in perfect_models])
EEV, ev_profits = grade_plan(mean_plan, THREE)
print("Policy expected profit ($)")
print(f"Perfect information (WS) {WS:17,.2f}")
print(f"Shared stochastic plan (RP) {rp_expected:17,.2f}")
print(f"Mean-yield plan evaluated (EEV) {EEV:17,.2f}")
print(f"EVPI = WS - RP: ${WS - rp_expected:,.2f}")
print(f"VSS = RP - EEV: ${rp_expected - EEV:,.2f}")
print("EEV scenario profits [above, average, below]:", ev_profits)
assert np.isclose(EEV, 107240)
assert WS >= rp_expected >= EEVPolicy expected profit ($)
Perfect information (WS) 115,405.56
Shared stochastic plan (RP) 108,390.00
Mean-yield plan evaluated (EEV) 107,240.00
EVPI = WS - RP: $7,015.56
VSS = RP - EEV: $1,150.00
EEV scenario profits [above, average, below]: [148000. 118600. 55120.]
EVPI measures the value of perfect pre-decision information. VSS compares the stochastic optimum with the evaluated mean-data plan; it does not measure the value of learning an unknown distribution.
Reference vocabulary and notation¶
These definitions use profit maximization. The CVaR guide, SAA guide, and advanced sampling study develop their respective formulations and computations.
| Term | Meaning |
|---|---|
| Chance constraint | A requirement such as under a specified probability model. The allowed violation probability is . |
| Conditional value at risk (CVaR) | Average of the worst probability mass of a loss distribution, splitting an atom at the cutoff when necessary. For profit , the equivalent lower-tail profit is . |
| Empirical distribution | A discrete law assigning probability weights to observed or sampled outcomes; an equally weighted sample of harvests assigns weight to each row. |
| Epigraph | The points on or above a function’s graph: . Auxiliary variables can represent positive-part shortfalls using linear inequalities. |
| Expected result of the EV policy (EEV) | Expected performance after fixing a mean-data optimal first-stage decision and optimizing recourse under the original probability law. |
| Expectation | The probability-weighted average of a random quantity, when it exists. See the finite-sum and density-integral formulas above. |
| Expected-value problem (EV) | Deterministic optimization with uncertain data replaced by their means. Its solution is an EV policy; its objective value generally differs from EEV. |
| Expected value of perfect information (EVPI) | Increase in optimal expected profit from knowing all modeled uncertainty before choosing any decisions: . |
| First-stage decision | A choice made before the modeled uncertainty is observed, such as acreage. |
| Nonanticipativity | Decisions use only available information; scenarios with the same observed history must share the corresponding decisions. |
| Recourse | Actions available after observing uncertainty, subject to earlier decisions, such as purchases and sales after planting. |
| Recourse problem (RP) | Optimize shared first-stage decisions and adaptive recourse for the specified expected objective. |
| Risk aversion | A preference that accounts for unfavorable outcomes beyond their contribution to the mean. A CVaR objective is one modeling choice. |
| Risk neutrality | Comparing uncertain outcomes by their expected objective value. |
| Sample average approximation (SAA) | Replace an expectation by an average over sampled scenarios and optimize the resulting model. Training chooses the policy; independent evaluation assesses that fixed policy. |
| Scenario | One joint realization of the uncertain quantities, together with its probability weight. Preserve the association among quantities within each row. |
| Second-stage decision | A recourse choice made after the modeled uncertainty is revealed. |
| Value at risk (VaR) | At , the loss quantile . It gives a threshold, rather than the average severity beyond it. |
| Value of the stochastic solution (VSS) | Increase in expected profit from optimizing the probability model instead of evaluating an EV policy: . |
| Wait-and-see (WS) | Expected optimal performance if all modeled uncertainty is known before any decision; a perfect-information benchmark. |
| Symbol | Role |
|---|---|
| CVaR confidence level; is the tail probability. The finite-scenario CVaR formula has a direct endpoint extension at ; it is undefined at . | |
| Optional mixing weight in . It weights the criteria, while selects which tail is measured. | |
| and | Profit and loss; larger profit is better, larger loss is worse. Positive profit is a negative loss. |
| and | Random vector and realized scenario data. |
| and | Shared first-stage decision and scenario-dependent recourse. |
Core stage, probability, and information-value definitions follow Birge and Louveaux, §§2.1–2.4, pp. 55–60, and §§4.1–4.3, pp. 163–166. General-distribution VaR/CVaR definitions follow Rockafellar and Uryasev (2002), Definitions 1 and 3 and Proposition 8; see the primary source links. The SAA guide cites Mak, Morton and Wood (1999) and Bayraksan and Morton (2006).
Complete solutions: acreage and all trades¶
Every purchase and sale quantity below is in metric tons. Profit includes planting cost. Perfect-information solutions choose a different acreage vector for each known harvest; the recourse and expected-value policies each plant once.
These are the full solutions used in the lecture handouts. The expected-value policy is evaluated with new optimal trades in each scenario, rather than reusing trades computed at mean yields.
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
classical = {
"known_mean": solution_record(mean_model),
"perfect": [solution_record(m) for m in perfect_models],
"rp": solution_record(RP),
"eev": evaluate_solution(mean_plan, THREE),
"inputs": THREE, "WS": float(WS), "RP": float(rp_expected), "EEV": float(EEV),
"EVPI": float(WS-rp_expected), "VSS": float(rp_expected-EEV),
}
def full_solution_table(record):
rows=[]
for s in record["scenarios"]:
rows.append({"Scenario":s["id"], "Probability":s["probability"],
**{"Buy "+k+" (T)":v for k,v in s["purchases_tons"].items()},
**{"Sell "+k+" (T)":v for k,v in s["sales_tons"].items()},
"Profit ($)":s["profit"]})
display(pd.Series(record["acreage"],name="Planted acres").to_frame().round(3))
display(pd.DataFrame(rows).set_index("Scenario").round(2))
print(f"Expected profit: ${record['expected_profit']:,.2f}")
for title,record in [("Mean yields",classical["known_mean"]),
*[(name,r) for name,r in zip(["Known high","Known average","Known low"],classical["perfect"])],
("Shared stochastic plan",classical["rp"]),("Mean-yield plan evaluated",classical["eev"])]:
print(title)
full_solution_table(record)
results=classical
helper.save_results("farmer-information-values",results,
notebook="notebooks/4-dev/SP.ipynb",source_tag="handout:farmer-root",
solver="appsi_highs",description="Complete classical farmer solutions and information values",
source_files=["notebooks/farmer.py"],
source_cells=[("notebooks/4-dev/SP.ipynb", "provenance:farmer-information-solve")])
Mean yields
Expected profit: $118,600.00
Known high
Expected profit: $167,666.67
Known average
Expected profit: $118,600.00
Known low
Expected profit: $59,950.00
Shared stochastic plan
Expected profit: $108,390.00
Mean-yield plan evaluated
Expected profit: $107,240.00
[helper] wrote figures/results/farmer-information-values.json
fig,axes=plt.subplots(1,2,figsize=(10,4),layout="constrained")
values=[results[k]/1000 for k in ["EEV","RP","WS"]]
colors=["#009E73","#0072B2","#D55E00"]
for i,(value,label,color) in enumerate(zip(values,["EEV","RP","WS"],colors)):
axes[0].plot([i-.25,i+.25],[value,value],color=color,linewidth=3)
axes[0].text(i,value+.4,f"{label}\n${value:,.2f}k",ha="center",fontsize=11)
for i,label in [(0,"VSS"),(1,"EVPI")]:
axes[0].annotate("",xy=(i+.5,values[i+1]),xytext=(i+.5,values[i]),
arrowprops={"arrowstyle":"<->","color":"black"})
axes[0].text(i+.57,(values[i]+values[i+1])/2,label,fontsize=10,va="center")
axes[0].set(xlim=(-.5,2.5),ylim=(105,119),ylabel="Expected profit ($1000)",xticks=[])
for label,key,marker,color in [("EEV","eev","s",colors[0]),("RP","rp","o",colors[1])]:
p=[s["profit"]/1000 for s in results[key]["scenarios"]]
axes[1].plot([0,1,2],p,marker=marker,label=label,color=color)
axes[1].plot([0,1,2],[r["expected_profit"]/1000 for r in results["perfect"]],
marker="^",linestyle="--",color=colors[2],label="WS: perfect information")
axes[1].set(xticks=[0,1,2],xticklabels=["High","Average","Low"],
xlabel="Harvest scenario",ylabel="Profit ($1000)")
fig.legend(*axes[1].get_legend_handles_labels(),loc="outside upper center",ncol=3,fontsize=11)
helper.save_figure(fig,"farmer-information-values")
[helper] wrote media/figures/farmer-information-values.png and .pdf

Applications: identify what can adapt¶
For every model, specify the decision, the uncertain information, and which decisions remain available afterward. The stages follow that information structure. They do not follow whether a variable happens to appear in a scenario Block.
One robot for many tasks¶
Bravo-Palacios, Del Prete, and Wensing (2020) optimize a shared robot morphology together with scenario-specific trajectories. The physical design includes quantities such as limb dimensions and actuator gear ratios. A scenario describes the task or environment; its state and control trajectories may adapt to it. Their multiple-task formulation has the structure
subject to each scenario’s dynamics, path constraints, and boundary conditions, with one shared design. The paper uses direct collocation to transcribe the trajectory problems into an NLP. Its planar-manipulator and hopping-robot cases therefore connect the course’s dynamic optimization and stochastic modeling topics; they are not farmer LPs. A high probability weight does not make an infeasible design feasible in another required scenario.

Course connection, reported by Prof. Dowling: this work grew out of a Notre Dame student taking an early offering of this course. That teaching history is instructor-supplied context, separate from the paper’s claims.
Source: Bravo-Palacios, Del Prete & Wensing (2020), pp. 1680–1682, Fig. 1 and §II.A, Eq. (6).
Port capacity before freight demand¶
Bhurtyal et al. (2024) choose integer investments in loading equipment and storage facilities under a budget. After freight demand is realized, transportation and inventory decisions adapt within the installed capacity. The objective combines investment cost with expected transportation cost. The modeled network includes ports, supply/demand regions, truck and rail links, and barge movement.
The farmer analogy is capacity rather than crop identity: installed equipment and storage correspond to shared acreage; scenario-specific flows correspond to recourse trades. Integer investment variables distinguish this model from the continuous farmer LP. The source’s reported economic effects belong to its case data and are not generic benefits guaranteed by stochastic optimization.
Source: Bhurtyal et al. (2024), §3, pp. 190–191, Eqs. (1)–(5).
Mitigate now, restore after a flood¶
Zhang and Alipour (2023) choose pre-event bridge mitigation and construction actions, then scenario-specific restoration resources and techniques after weather-related damage. Traffic assignment represents network use during construction and recovery. Their formulation combines investment and disruption consequences with expected post-event costs and contains integer decisions and nonlinear network behavior.
The two-stage restriction keeps the mitigation choice common across flood scenarios. Recovery may vary because damage is then observed. Calling this second-stage action recourse distinguishes it from a WS benchmark, which would also allow the earlier mitigation decision to use perfect information. User-equilibrium traffic assignment should not be interpreted as a general guarantee of minimum total system travel time.
Source: Zhang & Alipour (2023), §3.3, pp. 416–417, first-stage formulation (1)–(15) and recourse formulation (16)–(27).
CO₂ transport investment as capacity changes¶
Zhang, Anjos, and Chalmers (2026) plan pipeline and ship infrastructure while future capture capacity may expand, close, or reactivate. Decisions adapt over several information stages. At a common scenario-tree node, scenarios with the same observed history share the investment decision; a model that allowed a different early investment for each full future path would anticipate information.
The Humber-cluster study uses a 40-year horizon with seven stages, including a final operating stage without new construction. Those are case assumptions. The general lesson is the evolving information structure and the cost of committing capacity before demand for transport is resolved. A two-stage simplification would need to state when the remaining uncertainty becomes known and which subsequent choices it allows.
Source: Zhang, Anjos & Chalmers (2026), §§2.1–2.2, physical PDF pp. 5–6 and Fig. 1; article 11, rather than an eleven-page range.
Process systems: match the model to the uncertainty¶
Li and Grossmann’s 2021 review surveys stochastic programming for process design, planning, scheduling, and control. Table 1 pairs applications with uncertain quantities, including supply, prices, feed composition, demand, yields, and technology or clinical outcomes. These are entry points for choosing a modeling structure; a common vocabulary does not make their feasible sets or information assumptions interchangeable.
For a process-design application, identify installed equipment and sizing decisions, then identify the operating actions available after uncertain feeds or demands are observed. A scheduling application may instead place decisions at several release times. Risk-neutral expected cost, tail-risk criteria, chance constraints, and robust requirements answer different questions.
Source: Li & Grossmann (2021), §1 and Table 1, pp. 2–3. Its literature counts describe the review’s historical search window, not a current field census.
For another complete formulation and executable Block example, see A Small Two-Stage Production Plan. It is optional reading; the farmer remains the common example of the two lecture handouts.
Probability of compliance and further directions¶
For a specified probability law and allowed violation probability , a chance constraint requires
A joint chance constraint instead protects all listed requirements together:
Separate bounds imply only the union-bound guarantee , without further assumptions. For example, among 20 equal-probability outcomes, let only outcome 1 violate A and only outcome 2 violate B. Each requirement holds with probability 0.95; both hold together with probability 0.90. Independence is not assumed in this construction.

Chance constraints limit violation probability under the declared law. CVaR instead measures the severity of unfavorable outcomes in a chosen tail. Neither criterion corrects an incorrectly specified probability law. In an empirical chance constraint, meeting the retained sample requirement is also different from certifying population reliability. See Pagnoncelli, Ahmed & Shapiro (2009), §1, Eq. (1), p. 400 and §2, for the sampled formulation and its theory.
Application and theory entry points¶
Process optimization: Li, Arellano-Garcia & Wozny (2008), §§2–3, uses probability requirements for uncertain operating quantities. State whether one requirement or simultaneous compliance is protected.
Assistive-robot activities: Bevilacqua et al. (2021), §III, p. 3953, concerns activity/point-of-interest selection with probabilistic resource limits. This is not a general guarantee of low-level physical safety.
Sampling: Pagnoncelli et al., §§2–4, develops SAA theory with portfolio and blending applications. Sampling error and the chosen violation probability are separate quantities.
Information can arrive repeatedly or depend on decisions¶
Multistage programming adapts decisions as information arrives. Scenarios sharing the observed history must share decisions at that node. Ding, Hu & Bie (2018), §II, pp. 319 onward, applies nonanticipativity to joint power and natural-gas expansion. The CO₂ transport case is another multistage example. A full-path scenario index alone does not enforce the correct information restriction.
Decision-dependent uncertainty permits decisions to alter the probability law, the information available later, or both. Yin & Hou (2024), §2, reviews these distinctions in power systems. The farmer’s exogenous yield generator does not model either effect: planting changes harvest quantity at a realized yield, rather than changing the yield distribution.
Distributionally robust chance constraints require probability compliance across an ambiguity set of candidate laws, for example
Jiang & Guan (2016), §1.2, pp. 294–295 and §3, Theorem 1, p. 298, studies a particular -divergence ambiguity set. Its reformulation should not be generalized to arbitrary ambiguity sets. Ambiguity about a law differs from variation of outcomes under one known law.
For alternative risk measures and a complete fixed-production case, see Risk Measures and Portfolio Optimization. For replicated optimality-gap estimation and quadrature detail, see Advanced Topics in Stochastic Programming. Those pages supplement the handouts; their assumptions and sign conventions must be carried into any comparison.
- Bravo-Palacios, G., Prete, A. D., & Wensing, P. M. (2020). One Robot for Many Tasks: Versatile Co-Design Through Stochastic Programming. IEEE Robotics and Automation Letters, 5(2), 1680–1687. 10.1109/lra.2020.2969948
- Bhurtyal, S., Hernandez, S., Eksioglu, S., & Yves, M. (2023). A two-stage stochastic optimization model for port infrastructure planning. Maritime Economics & Logistics, 26(2), 185–211. 10.1057/s41278-023-00262-0
- Zhang, N., & Alipour, A. (2023). A stochastic programming approach to enhance the resilience of infrastructure under weather-related risk. Computer-Aided Civil and Infrastructure Engineering, 38(4), 411–432. 10.1111/mice.12843
- Zhang, L., Anjos, M. F., & Chalmers, H. (2026). A stochastic programming model for planning CO2 transport infrastructure with uncertainty. Computational Management Science, 23(1). 10.1007/s10287-026-00555-8
- Li, C., & Grossmann, I. E. (2021). A Review of Stochastic Programming Methods for Optimization of Process Systems Under Uncertainty. Frontiers in Chemical Engineering, 2. 10.3389/fceng.2020.622241
- Pagnoncelli, B. K., Ahmed, S., & Shapiro, A. (2009). Sample Average Approximation Method for Chance Constrained Programming: Theory and Applications. Journal of Optimization Theory and Applications, 142(2), 399–416. 10.1007/s10957-009-9523-6
- Li, P., Arellano-Garcia, H., & Wozny, G. (2008). Chance constrained programming approach to process optimization under uncertainty. Computers & Chemical Engineering, 32(1–2), 25–45. 10.1016/j.compchemeng.2007.05.009
- Bevilacqua, P., Frego, M., Palopoli, L., & Fontanelli, D. (2021). Activity Planning for Assistive Robots Using Chance-Constrained Stochastic Programming. IEEE Transactions on Industrial Informatics, 17(6), 3950–3961. 10.1109/tii.2020.3012094
- Ding, T., Hu, Y., & Bie, Z. (2018). Multi-Stage Stochastic Programming With Nonanticipativity Constraints for Expansion of Combined Power and Natural Gas Systems. IEEE Transactions on Power Systems, 33(1), 317–328. 10.1109/tpwrs.2017.2701881
- Yin, W., & Hou, Y. (2024). Models and applications of stochastic programming with decision‐dependent uncertainty in power systems: A review. IET Renewable Power Generation, 18(14), 2819–2834. 10.1049/rpg2.13082
- Jiang, R., & Guan, Y. (2015). Data-driven chance constrained stochastic program. Mathematical Programming, 158(1–2), 291–327. 10.1007/s10107-015-0929-7