mirror of https://github.com/ANL-CEEESA/RELOG.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
11 KiB
365 lines
11 KiB
# RELOG: Reverse Logistics Optimization
|
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
|
# Released under the modified BSD license. See COPYING.md for more details.
|
|
|
|
using JuMP, LinearAlgebra, Geodesy, ProgressBars, Printf, DataStructures, StochasticPrograms
|
|
|
|
function build_model(
|
|
instance::Instance,
|
|
graph::Graph,
|
|
optimizer,
|
|
)
|
|
return build_model(
|
|
instance,
|
|
[graph],
|
|
[1.0],
|
|
optimizer=optimizer,
|
|
method=:ef,
|
|
)
|
|
end
|
|
|
|
function build_model(
|
|
instance::Instance,
|
|
graphs::Vector{Graph},
|
|
probs::Vector{Float64};
|
|
optimizer,
|
|
method=:ef,
|
|
tol=0.1,
|
|
)
|
|
T = instance.time
|
|
|
|
@stochastic_model model begin
|
|
# Stage 1: Build plants
|
|
# =====================================================================
|
|
@stage 1 begin
|
|
pn = graphs[1].process_nodes
|
|
PN = length(pn)
|
|
|
|
# Var: open_plant
|
|
@decision(
|
|
model,
|
|
open_plant[n in 1:PN, t in 1:T],
|
|
binary = true,
|
|
)
|
|
|
|
# Var: is_open
|
|
@decision(
|
|
model,
|
|
is_open[n in 1:PN, t in 1:T],
|
|
binary = true,
|
|
)
|
|
|
|
# Objective function
|
|
@objective(
|
|
model,
|
|
Min,
|
|
|
|
# Opening, fixed operating costs
|
|
sum(
|
|
pn[n].location.sizes[1].opening_cost[t] * open_plant[n, t] +
|
|
pn[n].location.sizes[1].fixed_operating_cost[t] * is_open[n, t]
|
|
for n in 1:PN
|
|
for t in 1:T
|
|
),
|
|
)
|
|
|
|
for t = 1:T, n in 1:PN
|
|
# Plant is currently open if it was already open in the previous time period or
|
|
# if it was built just now
|
|
if t > 1
|
|
@constraint(
|
|
model,
|
|
is_open[n, t] == is_open[n, t-1] + open_plant[n, t]
|
|
)
|
|
else
|
|
@constraint(model, is_open[n, t] == open_plant[n, t])
|
|
end
|
|
|
|
# Plant can only be opened during building period
|
|
if t ∉ instance.building_period
|
|
@constraint(model, open_plant[n, t] == 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Stage 2: Flows, disposal, capacity & storage
|
|
# =====================================================================
|
|
@stage 2 begin
|
|
@uncertain graph
|
|
pn = graph.process_nodes
|
|
psn = graph.plant_shipping_nodes
|
|
csn = graph.collection_shipping_nodes
|
|
arcs = graph.arcs
|
|
|
|
A = length(arcs)
|
|
PN = length(pn)
|
|
CSN = length(csn)
|
|
PSN = length(psn)
|
|
|
|
# Var: flow
|
|
@recourse(
|
|
model,
|
|
flow[a in 1:A, t in 1:T],
|
|
lower_bound = 0,
|
|
)
|
|
|
|
# Var: plant_dispose
|
|
@recourse(
|
|
model,
|
|
plant_dispose[n in 1:PSN, t in 1:T],
|
|
lower_bound = 0,
|
|
upper_bound = psn[n].location.disposal_limit[psn[n].product][t],
|
|
)
|
|
|
|
# Var: collection_dispose
|
|
@recourse(
|
|
model,
|
|
collection_dispose[n in 1:CSN, t in 1:T],
|
|
lower_bound = 0,
|
|
upper_bound = graph.collection_shipping_nodes[n].location.amount[t],
|
|
)
|
|
|
|
# Var: collection_shortfall
|
|
@recourse(
|
|
model,
|
|
collection_shortfall[n in 1:CSN, t in 1:T],
|
|
lower_bound = 0,
|
|
)
|
|
|
|
# Var: store
|
|
@recourse(
|
|
model,
|
|
store[
|
|
n in 1:PN,
|
|
t in 1:T,
|
|
],
|
|
lower_bound = 0,
|
|
upper_bound = pn[n].location.storage_limit,
|
|
)
|
|
|
|
# Var: process
|
|
@recourse(
|
|
model,
|
|
process[
|
|
n in 1:PN,
|
|
t in 1:T,
|
|
],
|
|
lower_bound = 0,
|
|
)
|
|
|
|
# Var: capacity
|
|
@recourse(
|
|
model,
|
|
capacity[
|
|
n in 1:PN,
|
|
t in 1:T,
|
|
],
|
|
lower_bound = 0,
|
|
upper_bound = pn[n].location.sizes[2].capacity,
|
|
)
|
|
|
|
# Var: expansion
|
|
@recourse(
|
|
model,
|
|
expansion[
|
|
n in 1:PN,
|
|
t in 1:T,
|
|
],
|
|
lower_bound = 0,
|
|
upper_bound = (
|
|
pn[n].location.sizes[2].capacity -
|
|
pn[n].location.sizes[1].capacity
|
|
),
|
|
)
|
|
|
|
# Objective function
|
|
@objective(
|
|
model,
|
|
Min,
|
|
sum(
|
|
# Transportation costs
|
|
pn[n].location.input.transportation_cost[t] *
|
|
a.values["distance"] *
|
|
flow[a.index,t]
|
|
|
|
for n in 1:PN
|
|
for a in pn[n].incoming_arcs
|
|
for t in 1:T
|
|
) + sum(
|
|
# Fixed operating costs (expansion)
|
|
slope_fix_oper_cost(pn[n].location, t) * expansion[n, t] +
|
|
|
|
# Processing costs
|
|
pn[n].location.sizes[1].variable_operating_cost[t] * process[n, t] +
|
|
|
|
# Storage costs
|
|
pn[n].location.storage_cost[t] * store[n, t] +
|
|
|
|
# Expansion costs
|
|
(
|
|
t < T ? (
|
|
(
|
|
slope_open(pn[n].location, t) -
|
|
slope_open(pn[n].location, t + 1)
|
|
) * expansion[n, t]
|
|
) : slope_open(pn[n].location, t) * expansion[n, t]
|
|
)
|
|
|
|
for n in 1:PN
|
|
for t in 1:T
|
|
) + sum(
|
|
# Disposal costs (plants)
|
|
psn[n].location.disposal_cost[psn[n].product][t] * plant_dispose[n, t]
|
|
for n in 1:PSN
|
|
for t in 1:T
|
|
) + sum(
|
|
# Disposal costs (collection centers)
|
|
csn[n].location.product.disposal_cost[t] * collection_dispose[n, t]
|
|
for n in 1:CSN
|
|
for t in 1:T
|
|
) + sum(
|
|
# Collection shortfall
|
|
1e4 * collection_shortfall[n, t]
|
|
for n in 1:CSN
|
|
for t in 1:T
|
|
)
|
|
)
|
|
|
|
# Process node constraints
|
|
for t = 1:T, n in 1:PN
|
|
node = pn[n]
|
|
|
|
# Output amount is implied by amount processed
|
|
for arc in node.outgoing_arcs
|
|
@constraint(
|
|
model,
|
|
flow[arc.index, t] == arc.values["weight"] * process[n, t]
|
|
)
|
|
end
|
|
|
|
# If plant is closed, capacity is zero
|
|
@constraint(
|
|
model,
|
|
capacity[n, t] <= node.location.sizes[2].capacity * is_open[n, t]
|
|
)
|
|
|
|
# If plant is open, capacity is greater than base
|
|
@constraint(
|
|
model,
|
|
capacity[n, t] >= node.location.sizes[1].capacity * is_open[n, t]
|
|
)
|
|
|
|
# Capacity is linked to expansion
|
|
@constraint(
|
|
model,
|
|
capacity[n, t] <=
|
|
node.location.sizes[1].capacity + expansion[n, t]
|
|
)
|
|
|
|
# Can only process up to capacity
|
|
@constraint(model, process[n, t] <= capacity[n, t])
|
|
|
|
if t > 1
|
|
# Plant capacity can only increase over time
|
|
@constraint(model, capacity[n, t] >= capacity[n, t-1])
|
|
@constraint(model, expansion[n, t] >= expansion[n, t-1])
|
|
end
|
|
|
|
# Amount received equals amount processed plus stored
|
|
store_in = 0
|
|
if t > 1
|
|
store_in = store[n, t-1]
|
|
end
|
|
if t == T
|
|
@constraint(model, store[n, t] == 0)
|
|
end
|
|
@constraint(
|
|
model,
|
|
sum(
|
|
flow[arc.index, t]
|
|
for arc in node.incoming_arcs
|
|
) + store_in == store[n, t] + process[n, t]
|
|
)
|
|
|
|
end
|
|
|
|
# Material flow at collection shipping nodes
|
|
@constraint(
|
|
model,
|
|
eq_balance_centers[
|
|
n in 1:CSN,
|
|
t in 1:T,
|
|
],
|
|
sum(
|
|
flow[arc.index, t]
|
|
for arc in csn[n].outgoing_arcs
|
|
) == csn[n].location.amount[t] - collection_dispose[n, t] - collection_shortfall[n, t]
|
|
)
|
|
|
|
# Material flow at plant shipping nodes
|
|
@constraint(
|
|
model,
|
|
eq_balance_plant[
|
|
n in 1:PSN,
|
|
t in 1:T,
|
|
],
|
|
sum(flow[a.index, t] for a in psn[n].incoming_arcs) ==
|
|
sum(flow[a.index, t] for a in psn[n].outgoing_arcs) +
|
|
plant_dispose[n, t]
|
|
)
|
|
|
|
# Enforce product disposal limit at collection centers
|
|
for t in 1:T, prod in instance.products
|
|
if isempty(prod.collection_centers)
|
|
continue
|
|
end
|
|
@constraint(
|
|
model,
|
|
sum(
|
|
collection_dispose[n, t]
|
|
for n in 1:CSN
|
|
if csn[n].product.name == prod.name
|
|
) <= prod.disposal_limit[t]
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
ξ = [
|
|
@scenario graph = graphs[i] probability = probs[i]
|
|
for i in 1:length(graphs)
|
|
]
|
|
|
|
if method == :ef
|
|
sp = instantiate(model, ξ; optimizer=optimizer)
|
|
elseif method == :lshaped
|
|
sp = instantiate(model, ξ; optimizer=LShaped.Optimizer)
|
|
set_optimizer_attribute(sp, MasterOptimizer(), optimizer)
|
|
set_optimizer_attribute(sp, SubProblemOptimizer(), optimizer)
|
|
set_optimizer_attribute(sp, RelativeTolerance(), tol)
|
|
else
|
|
error("unknown method: $method")
|
|
end
|
|
|
|
return sp
|
|
end
|
|
|
|
|
|
function slope_open(plant, t)
|
|
if plant.sizes[2].capacity <= plant.sizes[1].capacity
|
|
0.0
|
|
else
|
|
(plant.sizes[2].opening_cost[t] - plant.sizes[1].opening_cost[t]) /
|
|
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
|
end
|
|
end
|
|
|
|
function slope_fix_oper_cost(plant, t)
|
|
if plant.sizes[2].capacity <= plant.sizes[1].capacity
|
|
0.0
|
|
else
|
|
(plant.sizes[2].fixed_operating_cost[t] - plant.sizes[1].fixed_operating_cost[t]) /
|
|
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
|
end
|
|
end
|