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"
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"

@ -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
-------

@ -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]
}
}
}

@ -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)

@ -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

@ -23,60 +23,76 @@ end
function create_vars!(model::ManufacturingModel)
mip, vars, graph = model.mip, model.vars, model.graph
vars.flow = Dict(a => @variable(mip, lower_bound=0)
for a in graph.arcs)
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.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.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))
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
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, 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, 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, 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
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] for a in n.incoming_arcs) ==
sum(vars.flow[a] for a in n.outgoing_arcs) + vars.dispose[n])
# 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
for n in graph.process_nodes
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
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()
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
# # 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
# 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"])

@ -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" }
},

@ -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

@ -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

@ -1,7 +1,7 @@
# Copyright (C) 2020 Argonne National Laboratory
# 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 "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

Loading…
Cancel
Save