Implement initial plant capacity

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

@ -110,13 +110,14 @@ 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:
| Key | Description |
| :------------------- | -------------------------------------------------------------------------------- |
| `latitude (deg)` | The latitude 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. |
| `storage` | A dictionary describing the plant's storage. |
| `capacities (tonne)` | A dictionary describing what plant sizes are allowed, and their characteristics. |
| Key | Description |
| :------------------------- | -------------------------------------------------------------------------------- |
| `latitude (deg)` | The latitude 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. |
| `storage` | A dictionary describing the plant's storage. |
| `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:

@ -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{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{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{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{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[
c^\text{open}_{pt} u_{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}
\right] + \\
&
@ -138,7 +139,7 @@ In the fifth line, we have acquisition and disposal cost at the collection cente
```math
\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
\end{align*}
```
@ -156,7 +157,7 @@ In the fifth line, we have acquisition and disposal cost at the collection cente
```math
\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
\end{align*}
```
@ -184,9 +185,19 @@ In the fifth line, we have acquisition and disposal cost at the collection cente
```math
\begin{align*}
& x_{pt} = x_{p,t-1} + u_{pt}
& \forall p \in P, t \in T \setminus \{1\} \\
& x_{p,1} = u_{p,1}
& \forall p \in P
& \forall p \in P, t \in T \\
\end{align*}
```
- 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*}
```

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

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

@ -44,7 +44,7 @@ function create_vars!(model::JuMP.Model)
(n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
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),
t = 1:T
)
@ -55,13 +55,21 @@ function create_vars!(model::JuMP.Model)
upper_bound = n.location.sizes[2].capacity
) for n in values(graph.process_nodes), t = 1:T
)
model[:expansion] = Dict(
model[:expansion] = Dict{Tuple,Any}(
(n, t) => @variable(
model,
lower_bound = 0,
upper_bound = n.location.sizes[2].capacity - n.location.sizes[1].capacity
) 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
@ -132,6 +140,7 @@ function create_objective_function!(model::JuMP.Model)
)
else
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
@ -244,11 +253,11 @@ function create_process_node_constraints!(model::JuMP.Model)
# Can only process up to capacity
@constraint(model, model[:process][n, t] <= model[:capacity][n, t])
# Plant capacity can only increase over time
if t > 1
# Plant capacity can only increase over time
@constraint(model, model[:capacity][n, t] >= model[:capacity][n, t-1])
@constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
end
@constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
# Amount received equals amount processed plus stored
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
# if it was built just now
if t > 1
@constraint(
model,
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
@constraint(
model,
model[:is_open][n, t] == model[:is_open][n, t-1] + model[:open_plant][n, t]
)
# Plant can only be opened during building period
if t model[:instance].building_period

@ -96,7 +96,10 @@ function get_solution(model::JuMP.Model; marginal_costs = true)
"Expansion cost (\$)" => [
(
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
slope_open(plant, t) * (
JuMP.value(model[:expansion][process_node, t]) -

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

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

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

@ -29,6 +29,7 @@ function instance_parse_test()
@test plant.input.name == "P1"
@test plant.latitude == 0
@test plant.longitude == 0
@test plant.initial_capacity == 500.0
@test length(plant.sizes) == 2
@test plant.sizes[1].capacity == 250
@ -64,6 +65,7 @@ function instance_parse_test()
@test plant.input.name == "P2"
@test plant.latitude == 25
@test plant.longitude == 65
@test plant.initial_capacity == 0
@test length(plant.sizes) == 2
@test plant.sizes[1].capacity == 1000.0

@ -21,9 +21,12 @@ function model_build_test()
@test length(model[:plant_dispose]) == 16
@test length(model[:open_plant]) == 12
@test length(model[:capacity]) == 12
@test length(model[:expansion]) == 12
@test length(model[:expansion]) == 18
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]
@test lower_bound(v) == 0.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]
@test lower_bound(v) == 0.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

Loading…
Cancel
Save