Replace ManufacturingModel by JuMP.Model

feature/lint
Alinson S. Xavier 4 years ago
parent 93cc6fbf32
commit 7e783c8b91

@ -5,15 +5,10 @@
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
mutable struct ManufacturingModel function build_model(instance::Instance, graph::Graph, optimizer)::JuMP.Model
mip::JuMP.Model model = Model(optimizer)
instance::Instance model[:instance] = instance
graph::Graph model[:graph] = graph
end
function build_model(instance::Instance, graph::Graph, optimizer)::ManufacturingModel
model = ManufacturingModel(Model(optimizer), instance, graph)
create_vars!(model) create_vars!(model)
create_objective_function!(model) create_objective_function!(model)
create_shipping_node_constraints!(model) create_shipping_node_constraints!(model)
@ -22,42 +17,42 @@ function build_model(instance::Instance, graph::Graph, optimizer)::Manufacturing
end end
function create_vars!(model::ManufacturingModel) function create_vars!(model::JuMP.Model)
mip, graph, T = model.mip, model.graph, model.instance.time graph, T = model[:graph], model[:instance].time
mip[:flow] = model[:flow] =
Dict((a, t) => @variable(mip, lower_bound = 0) for a in graph.arcs, t = 1:T) Dict((a, t) => @variable(model, lower_bound = 0) for a in graph.arcs, t = 1:T)
mip[:dispose] = Dict( model[:dispose] = Dict(
(n, t) => @variable( (n, t) => @variable(
mip, model,
lower_bound = 0, lower_bound = 0,
upper_bound = n.location.disposal_limit[n.product][t] upper_bound = n.location.disposal_limit[n.product][t]
) for n in values(graph.plant_shipping_nodes), t = 1:T ) for n in values(graph.plant_shipping_nodes), t = 1:T
) )
mip[:store] = Dict( model[:store] = Dict(
(n, t) => (n, t) =>
@variable(mip, lower_bound = 0, upper_bound = n.location.storage_limit) for @variable(model, lower_bound = 0, upper_bound = n.location.storage_limit) for
n in values(graph.process_nodes), t = 1:T n in values(graph.process_nodes), t = 1:T
) )
mip[:process] = Dict( model[:process] = Dict(
(n, t) => @variable(mip, lower_bound = 0) for n in values(graph.process_nodes), (n, t) => @variable(model, lower_bound = 0) for n in values(graph.process_nodes),
t = 1:T t = 1:T
) )
mip[:open_plant] = Dict( model[:open_plant] = Dict(
(n, t) => @variable(mip, binary = true) for n in values(graph.process_nodes), (n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
t = 1:T t = 1:T
) )
mip[:is_open] = Dict( model[:is_open] = Dict(
(n, t) => @variable(mip, binary = true) for n in values(graph.process_nodes), (n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
t = 1:T t = 1:T
) )
mip[:capacity] = Dict( model[:capacity] = Dict(
(n, t) => (n, t) =>
@variable(mip, lower_bound = 0, upper_bound = n.location.sizes[2].capacity) @variable(model, lower_bound = 0, upper_bound = n.location.sizes[2].capacity)
for n in values(graph.process_nodes), t = 1:T for n in values(graph.process_nodes), t = 1:T
) )
mip[:expansion] = Dict( model[:expansion] = Dict(
(n, t) => @variable( (n, t) => @variable(
mip, model,
lower_bound = 0, lower_bound = 0,
upper_bound = n.location.sizes[2].capacity - n.location.sizes[1].capacity upper_bound = n.location.sizes[2].capacity - n.location.sizes[1].capacity
) for n in values(graph.process_nodes), t = 1:T ) for n in values(graph.process_nodes), t = 1:T
@ -83,8 +78,8 @@ function slope_fix_oper_cost(plant, t)
end end
end end
function create_objective_function!(model::ManufacturingModel) function create_objective_function!(model::JuMP.Model)
mip, graph, T = model.mip, model.graph, model.instance.time graph, T = model[:graph], model[:instance].time
obj = AffExpr(0.0) obj = AffExpr(0.0)
# Process node costs # Process node costs
@ -93,41 +88,41 @@ function create_objective_function!(model::ManufacturingModel)
# Transportation and variable operating costs # Transportation and variable operating costs
for a in n.incoming_arcs for a in n.incoming_arcs
c = n.location.input.transportation_cost[t] * a.values["distance"] c = n.location.input.transportation_cost[t] * a.values["distance"]
add_to_expression!(obj, c, mip[:flow][a, t]) add_to_expression!(obj, c, model[:flow][a, t])
end end
# Opening costs # Opening costs
add_to_expression!(obj, n.location.sizes[1].opening_cost[t], mip[:open_plant][n, t]) add_to_expression!(obj, n.location.sizes[1].opening_cost[t], model[:open_plant][n, t])
# Fixed operating costs (base) # Fixed operating costs (base)
add_to_expression!( add_to_expression!(
obj, obj,
n.location.sizes[1].fixed_operating_cost[t], n.location.sizes[1].fixed_operating_cost[t],
mip[:is_open][n, t], model[:is_open][n, t],
) )
# Fixed operating costs (expansion) # Fixed operating costs (expansion)
add_to_expression!(obj, slope_fix_oper_cost(n.location, t), mip[:expansion][n, t]) add_to_expression!(obj, slope_fix_oper_cost(n.location, t), model[:expansion][n, t])
# Processing costs # Processing costs
add_to_expression!( add_to_expression!(
obj, obj,
n.location.sizes[1].variable_operating_cost[t], n.location.sizes[1].variable_operating_cost[t],
mip[:process][n, t], model[:process][n, t],
) )
# Storage costs # Storage costs
add_to_expression!(obj, n.location.storage_cost[t], mip[:store][n, t]) add_to_expression!(obj, n.location.storage_cost[t], model[:store][n, t])
# Expansion costs # Expansion costs
if t < T if t < T
add_to_expression!( add_to_expression!(
obj, obj,
slope_open(n.location, t) - slope_open(n.location, t + 1), slope_open(n.location, t) - slope_open(n.location, t + 1),
mip[:expansion][n, t], model[:expansion][n, t],
) )
else else
add_to_expression!(obj, slope_open(n.location, t), mip[:expansion][n, t]) add_to_expression!(obj, slope_open(n.location, t), model[:expansion][n, t])
end end
end end
@ -135,31 +130,31 @@ function create_objective_function!(model::ManufacturingModel)
for n in values(graph.plant_shipping_nodes), t = 1:T for n in values(graph.plant_shipping_nodes), t = 1:T
# Disposal costs # Disposal costs
add_to_expression!(obj, n.location.disposal_cost[n.product][t], mip[:dispose][n, t]) add_to_expression!(obj, n.location.disposal_cost[n.product][t], model[:dispose][n, t])
end end
@objective(mip, Min, obj) @objective(model, Min, obj)
end end
function create_shipping_node_constraints!(model::ManufacturingModel) function create_shipping_node_constraints!(model::JuMP.Model)
mip, graph, T = model.mip, model.graph, model.instance.time graph, T = model[:graph], model[:instance].time
mip[:eq_balance] = OrderedDict() model[:eq_balance] = OrderedDict()
for t = 1:T for t = 1:T
# Collection centers # Collection centers
for n in graph.collection_shipping_nodes for n in graph.collection_shipping_nodes
mip[:eq_balance][n, t] = @constraint( model[:eq_balance][n, t] = @constraint(
mip, model,
sum(mip[:flow][a, t] for a in n.outgoing_arcs) == n.location.amount[t] sum(model[:flow][a, t] for a in n.outgoing_arcs) == n.location.amount[t]
) )
end end
# Plants # Plants
for n in graph.plant_shipping_nodes for n in graph.plant_shipping_nodes
@constraint( @constraint(
mip, model,
sum(mip[:flow][a, t] for a in n.incoming_arcs) == sum(model[:flow][a, t] for a in n.incoming_arcs) ==
sum(mip[:flow][a, t] for a in n.outgoing_arcs) + mip[:dispose][n, t] sum(model[:flow][a, t] for a in n.outgoing_arcs) + model[:dispose][n, t]
) )
end end
end end
@ -167,72 +162,72 @@ function create_shipping_node_constraints!(model::ManufacturingModel)
end end
function create_process_node_constraints!(model::ManufacturingModel) function create_process_node_constraints!(model::JuMP.Model)
mip, graph, T = model.mip, model.graph, model.instance.time graph, T = model[:graph], model[:instance].time
for t = 1:T, n in graph.process_nodes for t = 1:T, n in graph.process_nodes
input_sum = AffExpr(0.0) input_sum = AffExpr(0.0)
for a in n.incoming_arcs for a in n.incoming_arcs
add_to_expression!(input_sum, 1.0, mip[:flow][a, t]) add_to_expression!(input_sum, 1.0, model[:flow][a, t])
end end
# Output amount is implied by amount processed # Output amount is implied by amount processed
for a in n.outgoing_arcs for a in n.outgoing_arcs
@constraint(mip, mip[:flow][a, t] == a.values["weight"] * mip[:process][n, t]) @constraint(model, model[:flow][a, t] == a.values["weight"] * model[:process][n, t])
end end
# If plant is closed, capacity is zero # If plant is closed, capacity is zero
@constraint( @constraint(
mip, model,
mip[:capacity][n, t] <= n.location.sizes[2].capacity * mip[:is_open][n, t] model[:capacity][n, t] <= n.location.sizes[2].capacity * model[:is_open][n, t]
) )
# If plant is open, capacity is greater than base # If plant is open, capacity is greater than base
@constraint( @constraint(
mip, model,
mip[:capacity][n, t] >= n.location.sizes[1].capacity * mip[:is_open][n, t] model[:capacity][n, t] >= n.location.sizes[1].capacity * model[:is_open][n, t]
) )
# Capacity is linked to expansion # Capacity is linked to expansion
@constraint( @constraint(
mip, model,
mip[:capacity][n, t] <= n.location.sizes[1].capacity + mip[:expansion][n, t] model[:capacity][n, t] <= n.location.sizes[1].capacity + model[:expansion][n, t]
) )
# Can only process up to capacity # Can only process up to capacity
@constraint(mip, mip[:process][n, t] <= mip[:capacity][n, t]) @constraint(model, model[:process][n, t] <= model[:capacity][n, t])
if t > 1 if t > 1
# Plant capacity can only increase over time # Plant capacity can only increase over time
@constraint(mip, mip[:capacity][n, t] >= mip[:capacity][n, t-1]) @constraint(model, model[:capacity][n, t] >= model[:capacity][n, t-1])
@constraint(mip, mip[:expansion][n, t] >= mip[:expansion][n, t-1]) @constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
end end
# Amount received equals amount processed plus stored # Amount received equals amount processed plus stored
store_in = 0 store_in = 0
if t > 1 if t > 1
store_in = mip[:store][n, t-1] store_in = model[:store][n, t-1]
end end
if t == T if t == T
@constraint(mip, mip[:store][n, t] == 0) @constraint(model, model[:store][n, t] == 0)
end end
@constraint(mip, input_sum + store_in == mip[:store][n, t] + mip[:process][n, t]) @constraint(model, input_sum + store_in == model[:store][n, t] + model[:process][n, t])
# Plant is currently open if it was already open in the previous time period or # Plant is currently open if it was already open in the previous time period or
# if it was built just now # if it was built just now
if t > 1 if t > 1
@constraint( @constraint(
mip, model,
mip[:is_open][n, t] == mip[:is_open][n, t-1] + mip[:open_plant][n, t] model[:is_open][n, t] == model[:is_open][n, t-1] + model[:open_plant][n, t]
) )
else else
@constraint(mip, mip[:is_open][n, t] == mip[:open_plant][n, t]) @constraint(model, model[:is_open][n, t] == model[:open_plant][n, t])
end end
# Plant can only be opened during building period # Plant can only be opened during building period
if t model.instance.building_period if t model[:instance].building_period
@constraint(mip, mip[:open_plant][n, t] == 0) @constraint(model, model[:open_plant][n, t] == 0)
end end
end end
end end
@ -268,25 +263,25 @@ function solve(
model = RELOG.build_model(instance, graph, milp_optimizer) model = RELOG.build_model(instance, graph, milp_optimizer)
@info "Optimizing MILP..." @info "Optimizing MILP..."
JuMP.optimize!(model.mip) JuMP.optimize!(model)
if !has_values(model.mip) if !has_values(model)
@warn "No solution available" @warn "No solution available"
return OrderedDict() return OrderedDict()
end end
if marginal_costs if marginal_costs
@info "Re-optimizing with integer variables fixed..." @info "Re-optimizing with integer variables fixed..."
all_vars = JuMP.all_variables(model.mip) all_vars = JuMP.all_variables(model)
vals = OrderedDict(var => JuMP.value(var) for var in all_vars) vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
JuMP.set_optimizer(model.mip, lp_optimizer) JuMP.set_optimizer(model, lp_optimizer)
for var in all_vars for var in all_vars
if JuMP.is_binary(var) if JuMP.is_binary(var)
JuMP.unset_binary(var) JuMP.unset_binary(var)
JuMP.fix(var, vals[var]) JuMP.fix(var, vals[var])
end end
end end
JuMP.optimize!(model.mip) JuMP.optimize!(model)
end end
@info "Extracting solution..." @info "Extracting solution..."
@ -327,8 +322,8 @@ function solve(filename::AbstractString; heuristic = false, kwargs...)
end end
function get_solution(model::ManufacturingModel; marginal_costs = true) function get_solution(model::JuMP.Model; marginal_costs = true)
mip, graph, instance = model.mip, model.graph, model.instance graph, instance = model[:graph], model[:instance]
T = instance.time T = instance.time
output = OrderedDict( output = OrderedDict(
@ -366,7 +361,7 @@ function get_solution(model::ManufacturingModel; marginal_costs = true)
for n in graph.collection_shipping_nodes for n in graph.collection_shipping_nodes
location_dict = OrderedDict{Any,Any}( location_dict = OrderedDict{Any,Any}(
"Marginal cost (\$/tonne)" => [ "Marginal cost (\$/tonne)" => [
round(abs(JuMP.shadow_price(mip[:eq_balance][n, t])), digits = 2) for t = 1:T round(abs(JuMP.shadow_price(model[:eq_balance][n, t])), digits = 2) for t = 1:T
], ],
) )
if n.product.name keys(output["Products"]) if n.product.name keys(output["Products"])
@ -390,38 +385,38 @@ function get_solution(model::ManufacturingModel; marginal_costs = true)
"Latitude (deg)" => plant.latitude, "Latitude (deg)" => plant.latitude,
"Longitude (deg)" => plant.longitude, "Longitude (deg)" => plant.longitude,
"Capacity (tonne)" => "Capacity (tonne)" =>
[JuMP.value(mip[:capacity][process_node, t]) for t = 1:T], [JuMP.value(model[:capacity][process_node, t]) for t = 1:T],
"Opening cost (\$)" => [ "Opening cost (\$)" => [
JuMP.value(mip[:open_plant][process_node, t]) * JuMP.value(model[:open_plant][process_node, t]) *
plant.sizes[1].opening_cost[t] for t = 1:T plant.sizes[1].opening_cost[t] for t = 1:T
], ],
"Fixed operating cost (\$)" => [ "Fixed operating cost (\$)" => [
JuMP.value(mip[:is_open][process_node, t]) * JuMP.value(model[:is_open][process_node, t]) *
plant.sizes[1].fixed_operating_cost[t] + plant.sizes[1].fixed_operating_cost[t] +
JuMP.value(mip[:expansion][process_node, t]) * JuMP.value(model[:expansion][process_node, t]) *
slope_fix_oper_cost(plant, t) for t = 1:T slope_fix_oper_cost(plant, t) for t = 1:T
], ],
"Expansion cost (\$)" => [ "Expansion cost (\$)" => [
( (
if t == 1 if t == 1
slope_open(plant, t) * JuMP.value(mip[:expansion][process_node, t]) slope_open(plant, t) * JuMP.value(model[:expansion][process_node, t])
else else
slope_open(plant, t) * ( slope_open(plant, t) * (
JuMP.value(mip[:expansion][process_node, t]) - JuMP.value(model[:expansion][process_node, t]) -
JuMP.value(mip[:expansion][process_node, t-1]) JuMP.value(model[:expansion][process_node, t-1])
) )
end end
) for t = 1:T ) for t = 1:T
], ],
"Process (tonne)" => "Process (tonne)" =>
[JuMP.value(mip[:process][process_node, t]) for t = 1:T], [JuMP.value(model[:process][process_node, t]) for t = 1:T],
"Variable operating cost (\$)" => [ "Variable operating cost (\$)" => [
JuMP.value(mip[:process][process_node, t]) * JuMP.value(model[:process][process_node, t]) *
plant.sizes[1].variable_operating_cost[t] for t = 1:T plant.sizes[1].variable_operating_cost[t] for t = 1:T
], ],
"Storage (tonne)" => [JuMP.value(mip[:store][process_node, t]) for t = 1:T], "Storage (tonne)" => [JuMP.value(model[:store][process_node, t]) for t = 1:T],
"Storage cost (\$)" => [ "Storage cost (\$)" => [
JuMP.value(mip[:store][process_node, t]) * plant.storage_cost[t] for JuMP.value(model[:store][process_node, t]) * plant.storage_cost[t] for
t = 1:T t = 1:T
], ],
) )
@ -434,7 +429,7 @@ function get_solution(model::ManufacturingModel; marginal_costs = true)
# Inputs # Inputs
for a in process_node.incoming_arcs for a in process_node.incoming_arcs
vals = [JuMP.value(mip[:flow][a, t]) for t = 1:T] vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
if sum(vals) <= 1e-3 if sum(vals) <= 1e-3
continue continue
end end
@ -497,13 +492,13 @@ function get_solution(model::ManufacturingModel; marginal_costs = true)
plant_dict["Total output"][product_name] = zeros(T) plant_dict["Total output"][product_name] = zeros(T)
plant_dict["Output"]["Send"][product_name] = product_dict = OrderedDict() plant_dict["Output"]["Send"][product_name] = product_dict = OrderedDict()
disposal_amount = [JuMP.value(mip[:dispose][shipping_node, t]) for t = 1:T] disposal_amount = [JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T]
if sum(disposal_amount) > 1e-5 if sum(disposal_amount) > 1e-5
skip_plant = false skip_plant = false
plant_dict["Output"]["Dispose"][product_name] = plant_dict["Output"]["Dispose"][product_name] =
disposal_dict = OrderedDict() disposal_dict = OrderedDict()
disposal_dict["Amount (tonne)"] = disposal_dict["Amount (tonne)"] =
[JuMP.value(model.mip[:dispose][shipping_node, t]) for t = 1:T] [JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T]
disposal_dict["Cost (\$)"] = [ disposal_dict["Cost (\$)"] = [
disposal_dict["Amount (tonne)"][t] * disposal_dict["Amount (tonne)"][t] *
plant.disposal_cost[shipping_node.product][t] for t = 1:T plant.disposal_cost[shipping_node.product][t] for t = 1:T
@ -513,7 +508,7 @@ function get_solution(model::ManufacturingModel; marginal_costs = true)
end end
for a in shipping_node.outgoing_arcs for a in shipping_node.outgoing_arcs
vals = [JuMP.value(mip[:flow][a, t]) for t = 1:T] vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
if sum(vals) <= 1e-3 if sum(vals) <= 1e-3
continue continue
end end

@ -9,7 +9,7 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
instance = RELOG.parsefile("$basedir/../instances/s1.json") instance = RELOG.parsefile("$basedir/../instances/s1.json")
graph = RELOG.build_graph(instance) graph = RELOG.build_graph(instance)
model = RELOG.build_model(instance, graph, Cbc.Optimizer) model = RELOG.build_model(instance, graph, Cbc.Optimizer)
set_optimizer_attribute(model.mip, "logLevel", 0) set_optimizer_attribute(model, "logLevel", 0)
process_node_by_location_name = process_node_by_location_name =
Dict(n.location.location_name => n for n in graph.process_nodes) Dict(n.location.location_name => n for n in graph.process_nodes)
@ -19,27 +19,27 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
n in graph.plant_shipping_nodes n in graph.plant_shipping_nodes
) )
@test length(model.mip[:flow]) == 76 @test length(model[:flow]) == 76
@test length(model.mip[:dispose]) == 16 @test length(model[:dispose]) == 16
@test length(model.mip[:open_plant]) == 12 @test length(model[:open_plant]) == 12
@test length(model.mip[:capacity]) == 12 @test length(model[:capacity]) == 12
@test length(model.mip[:expansion]) == 12 @test length(model[:expansion]) == 12
l1 = process_node_by_location_name["L1"] l1 = process_node_by_location_name["L1"]
v = model.mip[:capacity][l1, 1] v = model[:capacity][l1, 1]
@test lower_bound(v) == 0.0 @test lower_bound(v) == 0.0
@test upper_bound(v) == 1000.0 @test upper_bound(v) == 1000.0
v = model.mip[:expansion][l1, 1] v = model[:expansion][l1, 1]
@test lower_bound(v) == 0.0 @test lower_bound(v) == 0.0
@test upper_bound(v) == 750.0 @test upper_bound(v) == 750.0
v = model.mip[:dispose][shipping_node_by_location_and_product_names["L1", "P2"], 1] v = model[:dispose][shipping_node_by_location_and_product_names["L1", "P2"], 1]
@test lower_bound(v) == 0.0 @test lower_bound(v) == 0.0
@test upper_bound(v) == 1.0 @test upper_bound(v) == 1.0
# dest = FileFormats.Model(format = FileFormats.FORMAT_LP) # dest = FileFormats.Model(format = FileFormats.FORMAT_LP)
# MOI.copy_to(dest, model.mip) # MOI.copy_to(dest, model)
# MOI.write_to_file(dest, "model.lp") # MOI.write_to_file(dest, "model.lp")
end end

Loading…
Cancel
Save