diff --git a/docs/src/format.md b/docs/src/format.md index 1366f21..5aea78b 100644 --- a/docs/src/format.md +++ b/docs/src/format.md @@ -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: diff --git a/docs/src/model.md b/docs/src/model.md index 514303d..c49d01b 100644 --- a/docs/src/model.md +++ b/docs/src/model.md @@ -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*} ``` diff --git a/src/instance/parse.jl b/src/instance/parse.jl index d2f8b5f..ac7b88f 100644 --- a/src/instance/parse.jl +++ b/src/instance/parse.jl @@ -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 diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 6383324..bf00d54 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -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 diff --git a/src/model/build.jl b/src/model/build.jl index 696f86c..c6699c2 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -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 diff --git a/src/model/getsol.jl b/src/model/getsol.jl index 8c9b37c..1a4dafb 100644 --- a/src/model/getsol.jl +++ b/src/model/getsol.jl @@ -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]) - diff --git a/src/model/resolve.jl b/src/model/resolve.jl index 6cd7862..5d4f2d1 100644 --- a/src/model/resolve.jl +++ b/src/model/resolve.jl @@ -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, diff --git a/src/schemas/input.json b/src/schemas/input.json index 4659024..9d9f152 100644 --- a/src/schemas/input.json +++ b/src/schemas/input.json @@ -65,6 +65,9 @@ "longitude (deg)": { "type": "number" }, + "initial capacity (tonne)": { + "type": "number" + }, "disposal": { "type": "object", "additionalProperties": { diff --git a/test/fixtures/s1.json b/test/fixtures/s1.json index 4d6f83e..ba2acd1 100644 --- a/test/fixtures/s1.json +++ b/test/fixtures/s1.json @@ -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], diff --git a/test/src/instance/parse_test.jl b/test/src/instance/parse_test.jl index add4978..7511afc 100644 --- a/test/src/instance/parse_test.jl +++ b/test/src/instance/parse_test.jl @@ -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 diff --git a/test/src/model/build_test.jl b/test/src/model/build_test.jl index f64a4c9..7deef22 100644 --- a/test/src/model/build_test.jl +++ b/test/src/model/build_test.jl @@ -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