mirror of
https://github.com/ANL-CEEESA/RELOG.git
synced 2025-12-06 07:48:50 -06:00
Implement linear plant sizes
This commit is contained in:
@@ -163,7 +163,7 @@
|
||||
"latitude": 100.0,
|
||||
"longitude": 100.0,
|
||||
"capacities": {
|
||||
"10000": {
|
||||
"15000": {
|
||||
"opening cost": [0.0, 0.0],
|
||||
"fixed operating cost": [0.0, 0.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": {
|
||||
"opening cost": [750, 760],
|
||||
"fixed operating cost": [400.0, 450.0],
|
||||
"variable operating cost": [4.5, 4.7]
|
||||
},
|
||||
"700": {
|
||||
"opening cost": [1000, 1000],
|
||||
"fixed operating cost": [500.0, 600.0],
|
||||
"variable operating cost": [4.0, 4.4]
|
||||
"variable operating cost": [5.0, 5.2]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
* 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.
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
mutable struct PlantSize
|
||||
capacity::Float64
|
||||
variable_operating_cost::Array{Float64}
|
||||
fixed_operating_cost::Array{Float64}
|
||||
opening_cost::Array{Float64}
|
||||
end
|
||||
|
||||
|
||||
mutable struct Plant
|
||||
index::Int64
|
||||
plant_name::String
|
||||
@@ -29,14 +37,9 @@ mutable struct Plant
|
||||
output::Dict{Product, Float64}
|
||||
latitude::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_cost::Dict{Product, Array{Float64}}
|
||||
sizes::Array{PlantSize}
|
||||
end
|
||||
|
||||
|
||||
@@ -68,13 +71,13 @@ function load(path::String)::Instance
|
||||
plants = Plant[]
|
||||
products = Product[]
|
||||
collection_centers = CollectionCenter[]
|
||||
product_name_to_product = Dict{String, Product}()
|
||||
prod_name_to_product = Dict{String, Product}()
|
||||
|
||||
# Create products
|
||||
for (product_name, product_dict) in json["products"]
|
||||
product = Product(product_name, product_dict["transportation cost"])
|
||||
push!(products, product)
|
||||
product_name_to_product[product_name] = product
|
||||
prod_name_to_product[product_name] = product
|
||||
|
||||
# Create collection centers
|
||||
if "initial amounts" in keys(product_dict)
|
||||
@@ -92,46 +95,49 @@ function load(path::String)::Instance
|
||||
|
||||
# Create 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()
|
||||
|
||||
# Plant outputs
|
||||
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"]
|
||||
if value > 0)
|
||||
end
|
||||
|
||||
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_cost = Dict(p => [0.0 for t in 1:T] for p in keys(output))
|
||||
|
||||
# Plant disposal
|
||||
# Disposal
|
||||
if "disposal" in keys(location_dict)
|
||||
for (product_name, disposal_dict) in location_dict["disposal"]
|
||||
limit = [1e8 for t in 1:T]
|
||||
if "limit" in keys(disposal_dict)
|
||||
limit = disposal_dict["limit"]
|
||||
end
|
||||
disposal_limit[product_name_to_product[product_name]] = limit
|
||||
disposal_cost[product_name_to_product[product_name]] = disposal_dict["cost"]
|
||||
disposal_limit[prod_name_to_product[product_name]] = limit
|
||||
disposal_cost[prod_name_to_product[product_name]] = disposal_dict["cost"]
|
||||
end
|
||||
end
|
||||
|
||||
base_capacity = 1e8
|
||||
max_capacity = 1e8
|
||||
expansion_cost = [0.0 for t in 1:T]
|
||||
|
||||
if "base capacity" in keys(location_dict)
|
||||
base_capacity = location_dict["base capacity"]
|
||||
# Capacities
|
||||
for (capacity_name, capacity_dict) in location_dict["capacities"]
|
||||
push!(sizes, PlantSize(parse(Float64, capacity_name),
|
||||
capacity_dict["variable operating cost"],
|
||||
capacity_dict["fixed operating cost"],
|
||||
capacity_dict["opening cost"]))
|
||||
end
|
||||
length(sizes) > 1 || push!(sizes, sizes[1])
|
||||
sort!(sizes, by = x -> x.capacity)
|
||||
|
||||
if "max capacity" in keys(location_dict)
|
||||
max_capacity = location_dict["max capacity"]
|
||||
# Validation: Capacities
|
||||
if length(sizes) != 2
|
||||
throw("At most two capacities are supported")
|
||||
end
|
||||
|
||||
if "expansion cost" in keys(location_dict)
|
||||
expansion_cost = location_dict["expansion cost"]
|
||||
if sizes[1].variable_operating_cost != sizes[2].variable_operating_cost
|
||||
throw("Variable operating costs must be the same for all capacities")
|
||||
end
|
||||
|
||||
plant = Plant(length(plants) + 1,
|
||||
@@ -141,14 +147,10 @@ function load(path::String)::Instance
|
||||
output,
|
||||
location_dict["latitude"],
|
||||
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_cost)
|
||||
disposal_cost,
|
||||
sizes)
|
||||
|
||||
push!(plants, plant)
|
||||
end
|
||||
end
|
||||
|
||||
75
src/model.jl
75
src/model.jl
@@ -19,6 +19,7 @@ function build_model(instance::Instance, graph::Graph, optimizer)::Manufacturing
|
||||
create_objective_function!(model)
|
||||
create_shipping_node_constraints!(model)
|
||||
create_process_node_constraints!(model)
|
||||
JuMP.write_to_file(model.mip, "model.lp")
|
||||
return model
|
||||
end
|
||||
|
||||
@@ -49,18 +50,37 @@ function create_vars!(model::ManufacturingModel)
|
||||
|
||||
vars.capacity = Dict((n, t) => @variable(mip,
|
||||
lower_bound = 0,
|
||||
upper_bound = n.location.max_capacity,
|
||||
upper_bound = n.location.sizes[2].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),
|
||||
upper_bound = n.location.sizes[2].capacity -
|
||||
n.location.sizes[1].capacity,
|
||||
base_name="expansion($(n.location.index),$t)")
|
||||
for n in values(graph.process_nodes), t in 1:T)
|
||||
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)
|
||||
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
|
||||
obj = @expression(mip, 0 * @variable(mip))
|
||||
@@ -71,23 +91,34 @@ function create_objective_function!(model::ManufacturingModel)
|
||||
# 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]
|
||||
c += n.location.sizes[1].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])
|
||||
add_to_expression!(obj,
|
||||
n.location.sizes[1].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])
|
||||
# Fixed operating costs (base)
|
||||
add_to_expression!(obj,
|
||||
n.location.sizes[1].fixed_operating_cost[t],
|
||||
vars.is_open[n, t])
|
||||
|
||||
# Fixed operating costs (expansion)
|
||||
add_to_expression!(obj,
|
||||
slope_fix_oper_cost(n.location, t),
|
||||
vars.expansion[n, t])
|
||||
|
||||
# Expansion costs
|
||||
if t < T
|
||||
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])
|
||||
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
|
||||
|
||||
@@ -130,10 +161,13 @@ function create_process_node_constraints!(model::ManufacturingModel)
|
||||
end
|
||||
|
||||
# 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
|
||||
@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
|
||||
@constraint(mip, input_sum <= vars.capacity[n, t])
|
||||
@@ -212,15 +246,19 @@ function get_solution(model::ManufacturingModel)
|
||||
"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]
|
||||
"opening cost" => [JuMP.value(vars.open_plant[process_node, t]) *
|
||||
plant.sizes[1].opening_cost[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],
|
||||
"expansion cost" => [plant.expansion_cost[t] *
|
||||
(if t > 1
|
||||
JuMP.value(vars.expansion[process_node, t]) - JuMP.value(vars.expansion[process_node, t-1])
|
||||
"expansion cost" => [JuMP.value(vars.expansion[process_node, t]) *
|
||||
(if t < T
|
||||
slope_open(plant, t) - slope_open(plant, t + 1)
|
||||
else
|
||||
JuMP.value(vars.expansion[process_node, t])
|
||||
slope_open(plant, t)
|
||||
end)
|
||||
for t in 1:T],
|
||||
)
|
||||
@@ -242,7 +280,7 @@ function get_solution(model::ManufacturingModel)
|
||||
"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]
|
||||
"variable operating cost" => [plant.sizes[1].variable_operating_cost[t] * vals[t]
|
||||
for t in 1:T],
|
||||
)
|
||||
if a.source.location isa CollectionCenter
|
||||
@@ -273,7 +311,8 @@ function get_solution(model::ManufacturingModel)
|
||||
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]
|
||||
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"]
|
||||
|
||||
@@ -15,7 +15,6 @@ using RELOG
|
||||
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)
|
||||
|
||||
|
||||
@test length(centers) == 10
|
||||
@test centers[1].name == "C1"
|
||||
@test centers[1].latitude == 7
|
||||
@@ -32,12 +31,16 @@ using RELOG
|
||||
@test plant.input.name == "P1"
|
||||
@test plant.latitude == 0
|
||||
@test plant.longitude == 0
|
||||
@test plant.opening_cost == [500, 500]
|
||||
@test plant.fixed_operating_cost == [30, 30]
|
||||
@test plant.variable_operating_cost == [30, 30]
|
||||
@test plant.base_capacity == 250
|
||||
@test plant.max_capacity == 1000
|
||||
@test plant.expansion_cost == [1, 1]
|
||||
|
||||
@test length(plant.sizes) == 2
|
||||
@test plant.sizes[1].capacity == 250
|
||||
@test plant.sizes[1].opening_cost == [500, 500]
|
||||
@test plant.sizes[1].fixed_operating_cost == [30, 30]
|
||||
@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"]
|
||||
p3 = product_name_to_product["P3"]
|
||||
@@ -54,12 +57,13 @@ using RELOG
|
||||
@test plant.input.name == "P2"
|
||||
@test plant.latitude == 25
|
||||
@test plant.longitude == 65
|
||||
@test plant.opening_cost == [3000, 3000]
|
||||
@test plant.fixed_operating_cost == [50, 50]
|
||||
@test plant.variable_operating_cost == [50, 50]
|
||||
@test plant.base_capacity == 1e8
|
||||
@test plant.max_capacity == 1e8
|
||||
@test plant.expansion_cost == [0, 0]
|
||||
|
||||
@test length(plant.sizes) == 2
|
||||
@test plant.sizes[1].capacity == 1000.0
|
||||
@test plant.sizes[1].opening_cost == [3000, 3000]
|
||||
@test plant.sizes[1].fixed_operating_cost == [50, 50]
|
||||
@test plant.sizes[1].variable_operating_cost == [50, 50]
|
||||
@test plant.sizes[1] == plant.sizes[2]
|
||||
|
||||
p4 = product_name_to_product["P4"]
|
||||
@test plant.output[p3] == 0.05
|
||||
|
||||
Reference in New Issue
Block a user