diff --git a/instances/s1.json b/instances/s1.json index 95c8fdc..ac1b1b6 100644 --- a/instances/s1.json +++ b/instances/s1.json @@ -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] diff --git a/src/docs/model.md b/src/docs/model.md index 1a03486..54b4fb7 100644 --- a/src/docs/model.md +++ b/src/docs/model.md @@ -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. \ No newline at end of file diff --git a/src/instance.jl b/src/instance.jl index 14cb0e4..df33cf8 100644 --- a/src/instance.jl +++ b/src/instance.jl @@ -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,48 +95,51 @@ 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, plant_name, location_name, @@ -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 diff --git a/src/model.jl b/src/model.jl index 5265911..5b9212f 100644 --- a/src/model.jl +++ b/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 costs (base) + add_to_expression!(obj, + n.location.sizes[1].fixed_operating_cost[t], + vars.is_open[n, t]) - # Fixed operating costss - add_to_expression!(obj, n.location.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"] diff --git a/test/instance_test.jl b/test/instance_test.jl index 5865ad2..8e5bc5b 100644 --- a/test/instance_test.jl +++ b/test/instance_test.jl @@ -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