Implement initial plant capacity

feature/CapEx
Alinson S. Xavier 3 years ago
parent 1f3a3c9317
commit 256b863c34
Signed by: isoron
GPG Key ID: 0DA8E4B9E1109DCA

@ -111,12 +111,13 @@ The **plants** section describes the available types of reverse manufacturing pl
Each type of plant is associated with a set of potential locations where it can be built. Each location is represented by a dictionary with the following keys: Each type of plant is associated with a set of potential locations where it can be built. Each location is represented by a dictionary with the following keys:
| Key | Description | | Key | Description |
| :------------------- | -------------------------------------------------------------------------------- | | :------------------------- | -------------------------------------------------------------------------------- |
| `latitude (deg)` | The latitude of the location, in degrees. | | `latitude (deg)` | The latitude of the location, in degrees. |
| `longitude (deg)` | The longitude of the location, in degrees. | | `longitude (deg)` | The longitude of the location, in degrees. |
| `disposal` | A dictionary describing what products can be disposed locally at the plant. | | `disposal` | A dictionary describing what products can be disposed locally at the plant. |
| `storage` | A dictionary describing the plant's storage. | | `storage` | A dictionary describing the plant's storage. |
| `capacities (tonne)` | A dictionary describing what plant sizes are allowed, and their characteristics. | | `capacities (tonne)` | A dictionary describing what plant sizes are allowed, and their characteristics. |
| `initial capacity (tonne)` | Capacity already available at this location. Optional. |
The `storage` dictionary should contain the following keys: The `storage` dictionary should contain the following keys:

@ -25,9 +25,10 @@ In this page, we describe the precise mathematical optimization model used by RE
| $c^\text{open}_{pt}$ | Cost of opening plant $p$ at time $t$, at minimum capacity | $ | | $c^\text{open}_{pt}$ | Cost of opening plant $p$ at time $t$, at minimum capacity | $ |
| $c^\text{p-disp}_{pmt}$ | Cost of disposing recovered material $m$ at plant $p$ during time $t$ | \$/tonne/km | | $c^\text{p-disp}_{pmt}$ | Cost of disposing recovered material $m$ at plant $p$ during time $t$ | \$/tonne/km |
| $c^\text{store}_{pt}$ | Cost of storing primary material at plant $p$ at time $t$ | \$/tonne | | $c^\text{store}_{pt}$ | Cost of storing primary material at plant $p$ at time $t$ | \$/tonne |
| $c^\text{var}_{pt}$ | Variable cost of processing primary material at plant $p$ at time $t$ | \$/tonne | | $c^\text{proc}_{pt}$ | Variable cost of processing primary material at plant $p$ at time $t$ | \$/tonne |
| $m^\text{max}_p$ | Maximum capacity of plant $p$ | tonne | | $m^\text{max}_p$ | Maximum capacity of plant $p$ | tonne |
| $m^\text{min}_p$ | Minimum capacity of plant $p$ | tonne | | $m^\text{min}_p$ | Minimum capacity of plant $p$ | tonne |
| $m^\text{init}_p$ | Initial capacity of plant $p$ | tonne |
| $m^\text{p-disp}_{pmt}$ | Maximum amount of recovered material $m$ that plant $p$ can dispose of during time $t$ | tonne | | $m^\text{p-disp}_{pmt}$ | Maximum amount of recovered material $m$ that plant $p$ can dispose of during time $t$ | tonne |
| $m^\text{store}_p$ | Maximum amount of primary material that plant $p$ can store for later processing. | tonne | | $m^\text{store}_p$ | Maximum amount of primary material that plant $p$ can store for later processing. | tonne |
@ -72,7 +73,7 @@ RELOG minimizes the overall capital, production and transportation costs:
\sum_{t \in T} \sum_{p \in P} \left[ \sum_{t \in T} \sum_{p \in P} \left[
c^\text{open}_{pt} u_{pt} + c^\text{open}_{pt} u_{pt} +
c^\text{f-base}_{pt} x_{pt} + c^\text{f-base}_{pt} x_{pt} +
\sum_{i=1}^t c^\text{f-exp}_{pt} w_{pi} + c^\text{f-exp}_{pt} \left( \sum_{i=0}^t w_{pi} \right) +
c^{\text{exp}}_{pt} w_{pt} c^{\text{exp}}_{pt} w_{pt}
\right] + \\ \right] + \\
& &
@ -138,7 +139,7 @@ In the fifth line, we have acquisition and disposal cost at the collection cente
```math ```math
\begin{align*} \begin{align*}
& z^{\text{proc}}_{pt} \leq m^\text{min}_p x_p + \sum_{i=1}^t w_p & z^{\text{proc}}_{pt} \leq m^\text{min}_p x_p + \sum_{i=0}^t w_p
& \forall p \in P, t \in T & \forall p \in P, t \in T
\end{align*} \end{align*}
``` ```
@ -156,7 +157,7 @@ In the fifth line, we have acquisition and disposal cost at the collection cente
```math ```math
\begin{align*} \begin{align*}
& \sum_{i=1}^t w_p \leq m^\text{max}_p x_p & \sum_{i=0}^t w_p \leq \left( m^\text{max}_p - m^\text{min}_p \right) x_p
& \forall p \in P, t \in T & \forall p \in P, t \in T
\end{align*} \end{align*}
``` ```
@ -184,9 +185,19 @@ In the fifth line, we have acquisition and disposal cost at the collection cente
```math ```math
\begin{align*} \begin{align*}
& x_{pt} = x_{p,t-1} + u_{pt} & x_{pt} = x_{p,t-1} + u_{pt}
& \forall p \in P, t \in T \setminus \{1\} \\ & \forall p \in P, t \in T \\
& x_{p,1} = u_{p,1} \end{align*}
& \forall p \in P ```
- Boundary constants:
```math
\begin{align*}
& x_{p,0} = \begin{cases}
0 & \text{ if } m^\text{init}_p = 0 \\
1 & \text{ otherwise }
\end{cases} \\
& w_{p,0} = \max\left\{0, m^\text{init}_p - m^\text{min}_p \right\}
\end{align*} \end{align*}
``` ```

@ -76,14 +76,14 @@ function parse(json)::Instance
prod_centers = [] prod_centers = []
product = Product( product = Product(
product_name, acquisition_cost = acquisition_cost,
cost, collection_centers = prod_centers,
energy, disposal_cost = disposal_cost,
emissions, disposal_limit = disposal_limit,
disposal_limit, name = product_name,
disposal_cost, transportation_cost = cost,
acquisition_cost, transportation_emissions = emissions,
prod_centers, transportation_energy = energy,
) )
push!(products, product) push!(products, product)
prod_name_to_product[product_name] = product prod_name_to_product[product_name] = product
@ -97,12 +97,12 @@ function parse(json)::Instance
center_dict["longitude (deg)"] = region.centroid.lon center_dict["longitude (deg)"] = region.centroid.lon
end end
center = CollectionCenter( center = CollectionCenter(
length(collection_centers) + 1, amount = center_dict["amount (tonne)"],
center_name, index = length(collection_centers) + 1,
center_dict["latitude (deg)"], latitude = center_dict["latitude (deg)"],
center_dict["longitude (deg)"], longitude = center_dict["longitude (deg)"],
product, name = center_name,
center_dict["amount (tonne)"], product = product,
) )
push!(prod_centers, center) push!(prod_centers, center)
push!(collection_centers, center) push!(collection_centers, center)
@ -164,16 +164,22 @@ function parse(json)::Instance
push!( push!(
sizes, sizes,
PlantSize( PlantSize(
Base.parse(Float64, capacity_name), capacity = Base.parse(Float64, capacity_name),
capacity_dict["variable operating cost (\$/tonne)"], fixed_operating_cost = capacity_dict["fixed operating cost (\$)"],
capacity_dict["fixed operating cost (\$)"], opening_cost = capacity_dict["opening cost (\$)"],
capacity_dict["opening cost (\$)"], variable_operating_cost = capacity_dict["variable operating cost (\$/tonne)"],
), ),
) )
end end
length(sizes) > 1 || push!(sizes, sizes[1]) length(sizes) > 1 || push!(sizes, sizes[1])
sort!(sizes, by = x -> x.capacity) sort!(sizes, by = x -> x.capacity)
# Initial capacity
initial_capacity = 0
if "initial capacity (tonne)" in keys(location_dict)
initial_capacity = location_dict["initial capacity (tonne)"]
end
# Storage # Storage
storage_limit = 0 storage_limit = 0
storage_cost = zeros(T) storage_cost = zeros(T)
@ -192,20 +198,21 @@ function parse(json)::Instance
end end
plant = Plant( plant = Plant(
length(plants) + 1, disposal_cost = disposal_cost,
plant_name, disposal_limit = disposal_limit,
location_name, emissions = emissions,
input, energy = energy,
output, index = length(plants) + 1,
location_dict["latitude (deg)"], initial_capacity = initial_capacity,
location_dict["longitude (deg)"], input = input,
disposal_limit, latitude = location_dict["latitude (deg)"],
disposal_cost, location_name = location_name,
sizes, longitude = location_dict["longitude (deg)"],
energy, output = output,
emissions, plant_name = plant_name,
storage_limit, sizes = sizes,
storage_cost, storage_cost = storage_cost,
storage_limit = storage_limit,
) )
push!(plants, plant) push!(plants, plant)
@ -216,11 +223,11 @@ function parse(json)::Instance
@info @sprintf("%12d candidate plant locations", length(plants)) @info @sprintf("%12d candidate plant locations", length(plants))
return Instance( return Instance(
T, time = T,
products, products = products,
collection_centers, collection_centers = collection_centers,
plants, plants = plants,
building_period, building_period = building_period,
distance_metric, distance_metric = distance_metric,
) )
end end

@ -8,48 +8,49 @@ using JSONSchema
using Printf using Printf
using Statistics using Statistics
mutable struct Product Base.@kwdef mutable struct Product
acquisition_cost::Vector{Float64}
collection_centers::Vector
disposal_cost::Vector{Float64}
disposal_limit::Vector{Float64}
name::String name::String
transportation_cost::Vector{Float64} transportation_cost::Vector{Float64}
transportation_energy::Vector{Float64}
transportation_emissions::Dict{String,Vector{Float64}} transportation_emissions::Dict{String,Vector{Float64}}
disposal_limit::Vector{Float64} transportation_energy::Vector{Float64}
disposal_cost::Vector{Float64}
acquisition_cost::Vector{Float64}
collection_centers::Vector
end end
mutable struct CollectionCenter Base.@kwdef mutable struct CollectionCenter
amount::Vector{Float64}
index::Int64 index::Int64
name::String
latitude::Float64 latitude::Float64
longitude::Float64 longitude::Float64
name::String
product::Product product::Product
amount::Vector{Float64}
end end
mutable struct PlantSize Base.@kwdef mutable struct PlantSize
capacity::Float64 capacity::Float64
variable_operating_cost::Vector{Float64}
fixed_operating_cost::Vector{Float64} fixed_operating_cost::Vector{Float64}
opening_cost::Vector{Float64} opening_cost::Vector{Float64}
variable_operating_cost::Vector{Float64}
end end
mutable struct Plant Base.@kwdef mutable struct Plant
disposal_cost::Dict{Product,Vector{Float64}}
disposal_limit::Dict{Product,Vector{Float64}}
emissions::Dict{String,Vector{Float64}}
energy::Vector{Float64}
index::Int64 index::Int64
plant_name::String initial_capacity::Float64
location_name::String
input::Product input::Product
output::Dict{Product,Float64}
latitude::Float64 latitude::Float64
location_name::String
longitude::Float64 longitude::Float64
disposal_limit::Dict{Product,Vector{Float64}} output::Dict{Product,Float64}
disposal_cost::Dict{Product,Vector{Float64}} plant_name::String
sizes::Vector{PlantSize} sizes::Vector{PlantSize}
energy::Vector{Float64}
emissions::Dict{String,Vector{Float64}}
storage_limit::Float64
storage_cost::Vector{Float64} storage_cost::Vector{Float64}
storage_limit::Float64
end end
@ -62,11 +63,11 @@ end
mutable struct EuclideanDistance <: DistanceMetric end mutable struct EuclideanDistance <: DistanceMetric end
mutable struct Instance Base.@kwdef mutable struct Instance
time::Int64
products::Vector{Product}
collection_centers::Vector{CollectionCenter}
plants::Vector{Plant}
building_period::Vector{Int64} building_period::Vector{Int64}
collection_centers::Vector{CollectionCenter}
distance_metric::DistanceMetric distance_metric::DistanceMetric
plants::Vector{Plant}
products::Vector{Product}
time::Int64
end end

@ -44,7 +44,7 @@ function create_vars!(model::JuMP.Model)
(n, t) => @variable(model, 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
) )
model[:is_open] = Dict( model[:is_open] = Dict{Tuple,Any}(
(n, t) => @variable(model, 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
) )
@ -55,13 +55,21 @@ function create_vars!(model::JuMP.Model)
upper_bound = n.location.sizes[2].capacity 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
) )
model[:expansion] = Dict( model[:expansion] = Dict{Tuple,Any}(
(n, t) => @variable( (n, t) => @variable(
model, 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
) )
# Boundary constants
for n in values(graph.process_nodes)
m_init = n.location.initial_capacity
m_min = n.location.sizes[1].capacity
model[:is_open][n, 0] = m_init == 0 ? 0 : 1
model[:expansion][n, 0] = max(0, m_init - m_min)
end
end end
@ -132,6 +140,7 @@ function create_objective_function!(model::JuMP.Model)
) )
else else
add_to_expression!(obj, slope_open(n.location, t), model[:expansion][n, t]) add_to_expression!(obj, slope_open(n.location, t), model[:expansion][n, t])
add_to_expression!(obj, -slope_open(n.location, 1) * model[:expansion][n, 0])
end end
end end
@ -244,11 +253,11 @@ function create_process_node_constraints!(model::JuMP.Model)
# Can only process up to capacity # Can only process up to capacity
@constraint(model, model[:process][n, t] <= model[:capacity][n, t]) @constraint(model, model[:process][n, t] <= model[:capacity][n, t])
if t > 1
# Plant capacity can only increase over time # Plant capacity can only increase over time
if t > 1
@constraint(model, model[:capacity][n, t] >= model[:capacity][n, t-1]) @constraint(model, model[:capacity][n, t] >= model[:capacity][n, t-1])
@constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
end end
@constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
# Amount received equals amount processed plus stored # Amount received equals amount processed plus stored
store_in = 0 store_in = 0
@ -266,14 +275,10 @@ function create_process_node_constraints!(model::JuMP.Model)
# 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
@constraint( @constraint(
model, model,
model[:is_open][n, t] == model[:is_open][n, t-1] + model[:open_plant][n, t] model[:is_open][n, t] == model[:is_open][n, t-1] + model[:open_plant][n, t]
) )
else
@constraint(model, model[:is_open][n, t] == model[:open_plant][n, t])
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

@ -96,7 +96,10 @@ function get_solution(model::JuMP.Model; marginal_costs = true)
"Expansion cost (\$)" => [ "Expansion cost (\$)" => [
( (
if t == 1 if t == 1
slope_open(plant, t) * JuMP.value(model[:expansion][process_node, t]) slope_open(plant, t) * (
JuMP.value(model[:expansion][process_node, t]) -
model[:expansion][process_node, 0]
)
else else
slope_open(plant, t) * ( slope_open(plant, t) * (
JuMP.value(model[:expansion][process_node, t]) - JuMP.value(model[:expansion][process_node, t]) -

@ -59,6 +59,7 @@ function _fix_plants!(model_old, model_new)::Nothing
# Fix is_open variables # Fix is_open variables
for ((node_old, t), var_old) in model_old[:is_open] for ((node_old, t), var_old) in model_old[:is_open]
t > 0 || continue
value_old = JuMP.value(var_old) value_old = JuMP.value(var_old)
node_new = model_new[:graph].name_to_process_node_map[( node_new = model_new[:graph].name_to_process_node_map[(
node_old.location.plant_name, node_old.location.plant_name,
@ -84,6 +85,7 @@ function _fix_plants!(model_old, model_new)::Nothing
# Fix plant expansion # Fix plant expansion
for ((node_old, t), var_old) in model_old[:expansion] for ((node_old, t), var_old) in model_old[:expansion]
t > 0 || continue
value_old = JuMP.value(var_old) value_old = JuMP.value(var_old)
node_new = model_new[:graph].name_to_process_node_map[( node_new = model_new[:graph].name_to_process_node_map[(
node_old.location.plant_name, node_old.location.plant_name,

@ -65,6 +65,9 @@
"longitude (deg)": { "longitude (deg)": {
"type": "number" "type": "number"
}, },
"initial capacity (tonne)": {
"type": "number"
},
"disposal": { "disposal": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {

@ -103,6 +103,7 @@
"limit (tonne)": [1.0, 1.0] "limit (tonne)": [1.0, 1.0]
} }
}, },
"initial capacity (tonne)": 500.0,
"capacities (tonne)": { "capacities (tonne)": {
"250.0": { "250.0": {
"opening cost ($)": [500.0, 500.0], "opening cost ($)": [500.0, 500.0],

@ -29,6 +29,7 @@ function instance_parse_test()
@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.initial_capacity == 500.0
@test length(plant.sizes) == 2 @test length(plant.sizes) == 2
@test plant.sizes[1].capacity == 250 @test plant.sizes[1].capacity == 250
@ -64,6 +65,7 @@ function instance_parse_test()
@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.initial_capacity == 0
@test length(plant.sizes) == 2 @test length(plant.sizes) == 2
@test plant.sizes[1].capacity == 1000.0 @test plant.sizes[1].capacity == 1000.0

@ -21,9 +21,12 @@ function model_build_test()
@test length(model[:plant_dispose]) == 16 @test length(model[:plant_dispose]) == 16
@test length(model[:open_plant]) == 12 @test length(model[:open_plant]) == 12
@test length(model[:capacity]) == 12 @test length(model[:capacity]) == 12
@test length(model[:expansion]) == 12 @test length(model[:expansion]) == 18
l1 = process_node_by_location_name["L1"] l1 = process_node_by_location_name["L1"]
@test model[:is_open][l1, 0] == 1
@test model[:expansion][l1, 0] == 250
v = model[: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
@ -35,5 +38,9 @@ function model_build_test()
v = model[:plant_dispose][shipping_node_by_loc_and_prod_names["L1", "P2"], 1] v = model[:plant_dispose][shipping_node_by_loc_and_prod_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
l2 = process_node_by_location_name["L2"]
@test model[:is_open][l2, 0] == 0
@test model[:expansion][l2, 0] == 0
end end
end end

Loading…
Cancel
Save