Implement first version of multi-period simulations

v0.1
Alinson S. Xavier 5 years ago
parent f970dca68d
commit 9f6bfef327

@ -10,6 +10,7 @@ JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572" JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"

@ -32,45 +32,63 @@ Typical Usage
### Describing an instance ### 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 #### 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: 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. | Key | Description
* `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. |:------------------------|---------------|
|`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: 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. | Key | Description
* `longitude`, the longitude of the location, in degrees. |:------------------------|---------------|
* `amount`, the amount (in kg) of the product initially available at the location. | `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 #### 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: 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. | Key | Description
* `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). | `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: 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. | Key | Description
* `longitude`, the longitude of the location, in degrees. |:------------------------|---------------|
* `opening cost`, the cost (in dollars) to open the plant. | `latitude` | The latitude of the location, in degrees.
* `fixed operating cost`, the cost (in dollars) to keep the plant open, even if the plant doesn't process anything. | `longitude` | The longitude of the location, in degrees.
* `variable operating cost`, the cost (in dollars per kg) that the plant incurs to process each kg of input. | `opening cost` | The cost (in dollars) to open the plant.
* `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. | `fixed operating cost` | The cost (in dollars) to keep the plant open, even if the plant doesn't process anything. Must be a timeseries.
* `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. | `variable operating cost` | The cost (in dollars per tonnes) that the plant incurs to process each tonnes of input. Must be a timeseries.
* `expansion cost`, the cost (in dollars per kg) to increase the plant capacity beyond its base capacity. If zero, this key may be omitted. | `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.
* `disposal`, a dictionary describing what products can be disposed locally at the plant. | `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: 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. | Key | Description
* `limit`, the maximum amount (in kg) that can be disposed of. If an unlimited amount can be disposed, this key may be omitted. |:------------------------|---------------|
| `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 ### Optimizing
@ -83,10 +101,12 @@ ReverseManufacturing.solve("/home/user/instance.json")
The optimal logistics plan will be printed to the screen. The optimal logistics plan will be printed to the screen.
Current Limitations Model Assumptions
------------------- -----------------
* Each plant is only allowed exactly one product as input * Each plant can only be opened exactly once. After open, the plant remains open until the end of the simulation.
* No support for multi-period simulations * 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 Authors
------- -------

@ -1,68 +1,71 @@
{ {
"parameters": {
"time periods": 2
},
"products": { "products": {
"P1": { "P1": {
"transportation cost": 0.015, "transportation cost": [0.015, 0.015],
"initial amounts": { "initial amounts": {
"C1": { "C1": {
"latitude": 7.0, "latitude": 7.0,
"longitude": 7.0, "longitude": 7.0,
"amount": 934.56 "amount": [934.56, 934.56]
}, },
"C2": { "C2": {
"latitude": 7.0, "latitude": 7.0,
"longitude": 19.0, "longitude": 19.0,
"amount": 198.95 "amount": [198.95, 198.95]
}, },
"C3": { "C3": {
"latitude": 84.0, "latitude": 84.0,
"longitude": 76.0, "longitude": 76.0,
"amount": 212.97 "amount": [212.97, 212.97]
}, },
"C4": { "C4": {
"latitude": 21.0, "latitude": 21.0,
"longitude": 16.0, "longitude": 16.0,
"amount": 352.19 "amount": [352.19, 352.19]
}, },
"C5": { "C5": {
"latitude": 32.0, "latitude": 32.0,
"longitude": 92.0, "longitude": 92.0,
"amount": 510.33 "amount": [510.33, 510.33]
}, },
"C6": { "C6": {
"latitude": 14.0, "latitude": 14.0,
"longitude": 62.0, "longitude": 62.0,
"amount": 471.66 "amount": [471.66, 471.66]
}, },
"C7": { "C7": {
"latitude": 30.0, "latitude": 30.0,
"longitude": 83.0, "longitude": 83.0,
"amount": 785.21 "amount": [785.21, 785.21]
}, },
"C8": { "C8": {
"latitude": 35.0, "latitude": 35.0,
"longitude": 40.0, "longitude": 40.0,
"amount": 706.17 "amount": [706.17, 706.17]
}, },
"C9": { "C9": {
"latitude": 74.0, "latitude": 74.0,
"longitude": 52.0, "longitude": 52.0,
"amount": 30.08 "amount": [30.08, 30.08]
}, },
"C10": { "C10": {
"latitude": 22.0, "latitude": 22.0,
"longitude": 54.0, "longitude": 54.0,
"amount": 536.52 "amount": [536.52, 536.52]
} }
} }
}, },
"P2": { "P2": {
"transportation cost": 0.02 "transportation cost": [0.02, 0.02]
}, },
"P3": { "P3": {
"transportation cost": 0.0125 "transportation cost": [0.0125, 0.0125]
}, },
"P4": { "P4": {
"transportation cost": 0.0175 "transportation cost": [0.0175, 0.0175]
} }
}, },
"plants": { "plants": {
@ -76,32 +79,32 @@
"L1": { "L1": {
"latitude": 0.0, "latitude": 0.0,
"longitude": 0.0, "longitude": 0.0,
"opening cost": 500, "opening cost": [500, 500],
"base capacity": 250.0, "base capacity": 250.0,
"max capacity": 1000.0, "max capacity": 1000.0,
"expansion cost": 1.0, "expansion cost": [1.0, 1.0],
"fixed operating cost": 30.0, "fixed operating cost": [30.0, 30.0],
"variable operating cost": 30.0, "variable operating cost": [30.0, 30.0],
"disposal": { "disposal": {
"P2": { "P2": {
"cost": -10.0, "cost": [-10.0, -10.0],
"limit": 1.0 "limit": [1.0, 1.0]
}, },
"P3": { "P3": {
"cost": -10.0, "cost": [-10.0, -10.0],
"limit": 1.0 "limit": [1.0, 1.0]
} }
} }
}, },
"L2": { "L2": {
"latitude": 0.5, "latitude": 0.5,
"longitude": 0.5, "longitude": 0.5,
"opening cost": 1000, "opening cost": [1000, 1000],
"base capacity": 0.0, "base capacity": 0.0,
"max capacity": 10000.0, "max capacity": 10000.0,
"expansion cost": 1.0, "expansion cost": [1.0, 1.0],
"fixed operating cost": 50.0, "fixed operating cost": [50.0, 50.0],
"variable operating cost": 50.0 "variable operating cost": [50.0, 50.0]
} }
} }
}, },
@ -116,17 +119,17 @@
"latitude": 25.0, "latitude": 25.0,
"longitude": 65.0, "longitude": 65.0,
"capacity": 1000, "capacity": 1000,
"opening cost": 3000, "opening cost": [3000, 3000],
"fixed operating cost": 50.0, "fixed operating cost": [50.0, 50.0],
"variable operating cost": 50.0 "variable operating cost": [50.0, 50.0]
}, },
"L4": { "L4": {
"latitude": 0.75, "latitude": 0.75,
"longitude": 0.20, "longitude": 0.20,
"processing cost": 250.0, "processing cost": 250.0,
"opening cost": 3000, "opening cost": [3000, 3000],
"fixed operating cost": 50.0, "fixed operating cost": [50.0, 50.0],
"variable operating cost": 50.0 "variable operating cost": [50.0, 50.0]
} }
} }
}, },
@ -136,9 +139,9 @@
"L5": { "L5": {
"latitude": 100.0, "latitude": 100.0,
"longitude": 100.0, "longitude": 100.0,
"opening cost": 0.0, "opening cost": [0.0, 0.0],
"fixed operating cost": 0.0, "fixed operating cost": [0.0, 0.0],
"variable operating cost": -15.0 "variable operating cost": [-15.0, -15.0]
} }
} }
}, },
@ -148,9 +151,9 @@
"L6": { "L6": {
"latitude": 50.0, "latitude": 50.0,
"longitude": 50.0, "longitude": 50.0,
"opening cost": 0.0, "opening cost": [0.0, 0.0],
"fixed operating cost": 0.0, "fixed operating cost": [0.0, 0.0],
"variable operating cost": -15.0 "variable operating cost": [-15.0, -15.0]
} }
} }
} }

@ -17,7 +17,7 @@ end
mutable struct ProcessNode <: Node mutable struct ProcessNode <: Node
index::Int index::Int
plant::Plant location::Plant
incoming_arcs::Array{Arc} incoming_arcs::Array{Arc}
outgoing_arcs::Array{Arc} outgoing_arcs::Array{Arc}
end end
@ -79,8 +79,8 @@ function build_graph(instance::Instance)::Graph
for dest in process_nodes_by_input_product[source.product] for dest in process_nodes_by_input_product[source.product]
distance = calculate_distance(source.location.latitude, distance = calculate_distance(source.location.latitude,
source.location.longitude, source.location.longitude,
dest.plant.latitude, dest.location.latitude,
dest.plant.longitude) dest.location.longitude)
values = Dict("distance" => distance) values = Dict("distance" => distance)
arc = Arc(source, dest, values) arc = Arc(source, dest, values)
push!(source.outgoing_arcs, arc) 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 # Build arcs from process nodes to shipping nodes within a plant
for source in process_nodes for source in process_nodes
plant = source.plant plant = source.location
for dest in shipping_nodes_by_plant[plant] for dest in shipping_nodes_by_plant[plant]
weight = plant.output[dest.product] weight = plant.output[dest.product]
values = Dict("weight" => weight) values = Dict("weight" => weight)

@ -6,38 +6,41 @@ using JSON, JSONSchema
mutable struct Product mutable struct Product
name::String name::String
transportation_cost::Float64 transportation_cost::Array{Float64}
end end
mutable struct CollectionCenter mutable struct CollectionCenter
index::Int64
name::String name::String
latitude::Float64 latitude::Float64
longitude::Float64 longitude::Float64
product::Product product::Product
amount::Float64 amount::Array{Float64}
end end
mutable struct Plant mutable struct Plant
index::Int64
plant_name::String plant_name::String
location_name::String location_name::String
input::Product input::Product
output::Dict{Product, Float64} output::Dict{Product, Float64}
latitude::Float64 latitude::Float64
longitude::Float64 longitude::Float64
variable_operating_cost::Float64 variable_operating_cost::Array{Float64}
fixed_operating_cost::Float64 fixed_operating_cost::Array{Float64}
opening_cost::Float64 opening_cost::Array{Float64}
base_capacity::Float64 base_capacity::Float64
max_capacity::Float64 max_capacity::Float64
expansion_cost::Float64 expansion_cost::Array{Float64}
disposal_limit::Dict{Product, Float64} disposal_limit::Dict{Product, Array{Float64}}
disposal_cost::Dict{Product, Float64} disposal_cost::Dict{Product, Array{Float64}}
end end
mutable struct Instance mutable struct Instance
time::Int64
products::Array{Product, 1} products::Array{Product, 1}
collection_centers::Array{CollectionCenter, 1} collection_centers::Array{CollectionCenter, 1}
plants::Array{Plant, 1} plants::Array{Plant, 1}
@ -49,16 +52,21 @@ function load(path::String)::Instance
json = JSON.parsefile(path) json = JSON.parsefile(path)
schema = Schema(JSON.parsefile("$basedir/schemas/input.json")) schema = Schema(JSON.parsefile("$basedir/schemas/input.json"))
validation_results = JSONSchema.validate(json, schema) result = JSONSchema.validate(json, schema)
if validation_results !== nothing if result !== nothing
println(validation_results) if result isa JSONSchema.SingleIssue
throw("Invalid input file") path = join(result.path, "")
msg = "$(result.x) $(result.msg) in $(path)"
else
msg = convert(String, result)
end
throw(msg)
end end
T = json["parameters"]["time periods"]
plants = Plant[]
products = Product[] products = Product[]
collection_centers = CollectionCenter[] collection_centers = CollectionCenter[]
plants = Plant[]
product_name_to_product = Dict{String, Product}() product_name_to_product = Dict{String, Product}()
# Create products # Create products
@ -70,7 +78,8 @@ function load(path::String)::Instance
# Create collection centers # Create collection centers
if "initial amounts" in keys(product_dict) if "initial amounts" in keys(product_dict)
for (center_name, center_dict) in product_dict["initial amounts"] 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["latitude"],
center_dict["longitude"], center_dict["longitude"],
product, product,
@ -93,8 +102,8 @@ function load(path::String)::Instance
end end
for (location_name, location_dict) in plant_dict["locations"] for (location_name, location_dict) in plant_dict["locations"]
disposal_limit = 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 p in keys(output)) disposal_cost = Dict(p => [0.0 for t in 1:T] for p in keys(output))
# Plant disposal # Plant disposal
if "disposal" in keys(location_dict) if "disposal" in keys(location_dict)
@ -106,7 +115,7 @@ function load(path::String)::Instance
base_capacity = 1e8 base_capacity = 1e8
max_capacity = 1e8 max_capacity = 1e8
expansion_cost = 0 expansion_cost = [0.0 for t in 1:T]
if "base capacity" in keys(location_dict) if "base capacity" in keys(location_dict)
base_capacity = location_dict["base capacity"] base_capacity = location_dict["base capacity"]
@ -120,7 +129,8 @@ function load(path::String)::Instance
expansion_cost = location_dict["expansion cost"] expansion_cost = location_dict["expansion cost"]
end end
plant = Plant(plant_name, plant = Plant(length(plants) + 1,
plant_name,
location_name, location_name,
input, input,
output, output,
@ -138,5 +148,5 @@ function load(path::String)::Instance
end end
end end
return Instance(products, collection_centers, plants) return Instance(T, products, collection_centers, plants)
end end

@ -23,60 +23,76 @@ end
function create_vars!(model::ManufacturingModel) 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) vars.flow = Dict((a, t) => @variable(mip,
for a in graph.arcs) 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, vars.dispose = Dict((n, t) => @variable(mip,
lower_bound = 0, lower_bound=0,
upper_bound = n.location.disposal_limit[n.product]) upper_bound=n.location.disposal_limit[n.product][t],
for n in values(graph.plant_shipping_nodes)) 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) vars.open_plant = Dict((n, t) => @variable(mip,
for n in values(graph.process_nodes)) binary=true,
base_name="open_plant($(n.location.index),$t)")
vars.capacity = Dict(n => @variable(mip, for n in values(graph.process_nodes), t in 1:T)
lower_bound = 0,
upper_bound = n.plant.max_capacity) vars.is_open = Dict((n, t) => @variable(mip,
for n in values(graph.process_nodes)) 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, vars.expansion = Dict((n, t) => @variable(mip,
lower_bound = 0, lower_bound = 0,
upper_bound = (n.plant.max_capacity - n.plant.base_capacity)) upper_bound = (n.location.max_capacity - n.location.base_capacity),
for n in values(graph.process_nodes)) base_name="expansion($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
end end
function create_objective_function!(model::ManufacturingModel) 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)) obj = @expression(mip, 0 * @variable(mip))
# Process node costs # 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 # Transportation and variable operating costs
for a in n.incoming_arcs for a in n.incoming_arcs
c = n.plant.input.transportation_cost * a.values["distance"] c = n.location.input.transportation_cost[t] * a.values["distance"]
c += n.plant.variable_operating_cost c += n.location.variable_operating_cost[t]
add_to_expression!(obj, c, vars.flow[a]) add_to_expression!(obj, c, vars.flow[a, t])
end end
# Fixed and opening costss # Opening costs
add_to_expression!(obj, add_to_expression!(obj, n.location.opening_cost[t], vars.open_plant[n, t])
n.plant.fixed_operating_cost + n.plant.opening_cost,
vars.open_plant[n]) # Fixed operating costss
add_to_expression!(obj, n.location.fixed_operating_cost[t], vars.is_open[n, t])
# Expansion costs # Expansion costs
add_to_expression!(obj, n.plant.expansion_cost, if t < T
vars.expansion[n]) 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 end
# Disposal costs # Disposal costs
for n in values(graph.plant_shipping_nodes) for n in values(graph.plant_shipping_nodes), t in 1:T
add_to_expression!(obj, add_to_expression!(obj, n.location.disposal_cost[n.product][t], vars.dispose[n, t])
n.location.disposal_cost[n.product],
vars.dispose[n])
end end
@objective(mip, Min, obj) @objective(mip, Min, obj)
@ -84,41 +100,56 @@ end
function create_shipping_node_constraints!(model::ManufacturingModel) 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 t in 1:T
for n in graph.collection_shipping_nodes # Collection centers
@constraint(mip, sum(vars.flow[a] for a in n.outgoing_arcs) == n.location.amount) for n in graph.collection_shipping_nodes
end @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 # Plants
@constraint(mip, for n in graph.plant_shipping_nodes
sum(vars.flow[a] for a in n.incoming_arcs) == @constraint(mip,
sum(vars.flow[a] for a in n.outgoing_arcs) + vars.dispose[n]) 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
end end
function create_process_node_constraints!(model::ManufacturingModel) 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 # 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 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 end
# If plant is closed, capacity is zero # 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 # 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 # 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
end end
@ -141,19 +172,22 @@ end
function get_solution(model::ManufacturingModel) function get_solution(model::ManufacturingModel)
mip, vars, graph, instance = model.mip, model.vars, model.graph, model.instance mip, vars, graph, instance = model.mip, model.vars, model.graph, model.instance
T = instance.time
output = Dict( output = Dict(
"plants" => Dict(), "plants" => Dict(),
"costs" => Dict( "costs" => Dict(
"fixed" => 0.0, "fixed operating" => zeros(T),
"variable" => 0.0, "variable operating" => zeros(T),
"transportation" => 0.0, "opening" => zeros(T),
"disposal" => 0.0, "transportation" => zeros(T),
"total" => 0.0, "disposal" => zeros(T),
"expansion" => 0.0, "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() plant_to_shipping_nodes = Dict()
for p in instance.plants for p in instance.plants
plant_to_shipping_nodes[p] = [] plant_to_shipping_nodes[p] = []
@ -163,7 +197,7 @@ function get_solution(model::ManufacturingModel)
end end
for plant in instance.plants for plant in instance.plants
skip_plant = true skip_plant = false
process_node = plant_to_process_node[plant] process_node = plant_to_process_node[plant]
plant_dict = Dict{Any, Any}( plant_dict = Dict{Any, Any}(
"input" => Dict(), "input" => Dict(),
@ -171,31 +205,44 @@ function get_solution(model::ManufacturingModel)
"send" => Dict(), "send" => Dict(),
"dispose" => Dict(), "dispose" => Dict(),
), ),
"total input" => 0.0, "total input" => [0.0 for t in 1:T],
"total output" => Dict(), "total output" => Dict(),
"latitude" => plant.latitude, "latitude" => plant.latitude,
"longitude" => plant.longitude, "longitude" => plant.longitude,
"capacity" => JuMP.value(vars.capacity[process_node]), "capacity" => [JuMP.value(vars.capacity[process_node, t])
"fixed cost" => JuMP.value(vars.open_plant[process_node]) * (plant.opening_cost + plant.fixed_operating_cost), for t in 1:T],
"expansion cost" => JuMP.value(vars.expansion[process_node]) * plant.expansion_cost, "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"] output["costs"]["expansion"] += plant_dict["expansion cost"]
# Inputs # Inputs
for a in process_node.incoming_arcs for a in process_node.incoming_arcs
val = JuMP.value(vars.flow[a]) vals = [JuMP.value(vars.flow[a, t]) for t in 1:T]
if val <= 1e-3 if sum(vals) <= 1e-3
continue continue
end end
skip_plant = false skip_plant = false
dict = Dict{Any, Any}( dict = Dict{Any, Any}(
"amount" => val, "amount" => vals,
"distance" => a.values["distance"], "distance" => a.values["distance"],
"latitude" => a.source.location.latitude, "latitude" => a.source.location.latitude,
"longitude" => a.source.location.longitude, "longitude" => a.source.location.longitude,
"transportation cost" => a.source.product.transportation_cost * val, "transportation cost" => [a.source.product.transportation_cost[t] * vals[t]
"variable operating cost" => plant.variable_operating_cost * val, 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 if a.source.location isa CollectionCenter
plant_name = "Origin" plant_name = "Origin"
@ -209,45 +256,45 @@ function get_solution(model::ManufacturingModel)
plant_dict["input"][plant_name] = Dict() plant_dict["input"][plant_name] = Dict()
end end
plant_dict["input"][plant_name][location_name] = dict 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"]["transportation"] += dict["transportation cost"]
output["costs"]["variable"] += dict["variable operating cost"] output["costs"]["variable operating"] += dict["variable operating cost"]
end end
# Outputs # # Outputs
for shipping_node in plant_to_shipping_nodes[plant] # for shipping_node in plant_to_shipping_nodes[plant]
product_name = shipping_node.product.name # product_name = shipping_node.product.name
plant_dict["total output"][product_name] = 0.0 # plant_dict["total output"][product_name] = 0.0
plant_dict["output"]["send"][product_name] = product_dict = Dict() # plant_dict["output"]["send"][product_name] = product_dict = Dict()
disposal_amount = JuMP.value(vars.dispose[shipping_node]) # disposal_amount = JuMP.value(vars.dispose[shipping_node])
if disposal_amount > 1e-5 # if disposal_amount > 1e-5
plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict() # plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict()
disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node]) # disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node])
disposal_dict["cost"] = disposal_dict["amount"] * plant.disposal_cost[shipping_node.product] # disposal_dict["cost"] = disposal_dict["amount"] * plant.disposal_cost[shipping_node.product]
plant_dict["total output"][product_name] += disposal_amount # plant_dict["total output"][product_name] += disposal_amount
output["costs"]["disposal"] += disposal_dict["cost"] # output["costs"]["disposal"] += disposal_dict["cost"]
end # end
for a in shipping_node.outgoing_arcs # for a in shipping_node.outgoing_arcs
val = JuMP.value(vars.flow[a]) # val = JuMP.value(vars.flow[a])
if val <= 1e-3 # if val <= 1e-3
continue # continue
end # end
skip_plant = false # skip_plant = false
dict = Dict( # dict = Dict(
"amount" => val, # "amount" => val,
"distance" => a.values["distance"], # "distance" => a.values["distance"],
"latitude" => a.dest.plant.latitude, # "latitude" => a.dest.location.latitude,
"longitude" => a.dest.plant.longitude, # "longitude" => a.dest.location.longitude,
) # )
if a.dest.plant.plant_name keys(product_dict) # if a.dest.location.plant_name ∉ keys(product_dict)
product_dict[a.dest.plant.plant_name] = Dict() # product_dict[a.dest.location.plant_name] = Dict()
end # end
product_dict[a.dest.plant.plant_name][a.dest.plant.location_name] = dict # product_dict[a.dest.location.plant_name][a.dest.location.location_name] = dict
plant_dict["total output"][product_name] += val # plant_dict["total output"][product_name] += val
end # end
end # end
if !skip_plant if !skip_plant
if plant.plant_name keys(output["plants"]) if plant.plant_name keys(output["plants"])

@ -3,6 +3,21 @@
"$id": "https://axavier.org/ReverseManufacturing/input/schema", "$id": "https://axavier.org/ReverseManufacturing/input/schema",
"title": "Schema for ReverseManufacturing Input File", "title": "Schema for ReverseManufacturing Input File",
"definitions": { "definitions": {
"TimeSeries": {
"type": "array",
"items": {
"type": "number"
}
},
"Parameters": {
"type": "object",
"properties": {
"time": { "type": "number" }
},
"required": [
"time periods"
]
},
"Plant": { "Plant": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
@ -28,19 +43,19 @@
"properties": { "properties": {
"latitude": { "type": "number" }, "latitude": { "type": "number" },
"longitude": { "type": "number" }, "longitude": { "type": "number" },
"variable operating cost": { "type": "number" }, "variable operating cost": { "$ref": "#/definitions/TimeSeries" },
"fixed operating cost": { "type": "number" }, "fixed operating cost": { "$ref": "#/definitions/TimeSeries" },
"opening cost": { "type": "number" }, "opening cost": { "$ref": "#/definitions/TimeSeries" },
"base capacity": { "type": "number" }, "base capacity": { "type": "number" },
"max capacity": { "type": "number" }, "max capacity": { "type": "number" },
"expansion cost": { "type": "number" }, "expansion cost": { "$ref": "#/definitions/TimeSeries" },
"disposal": { "disposal": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "object", "type": "object",
"properties": { "properties": {
"cost": { "type": "number" }, "cost": { "$ref": "#/definitions/TimeSeries" },
"limit": { "type": "number" } "limit": { "$ref": "#/definitions/TimeSeries" }
}, },
"required": [ "required": [
"cost" "cost"
@ -64,7 +79,7 @@
"properties": { "properties": {
"latitude": { "type": "number" }, "latitude": { "type": "number" },
"longitude": { "type": "number" }, "longitude": { "type": "number" },
"amount": { "type": "number" } "amount": { "$ref": "#/definitions/TimeSeries" }
}, },
"required": [ "required": [
"latitude", "latitude",
@ -78,7 +93,7 @@
"additionalProperties": { "additionalProperties": {
"type": "object", "type": "object",
"properties": { "properties": {
"transportation cost": { "type": "number" }, "transportation cost": { "$ref": "#/definitions/TimeSeries" },
"initial amounts": { "$ref": "#/definitions/InitialAmount" } "initial amounts": { "$ref": "#/definitions/InitialAmount" }
}, },
"required": [ "required": [
@ -89,6 +104,7 @@
}, },
"type": "object", "type": "object",
"properties": { "properties": {
"parameters": { "$ref": "#/definitions/Parameters" },
"plants": { "$ref": "#/definitions/Plant" }, "plants": { "$ref": "#/definitions/Plant" },
"products": { "$ref": "#/definitions/Product" } "products": { "$ref": "#/definitions/Product" }
}, },

@ -8,7 +8,7 @@ using ReverseManufacturing
basedir = dirname(@__FILE__) basedir = dirname(@__FILE__)
instance = ReverseManufacturing.load("$basedir/../instances/samples/s1.json") instance = ReverseManufacturing.load("$basedir/../instances/samples/s1.json")
graph = ReverseManufacturing.build_graph(instance) 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) for n in graph.process_nodes)
@test length(graph.plant_shipping_nodes) == 8 @test length(graph.plant_shipping_nodes) == 8
@ -20,19 +20,19 @@ using ReverseManufacturing
@test length(node.incoming_arcs) == 0 @test length(node.incoming_arcs) == 0
@test length(node.outgoing_arcs) == 2 @test length(node.outgoing_arcs) == 2
@test node.outgoing_arcs[1].source.location.name == "C1" @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.location.plant_name == "F1"
@test node.outgoing_arcs[1].dest.plant.location_name == "L1" @test node.outgoing_arcs[1].dest.location.location_name == "L1"
@test node.outgoing_arcs[1].values["distance"] == 1095.62 @test node.outgoing_arcs[1].values["distance"] == 1095.62
node = process_node_by_location_name["L1"] node = process_node_by_location_name["L1"]
@test node.plant.plant_name == "F1" @test node.location.plant_name == "F1"
@test node.plant.location_name == "L1" @test node.location.location_name == "L1"
@test length(node.incoming_arcs) == 10 @test length(node.incoming_arcs) == 10
@test length(node.outgoing_arcs) == 2 @test length(node.outgoing_arcs) == 2
node = process_node_by_location_name["L3"] node = process_node_by_location_name["L3"]
@test node.plant.plant_name == "F2" @test node.location.plant_name == "F2"
@test node.plant.location_name == "L3" @test node.location.location_name == "L3"
@test length(node.incoming_arcs) == 2 @test length(node.incoming_arcs) == 2
@test length(node.outgoing_arcs) == 2 @test length(node.outgoing_arcs) == 2

@ -21,7 +21,7 @@ using ReverseManufacturing
@test centers[1].latitude == 7 @test centers[1].latitude == 7
@test centers[1].latitude == 7 @test centers[1].latitude == 7
@test centers[1].longitude == 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 centers[1].product.name == "P1"
@test length(plants) == 6 @test length(plants) == 6
@ -32,40 +32,40 @@ using ReverseManufacturing
@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.opening_cost == 500 @test plant.opening_cost == [500, 500]
@test plant.fixed_operating_cost == 30 @test plant.fixed_operating_cost == [30, 30]
@test plant.variable_operating_cost == 30 @test plant.variable_operating_cost == [30, 30]
@test plant.base_capacity == 250 @test plant.base_capacity == 250
@test plant.max_capacity == 1000 @test plant.max_capacity == 1000
@test plant.expansion_cost == 1 @test plant.expansion_cost == [1, 1]
p2 = product_name_to_product["P2"] p2 = product_name_to_product["P2"]
p3 = product_name_to_product["P3"] p3 = product_name_to_product["P3"]
@test length(plant.output) == 2 @test length(plant.output) == 2
@test plant.output[p2] == 0.2 @test plant.output[p2] == 0.2
@test plant.output[p3] == 0.5 @test plant.output[p3] == 0.5
@test plant.disposal_limit[p2] == 1 @test plant.disposal_limit[p2] == [1, 1]
@test plant.disposal_limit[p3] == 1 @test plant.disposal_limit[p3] == [1, 1]
@test plant.disposal_cost[p2] == -10 @test plant.disposal_cost[p2] == [-10, -10]
@test plant.disposal_cost[p3] == -10 @test plant.disposal_cost[p3] == [-10, -10]
plant = location_name_to_plant["L3"] plant = location_name_to_plant["L3"]
@test plant.location_name == "L3" @test plant.location_name == "L3"
@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.opening_cost == 3000 @test plant.opening_cost == [3000, 3000]
@test plant.fixed_operating_cost == 50 @test plant.fixed_operating_cost == [50, 50]
@test plant.variable_operating_cost == 50 @test plant.variable_operating_cost == [50, 50]
@test plant.base_capacity == 1e8 @test plant.base_capacity == 1e8
@test plant.max_capacity == 1e8 @test plant.max_capacity == 1e8
@test plant.expansion_cost == 0 @test plant.expansion_cost == [0, 0]
p4 = product_name_to_product["P4"] p4 = product_name_to_product["P4"]
@test plant.output[p3] == 0.05 @test plant.output[p3] == 0.05
@test plant.output[p4] == 0.8 @test plant.output[p4] == 0.8
@test plant.disposal_limit[p3] == 0.0 @test plant.disposal_limit[p3] == [0, 0]
@test plant.disposal_limit[p4] == 0.0 @test plant.disposal_limit[p4] == [0, 0]
end end
end end

@ -1,7 +1,7 @@
# Copyright (C) 2020 Argonne National Laboratory # Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov> # Written by Alinson Santos Xavier <axavier@anl.gov>
using ReverseManufacturing, Cbc, JuMP, Printf, JSON using ReverseManufacturing, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
@testset "Model" begin @testset "Model" begin
@testset "build" begin @testset "build" begin
@ -10,42 +10,46 @@ using ReverseManufacturing, Cbc, JuMP, Printf, JSON
graph = ReverseManufacturing.build_graph(instance) graph = ReverseManufacturing.build_graph(instance)
model = ReverseManufacturing.build_model(instance, graph, Cbc.Optimizer) 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) for n in graph.process_nodes)
shipping_node_by_location_and_product_names = Dict((n.location.location_name, n.product.name) => n shipping_node_by_location_and_product_names = Dict((n.location.location_name, n.product.name) => n
for n in graph.plant_shipping_nodes) for n in graph.plant_shipping_nodes)
@test length(model.vars.flow) == 38 @test length(model.vars.flow) == 76
@test length(model.vars.dispose) == 8 @test length(model.vars.dispose) == 16
@test length(model.vars.open_plant) == 6 @test length(model.vars.open_plant) == 12
@test length(model.vars.capacity) == 6 @test length(model.vars.capacity) == 12
@test length(model.vars.expansion) == 6 @test length(model.vars.expansion) == 12
l1 = process_node_by_location_name["L1"] 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 lower_bound(v) == 0.0
@test upper_bound(v) == 1000.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 lower_bound(v) == 0.0
@test upper_bound(v) == 750.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 lower_bound(v) == 0.0
@test upper_bound(v) == 1.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 end
@testset "solve" begin @testset "solve" begin
solution = ReverseManufacturing.solve("$(pwd())/../instances/samples/s1.json") solution = ReverseManufacturing.solve("$(pwd())/../instances/samples/s1.json")
JSON.print(stdout, solution, 4) #JSON.print(stdout, solution, 4)
@test "costs" in keys(solution) @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 "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 "total" in keys(solution["costs"])
@test "plants" in keys(solution) @test "plants" in keys(solution)
@ -53,10 +57,6 @@ using ReverseManufacturing, Cbc, JuMP, Printf, JSON
@test "F2" in keys(solution["plants"]) @test "F2" in keys(solution["plants"])
@test "F3" in keys(solution["plants"]) @test "F3" in keys(solution["plants"])
@test "F4" 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
end end

Loading…
Cancel
Save