This notebook contains advanced topics for optimization under uncertainty.
Sample average approximation¶
Sample average approximation (SAA) replaces an expectation with a finite sample:
[ z_N = \min_{\mathbf{x}\in X}\frac{1}{N}\sum_{i=1}^{N}g(\mathbf{x},\boldsymbol{\xi}^i), \qquad \boldsymbol{\xi}^i \overset{\mathrm{iid}}{\sim} P. ]
A responsible SAA study separates:
a training sample used to choose (\mathbf{x}_N);
a larger, independent evaluation sample used to estimate out-of-sample performance; and
repeated samples used to measure solution variability.
One SAA solve is not a statistical guarantee. Sample size, random seed, and evaluation protocol are part of the model. A fixed candidate must be independent of assessment data. Within an assessment batch, candidate evaluation and re-optimization may deliberately share common random numbers; the paired gap is nonnegative. Independent replications estimate its uncertainty. See Mak, Morton and Wood (1999), and Bayraksan and Morton (2006).
Quadrature and sparse grids¶
Quadrature replaces an integral with a weighted sum,
[ \int f(x),dx \approx \sum_{i=1}^{n}\omega_i f(x_i). ]
Tensor-product quadrature grows exponentially with dimension. Sparse grids retain selected tensor-product points and can be effective for smooth, moderately dimensional integrands. They do not universally “defeat” dimensionality: performance depends on dimension, regularity, anisotropy, and the grid rule.
For (y(x)=7x^3-8x^2-3x+3) on ([-1,1]):
the exact integral is (2/3);
the endpoint trapezoid gives (-10); and
two-point Gauss--Legendre quadrature is exact because the integrand is cubic.

# Install course dependencies before importing Pyomo on Colab.
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()
import numpy as np
lower, upper = -1.0, 1.0
def integrand(x):
return 7 * x**3 - 8 * x**2 - 3 * x + 3
# Odd powers integrate to zero; integrate -8*x**2 + 3 analytically.
exact = 2 / 3
trapezoid = (upper - lower) * (integrand(lower) + integrand(upper)) / 2
# Two nodes integrate polynomials through degree 3 exactly on [-1, 1].
nodes, weights = np.polynomial.legendre.leggauss(2)
gauss_legendre = sum(weights * integrand(nodes))
print(f"exact: {exact:.12f}")
print(f"trapezoid: {trapezoid:.12f}")
print(f"two-point Gauss--Legendre: {gauss_legendre:.12f}")
assert abs(gauss_legendre - exact) < 1e-12exact: 0.666666666667
trapezoid: -10.000000000000
two-point Gauss--Legendre: 0.666666666667
Python implementations¶
NumPy supplies one-dimensional Gauss--Legendre nodes through numpy.polynomial.legendre.leggauss. The optional Tasmanian package provides several sparse-grid constructions.
The course does not require Tasmanian. This notebook keeps the executable calculation dependency-light and uses the repository figure below for the sparse-grid concept.
The comparison uses the same number of points in two dimensions:
the sparse grid is a union of tensor products of nested Clenshaw--Curtis nodes; and
the Monte Carlo sample uses a fixed random seed.
Visual coverage alone does not establish accuracy. Compare integration error on an independent test problem.
A sample average approximation study¶
This computational study builds sampled farmer models, compares their plans as sample size changes, measures the optimism of sampled optima, and estimates a multiple-replication optimality-gap bound.
The data are the farmer example of Birge and Louveaux, Introduction to Stochastic Programming, 2nd ed., §1.1a–b, pp. 4–8. Acreage is chosen before yields are known; purchases and sales adapt afterward. The core farmer guide supplies every balance and the complete three-scenario solutions.
A continuous law to sample. The book’s three equally likely yield vectors at 80%, 100%, and 120% form an exact finite law; using all three is not an approximation to that law. Here instead one continuous multiplier scales every crop:
Section 1.1d, pp. 11–15, gives each crop an independent uniform yield on the same marginal interval. This study uses perfectly correlated uniform yields. The joint laws differ, but the marginal laws match. At fixed acreage the farmer’s recourse separates by crop, so linearity of expectation gives the same population expected-cost function and risk-neutral optimum under either dependence model. Independence is unnecessary for that equality. The full derivation and a controlled tail-risk comparison are in Further Modeling Examples.
A common multiplier also permits one-dimensional quadrature. Independent yields would produce a three-dimensional joint integral, although separability reduces its expected-cost calculation to three one-dimensional integrals. Dependence still changes the distribution of total profit and can change a CVaR-optimal policy.
Cost convention. This optional study minimizes net cost. The core farmer handouts maximize its negative, profit; the shared three-scenario profit of is therefore a cost of here. Continuous uniform and three-point marginals are different, so their risk-neutral optima need not agree. The continuous law is a teaching assumption, not a fitted agricultural model.
# The scenario structure is built with Pyomo Blocks: the stage 1 acreage lives on
# the model, and each sampled realization gets its own Block holding that
# sample's stage 2 purchases and sales. This is the same pattern used in
# "Stochastic Programming" (notebook 4/SP); the only difference is that the
# blocks are now indexed by a sample number instead of a named scenario, and
# there can be hundreds of them.
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
from scipy import stats
# Pyomo's unit library has acres and metric tons but no money, so declare one.
if not hasattr(u, "USD"):
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
# Birge & Louveaux, 2nd ed., Table 1, p. 5: mean yields [T/acre], planting costs
# [USD/acre], purchase and selling prices [USD/T], and the cattle feed
# requirements [T]. The 6000 T quota caps sales at the favorable beet price.
CROPS = ["WHEAT", "CORN", "BEETS"]
FEED_CROPS = ["WHEAT", "CORN"]
SALE_TYPES = ["WHEAT", "CORN", "BEETS_FAVORABLE", "BEETS_UNFAVORABLE"]
MEAN_YIELD = {"WHEAT": 2.5, "CORN": 3.0, "BEETS": 20.0}
PLANT_COST = {"WHEAT": 150.0, "CORN": 230.0, "BEETS": 260.0}
BUY_PRICE = {"WHEAT": 238.0, "CORN": 210.0}
SELL_PRICE = {
"WHEAT": 170.0,
"CORN": 150.0,
"BEETS_FAVORABLE": 36.0,
"BEETS_UNFAVORABLE": 10.0,
}
FEED_NEED = {"WHEAT": 200.0, "CORN": 240.0}
TOTAL_LAND = 500.0
BEET_QUOTA = 6000.0
def build_saa_model(multipliers, weights=None, check_units=False):
"""Extensive form over a list of yield multipliers, one Block per sample.
Arguments:
multipliers: yield multipliers, one per sample. Each scales all three
mean yields.
weights: probability weight of each sample. Defaults to 1/N, which is
the sample average approximation. Passing quadrature weights
instead turns the same builder into a deterministic integration
rule, which is how the reference answer below is computed.
check_units: run assert_units_consistent. Left off by default because
it walks every constraint on every block, and these models are
built hundreds of times; it is exercised on a small model below.
Return:
A Pyomo model whose objective is the weighted average cost in USD.
"""
multipliers = np.asarray(multipliers, dtype=float)
n = len(multipliers)
if weights is None:
weights = np.full(n, 1.0 / n)
weights = np.asarray(weights, dtype=float)
assert len(weights) == n, "one weight per sample"
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.SAMPLES = pyo.Set(initialize=range(n))
# Stage 1: acres of each crop, chosen before any yield is observed [acre]
m.acreage = pyo.Var(m.CROPS, domain=pyo.NonNegativeReals, units=u.acre)
# Stage 1 cost [USD]
m.planting_cost = sum(PLANT_COST[c] * USD_PER_ACRE * m.acreage[c] for c in CROPS)
# Stage 1 constraint: the farm is 500 acres [acre]
m.total_land_constraint = pyo.Constraint(
expr=pyo.summation(m.acreage) <= TOTAL_LAND * u.acre
)
def scenario_block_rule(b, i):
"""Stage 2 for sample i. `b` is the block; `m` comes from the closure."""
# Stage 2 decisions, made after the yield is observed [T]
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.purchase_cost = sum(
BUY_PRICE[c] * USD_PER_TON * b.purchases[c] for c in FEED_CROPS
)
b.sales_revenue = sum(
SELL_PRICE[s] * USD_PER_TON * b.sales[s] for s in SALE_TYPES
)
# Yields realized in this sample [T/acre]
y = {c: MEAN_YIELD[c] * multipliers[i] for c in CROPS}
# Feed requirements: grow it, buy it, or do not sell it [T]
b.wheat_constraint = pyo.Constraint(
expr=y["WHEAT"] * TON_PER_ACRE * m.acreage["WHEAT"]
+ b.purchases["WHEAT"]
- b.sales["WHEAT"]
>= FEED_NEED["WHEAT"] * u.metric_ton
)
b.corn_constraint = pyo.Constraint(
expr=y["CORN"] * TON_PER_ACRE * m.acreage["CORN"]
+ b.purchases["CORN"]
- b.sales["CORN"]
>= FEED_NEED["CORN"] * u.metric_ton
)
# Beet sales cannot exceed beet production [T]
b.beets_constraint = pyo.Constraint(
expr=y["BEETS"] * TON_PER_ACRE * m.acreage["BEETS"]
- b.sales["BEETS_FAVORABLE"]
- b.sales["BEETS_UNFAVORABLE"]
>= 0 * u.metric_ton
)
b.sales["BEETS_FAVORABLE"].setub(BEET_QUOTA)
# One Block per sample: identical structure, different yield data
m.scenarios = pyo.Block(m.SAMPLES, rule=scenario_block_rule)
# Weighted average cost [USD]
m.objective = pyo.Objective(
expr=m.planting_cost
+ sum(
weights[i] * (m.scenarios[i].purchase_cost - m.scenarios[i].sales_revenue)
for i in range(n)
),
sense=pyo.minimize,
)
if check_units:
assert_units_consistent(m)
return m
solver = pyo.SolverFactory("appsi_highs")
assert solver.available(), "HiGHS is required for this section"
print("HiGHS version:", solver.version())
def solve_model(m):
"""Solve and return the objective value, refusing anything but optimal."""
results = solver.solve(m)
assert pyo.check_optimal_termination(
results
), f"Solve failed: termination={results.termination_condition}"
return pyo.value(m.objective)
def first_stage(m):
"""The 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 saa_optimum(multipliers, weights=None):
"""Solve the sampled problem: returns (objective, acreage)."""
m = build_saa_model(multipliers, weights)
return solve_model(m), first_stage(m)
def evaluate_candidate(acreage, multipliers, weights=None):
"""Weighted average cost of a FIXED stage 1 plan, recourse still optimized.
Fixing the stage 1 variables on the same model is all that separates
"choose a plan" from "grade a plan". Every block still solves its own
recourse problem.
"""
m = build_saa_model(multipliers, weights)
for c, acres in zip(CROPS, acreage):
m.acreage[c].fix(acres)
return solve_model(m)HiGHS version: (1, 15, 1)
Check the builder against the book before trusting it¶
Passing the three multipliers 0.8, 1.0, 1.2 with equal weights rebuilds exactly the book’s three-scenario extensive form, so its answer is known: Birge and Louveaux, Table 5, p. 8, report 170 acres of wheat, 80 of corn and 250 of sugar beets, for an expected profit of . The information-value comparison uses the same number as .
# Birge & Louveaux Table 5, p. 8 -- the RP solution quoted in Lecture 10.
book_cost, book_acres = saa_optimum([0.8, 1.0, 1.2])
print(f"expected profit = ${-book_cost:,.0f} (book: $108,390)")
print(
"acres wheat {:.1f} corn {:.1f} beets {:.1f} (book: 170, 80, 250)".format(
*book_acres
)
)
assert abs(-book_cost - 108390.0) < 1.0
assert np.allclose(book_acres, [170.0, 80.0, 250.0], atol=1e-6)
# Exercise the unit declarations once, on a small model.
assert_units_consistent(build_saa_model([0.8, 1.0, 1.2], check_units=True))
print("units consistent")expected profit = $108,390 (book: $108,390)
acres wheat 170.0 corn 80.0 beets 250.0 (book: 170, 80, 250)
units consistent
A reference answer, from the quadrature above¶
The common yield multiplier makes a one-dimensional integral. A dense grid with trapezoid weights approximates it; the same build_saa_model accepts those weights instead of sample weights. Refining the grid checks numerical stability.
For fixed acreage, recourse cost is piecewise linear and convex in the multiplier. The slope changes when a feed shortage becomes a surplus or beets reach the premium quota. A high-order smooth-integrand rule does not remove those kinks. The table below compares grid refinements before using the finest result as a numerical reference.
This is an approximate reference, not an exact population solution or a certified quadrature error bound. The crop-wise formulas provide a separate analytic check for the uniform-marginal model.
def trapezoid_grid(n, lo=0.8, hi=1.2):
"""n-point trapezoid rule on [lo, hi] for a Uniform(lo, hi) expectation.
The uniform density 1/(hi-lo) cancels the (hi-lo) in the quadrature
weights, so the weights below sum to one and are directly comparable with
the 1/N sampling weights.
"""
nodes = np.linspace(lo, hi, n)
weights = np.full(n, 1.0 / (n - 1))
weights[0] = weights[-1] = 0.5 / (n - 1)
return nodes, weights
print("grid reference cost wheat corn beets")
for n_grid in (201, 401, 801, 1601, 3201):
nodes, weights = trapezoid_grid(n_grid)
z_ref, x_ref = saa_optimum(nodes, weights)
print(
f"{n_grid:5d} {z_ref:16,.4f} {x_ref[0]:7.2f} {x_ref[1]:6.2f} {x_ref[2]:7.2f}"
)
# Keep the finest grid as the reference for the rest of the section.
REF_NODES, REF_WEIGHTS = trapezoid_grid(3201)
Z_STAR, X_STAR = saa_optimum(REF_NODES, REF_WEIGHTS)
print(f"\nreference z* = {Z_STAR:,.2f} USD (expected profit ${-Z_STAR:,.2f})")grid reference cost wheat corn beets
201 -111,237.2935 135.56 85.11 279.33
401 -111,237.4344 135.82 85.11 279.07
801 -111,237.4383 135.87 85.06 279.07
1601 -111,237.4383 135.87 85.06 279.07
3201 -111,237.4405 135.84 85.06 279.10
reference z* = -111,237.44 USD (expected profit $111,237.44)
The last grid refinements change the displayed objective by a fraction of a dollar. The code calls the finest-grid values Z_STAR and X_STAR; those names denote a numerical grid reference here. Agreement between successive grids does not by itself bound the remaining integration or optimization error.
The reference differs from the three-point result because a continuous uniform marginal puts probability inside the interval rather than only at the three discrete yields.
How much does the solution move with the sample size?¶
Each row uses one seeded training draw. It optimizes acreage, then evaluates that fixed plan on the reference grid. q_ref is this quadrature estimate of population cost. The output’s true gap column means gap relative to the numerical grid reference; it is not a certified population optimality gap.
# One independent, seeded draw per sample size.
rng = np.random.default_rng(20260920)
print(" N z_N q_ref(x_N) true gap wheat corn beets")
sweep = []
for N in (5, 10, 25, 50, 100, 200, 500):
xi = rng.uniform(0.8, 1.2, N)
z_N, x_N = saa_optimum(xi)
q_ref = evaluate_candidate(x_N, REF_NODES, REF_WEIGHTS)
sweep.append((N, z_N, q_ref))
print(
f"{N:5d} {z_N:12,.2f} {q_ref:14,.2f} {q_ref - Z_STAR:10,.2f} "
f"{x_N[0]:7.1f} {x_N[1]:6.1f} {x_N[2]:7.1f}"
) N z_N q_ref(x_N) true gap wheat corn beets
5 -98,025.42 -109,521.04 1,716.40 100.8 95.6 303.6
10 -110,484.82 -110,321.65 915.79 116.9 84.9 298.2
25 -101,488.46 -111,150.97 86.47 133.1 90.5 276.3
50 -116,479.50 -110,764.34 473.10 151.2 82.5 266.4
100 -108,751.64 -111,183.35 54.09 130.1 86.5 283.4
200 -114,580.54 -111,223.53 13.91 139.1 83.2 277.7
500 -111,873.95 -111,235.99 1.45 136.6 85.1 278.4
The table separates policy quality from the sampled objective:
q_ref(x_N) - Z_STARestimates how much the sampled plan gives up relative to the numerical reference. These seeded rows need not improve monotonically.z_Nis the optimum on the training sample. One realization may lie above or below the population optimum; it does not assess the plan on fresh weather.
Independent policy assessment is needed even when training and evaluation use the same declared probability law.
The sampled optimum is optimistically biased¶
Let , let be the fixed feasible first-stage set, and assume iid sampling, finite expectations, and an attained population optimum . For each training sample,
The inequality is in expectation; an individual sampled optimum need not be below . It follows from evaluating one feasible candidate, without a convexity assumption.
A paired difference reduces weather noise. For a fixed reference plan , each replication uses the same sample in both terms:
If , this expectation is exactly the optimization bias. The implementation instead uses the numerical X_STAR. Its difference measures bias relative to that plan’s population value; the discrepancy from the ideal bias is the reference plan’s unknown population suboptimality. A grid-refinement difference alone is not a bound on that discrepancy.
# Each replication solves the sampled problem and evaluates the reference plan
# on the SAME sample, so the shared sampling noise cancels in the difference.
rng_bias = np.random.default_rng(424242)
T_bias = 100
sizes = (5, 10, 20, 40, 80, 160)
bias_mean, bias_se = [], []
print(f"replications T = {T_bias}")
print(" N mean D (bias) std error worst-case D N x |bias|")
for N in sizes:
D = np.empty(T_bias)
for j in range(T_bias):
xi = rng_bias.uniform(0.8, 1.2, N)
D[j] = saa_optimum(xi)[0] - evaluate_candidate(X_STAR, xi)
assert (D <= 0).all(), "the inequality above is pathwise, not on average"
bias_mean.append(D.mean())
bias_se.append(D.std(ddof=1) / np.sqrt(T_bias))
print(
f"{N:5d} {D.mean():14,.2f} {bias_se[-1]:12,.2f} {D.max():14,.2f} "
f"{N * abs(D.mean()):14,.0f}"
)replications T = 100
N mean D (bias) std error worst-case D N x |bias|
5 -1,249.73 121.62 -36.07 6,249
10 -549.30 60.27 -11.22 5,493
20 -301.56 48.29 -8.59 6,031
40 -171.92 20.04 -1.28 6,877
80 -62.18 7.49 -0.12 4,974
160 -35.43 4.16 -0.96 5,669
Every displayed replication has because the sampled optimum can choose the reference plan. The worst-case D column checks this pathwise inequality. The table and plot show roughly decay in this experiment; this is an observed pattern, rather than a general rate proved here.
The standard errors describe the replicated paired differences, not the variability of the sampled optimum itself. The plot’s bias label uses the ideal notation, while the computed difference uses X_STAR; interpret it with the numerical-reference qualification above.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(5.5, 3.6))
ax.errorbar(
sizes,
[-b for b in bias_mean],
yerr=bias_se,
marker="o",
linestyle="-",
color="tab:blue",
label=r"measured $|\mathbb{E}[z_N] - z^*|$",
)
reference = abs(bias_mean[0]) * sizes[0] / np.array(sizes, dtype=float)
ax.plot(
sizes,
reference,
marker="s",
linestyle="--",
color="0.35",
label=r"$1/N$ reference, anchored at $N=5$",
)
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlabel("sample size $N$")
ax.set_ylabel("optimistic bias [USD]")
ax.set_title("Bias of the sampled optimum")
ax.legend(frameon=False, fontsize=8)
fig.tight_layout()
plt.show()
An optimality-gap certificate for one candidate plan¶
Choose a candidate using training data. Keep it fixed and assess it on fresh iid batches, independent of training and of each other. Within each batch, evaluate the candidate and reoptimize on the same realizations:
With ,
Thus an upper confidence bound for the mean paired difference can bound the candidate’s population gap. The code uses
This Student- construction is approximate for general iid replicate gaps with finite variance; it is not a deterministic certificate or an exact finite-sample coverage theorem. Reusing the training sample for assessment would invalidate the fixed-candidate independence condition. See Mak, Morton and Wood (1999), §2, and Bayraksan and Morton (2006), §§1–2.
# Training: a deliberately small sample, so the candidate is visibly imperfect.
rng_gap = np.random.default_rng(31415)
xi_train = rng_gap.uniform(0.8, 1.2, 20)
z_train, x_hat = saa_optimum(xi_train)
q_hat = evaluate_candidate(x_hat, REF_NODES, REF_WEIGHTS)
true_gap = q_hat - Z_STAR
print(
f"candidate from N = 20: wheat {x_hat[0]:.1f}, corn {x_hat[1]:.1f}, "
f"beets {x_hat[2]:.1f} acres"
)
print(f"training objective z_20 = {z_train:12,.2f} (optimistic, not a bound)")
print(f"true expected cost q(x_hat) = {q_hat:12,.2f}")
print(f"TRUE optimality gap = {true_gap:12,.2f} <- known only because")
print(" the reference exists\n")
def gap_bound(x_candidate, T, N, delta, seed):
"""Multiple-replication optimality-gap bound for a fixed candidate.
Returns (G, bound). Each batch is fresh and independent of the others;
within a batch the candidate evaluation and the re-optimization share the
realizations, which is the paired (common random numbers) estimator.
"""
rng_local = np.random.default_rng(seed)
G = np.empty(T)
for j in range(T):
xi = rng_local.uniform(0.8, 1.2, N)
G[j] = evaluate_candidate(x_candidate, xi) - saa_optimum(xi)[0]
t_crit = stats.t.ppf(1.0 - delta, T - 1) # ONE-sided critical value
bound = G.mean() + t_crit * G.std(ddof=1) / np.sqrt(T)
return G, bound
print(" T N G_bar s_G one-sided 95% bound covers true gap?")
for T, N, seed in ((30, 50, 9001), (30, 200, 9002), (100, 200, 9003)):
G, bound = gap_bound(x_hat, T=T, N=N, delta=0.05, seed=seed)
assert (G >= -1e-6).all(), "paired gaps are nonnegative by construction"
print(
f"{T:4d} {N:5d} {G.mean():9,.2f} {G.std(ddof=1):8,.2f} "
f"{bound:18,.2f} {'yes' if bound >= true_gap else 'NO'}"
)candidate from N = 20: wheat 133.5, corn 89.1, beets 277.4 acres
training objective z_20 = -107,210.81 (optimistic, not a bound)
true expected cost q(x_hat) = -111,193.97
TRUE optimality gap = 43.47 <- known only because
the reference exists
T N G_bar s_G one-sided 95% bound covers true gap?
30 50 138.27 96.77 168.29 yes
30 200 76.50 51.40 92.45 yes
100 200 71.88 48.45 79.92 yes
Read the output with these distinctions:
The mean paired difference exceeds the true gap in expectation. Its conservatism includes the sampled optimizer’s bias. Larger assessment samples can reduce that bias; more independent replications reduce uncertainty in the replicate mean.
The interval is approximate. Finite variance and sufficient replications support a central-limit approximation. is a choice for this demonstration, not a coverage guarantee for every distribution.
The rows are separate comparisons. Three nominal 95% bounds do not jointly confer 95% coverage. Combining separately estimated value intervals also requires accounting for their simultaneous coverage.
The printed
TRUE optimality gapandcovers true gap?use the numerical reference. They compare with a grid estimate, not an exactly known population optimum. A displayed “yes” is a diagnostic in this run, not evidence of repeated-sampling coverage.
All draws are seeded. Changing the seeds illustrates how the estimates and bounds vary; it does not change their stated assumptions.
References¶
Birge and Louveaux (2011), Introduction to Stochastic Programming, 2nd ed., Springer. The farmer’s problem and its data are §1.1a–b, pp. 4–8 (data in Table 1, p. 5); the three-scenario solution used as a check is Table 5, p. 8; the continuous uniform-yield version is §1.1d, p. 11.
Mak, Morton and Wood (1999), Monte Carlo bounding techniques for determining solution quality in stochastic programs, DOI: Mak et al. (1999).
Bayraksan and Morton (2006), Assessing solution quality in stochastic programs, DOI: Bayraksan & Morton (2006).
NumPy Gauss–Legendre documentation: exactness through degree .
Tasmanian project documentation: optional sparse-grid software.
The two comparison figures are original course illustrations. The sparse-grid picture displays nodes, not quadrature weights or an error bound.
- Mak, W.-K., Morton, D. P., & Wood, R. K. (1999). Monte Carlo bounding techniques for determining solution quality in stochastic programs. Operations Research Letters, 24(1–2), 47–56. 10.1016/s0167-6377(98)00054-6
- Bayraksan, G., & Morton, D. P. (2006). Assessing solution quality in stochastic programs. Mathematical Programming, 108(2–3), 495–514. 10.1007/s10107-006-0720-x