diff --git a/Project.toml b/Project.toml index bc449b8..2cec1cc 100644 --- a/Project.toml +++ b/Project.toml @@ -10,6 +10,7 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" diff --git a/README.md b/README.md index db7576a..7133fe0 100644 --- a/README.md +++ b/README.md @@ -32,45 +32,63 @@ Typical Usage ### Describing an instance -The first step when using ReverseManufacturing.jl is describing the reverse manufacturing pipeline and the relevant data. Each input file is a JSON file with two sections: `products` and `plants`. Below, we describe each section in more detail. For a concrete example, see the file `instances/samples/s2.json`. +The first step when using ReverseManufacturing.jl is describing the reverse manufacturing pipeline and the relevant data. Each input file is a JSON file with three sections: `parameters`, `products` and `plants`. Below, we describe each section in more detail. For a concrete example, see the file `instances/samples/s2.json`. + +### Parameters + +The **parameters** section describes details about the simulation itself. + +| Key | Description +|:------------------------|---------------| +|`time periods` | Number of time periods in the simulation. #### Products The **products** section describes all products and subproducts in the simulation. The field `instance["products"]` is a dictionary mapping the name of the product to a dictionary which describes its characteristics. Each product description contains the following keys: -* `transportation cost`, the cost (in dollars per km per kg) to transport this product. -* `initial amounts,` a dictionary mapping the name of each location to its description (see below). If this product is not initially available, this key may be omitted. +| Key | Description +|:------------------------|---------------| +|`transportation cost` | The cost (in dollars per km per tonnes) to transport this product. Must be a timeseries. +|`initial amounts` | A dictionary mapping the name of each location to its description (see below). If this product is not initially available, this key may be omitted. Must be a timeseries. Each product may have some amount available at the beginning of the simulation. In this case, the key `initial amounts` maps to a dictionary with the following keys: -* `latitude`, the latitude of the location, in degrees. -* `longitude`, the longitude of the location, in degrees. -* `amount`, the amount (in kg) of the product initially available at the location. +| Key | Description +|:------------------------|---------------| +| `latitude` | The latitude of the location, in degrees. +| `longitude` | The longitude of the location, in degrees. +| `amount` | The amount (in tonnes) of the product initially available at the location. Must be a timeseries. #### Processing Plants The **plants** section describes the available types of reverse manufacturing plants, their potential locations and associated costs, as well as their inputs and outputs. The field `instance["plants"]` is a dictionary mapping the name of the plant to a dictionary with the following keys: -* `input`, the name of the product that this plant takes as input. Only one input is accepted per plant. -* `outputs`, a dictionary specifying how many kg of each product is produced for each kg of input. For example, if the plant outputs 0.5 kg of P2 and 0.25 kg of P3 for each kg of P1 provided, then this entry should be `{"P2": 0.5, "P3": 0.25}`. If the plant does not output anything, this key may be omitted. -* `locations`, a dictionary mapping the name of the location to a dictionary which describes the site characteristics (see below). +| Key | Description +|:------------------------|---------------| +| `input` | The name of the product that this plant takes as input. Only one input is accepted per plant. +| `outputs` | A dictionary specifying how many tonnes of each product is produced for each tonnes of input. For example, if the plant outputs 0.5 tonnes of P2 and 0.25 tonnes of P3 for each tonnes of P1 provided, then this entry should be `{"P2": 0.5, "P3": 0.25}`. If the plant does not output anything, this key may be omitted. +| `locations` | A dictionary mapping the name of the location to a dictionary which describes the site characteristics (see below). 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: -* `latitude`, the latitude of the location, in degrees. -* `longitude`, the longitude of the location, in degrees. -* `opening cost`, the cost (in dollars) to open the plant. -* `fixed operating cost`, the cost (in dollars) to keep the plant open, even if the plant doesn't process anything. -* `variable operating cost`, the cost (in dollars per kg) that the plant incurs to process each kg of input. -* `base capacity`, the amount of input (in kg) the plant can process when zero dollars are spent on expansion. If unlimited, this key may be omitted. -* `max capacity`, the amount (in kg) the plant can process when the maximum amount of dollars are spent on expansion. If unlimited, this key may be omitted. -* `expansion cost`, the cost (in dollars per kg) to increase the plant capacity beyond its base capacity. If zero, this key may be omitted. -* `disposal`, a dictionary describing what products can be disposed locally at the plant. +| Key | Description +|:------------------------|---------------| +| `latitude` | The latitude of the location, in degrees. +| `longitude` | The longitude of the location, in degrees. +| `opening cost` | The cost (in dollars) to open the plant. +| `fixed operating cost` | The cost (in dollars) to keep the plant open, even if the plant doesn't process anything. Must be a timeseries. +| `variable operating cost` | The cost (in dollars per tonnes) that the plant incurs to process each tonnes of input. Must be a timeseries. +| `base capacity` | The amount of input (in tonnes) the plant can process when zero dollars are spent on expansion. If unlimited, this key may be omitted. +| `max capacity` | The amount (in tonnes) the plant can process when the maximum amount of dollars are spent on expansion. If unlimited, this key may be omitted. +| `expansion cost` | The cost (in dollars per tonnes) to increase the plant capacity beyond its base capacity. If zero, this key may be omitted. Must be a timeseries. +| `disposal` | A dictionary describing what products can be disposed locally at the plant. The keys in the disposal dictionary should be the names of the products. The values are dictionaries with the following keys: -* `cost`, the cost (in dollars per kg) to dispose of the product. -* `limit`, the maximum amount (in kg) that can be disposed of. If an unlimited amount can be disposed, this key may be omitted. +| Key | Description +|:------------------------|---------------| +| `cost` | The cost (in dollars per tonnes) to dispose of the product. Must be a timeseries. +| `limit` | The maximum amount (in tonnes) that can be disposed of. If an unlimited amount can be disposed, this key may be omitted. Must be a timeseries. ### Optimizing @@ -83,10 +101,12 @@ ReverseManufacturing.solve("/home/user/instance.json") The optimal logistics plan will be printed to the screen. -Current Limitations -------------------- -* Each plant is only allowed exactly one product as input -* No support for multi-period simulations +Model Assumptions +----------------- +* 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. +* Variable and fixed operating costs do not change according to plant size. +* 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. Authors ------- diff --git a/instances/samples/s1.json b/instances/samples/s1.json index c7112d7..5c9df2a 100644 --- a/instances/samples/s1.json +++ b/instances/samples/s1.json @@ -1,68 +1,71 @@ { + "parameters": { + "time periods": 2 + }, "products": { "P1": { - "transportation cost": 0.015, + "transportation cost": [0.015, 0.015], "initial amounts": { "C1": { "latitude": 7.0, "longitude": 7.0, - "amount": 934.56 + "amount": [934.56, 934.56] }, "C2": { "latitude": 7.0, "longitude": 19.0, - "amount": 198.95 + "amount": [198.95, 198.95] }, "C3": { "latitude": 84.0, "longitude": 76.0, - "amount": 212.97 + "amount": [212.97, 212.97] }, "C4": { "latitude": 21.0, "longitude": 16.0, - "amount": 352.19 + "amount": [352.19, 352.19] }, "C5": { "latitude": 32.0, "longitude": 92.0, - "amount": 510.33 + "amount": [510.33, 510.33] }, "C6": { "latitude": 14.0, "longitude": 62.0, - "amount": 471.66 + "amount": [471.66, 471.66] }, "C7": { "latitude": 30.0, "longitude": 83.0, - "amount": 785.21 + "amount": [785.21, 785.21] }, "C8": { "latitude": 35.0, "longitude": 40.0, - "amount": 706.17 + "amount": [706.17, 706.17] }, "C9": { "latitude": 74.0, "longitude": 52.0, - "amount": 30.08 + "amount": [30.08, 30.08] }, "C10": { "latitude": 22.0, "longitude": 54.0, - "amount": 536.52 + "amount": [536.52, 536.52] } } }, "P2": { - "transportation cost": 0.02 + "transportation cost": [0.02, 0.02] }, "P3": { - "transportation cost": 0.0125 + "transportation cost": [0.0125, 0.0125] }, "P4": { - "transportation cost": 0.0175 + "transportation cost": [0.0175, 0.0175] } }, "plants": { @@ -76,32 +79,32 @@ "L1": { "latitude": 0.0, "longitude": 0.0, - "opening cost": 500, + "opening cost": [500, 500], "base capacity": 250.0, "max capacity": 1000.0, - "expansion cost": 1.0, - "fixed operating cost": 30.0, - "variable operating cost": 30.0, + "expansion cost": [1.0, 1.0], + "fixed operating cost": [30.0, 30.0], + "variable operating cost": [30.0, 30.0], "disposal": { "P2": { - "cost": -10.0, - "limit": 1.0 + "cost": [-10.0, -10.0], + "limit": [1.0, 1.0] }, "P3": { - "cost": -10.0, - "limit": 1.0 + "cost": [-10.0, -10.0], + "limit": [1.0, 1.0] } } }, "L2": { "latitude": 0.5, "longitude": 0.5, - "opening cost": 1000, + "opening cost": [1000, 1000], "base capacity": 0.0, "max capacity": 10000.0, - "expansion cost": 1.0, - "fixed operating cost": 50.0, - "variable operating cost": 50.0 + "expansion cost": [1.0, 1.0], + "fixed operating cost": [50.0, 50.0], + "variable operating cost": [50.0, 50.0] } } }, @@ -116,17 +119,17 @@ "latitude": 25.0, "longitude": 65.0, "capacity": 1000, - "opening cost": 3000, - "fixed operating cost": 50.0, - "variable operating cost": 50.0 + "opening cost": [3000, 3000], + "fixed operating cost": [50.0, 50.0], + "variable operating cost": [50.0, 50.0] }, "L4": { "latitude": 0.75, "longitude": 0.20, "processing cost": 250.0, - "opening cost": 3000, - "fixed operating cost": 50.0, - "variable operating cost": 50.0 + "opening cost": [3000, 3000], + "fixed operating cost": [50.0, 50.0], + "variable operating cost": [50.0, 50.0] } } }, @@ -136,9 +139,9 @@ "L5": { "latitude": 100.0, "longitude": 100.0, - "opening cost": 0.0, - "fixed operating cost": 0.0, - "variable operating cost": -15.0 + "opening cost": [0.0, 0.0], + "fixed operating cost": [0.0, 0.0], + "variable operating cost": [-15.0, -15.0] } } }, @@ -148,9 +151,9 @@ "L6": { "latitude": 50.0, "longitude": 50.0, - "opening cost": 0.0, - "fixed operating cost": 0.0, - "variable operating cost": -15.0 + "opening cost": [0.0, 0.0], + "fixed operating cost": [0.0, 0.0], + "variable operating cost": [-15.0, -15.0] } } } diff --git a/src/graph.jl b/src/graph.jl index 8cd7db3..42802f6 100644 --- a/src/graph.jl +++ b/src/graph.jl @@ -17,7 +17,7 @@ end mutable struct ProcessNode <: Node index::Int - plant::Plant + location::Plant incoming_arcs::Array{Arc} outgoing_arcs::Array{Arc} end @@ -79,8 +79,8 @@ function build_graph(instance::Instance)::Graph for dest in process_nodes_by_input_product[source.product] distance = calculate_distance(source.location.latitude, source.location.longitude, - dest.plant.latitude, - dest.plant.longitude) + dest.location.latitude, + dest.location.longitude) values = Dict("distance" => distance) arc = Arc(source, dest, values) push!(source.outgoing_arcs, arc) @@ -91,7 +91,7 @@ function build_graph(instance::Instance)::Graph # Build arcs from process nodes to shipping nodes within a plant for source in process_nodes - plant = source.plant + plant = source.location for dest in shipping_nodes_by_plant[plant] weight = plant.output[dest.product] values = Dict("weight" => weight) diff --git a/src/instance.jl b/src/instance.jl index af559ed..a490aca 100644 --- a/src/instance.jl +++ b/src/instance.jl @@ -6,38 +6,41 @@ using JSON, JSONSchema mutable struct Product name::String - transportation_cost::Float64 + transportation_cost::Array{Float64} end mutable struct CollectionCenter + index::Int64 name::String latitude::Float64 longitude::Float64 product::Product - amount::Float64 + amount::Array{Float64} end mutable struct Plant + index::Int64 plant_name::String location_name::String input::Product output::Dict{Product, Float64} latitude::Float64 longitude::Float64 - variable_operating_cost::Float64 - fixed_operating_cost::Float64 - opening_cost::Float64 + variable_operating_cost::Array{Float64} + fixed_operating_cost::Array{Float64} + opening_cost::Array{Float64} base_capacity::Float64 max_capacity::Float64 - expansion_cost::Float64 - disposal_limit::Dict{Product, Float64} - disposal_cost::Dict{Product, Float64} + expansion_cost::Array{Float64} + disposal_limit::Dict{Product, Array{Float64}} + disposal_cost::Dict{Product, Array{Float64}} end mutable struct Instance + time::Int64 products::Array{Product, 1} collection_centers::Array{CollectionCenter, 1} plants::Array{Plant, 1} @@ -49,16 +52,21 @@ function load(path::String)::Instance json = JSON.parsefile(path) schema = Schema(JSON.parsefile("$basedir/schemas/input.json")) - validation_results = JSONSchema.validate(json, schema) - if validation_results !== nothing - println(validation_results) - throw("Invalid input file") + result = JSONSchema.validate(json, schema) + if result !== nothing + if result isa JSONSchema.SingleIssue + path = join(result.path, " → ") + msg = "$(result.x) $(result.msg) in $(path)" + else + msg = convert(String, result) + end + throw(msg) end + T = json["parameters"]["time periods"] + plants = Plant[] products = Product[] collection_centers = CollectionCenter[] - plants = Plant[] - product_name_to_product = Dict{String, Product}() # Create products @@ -70,7 +78,8 @@ function load(path::String)::Instance # Create collection centers if "initial amounts" in keys(product_dict) for (center_name, center_dict) in product_dict["initial amounts"] - center = CollectionCenter(center_name, + center = CollectionCenter(length(collection_centers) + 1, + center_name, center_dict["latitude"], center_dict["longitude"], product, @@ -93,8 +102,8 @@ function load(path::String)::Instance end for (location_name, location_dict) in plant_dict["locations"] - disposal_limit = Dict(p => 0.0 for p in keys(output)) - disposal_cost = Dict(p => 0.0 for p in keys(output)) + 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 if "disposal" in keys(location_dict) @@ -106,7 +115,7 @@ function load(path::String)::Instance base_capacity = 1e8 max_capacity = 1e8 - expansion_cost = 0 + expansion_cost = [0.0 for t in 1:T] if "base capacity" in keys(location_dict) base_capacity = location_dict["base capacity"] @@ -120,7 +129,8 @@ function load(path::String)::Instance expansion_cost = location_dict["expansion cost"] end - plant = Plant(plant_name, + plant = Plant(length(plants) + 1, + plant_name, location_name, input, output, @@ -138,5 +148,5 @@ function load(path::String)::Instance end end - return Instance(products, collection_centers, plants) + return Instance(T, products, collection_centers, plants) end diff --git a/src/model.jl b/src/model.jl index 789f809..0c76261 100644 --- a/src/model.jl +++ b/src/model.jl @@ -23,60 +23,76 @@ end function create_vars!(model::ManufacturingModel) - mip, vars, graph = model.mip, model.vars, model.graph + mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time - vars.flow = Dict(a => @variable(mip, lower_bound=0) - for a in graph.arcs) + vars.flow = Dict((a, t) => @variable(mip, + lower_bound=0, + base_name="flow($(a.source.location.index),$(a.dest.location.index),$t)") + for a in graph.arcs, t in 1:T) - vars.dispose = Dict(n => @variable(mip, - lower_bound = 0, - upper_bound = n.location.disposal_limit[n.product]) - for n in values(graph.plant_shipping_nodes)) + vars.dispose = Dict((n, t) => @variable(mip, + lower_bound=0, + upper_bound=n.location.disposal_limit[n.product][t], + base_name="dispose($(n.location.index),$(n.product.name),$t)") + for n in values(graph.plant_shipping_nodes), t in 1:T) - vars.open_plant = Dict(n => @variable(mip, binary=true) - for n in values(graph.process_nodes)) - - vars.capacity = Dict(n => @variable(mip, - lower_bound = 0, - upper_bound = n.plant.max_capacity) - for n in values(graph.process_nodes)) + vars.open_plant = Dict((n, t) => @variable(mip, + binary=true, + base_name="open_plant($(n.location.index),$t)") + for n in values(graph.process_nodes), t in 1:T) + + vars.is_open = Dict((n, t) => @variable(mip, + binary=true, + base_name="is_open($(n.location.index),$t)") + for n in values(graph.process_nodes), t in 1:T) + + vars.capacity = Dict((n, t) => @variable(mip, + lower_bound = 0, + upper_bound = n.location.max_capacity, + base_name="capacity($(n.location.index),$t)") + for n in values(graph.process_nodes), t in 1:T) - vars.expansion = Dict(n => @variable(mip, - lower_bound = 0, - upper_bound = (n.plant.max_capacity - n.plant.base_capacity)) - for n in values(graph.process_nodes)) + vars.expansion = Dict((n, t) => @variable(mip, + lower_bound = 0, + upper_bound = (n.location.max_capacity - n.location.base_capacity), + base_name="expansion($(n.location.index),$t)") + for n in values(graph.process_nodes), t in 1:T) end function create_objective_function!(model::ManufacturingModel) - mip, vars, graph = model.mip, model.vars, model.graph + mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time obj = @expression(mip, 0 * @variable(mip)) # Process node costs - for n in values(graph.process_nodes) + for n in values(graph.process_nodes), t in 1:T # Transportation and variable operating costs for a in n.incoming_arcs - c = n.plant.input.transportation_cost * a.values["distance"] - c += n.plant.variable_operating_cost - add_to_expression!(obj, c, vars.flow[a]) + c = n.location.input.transportation_cost[t] * a.values["distance"] + c += n.location.variable_operating_cost[t] + add_to_expression!(obj, c, vars.flow[a, t]) end - # Fixed and opening costss - add_to_expression!(obj, - n.plant.fixed_operating_cost + n.plant.opening_cost, - vars.open_plant[n]) + # Opening costs + add_to_expression!(obj, n.location.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]) # Expansion costs - add_to_expression!(obj, n.plant.expansion_cost, - vars.expansion[n]) + if t < T + add_to_expression!(obj, + n.location.expansion_cost[t] - n.location.expansion_cost[t + 1], + vars.expansion[n, t]) + else + add_to_expression!(obj, n.location.expansion_cost[t], vars.expansion[n, t]) + end end # Disposal costs - for n in values(graph.plant_shipping_nodes) - add_to_expression!(obj, - n.location.disposal_cost[n.product], - vars.dispose[n]) + for n in values(graph.plant_shipping_nodes), t in 1:T + add_to_expression!(obj, n.location.disposal_cost[n.product][t], vars.dispose[n, t]) end @objective(mip, Min, obj) @@ -84,41 +100,56 @@ end function create_shipping_node_constraints!(model::ManufacturingModel) - mip, vars, graph = model.mip, model.vars, model.graph + mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time - # Collection centers - for n in graph.collection_shipping_nodes - @constraint(mip, sum(vars.flow[a] for a in n.outgoing_arcs) == n.location.amount) - end - - # Plants - for n in graph.plant_shipping_nodes - @constraint(mip, - sum(vars.flow[a] for a in n.incoming_arcs) == - sum(vars.flow[a] for a in n.outgoing_arcs) + vars.dispose[n]) + for t in 1:T + # Collection centers + for n in graph.collection_shipping_nodes + @constraint(mip, sum(vars.flow[a, t] for a in n.outgoing_arcs) == n.location.amount[t]) + end + + # Plants + for n in graph.plant_shipping_nodes + @constraint(mip, + sum(vars.flow[a, t] for a in n.incoming_arcs) == + sum(vars.flow[a, t] for a in n.outgoing_arcs) + vars.dispose[n, t]) + end end end function create_process_node_constraints!(model::ManufacturingModel) - mip, vars, graph = model.mip, model.vars, model.graph + mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time - for n in graph.process_nodes - + for n in graph.process_nodes, t in 1:T # Output amount is implied by input amount - input_sum = isempty(n.incoming_arcs) ? 0 : sum(vars.flow[a] for a in n.incoming_arcs) + input_sum = isempty(n.incoming_arcs) ? 0 : sum(vars.flow[a, t] for a in n.incoming_arcs) for a in n.outgoing_arcs - @constraint(mip, vars.flow[a] == a.values["weight"] * input_sum) + @constraint(mip, vars.flow[a, t] == a.values["weight"] * input_sum) end # If plant is closed, capacity is zero - @constraint(mip, vars.capacity[n] <= n.plant.max_capacity * vars.open_plant[n]) + @constraint(mip, vars.capacity[n, t] <= n.location.max_capacity * vars.is_open[n, t]) # Capacity is linked to expansion - @constraint(mip, vars.capacity[n] <= n.plant.base_capacity + vars.expansion[n]) + @constraint(mip, vars.capacity[n, t] <= n.location.base_capacity + vars.expansion[n, t]) # Input sum must be smaller than capacity - @constraint(mip, input_sum <= vars.capacity[n]) + @constraint(mip, input_sum <= vars.capacity[n, t]) + + if t > 1 + # Plant capacity can only increase over time + @constraint(mip, vars.capacity[n, t] >= vars.capacity[n, t-1]) + @constraint(mip, vars.expansion[n, t] >= vars.expansion[n, t-1]) + end + + # 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(mip, vars.is_open[n, t] == vars.is_open[n, t-1] + vars.open_plant[n, t]) + else + @constraint(mip, vars.is_open[n, t] == vars.open_plant[n, t]) + end end end @@ -141,19 +172,22 @@ end function get_solution(model::ManufacturingModel) mip, vars, graph, instance = model.mip, model.vars, model.graph, model.instance + T = instance.time + output = Dict( "plants" => Dict(), "costs" => Dict( - "fixed" => 0.0, - "variable" => 0.0, - "transportation" => 0.0, - "disposal" => 0.0, - "total" => 0.0, - "expansion" => 0.0, + "fixed operating" => zeros(T), + "variable operating" => zeros(T), + "opening" => zeros(T), + "transportation" => zeros(T), + "disposal" => zeros(T), + "expansion" => zeros(T), + "total" => zeros(T), ) ) - plant_to_process_node = Dict(n.plant => n for n in graph.process_nodes) + plant_to_process_node = Dict(n.location => n for n in graph.process_nodes) plant_to_shipping_nodes = Dict() for p in instance.plants plant_to_shipping_nodes[p] = [] @@ -163,7 +197,7 @@ function get_solution(model::ManufacturingModel) end for plant in instance.plants - skip_plant = true + skip_plant = false process_node = plant_to_process_node[plant] plant_dict = Dict{Any, Any}( "input" => Dict(), @@ -171,31 +205,44 @@ function get_solution(model::ManufacturingModel) "send" => Dict(), "dispose" => Dict(), ), - "total input" => 0.0, + "total input" => [0.0 for t in 1:T], "total output" => Dict(), "latitude" => plant.latitude, "longitude" => plant.longitude, - "capacity" => JuMP.value(vars.capacity[process_node]), - "fixed cost" => JuMP.value(vars.open_plant[process_node]) * (plant.opening_cost + plant.fixed_operating_cost), - "expansion cost" => JuMP.value(vars.expansion[process_node]) * plant.expansion_cost, + "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] + for t in 1:T], + "fixed operating cost" => [JuMP.value(vars.is_open[process_node, t]) * plant.fixed_operating_cost[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]) + else + JuMP.value(vars.expansion[process_node, t]) + end) + for t in 1:T], ) - output["costs"]["fixed"] += plant_dict["fixed cost"] + output["costs"]["fixed operating"] += plant_dict["fixed operating cost"] + output["costs"]["opening"] += plant_dict["opening cost"] output["costs"]["expansion"] += plant_dict["expansion cost"] # Inputs for a in process_node.incoming_arcs - val = JuMP.value(vars.flow[a]) - if val <= 1e-3 + vals = [JuMP.value(vars.flow[a, t]) for t in 1:T] + if sum(vals) <= 1e-3 continue end skip_plant = false dict = Dict{Any, Any}( - "amount" => val, + "amount" => vals, "distance" => a.values["distance"], "latitude" => a.source.location.latitude, "longitude" => a.source.location.longitude, - "transportation cost" => a.source.product.transportation_cost * val, - "variable operating cost" => plant.variable_operating_cost * val, + "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] + for t in 1:T], ) if a.source.location isa CollectionCenter plant_name = "Origin" @@ -209,45 +256,45 @@ function get_solution(model::ManufacturingModel) plant_dict["input"][plant_name] = Dict() end plant_dict["input"][plant_name][location_name] = dict - plant_dict["total input"] += val + plant_dict["total input"] += vals output["costs"]["transportation"] += dict["transportation cost"] - output["costs"]["variable"] += dict["variable operating cost"] + output["costs"]["variable operating"] += dict["variable operating cost"] end - # Outputs - for shipping_node in plant_to_shipping_nodes[plant] - product_name = shipping_node.product.name - plant_dict["total output"][product_name] = 0.0 - plant_dict["output"]["send"][product_name] = product_dict = Dict() +# # Outputs +# for shipping_node in plant_to_shipping_nodes[plant] +# product_name = shipping_node.product.name +# plant_dict["total output"][product_name] = 0.0 +# plant_dict["output"]["send"][product_name] = product_dict = Dict() - disposal_amount = JuMP.value(vars.dispose[shipping_node]) - if disposal_amount > 1e-5 - plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict() - disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node]) - disposal_dict["cost"] = disposal_dict["amount"] * plant.disposal_cost[shipping_node.product] - plant_dict["total output"][product_name] += disposal_amount - output["costs"]["disposal"] += disposal_dict["cost"] - end +# disposal_amount = JuMP.value(vars.dispose[shipping_node]) +# if disposal_amount > 1e-5 +# plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict() +# disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node]) +# disposal_dict["cost"] = disposal_dict["amount"] * plant.disposal_cost[shipping_node.product] +# plant_dict["total output"][product_name] += disposal_amount +# output["costs"]["disposal"] += disposal_dict["cost"] +# end - for a in shipping_node.outgoing_arcs - val = JuMP.value(vars.flow[a]) - if val <= 1e-3 - continue - end - skip_plant = false - dict = Dict( - "amount" => val, - "distance" => a.values["distance"], - "latitude" => a.dest.plant.latitude, - "longitude" => a.dest.plant.longitude, - ) - if a.dest.plant.plant_name ∉ keys(product_dict) - product_dict[a.dest.plant.plant_name] = Dict() - end - product_dict[a.dest.plant.plant_name][a.dest.plant.location_name] = dict - plant_dict["total output"][product_name] += val - end - end +# for a in shipping_node.outgoing_arcs +# val = JuMP.value(vars.flow[a]) +# if val <= 1e-3 +# continue +# end +# skip_plant = false +# dict = Dict( +# "amount" => val, +# "distance" => a.values["distance"], +# "latitude" => a.dest.location.latitude, +# "longitude" => a.dest.location.longitude, +# ) +# if a.dest.location.plant_name ∉ keys(product_dict) +# product_dict[a.dest.location.plant_name] = Dict() +# end +# product_dict[a.dest.location.plant_name][a.dest.location.location_name] = dict +# plant_dict["total output"][product_name] += val +# end +# end if !skip_plant if plant.plant_name ∉ keys(output["plants"]) diff --git a/src/schemas/input.json b/src/schemas/input.json index 6c5e6a8..339117c 100644 --- a/src/schemas/input.json +++ b/src/schemas/input.json @@ -3,6 +3,21 @@ "$id": "https://axavier.org/ReverseManufacturing/input/schema", "title": "Schema for ReverseManufacturing Input File", "definitions": { + "TimeSeries": { + "type": "array", + "items": { + "type": "number" + } + }, + "Parameters": { + "type": "object", + "properties": { + "time": { "type": "number" } + }, + "required": [ + "time periods" + ] + }, "Plant": { "type": "object", "additionalProperties": { @@ -28,19 +43,19 @@ "properties": { "latitude": { "type": "number" }, "longitude": { "type": "number" }, - "variable operating cost": { "type": "number" }, - "fixed operating cost": { "type": "number" }, - "opening cost": { "type": "number" }, + "variable operating cost": { "$ref": "#/definitions/TimeSeries" }, + "fixed operating cost": { "$ref": "#/definitions/TimeSeries" }, + "opening cost": { "$ref": "#/definitions/TimeSeries" }, "base capacity": { "type": "number" }, "max capacity": { "type": "number" }, - "expansion cost": { "type": "number" }, + "expansion cost": { "$ref": "#/definitions/TimeSeries" }, "disposal": { "type": "object", "additionalProperties": { "type": "object", "properties": { - "cost": { "type": "number" }, - "limit": { "type": "number" } + "cost": { "$ref": "#/definitions/TimeSeries" }, + "limit": { "$ref": "#/definitions/TimeSeries" } }, "required": [ "cost" @@ -64,7 +79,7 @@ "properties": { "latitude": { "type": "number" }, "longitude": { "type": "number" }, - "amount": { "type": "number" } + "amount": { "$ref": "#/definitions/TimeSeries" } }, "required": [ "latitude", @@ -78,7 +93,7 @@ "additionalProperties": { "type": "object", "properties": { - "transportation cost": { "type": "number" }, + "transportation cost": { "$ref": "#/definitions/TimeSeries" }, "initial amounts": { "$ref": "#/definitions/InitialAmount" } }, "required": [ @@ -89,6 +104,7 @@ }, "type": "object", "properties": { + "parameters": { "$ref": "#/definitions/Parameters" }, "plants": { "$ref": "#/definitions/Plant" }, "products": { "$ref": "#/definitions/Product" } }, diff --git a/test/graph_test.jl b/test/graph_test.jl index fce0c40..d88554d 100644 --- a/test/graph_test.jl +++ b/test/graph_test.jl @@ -8,7 +8,7 @@ using ReverseManufacturing basedir = dirname(@__FILE__) instance = ReverseManufacturing.load("$basedir/../instances/samples/s1.json") graph = ReverseManufacturing.build_graph(instance) - process_node_by_location_name = Dict(n.plant.location_name => n + process_node_by_location_name = Dict(n.location.location_name => n for n in graph.process_nodes) @test length(graph.plant_shipping_nodes) == 8 @@ -20,19 +20,19 @@ using ReverseManufacturing @test length(node.incoming_arcs) == 0 @test length(node.outgoing_arcs) == 2 @test node.outgoing_arcs[1].source.location.name == "C1" - @test node.outgoing_arcs[1].dest.plant.plant_name == "F1" - @test node.outgoing_arcs[1].dest.plant.location_name == "L1" + @test node.outgoing_arcs[1].dest.location.plant_name == "F1" + @test node.outgoing_arcs[1].dest.location.location_name == "L1" @test node.outgoing_arcs[1].values["distance"] == 1095.62 node = process_node_by_location_name["L1"] - @test node.plant.plant_name == "F1" - @test node.plant.location_name == "L1" + @test node.location.plant_name == "F1" + @test node.location.location_name == "L1" @test length(node.incoming_arcs) == 10 @test length(node.outgoing_arcs) == 2 node = process_node_by_location_name["L3"] - @test node.plant.plant_name == "F2" - @test node.plant.location_name == "L3" + @test node.location.plant_name == "F2" + @test node.location.location_name == "L3" @test length(node.incoming_arcs) == 2 @test length(node.outgoing_arcs) == 2 diff --git a/test/instance_test.jl b/test/instance_test.jl index 9aecae5..5d0c6cf 100644 --- a/test/instance_test.jl +++ b/test/instance_test.jl @@ -21,7 +21,7 @@ using ReverseManufacturing @test centers[1].latitude == 7 @test centers[1].latitude == 7 @test centers[1].longitude == 7 - @test centers[1].amount == 934.56 + @test centers[1].amount == [934.56, 934.56] @test centers[1].product.name == "P1" @test length(plants) == 6 @@ -32,40 +32,40 @@ using ReverseManufacturing @test plant.input.name == "P1" @test plant.latitude == 0 @test plant.longitude == 0 - @test plant.opening_cost == 500 - @test plant.fixed_operating_cost == 30 - @test plant.variable_operating_cost == 30 + @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 + @test plant.expansion_cost == [1, 1] p2 = product_name_to_product["P2"] p3 = product_name_to_product["P3"] @test length(plant.output) == 2 @test plant.output[p2] == 0.2 @test plant.output[p3] == 0.5 - @test plant.disposal_limit[p2] == 1 - @test plant.disposal_limit[p3] == 1 - @test plant.disposal_cost[p2] == -10 - @test plant.disposal_cost[p3] == -10 + @test plant.disposal_limit[p2] == [1, 1] + @test plant.disposal_limit[p3] == [1, 1] + @test plant.disposal_cost[p2] == [-10, -10] + @test plant.disposal_cost[p3] == [-10, -10] plant = location_name_to_plant["L3"] @test plant.location_name == "L3" @test plant.input.name == "P2" @test plant.latitude == 25 @test plant.longitude == 65 - @test plant.opening_cost == 3000 - @test plant.fixed_operating_cost == 50 - @test plant.variable_operating_cost == 50 + @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 + @test plant.expansion_cost == [0, 0] p4 = product_name_to_product["P4"] @test plant.output[p3] == 0.05 @test plant.output[p4] == 0.8 - @test plant.disposal_limit[p3] == 0.0 - @test plant.disposal_limit[p4] == 0.0 + @test plant.disposal_limit[p3] == [0, 0] + @test plant.disposal_limit[p4] == [0, 0] end end diff --git a/test/model_test.jl b/test/model_test.jl index 67e7092..b3e53bc 100644 --- a/test/model_test.jl +++ b/test/model_test.jl @@ -1,7 +1,7 @@ # Copyright (C) 2020 Argonne National Laboratory # Written by Alinson Santos Xavier -using ReverseManufacturing, Cbc, JuMP, Printf, JSON +using ReverseManufacturing, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats @testset "Model" begin @testset "build" begin @@ -10,42 +10,46 @@ using ReverseManufacturing, Cbc, JuMP, Printf, JSON graph = ReverseManufacturing.build_graph(instance) model = ReverseManufacturing.build_model(instance, graph, Cbc.Optimizer) - process_node_by_location_name = Dict(n.plant.location_name => n + process_node_by_location_name = Dict(n.location.location_name => n for n in graph.process_nodes) shipping_node_by_location_and_product_names = Dict((n.location.location_name, n.product.name) => n for n in graph.plant_shipping_nodes) - @test length(model.vars.flow) == 38 - @test length(model.vars.dispose) == 8 - @test length(model.vars.open_plant) == 6 - @test length(model.vars.capacity) == 6 - @test length(model.vars.expansion) == 6 + @test length(model.vars.flow) == 76 + @test length(model.vars.dispose) == 16 + @test length(model.vars.open_plant) == 12 + @test length(model.vars.capacity) == 12 + @test length(model.vars.expansion) == 12 l1 = process_node_by_location_name["L1"] - v = model.vars.capacity[l1] + v = model.vars.capacity[l1, 1] @test lower_bound(v) == 0.0 @test upper_bound(v) == 1000.0 - v = model.vars.expansion[l1] + v = model.vars.expansion[l1, 1] @test lower_bound(v) == 0.0 @test upper_bound(v) == 750.0 - v = model.vars.dispose[shipping_node_by_location_and_product_names["L1", "P2"]] + v = model.vars.dispose[shipping_node_by_location_and_product_names["L1", "P2"], 1] @test lower_bound(v) == 0.0 @test upper_bound(v) == 1.0 + + dest = FileFormats.Model(format = FileFormats.FORMAT_LP) + MOI.copy_to(dest, model.mip) + MOI.write_to_file(dest, "model.lp") end @testset "solve" begin solution = ReverseManufacturing.solve("$(pwd())/../instances/samples/s1.json") - JSON.print(stdout, solution, 4) + #JSON.print(stdout, solution, 4) @test "costs" in keys(solution) - @test "fixed" in keys(solution["costs"]) + @test "fixed operating" in keys(solution["costs"]) @test "transportation" in keys(solution["costs"]) - @test "variable" in keys(solution["costs"]) + @test "variable operating" in keys(solution["costs"]) @test "total" in keys(solution["costs"]) @test "plants" in keys(solution) @@ -53,10 +57,6 @@ using ReverseManufacturing, Cbc, JuMP, Printf, JSON @test "F2" in keys(solution["plants"]) @test "F3" in keys(solution["plants"]) @test "F4" in keys(solution["plants"]) -# @test "L2" in keys(solution["plants"]["F1"]) -# @test "total output" in keys(solution["plants"]["F1"]["L2"]) - -# @test "capacity" in keys(solution["plants"]["F1"]["L1"]) end end