Implement linear plant sizes

v0.1
Alinson S. Xavier 5 years ago
parent 1335dc42b3
commit 968697dafa

@ -163,7 +163,7 @@
"latitude": 100.0, "latitude": 100.0,
"longitude": 100.0, "longitude": 100.0,
"capacities": { "capacities": {
"10000": { "15000": {
"opening cost": [0.0, 0.0], "opening cost": [0.0, 0.0],
"fixed operating cost": [0.0, 0.0], "fixed operating cost": [0.0, 0.0],
"variable operating cost": [-15.0, -15.0] "variable operating cost": [-15.0, -15.0]

@ -140,12 +140,7 @@ The keys in the `capacities` dictionary should be the amounts (in tonnes). The v
"500": { "500": {
"opening cost": [750, 760], "opening cost": [750, 760],
"fixed operating cost": [400.0, 450.0], "fixed operating cost": [400.0, 450.0],
"variable operating cost": [4.5, 4.7] "variable operating cost": [5.0, 5.2]
},
"700": {
"opening cost": [1000, 1000],
"fixed operating cost": [500.0, 600.0],
"variable operating cost": [4.0, 4.4]
} }
} }
} }
@ -155,9 +150,9 @@ The keys in the `capacities` dictionary should be the amounts (in tonnes). The v
} }
``` ```
Model Assumptions ## Current limitations
-----------------
* Each plant can only be opened exactly once. After open, the plant remains open until the end of the simulation. * Each plant can only be opened exactly once. After open, the plant remains open until the end of the simulation.
* Plants can be expanded at any time, even long after they are open. * Plants can be expanded at any time, even long after they are open.
* All material available at the beginning of a time period must be entirely processed by the end of that time period. It is not possible to store unprocessed materials from one time period to the next. * All material available at the beginning of a time period must be entirely processed by the end of that time period. It is not possible to store unprocessed materials from one time period to the next.
* Up to two plant sizes are currently supported. Variable operating costs must be the same for all plant sizes.

@ -21,6 +21,14 @@ mutable struct CollectionCenter
end end
mutable struct PlantSize
capacity::Float64
variable_operating_cost::Array{Float64}
fixed_operating_cost::Array{Float64}
opening_cost::Array{Float64}
end
mutable struct Plant mutable struct Plant
index::Int64 index::Int64
plant_name::String plant_name::String
@ -29,14 +37,9 @@ mutable struct Plant
output::Dict{Product, Float64} output::Dict{Product, Float64}
latitude::Float64 latitude::Float64
longitude::Float64 longitude::Float64
variable_operating_cost::Array{Float64}
fixed_operating_cost::Array{Float64}
opening_cost::Array{Float64}
base_capacity::Float64
max_capacity::Float64
expansion_cost::Array{Float64}
disposal_limit::Dict{Product, Array{Float64}} disposal_limit::Dict{Product, Array{Float64}}
disposal_cost::Dict{Product, Array{Float64}} disposal_cost::Dict{Product, Array{Float64}}
sizes::Array{PlantSize}
end end
@ -68,13 +71,13 @@ function load(path::String)::Instance
plants = Plant[] plants = Plant[]
products = Product[] products = Product[]
collection_centers = CollectionCenter[] collection_centers = CollectionCenter[]
product_name_to_product = Dict{String, Product}() prod_name_to_product = Dict{String, Product}()
# Create products # Create products
for (product_name, product_dict) in json["products"] for (product_name, product_dict) in json["products"]
product = Product(product_name, product_dict["transportation cost"]) product = Product(product_name, product_dict["transportation cost"])
push!(products, product) push!(products, product)
product_name_to_product[product_name] = product prod_name_to_product[product_name] = product
# Create collection centers # Create collection centers
if "initial amounts" in keys(product_dict) if "initial amounts" in keys(product_dict)
@ -92,48 +95,51 @@ function load(path::String)::Instance
# Create plants # Create plants
for (plant_name, plant_dict) in json["plants"] for (plant_name, plant_dict) in json["plants"]
input = product_name_to_product[plant_dict["input"]] input = prod_name_to_product[plant_dict["input"]]
output = Dict() output = Dict()
# Plant outputs # Plant outputs
if "outputs" in keys(plant_dict) if "outputs" in keys(plant_dict)
output = Dict(product_name_to_product[key] => value output = Dict(prod_name_to_product[key] => value
for (key, value) in plant_dict["outputs"] for (key, value) in plant_dict["outputs"]
if value > 0) if value > 0)
end end
for (location_name, location_dict) in plant_dict["locations"] for (location_name, location_dict) in plant_dict["locations"]
sizes = PlantSize[]
disposal_limit = Dict(p => [0.0 for t in 1:T] for p in keys(output)) disposal_limit = Dict(p => [0.0 for t in 1:T] for p in keys(output))
disposal_cost = Dict(p => [0.0 for t in 1:T] for p in keys(output)) disposal_cost = Dict(p => [0.0 for t in 1:T] for p in keys(output))
# Plant disposal # Disposal
if "disposal" in keys(location_dict) if "disposal" in keys(location_dict)
for (product_name, disposal_dict) in location_dict["disposal"] for (product_name, disposal_dict) in location_dict["disposal"]
limit = [1e8 for t in 1:T] limit = [1e8 for t in 1:T]
if "limit" in keys(disposal_dict) if "limit" in keys(disposal_dict)
limit = disposal_dict["limit"] limit = disposal_dict["limit"]
end end
disposal_limit[product_name_to_product[product_name]] = limit disposal_limit[prod_name_to_product[product_name]] = limit
disposal_cost[product_name_to_product[product_name]] = disposal_dict["cost"] disposal_cost[prod_name_to_product[product_name]] = disposal_dict["cost"]
end end
end end
base_capacity = 1e8 # Capacities
max_capacity = 1e8 for (capacity_name, capacity_dict) in location_dict["capacities"]
expansion_cost = [0.0 for t in 1:T] push!(sizes, PlantSize(parse(Float64, capacity_name),
capacity_dict["variable operating cost"],
if "base capacity" in keys(location_dict) capacity_dict["fixed operating cost"],
base_capacity = location_dict["base capacity"] capacity_dict["opening cost"]))
end end
length(sizes) > 1 || push!(sizes, sizes[1])
sort!(sizes, by = x -> x.capacity)
if "max capacity" in keys(location_dict) # Validation: Capacities
max_capacity = location_dict["max capacity"] if length(sizes) != 2
throw("At most two capacities are supported")
end end
if sizes[1].variable_operating_cost != sizes[2].variable_operating_cost
if "expansion cost" in keys(location_dict) throw("Variable operating costs must be the same for all capacities")
expansion_cost = location_dict["expansion cost"]
end end
plant = Plant(length(plants) + 1, plant = Plant(length(plants) + 1,
plant_name, plant_name,
location_name, location_name,
@ -141,14 +147,10 @@ function load(path::String)::Instance
output, output,
location_dict["latitude"], location_dict["latitude"],
location_dict["longitude"], location_dict["longitude"],
location_dict["variable operating cost"],
location_dict["fixed operating cost"],
location_dict["opening cost"],
base_capacity,
max_capacity,
expansion_cost,
disposal_limit, disposal_limit,
disposal_cost) disposal_cost,
sizes)
push!(plants, plant) push!(plants, plant)
end end
end end

@ -19,6 +19,7 @@ function build_model(instance::Instance, graph::Graph, optimizer)::Manufacturing
create_objective_function!(model) create_objective_function!(model)
create_shipping_node_constraints!(model) create_shipping_node_constraints!(model)
create_process_node_constraints!(model) create_process_node_constraints!(model)
JuMP.write_to_file(model.mip, "model.lp")
return model return model
end end
@ -49,18 +50,37 @@ function create_vars!(model::ManufacturingModel)
vars.capacity = Dict((n, t) => @variable(mip, vars.capacity = Dict((n, t) => @variable(mip,
lower_bound = 0, lower_bound = 0,
upper_bound = n.location.max_capacity, upper_bound = n.location.sizes[2].capacity,
base_name="capacity($(n.location.index),$t)") base_name="capacity($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T) for n in values(graph.process_nodes), t in 1:T)
vars.expansion = Dict((n, t) => @variable(mip, vars.expansion = Dict((n, t) => @variable(mip,
lower_bound = 0, lower_bound = 0,
upper_bound = (n.location.max_capacity - n.location.base_capacity), upper_bound = n.location.sizes[2].capacity -
n.location.sizes[1].capacity,
base_name="expansion($(n.location.index),$t)") base_name="expansion($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T) for n in values(graph.process_nodes), t in 1:T)
end 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
function create_objective_function!(model::ManufacturingModel) function create_objective_function!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
obj = @expression(mip, 0 * @variable(mip)) obj = @expression(mip, 0 * @variable(mip))
@ -71,23 +91,34 @@ 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"]
c += n.location.variable_operating_cost[t] c += n.location.sizes[1].variable_operating_cost[t]
add_to_expression!(obj, c, vars.flow[a, t]) add_to_expression!(obj, c, vars.flow[a, t])
end end
# Opening costs # Opening costs
add_to_expression!(obj, n.location.opening_cost[t], vars.open_plant[n, t]) add_to_expression!(obj,
n.location.sizes[1].opening_cost[t],
vars.open_plant[n, t])
# Fixed operating costs (base)
add_to_expression!(obj,
n.location.sizes[1].fixed_operating_cost[t],
vars.is_open[n, t])
# Fixed operating costss # Fixed operating costs (expansion)
add_to_expression!(obj, n.location.fixed_operating_cost[t], vars.is_open[n, t]) add_to_expression!(obj,
slope_fix_oper_cost(n.location, t),
vars.expansion[n, t])
# Expansion costs # Expansion costs
if t < T if t < T
add_to_expression!(obj, add_to_expression!(obj,
n.location.expansion_cost[t] - n.location.expansion_cost[t + 1], slope_open(n.location, t) - slope_open(n.location, t + 1),
vars.expansion[n, t]) vars.expansion[n, t])
else else
add_to_expression!(obj, n.location.expansion_cost[t], vars.expansion[n, t]) add_to_expression!(obj,
slope_open(n.location, t),
vars.expansion[n, t])
end end
end end
@ -130,10 +161,13 @@ function create_process_node_constraints!(model::ManufacturingModel)
end end
# If plant is closed, capacity is zero # If plant is closed, capacity is zero
@constraint(mip, vars.capacity[n, t] <= n.location.max_capacity * vars.is_open[n, t]) @constraint(mip, vars.capacity[n, t] <= n.location.sizes[2].capacity * vars.is_open[n, t])
# If plant is open, capacity is greater than base
@constraint(mip, vars.capacity[n, t] >= n.location.sizes[1].capacity * vars.is_open[n, t])
# Capacity is linked to expansion # Capacity is linked to expansion
@constraint(mip, vars.capacity[n, t] <= n.location.base_capacity + vars.expansion[n, t]) @constraint(mip, vars.capacity[n, t] <= n.location.sizes[1].capacity + vars.expansion[n, t])
# Input sum must be smaller than capacity # Input sum must be smaller than capacity
@constraint(mip, input_sum <= vars.capacity[n, t]) @constraint(mip, input_sum <= vars.capacity[n, t])
@ -212,15 +246,19 @@ function get_solution(model::ManufacturingModel)
"longitude" => plant.longitude, "longitude" => plant.longitude,
"capacity" => [JuMP.value(vars.capacity[process_node, t]) "capacity" => [JuMP.value(vars.capacity[process_node, t])
for t in 1:T], for t in 1:T],
"opening cost" => [JuMP.value(vars.open_plant[process_node, t]) * plant.opening_cost[t] "opening cost" => [JuMP.value(vars.open_plant[process_node, t]) *
plant.sizes[1].opening_cost[t]
for t in 1:T], for t in 1:T],
"fixed operating cost" => [JuMP.value(vars.is_open[process_node, t]) * plant.fixed_operating_cost[t] "fixed operating cost" => [JuMP.value(vars.is_open[process_node, t]) *
plant.sizes[1].fixed_operating_cost[t] +
JuMP.value(vars.expansion[process_node, t]) *
slope_fix_oper_cost(plant, t)
for t in 1:T], for t in 1:T],
"expansion cost" => [plant.expansion_cost[t] * "expansion cost" => [JuMP.value(vars.expansion[process_node, t]) *
(if t > 1 (if t < T
JuMP.value(vars.expansion[process_node, t]) - JuMP.value(vars.expansion[process_node, t-1]) slope_open(plant, t) - slope_open(plant, t + 1)
else else
JuMP.value(vars.expansion[process_node, t]) slope_open(plant, t)
end) end)
for t in 1:T], for t in 1:T],
) )
@ -242,7 +280,7 @@ function get_solution(model::ManufacturingModel)
"longitude" => a.source.location.longitude, "longitude" => a.source.location.longitude,
"transportation cost" => [a.source.product.transportation_cost[t] * vals[t] "transportation cost" => [a.source.product.transportation_cost[t] * vals[t]
for t in 1:T], for t in 1:T],
"variable operating cost" => [plant.variable_operating_cost[t] * vals[t] "variable operating cost" => [plant.sizes[1].variable_operating_cost[t] * vals[t]
for t in 1:T], for t in 1:T],
) )
if a.source.location isa CollectionCenter if a.source.location isa CollectionCenter
@ -273,7 +311,8 @@ function get_solution(model::ManufacturingModel)
skip_plant = false skip_plant = false
plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict() 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["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] disposal_dict["cost"] = [disposal_dict["amount"][t] *
plant.disposal_cost[shipping_node.product][t]
for t in 1:T] for t in 1:T]
plant_dict["total output"][product_name] += disposal_amount plant_dict["total output"][product_name] += disposal_amount
output["costs"]["disposal"] += disposal_dict["cost"] output["costs"]["disposal"] += disposal_dict["cost"]

@ -15,7 +15,6 @@ using RELOG
location_name_to_plant = Dict(p.location_name => p for p in plants) location_name_to_plant = Dict(p.location_name => p for p in plants)
product_name_to_product = Dict(p.name => p for p in products) product_name_to_product = Dict(p.name => p for p in products)
@test length(centers) == 10 @test length(centers) == 10
@test centers[1].name == "C1" @test centers[1].name == "C1"
@test centers[1].latitude == 7 @test centers[1].latitude == 7
@ -32,12 +31,16 @@ using RELOG
@test plant.input.name == "P1" @test plant.input.name == "P1"
@test plant.latitude == 0 @test plant.latitude == 0
@test plant.longitude == 0 @test plant.longitude == 0
@test plant.opening_cost == [500, 500]
@test plant.fixed_operating_cost == [30, 30] @test length(plant.sizes) == 2
@test plant.variable_operating_cost == [30, 30] @test plant.sizes[1].capacity == 250
@test plant.base_capacity == 250 @test plant.sizes[1].opening_cost == [500, 500]
@test plant.max_capacity == 1000 @test plant.sizes[1].fixed_operating_cost == [30, 30]
@test plant.expansion_cost == [1, 1] @test plant.sizes[1].variable_operating_cost == [30, 30]
@test plant.sizes[2].capacity == 1000
@test plant.sizes[2].opening_cost == [1250, 1250]
@test plant.sizes[2].fixed_operating_cost == [30, 30]
@test plant.sizes[2].variable_operating_cost == [30, 30]
p2 = product_name_to_product["P2"] p2 = product_name_to_product["P2"]
p3 = product_name_to_product["P3"] p3 = product_name_to_product["P3"]
@ -54,12 +57,13 @@ using RELOG
@test plant.input.name == "P2" @test plant.input.name == "P2"
@test plant.latitude == 25 @test plant.latitude == 25
@test plant.longitude == 65 @test plant.longitude == 65
@test plant.opening_cost == [3000, 3000]
@test plant.fixed_operating_cost == [50, 50] @test length(plant.sizes) == 2
@test plant.variable_operating_cost == [50, 50] @test plant.sizes[1].capacity == 1000.0
@test plant.base_capacity == 1e8 @test plant.sizes[1].opening_cost == [3000, 3000]
@test plant.max_capacity == 1e8 @test plant.sizes[1].fixed_operating_cost == [50, 50]
@test plant.expansion_cost == [0, 0] @test plant.sizes[1].variable_operating_cost == [50, 50]
@test plant.sizes[1] == plant.sizes[2]
p4 = product_name_to_product["P4"] p4 = product_name_to_product["P4"]
@test plant.output[p3] == 0.05 @test plant.output[p3] == 0.05

Loading…
Cancel
Save