Initial version

This commit is contained in:
2020-01-24 13:00:01 -06:00
commit ab644377b6
13 changed files with 1069 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
module ReverseManufacturing
include("dotdict.jl")
include("instance.jl")
include("model.jl")
end

67
src/dotdict.jl Normal file
View File

@@ -0,0 +1,67 @@
# Copyright (C) 2019 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
struct DotDict
inner::Dict
end
DotDict() = DotDict(Dict())
function Base.setproperty!(d::DotDict, key::Symbol, value)
setindex!(getfield(d, :inner), value, key)
end
function Base.getproperty(d::DotDict, key::Symbol)
(key == :inner ? getfield(d, :inner) : d.inner[key])
end
function Base.getindex(d::DotDict, key::Int64)
d.inner[Symbol(key)]
end
function Base.getindex(d::DotDict, key::Symbol)
d.inner[key]
end
function Base.keys(d::DotDict)
keys(d.inner)
end
function Base.values(d::DotDict)
values(d.inner)
end
function Base.iterate(d::DotDict)
iterate(values(d.inner))
end
function Base.iterate(d::DotDict, v::Int64)
iterate(values(d.inner), v)
end
function Base.length(d::DotDict)
length(values(d.inner))
end
function Base.show(io::IO, d::DotDict)
print(io, "DotDict with $(length(keys(d.inner))) entries:\n")
count = 0
for k in keys(d.inner)
count += 1
if count > 10
print(io, " ...\n")
break
end
print(io, " :$(k) => $(d.inner[k])\n")
end
end
function recursive_to_dot_dict(el)
if typeof(el) == Dict{String, Any}
return DotDict(Dict(Symbol(k) => recursive_to_dot_dict(el[k]) for k in keys(el)))
else
return el
end
end
export recursive_to_dot_dict

89
src/instance.jl Normal file
View File

@@ -0,0 +1,89 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
using Printf, JSON
import Base.getindex, Base.time
"""
mutable struct ReverseManufacturingInstance
Representation of an instance of the Facility Location for Reverse Manufacturing problem.
"""
mutable struct ReverseManufacturingInstance
json::Dict
products::Dict
plants::Dict
end
function Base.show(io::IO, instance::ReverseManufacturingInstance)
n_plants = length(instance["plants"])
n_products = length(instance["products"])
print(io, "ReverseManufacturingInstance with ")
print(io, "$n_plants plants, ")
print(io, "$n_products products")
end
"""
load(name::String)::ReverseManufacturingInstance
Loads an instance from the benchmark set.
Example
=======
julia> ReverseManufacturing.load("samples/s1.json")
"""
function load(name::String) :: ReverseManufacturingInstance
basedir = dirname(@__FILE__)
return ReverseManufacturing.readfile("$basedir/../instances/$name.json")
end
"""
readfile(path::String)::ReverseManufacturingInstance
Loads an instance from the given JSON file.
Example
=======
julia> ReverseManufacturing.load("/home/user/instance.json")
"""
function readfile(path::String)::ReverseManufacturingInstance
json = JSON.parsefile(path)
products = Dict(key => json["products"][key]
for key in keys(json["products"]))
plants = Dict(key => json["plants"][key]
for key in keys(json["plants"]))
for product_name in keys(products)
product = products[product_name]
product["name"] = product_name
product["input plants"] = []
product["output plants"] = []
end
for plant_name in keys(plants)
plant = plants[plant_name]
plant["name"] = plant_name
# Input product
input_product = products[plant["input"]]
plant["input product"] = input_product
push!(input_product["input plants"], plant)
# Output products
if haskey(plant, "outputs")
for product_name in keys(plant["outputs"])
product = products[product_name]
push!(product["output plants"], plant)
end
end
end
return ReverseManufacturingInstance(json, products, plants)
end
export ReverseManufacturingInstance

210
src/model.jl Normal file
View File

@@ -0,0 +1,210 @@
# Copyright (C) 2019 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
using JuMP, LinearAlgebra, Geodesy
mutable struct ReverseManufacturingModel
mip::JuMP.Model
vars::DotDict
arcs
decision_nodes
process_nodes
end
mutable struct Node
product_name::String
plant_name::String
location_name::String
balance::Float64
incoming_arcs::Array
outgoing_arcs::Array
cost::Float64
end
function Node(product_name::String,
plant_name::String,
location_name::String;
balance::Float64 = 0.0,
incoming_arcs::Array = [],
outgoing_arcs::Array = [],
cost::Float64 = 0.0,
) :: Node
return Node(product_name,
plant_name,
location_name,
balance,
incoming_arcs,
outgoing_arcs,
cost)
end
function Base.show(io::IO, node::Node)
print(io, "Node($(node.product_name), $(node.plant_name), $(node.location_name)")
if node.balance != 0.0
print(io, ", $(node.balance)")
end
print(io, ")")
end
mutable struct Arc
source::Node
dest::Node
costs::Dict
values::Dict
end
function Base.show(io::IO, arc::Arc)
print(io, "Arc($(arc.source), $(arc.dest))")
end
function build_model(instance::ReverseManufacturingInstance,
optimizer,
) :: ReverseManufacturingModel
mip = isa(optimizer, JuMP.OptimizerFactory) ? Model(optimizer) : direct_model(optimizer)
decision_nodes, process_nodes, arcs = create_nodes_and_arcs(instance)
vars = DotDict()
vars.flow = Dict(a => @variable(mip, lower_bound=0) for a in arcs)
vars.node = Dict(n => @variable(mip, binary=true) for n in values(process_nodes))
create_decision_node_constraints!(mip, decision_nodes, vars)
create_process_node_constraints!(mip, process_nodes, vars)
flow_costs = sum(a.costs[c] * vars.flow[a] for a in arcs for c in keys(a.costs))
node_costs = sum(n.cost * vars.node[n] for n in values(process_nodes))
@objective(mip, Min, flow_costs + node_costs)
return return ReverseManufacturingModel(mip,
vars,
arcs,
decision_nodes,
process_nodes)
end
function create_decision_node_constraints!(mip, nodes, vars)
for (id, n) in nodes
@constraint(mip,
sum(vars.flow[a] for a in n.incoming_arcs) + n.balance ==
sum(vars.flow[a] for a in n.outgoing_arcs))
end
end
function create_process_node_constraints!(mip, nodes, vars)
for (id, n) in nodes
# Output amount is implied by input amount
input_sum = sum(vars.flow[a] for a in n.incoming_arcs)
for a in n.outgoing_arcs
@constraint(mip, vars.flow[a] == a.values["weight"] * input_sum)
end
# If plant is closed, input must be zero
@constraint(mip, input_sum <= 1e6 * vars.node[n])
end
end
function create_nodes_and_arcs(instance)
arcs = Arc[]
decision_nodes = Dict()
process_nodes = Dict()
# Create all nodes
for (product_name, product) in instance.products
# Decision nodes for initial amounts
if haskey(product, "initial amounts")
for location_name in keys(product["initial amounts"])
amount = product["initial amounts"][location_name]["amount"]
n = Node(product_name, "Origin", location_name, balance=amount)
decision_nodes[n.product_name, n.plant_name, n.location_name] = n
end
end
# Process nodes for each plant
for plant in product["input plants"]
for (location_name, location) in plant["locations"]
cost = location["opening cost"] + location["fixed operating cost"]
n = Node(product_name, plant["name"], location_name, cost=cost)
process_nodes[n.product_name, n.plant_name, n.location_name] = n
end
end
# Decision nodes for each plant
for plant in product["output plants"]
for location_name in keys(plant["locations"])
n = Node(product_name, plant["name"], location_name)
decision_nodes[n.product_name, n.plant_name, n.location_name] = n
end
end
end
# Create arcs
for (product_name, product) in instance.products
# Transportation arcs from initial location to plants
if haskey(product, "initial amounts")
for source_location_name in keys(product["initial amounts"])
source_location = product["initial amounts"][source_location_name]
for dest_plant in product["input plants"]
for dest_location_name in keys(dest_plant["locations"])
dest_location = dest_plant["locations"][dest_location_name]
source = decision_nodes[product_name, "Origin", source_location_name]
dest = process_nodes[product_name, dest_plant["name"], dest_location_name]
distance = calculate_distance(source_location["latitude"],
source_location["longitude"],
dest_location["latitude"],
dest_location["longitude"])
costs = Dict("transportation" => product["transportation cost"] * distance,
"variable" => dest_location["variable operating cost"])
values = Dict("distance" => distance)
a = Arc(source, dest, costs, values)
push!(arcs, a)
push!(source.outgoing_arcs, a)
push!(dest.incoming_arcs, a)
end
end
end
end
for source_plant in product["output plants"]
for source_location_name in keys(source_plant["locations"])
source_location = source_plant["locations"][source_location_name]
# Process-arcs within a plant
source = process_nodes[source_plant["input"], source_plant["name"], source_location_name]
dest = decision_nodes[product_name, source_plant["name"], source_location_name]
costs = Dict()
values = Dict("weight" => source_plant["outputs"][product_name])
a = Arc(source, dest, costs, values)
push!(arcs, a)
push!(source.outgoing_arcs, a)
push!(dest.incoming_arcs, a)
# Transportation-arcs from one plant to another
for dest_plant in product["input plants"]
for dest_location_name in keys(dest_plant["locations"])
dest_location = dest_plant["locations"][dest_location_name]
source = decision_nodes[product_name, source_plant["name"], source_location_name]
dest = process_nodes[product_name, dest_plant["name"], dest_location_name]
distance = calculate_distance(source_location["latitude"],
source_location["longitude"],
dest_location["latitude"],
dest_location["longitude"])
costs = Dict("transportation" => product["transportation cost"] * distance,
"variable" => dest_location["variable operating cost"])
values = Dict("distance" => distance)
a = Arc(source, dest, costs, values)
push!(arcs, a)
push!(source.outgoing_arcs, a)
push!(dest.incoming_arcs, a)
end
end
end
end
end
return decision_nodes, process_nodes, arcs
end
function calculate_distance(source_lat, source_lon, dest_lat, dest_lon)::Float64
x = LLA(source_lat, source_lon, 0.0)
y = LLA(dest_lat, dest_lon, 0.0)
return round(distance(x, y) / 1000.0, digits=2)
end
export FlowArc