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.
RELOG/src/model.jl

312 lines
12 KiB

# Copyright (C) 2019 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
using JuMP, LinearAlgebra, Geodesy, Cbc, ProgressBars
mutable struct ManufacturingModel
mip::JuMP.Model
vars::DotDict
instance::Instance
graph::Graph
end
function build_model(instance::Instance, graph::Graph, optimizer)::ManufacturingModel
model = ManufacturingModel(Model(optimizer), DotDict(), instance, graph)
create_vars!(model)
create_objective_function!(model)
create_shipping_node_constraints!(model)
create_process_node_constraints!(model)
return model
end
function create_vars!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
vars.flow = Dict((a, t) => @variable(mip,
lower_bound=0,
base_name="flow($(a.source.location.index),$(a.dest.location.index),$t)")
for a in graph.arcs, t in 1:T)
vars.dispose = Dict((n, t) => @variable(mip,
lower_bound=0,
upper_bound=n.location.disposal_limit[n.product][t],
base_name="dispose($(n.location.index),$(n.product.name),$t)")
for n in values(graph.plant_shipping_nodes), t in 1:T)
vars.open_plant = Dict((n, t) => @variable(mip,
binary=true,
base_name="open_plant($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
vars.is_open = Dict((n, t) => @variable(mip,
binary=true,
base_name="is_open($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
vars.capacity = Dict((n, t) => @variable(mip,
lower_bound = 0,
upper_bound = n.location.max_capacity,
base_name="capacity($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
vars.expansion = Dict((n, t) => @variable(mip,
lower_bound = 0,
upper_bound = (n.location.max_capacity - n.location.base_capacity),
base_name="expansion($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
end
function create_objective_function!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
obj = @expression(mip, 0 * @variable(mip))
# Process node costs
for n in values(graph.process_nodes), t in 1:T
# Transportation and variable operating costs
for a in n.incoming_arcs
c = n.location.input.transportation_cost[t] * a.values["distance"]
c += n.location.variable_operating_cost[t]
add_to_expression!(obj, c, vars.flow[a, t])
end
# Opening costs
add_to_expression!(obj, n.location.opening_cost[t], vars.open_plant[n, t])
# Fixed operating costss
add_to_expression!(obj, n.location.fixed_operating_cost[t], vars.is_open[n, t])
# Expansion costs
if t < T
add_to_expression!(obj,
n.location.expansion_cost[t] - n.location.expansion_cost[t + 1],
vars.expansion[n, t])
else
add_to_expression!(obj, n.location.expansion_cost[t], vars.expansion[n, t])
end
end
# Disposal costs
for n in values(graph.plant_shipping_nodes), t in 1:T
add_to_expression!(obj, n.location.disposal_cost[n.product][t], vars.dispose[n, t])
end
@objective(mip, Min, obj)
end
function create_shipping_node_constraints!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
for t in 1:T
# Collection centers
for n in graph.collection_shipping_nodes
@constraint(mip, sum(vars.flow[a, t] for a in n.outgoing_arcs) == n.location.amount[t])
end
# Plants
for n in graph.plant_shipping_nodes
@constraint(mip,
sum(vars.flow[a, t] for a in n.incoming_arcs) ==
sum(vars.flow[a, t] for a in n.outgoing_arcs) + vars.dispose[n, t])
end
end
end
function create_process_node_constraints!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
for n in graph.process_nodes, t in 1:T
# Output amount is implied by input amount
input_sum = isempty(n.incoming_arcs) ? 0 : sum(vars.flow[a, t] for a in n.incoming_arcs)
for a in n.outgoing_arcs
@constraint(mip, vars.flow[a, t] == a.values["weight"] * input_sum)
end
# If plant is closed, capacity is zero
@constraint(mip, vars.capacity[n, t] <= n.location.max_capacity * vars.is_open[n, t])
# Capacity is linked to expansion
@constraint(mip, vars.capacity[n, t] <= n.location.base_capacity + vars.expansion[n, t])
# Input sum must be smaller than capacity
@constraint(mip, input_sum <= vars.capacity[n, t])
if t > 1
# Plant capacity can only increase over time
@constraint(mip, vars.capacity[n, t] >= vars.capacity[n, t-1])
@constraint(mip, vars.expansion[n, t] >= vars.expansion[n, t-1])
end
# 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(mip, vars.is_open[n, t] == vars.is_open[n, t-1] + vars.open_plant[n, t])
else
@constraint(mip, vars.is_open[n, t] == vars.open_plant[n, t])
end
end
end
function solve(filename::String; optimizer=Cbc.Optimizer)
println("Reading $filename...")
instance = ReverseManufacturing.load(filename)
println("Building graph...")
graph = ReverseManufacturing.build_graph(instance)
println("Building optimization model...")
model = ReverseManufacturing.build_model(instance, graph, optimizer)
println("Optimizing...")
JuMP.optimize!(model.mip)
println("Extracting solution...")
return get_solution(model)
end
function get_solution(model::ManufacturingModel)
mip, vars, graph, instance = model.mip, model.vars, model.graph, model.instance
T = instance.time
output = Dict(
"plants" => Dict(),
"costs" => Dict(
"fixed operating" => zeros(T),
"variable operating" => zeros(T),
"opening" => zeros(T),
"transportation" => zeros(T),
"disposal" => zeros(T),
"expansion" => zeros(T),
"total" => zeros(T),
)
)
plant_to_process_node = Dict(n.location => n for n in graph.process_nodes)
plant_to_shipping_nodes = Dict()
for p in instance.plants
plant_to_shipping_nodes[p] = []
for a in plant_to_process_node[p].outgoing_arcs
push!(plant_to_shipping_nodes[p], a.dest)
end
end
for plant in instance.plants
skip_plant = true
process_node = plant_to_process_node[plant]
plant_dict = Dict{Any, Any}(
"input" => Dict(),
"output" => Dict(
"send" => Dict(),
"dispose" => Dict(),
),
"total input" => [0.0 for t in 1:T],
"total output" => Dict(),
"latitude" => plant.latitude,
"longitude" => plant.longitude,
"capacity" => [JuMP.value(vars.capacity[process_node, t])
for t in 1:T],
"opening cost" => [JuMP.value(vars.open_plant[process_node, t]) * plant.opening_cost[t]
for t in 1:T],
"fixed operating cost" => [JuMP.value(vars.is_open[process_node, t]) * plant.fixed_operating_cost[t]
for t in 1:T],
"expansion cost" => [plant.expansion_cost[t] *
(if t > 1
JuMP.value(vars.expansion[process_node, t]) - JuMP.value(vars.expansion[process_node, t-1])
else
JuMP.value(vars.expansion[process_node, t])
end)
for t in 1:T],
)
output["costs"]["fixed operating"] += plant_dict["fixed operating cost"]
output["costs"]["opening"] += plant_dict["opening cost"]
output["costs"]["expansion"] += plant_dict["expansion cost"]
# Inputs
for a in process_node.incoming_arcs
vals = [JuMP.value(vars.flow[a, t]) for t in 1:T]
if sum(vals) <= 1e-3
continue
end
skip_plant = false
dict = Dict{Any, Any}(
"amount" => vals,
"distance" => a.values["distance"],
"latitude" => a.source.location.latitude,
"longitude" => a.source.location.longitude,
"transportation cost" => [a.source.product.transportation_cost[t] * vals[t]
for t in 1:T],
"variable operating cost" => [plant.variable_operating_cost[t] * vals[t]
for t in 1:T],
)
if a.source.location isa CollectionCenter
plant_name = "Origin"
location_name = a.source.location.name
else
plant_name = a.source.location.plant_name
location_name = a.source.location.location_name
end
if plant_name keys(plant_dict["input"])
plant_dict["input"][plant_name] = Dict()
end
plant_dict["input"][plant_name][location_name] = dict
plant_dict["total input"] += vals
output["costs"]["transportation"] += dict["transportation cost"]
output["costs"]["variable operating"] += dict["variable operating cost"]
end
# Outputs
for shipping_node in plant_to_shipping_nodes[plant]
product_name = shipping_node.product.name
plant_dict["total output"][product_name] = zeros(T)
plant_dict["output"]["send"][product_name] = product_dict = Dict()
disposal_amount = [JuMP.value(vars.dispose[shipping_node, t]) for t in 1:T]
if sum(disposal_amount) > 1e-5
skip_plant = false
plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict()
disposal_dict["amount"] = [JuMP.value(model.vars.dispose[shipping_node, t]) for t in 1:T]
disposal_dict["cost"] = [disposal_dict["amount"][t] * plant.disposal_cost[shipping_node.product][t]
for t in 1:T]
plant_dict["total output"][product_name] += disposal_amount
output["costs"]["disposal"] += disposal_dict["cost"]
end
for a in shipping_node.outgoing_arcs
vals = [JuMP.value(vars.flow[a, t]) for t in 1:T]
if sum(vals) <= 1e-3
continue
end
skip_plant = false
dict = Dict(
"amount" => vals,
"distance" => a.values["distance"],
"latitude" => a.dest.location.latitude,
"longitude" => a.dest.location.longitude,
)
if a.dest.location.plant_name keys(product_dict)
product_dict[a.dest.location.plant_name] = Dict()
end
product_dict[a.dest.location.plant_name][a.dest.location.location_name] = dict
plant_dict["total output"][product_name] += vals
end
end
if !skip_plant
if plant.plant_name keys(output["plants"])
output["plants"][plant.plant_name] = Dict()
end
output["plants"][plant.plant_name][plant.location_name] = plant_dict
end
end
output["costs"]["total"] = sum(values(output["costs"]))
return output
end