The farm planning problem
This tutorial was generated using Literate.jl. Download the source as a .jl
file. Download the source as a .ipynb
file.
There are four stages. The first stage is a deterministic planning stage. The next three are wait-and-see operational stages. The uncertainty in the three operational stages is a Markov chain for weather. There are three Markov states: dry, normal, and wet.
Inspired by R. McCardle, Farm management optimization. Masters thesis, University of Louisville, Louisville, Kentucky, United States of America (2009).
All data, including short variable names, is taken from that thesis.
using SDDP, HiGHS, Test
function test_mccardle_farm_model()
S = [ # cutting, stage
0 1 2
0 0 1
0 0 0
]
t = [60, 60, 245] # days in period
D = [210, 210, 858] # demand
q = [ # selling price per bale
[4.5 4.5 4.5; 4.5 4.5 4.5; 4.5 4.5 4.5],
[5.5 5.5 5.5; 5.5 5.5 5.5; 5.5 5.5 5.5],
[6.5 6.5 6.5; 6.5 6.5 6.5; 6.5 6.5 6.5],
]
b = [ # predicted yield (bales/acres) from cutting i in weather j.
30 75 37.5
15 37.5 18.25
7.5 18.75 9.325
]
w = 3000 # max storage
C = [50 50 50; 50 50 50; 50 50 50] # cost to grow hay
r = [ # Cost per bale of hay from cutting i during weather condition j.
[5 5 5; 5 5 5; 5 5 5],
[6 6 6; 6 6 6; 6 6 6],
[7 7 7; 7 7 7; 7 7 7],
]
M = 60.0 # max acreage for planting
H = 0.0 # initial inventory
V = [0.05, 0.05, 0.05] # inventory cost
L = 3000.0 # max demand for hay
graph = SDDP.MarkovianGraph([
ones(Float64, 1, 1),
[0.14 0.69 0.17],
[0.14 0.69 0.17; 0.14 0.69 0.17; 0.14 0.69 0.17],
[0.14 0.69 0.17; 0.14 0.69 0.17; 0.14 0.69 0.17],
])
model = SDDP.PolicyGraph(
graph;
lower_bound = 0.0,
optimizer = HiGHS.Optimizer,
) do subproblem, index
stage, weather = index
# ===================== State Variables =====================
# Area planted.
@variable(subproblem, 0 <= acres <= M, SDDP.State, initial_value = M)
@variable(
subproblem,
bales[i = 1:3] >= 0,
SDDP.State,
initial_value = (i == 1 ? H : 0)
)
# ===================== Variables =====================
@variables(subproblem, begin
buy[1:3] >= 0 # Quantity of bales to buy from each cutting.
sell[1:3] >= 0 # Quantity of bales to sell from each cutting.
eat[1:3] >= 0 # Quantity of bales to eat from each cutting.
pen_p[1:3] >= 0 # Penalties
pen_n[1:3] >= 0 # Penalties
end)
# ===================== Constraints =====================
if stage == 1
@constraint(subproblem, acres.out <= acres.in)
@constraint(subproblem, [i = 1:3], bales[i].in == bales[i].out)
else
@expression(
subproblem,
cut_ex[c = 1:3],
bales[c].in + buy[c] - eat[c] - sell[c] + pen_p[c] - pen_n[c]
)
@constraints(
subproblem,
begin
# Cannot plant more land than previously cropped.
acres.out <= acres.in
# In each stage we need to meet demand.
sum(eat) >= D[stage-1]
# We can buy and sell other cuttings.
bales[stage-1].out ==
cut_ex[stage-1] + acres.in * b[stage-1, weather]
[c = 1:3; c != stage - 1], bales[c].out == cut_ex[c]
# There is some maximum storage.
sum(bales[i].out for i in 1:3) <= w
# We can only sell what is in storage.
[c = 1:3], sell[c] <= bales[c].in
# Maximum sales quantity.
sum(sell) <= L
end
)
end
# ===================== Stage objective =====================
if stage == 1
@stageobjective(subproblem, 0.0)
else
@stageobjective(
subproblem,
1000 * (sum(pen_p) + sum(pen_n)) +
# cost of growing
C[stage-1, weather] * acres.in +
sum(
# inventory cost
V[stage-1] * bales[cutting].in * t[stage-1] +
# purchase cost
r[cutting][stage-1, weather] * buy[cutting] +
# feed cost
S[cutting, stage-1] * eat[cutting] -
# sell reward
q[cutting][stage-1, weather] * sell[cutting] for
cutting in 1:3
)
)
end
return
end
SDDP.train(model)
@test SDDP.termination_status(model) == :simulation_stopping
@test SDDP.calculate_bound(model) ≈ 4074.1391 atol = 1e-5
end
test_mccardle_farm_model()
Test Passed