diff --git a/src/ReverseManufacturing.jl b/src/ReverseManufacturing.jl index a84bdb2..f460478 100644 --- a/src/ReverseManufacturing.jl +++ b/src/ReverseManufacturing.jl @@ -2,7 +2,8 @@ # Written by Alinson Santos Xavier module ReverseManufacturing - #include("dotdict.jl") + include("dotdict.jl") include("instance.jl") - #include("model.jl") + include("graph.jl") + include("model.jl") end diff --git a/src/graph.jl b/src/graph.jl new file mode 100644 index 0000000..8cd7db3 --- /dev/null +++ b/src/graph.jl @@ -0,0 +1,125 @@ +# Copyright (C) 2019 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using Geodesy + + +abstract type Node +end + + +mutable struct Arc + source::Node + dest::Node + values::Dict{String, Float64} +end + + +mutable struct ProcessNode <: Node + index::Int + plant::Plant + incoming_arcs::Array{Arc} + outgoing_arcs::Array{Arc} +end + + +mutable struct ShippingNode <: Node + index::Int + location::Union{Plant, CollectionCenter} + product::Product + incoming_arcs::Array{Arc} + outgoing_arcs::Array{Arc} +end + + +mutable struct Graph + process_nodes::Array{ProcessNode} + plant_shipping_nodes::Array{ShippingNode} + collection_shipping_nodes::Array{ShippingNode} + arcs::Array{Arc} +end + + +function build_graph(instance::Instance)::Graph + arcs = [] + next_index = 0 + process_nodes = ProcessNode[] + plant_shipping_nodes = ShippingNode[] + collection_shipping_nodes = ShippingNode[] + + process_nodes_by_input_product = Dict(product => ProcessNode[] + for product in instance.products) + shipping_nodes_by_plant = Dict(plant => [] + for plant in instance.plants) + + # Build collection center shipping nodes + for center in instance.collection_centers + node = ShippingNode(next_index, center, center.product, [], []) + next_index += 1 + push!(collection_shipping_nodes, node) + end + + # Build process and shipping nodes for plants + for plant in instance.plants + pn = ProcessNode(next_index, plant, [], []) + next_index += 1 + push!(process_nodes, pn) + push!(process_nodes_by_input_product[plant.input], pn) + + for product in keys(plant.output) + sn = ShippingNode(next_index, plant, product, [], []) + next_index += 1 + push!(plant_shipping_nodes, sn) + push!(shipping_nodes_by_plant[plant], sn) + end + end + + # Build arcs from collection centers to plants, and from one plant to another + for source in [collection_shipping_nodes; plant_shipping_nodes] + 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) + values = Dict("distance" => distance) + arc = Arc(source, dest, values) + push!(source.outgoing_arcs, arc) + push!(dest.incoming_arcs, arc) + push!(arcs, arc) + end + end + + # Build arcs from process nodes to shipping nodes within a plant + for source in process_nodes + plant = source.plant + for dest in shipping_nodes_by_plant[plant] + weight = plant.output[dest.product] + values = Dict("weight" => weight) + arc = Arc(source, dest, values) + push!(source.outgoing_arcs, arc) + push!(dest.incoming_arcs, arc) + push!(arcs, arc) + end + end + + return Graph(process_nodes, + plant_shipping_nodes, + collection_shipping_nodes, + arcs) +end + + +function to_csv(graph::Graph) + result = "" + for a in graph.arcs + result *= "$(a.source.index),$(a.dest.index)\n" + end + return result +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 diff --git a/src/instance.jl b/src/instance.jl index ddc56d0..0d5b240 100644 --- a/src/instance.jl +++ b/src/instance.jl @@ -19,15 +19,9 @@ struct CollectionCenter end -struct DisposalEntry - product::Product - cost::Float64 - limit::Float64 -end - - struct Plant - name::String + plant_name::String + location_name::String input::Product output::Dict{Product, Float64} latitude::Float64 @@ -38,7 +32,8 @@ struct Plant base_capacity::Float64 max_capacity::Float64 expansion_cost::Float64 - disposal::Array{DisposalEntry} + disposal_limit::Dict{Product, Float64} + disposal_cost::Dict{Product, Float64} end @@ -98,19 +93,19 @@ function load(path::String)::Instance end for (location_name, location_dict) in plant_dict["locations"] - disposal = DisposalEntry[] + disposal_limit = Dict(p => 0.0 for p in keys(output)) + disposal_cost = Dict(p => 0.0 for p in keys(output)) # Plant disposal if "disposal" in keys(location_dict) for (product_name, disposal_dict) in location_dict["disposal"] - push!(disposal, DisposalEntry(product_name_to_product[product_name], - disposal_dict["cost"], - disposal_dict["limit"])) + disposal_limit[product_name_to_product[product_name]] = disposal_dict["limit"] + disposal_cost[product_name_to_product[product_name]] = disposal_dict["cost"] end end - base_capacity = Inf - max_capacity = Inf + base_capacity = 1e8 + max_capacity = 1e8 expansion_cost = 0 if "base capacity" in keys(location_dict) @@ -125,7 +120,8 @@ function load(path::String)::Instance expansion_cost = location_dict["expansion cost"] end - plant = Plant(location_name, + plant = Plant(plant_name, + location_name, input, output, location_dict["latitude"], @@ -136,7 +132,8 @@ function load(path::String)::Instance base_capacity, max_capacity, expansion_cost, - disposal) + disposal_limit, + disposal_cost) push!(plants, plant) end end diff --git a/src/model.jl b/src/model.jl index ed5132f..84e2586 100644 --- a/src/model.jl +++ b/src/model.jl @@ -3,460 +3,269 @@ using JuMP, LinearAlgebra, Geodesy, Cbc, ProgressBars -mutable struct ReverseManufacturingModel + +mutable struct ManufacturingModel mip::JuMP.Model vars::DotDict - arcs - shipping_nodes - process_nodes -end - -abstract type Node -end - -mutable struct ProcessNode <: Node - product_name::String - plant_name::String - location_name::String - incoming_arcs::Array - outgoing_arcs::Array - fixed_cost::Float64 - expansion_cost::Float64 - base_capacity::Float64 - max_capacity::Float64 + instance::Instance + graph::Graph end -mutable struct ShippingNode <: Node - product_name::String - plant_name::String - location_name::String - incoming_arcs::Array - outgoing_arcs::Array - balance::Float64 - disposal_cost::Float64 - disposal_limit::Float64 -end - -function Base.show(io::IO, node::ProcessNode) - print(io, "ProcessNode($(node.product_name), $(node.plant_name), $(node.location_name), fixed_cost=$(node.fixed_cost))") -end -function Base.show(io::IO, node::ShippingNode) - print(io, "ShippingNode($(node.product_name), $(node.plant_name), $(node.location_name), balance=$(node.balance), ") - print(io, "disposal_cost=$(node.disposal_cost), disposal_limit=$(node.disposal_limit))") +function build_model(instance::Instance, graph::Graph, optimizer)::ManufacturingModel + model = ManufacturingModel(Model(optimizer), DotDict(), instance, graph) + create_vars!(model) + create_objective_function!(model) + create_shipping_node_constraints!(model) + create_process_node_constraints!(model) + return model end -mutable struct Arc - # Origin of the arc - source::Node - - # Destination of the arc - dest::Node - - # Costs dictionary. Each value in this dictionary is multiplied by the arc flow variable - # and added to the objective function. - costs::Dict - # Values dictionary. This dictionary is used to store extra information about the - # arc. They are not used automatically by the model. - 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 - - println("Building optimization model...") - mip = Model(optimizer) - shipping_nodes, process_nodes, arcs = create_nodes_and_arcs(instance) +function create_vars!(model::ManufacturingModel) + mip, vars, graph = model.mip, model.vars, model.graph - println(" $(length(shipping_nodes)) shipping nodes") - println(" $(length(process_nodes)) process nodes") - println(" $(length(arcs)) arcs") - - vars = DotDict() - vars.flow = Dict(a => @variable(mip, lower_bound=0) for a in arcs) + 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.disposal_limit) - for n in values(shipping_nodes)) - vars.open_plant = Dict(n => @variable(mip, binary=true) for n in values(process_nodes)) - vars.capacity = Dict(n => @variable(mip, lower_bound = 0, upper_bound = n.max_capacity) - for n in values(process_nodes)) - vars.expansion = Dict(n => @variable(mip, lower_bound = 0, upper_bound = (n.max_capacity - n.base_capacity)) - for n in values(process_nodes)) - create_shipping_node_constraints!(mip, shipping_nodes, vars) - create_process_node_constraints!(mip, process_nodes, vars) + upper_bound = n.location.disposal_limit[n.product]) + for n in values(graph.plant_shipping_nodes)) - println(" Creating objective function...") - obj = @expression(mip, 0 * @variable(mip)) + 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)) +end - # Shipping and variable operating costs - for a in tqdm(arcs) - for c in keys(a.costs) - add_to_expression!(obj, a.costs[c], vars.flow[a]) - end - end - # Opening and fixed operating costs - for n in tqdm(values(process_nodes)) - add_to_expression!(obj, n.fixed_cost, vars.open_plant[n]) - end +function create_objective_function!(model::ManufacturingModel) + mip, vars, graph = model.mip, model.vars, model.graph + obj = @expression(mip, 0 * @variable(mip)) - # Expansion cost - for n in tqdm(values(process_nodes)) - add_to_expression!(obj, n.expansion_cost, vars.expansion[n]) + # Process node costs + for n in values(graph.process_nodes) + + # 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]) + end + + # Fixed and opening costss + add_to_expression!(obj, + n.plant.fixed_operating_cost + n.plant.opening_cost, + vars.open_plant[n]) + + # Expansion costs + add_to_expression!(obj, n.plant.expansion_cost, + vars.expansion[n]) end # Disposal costs - for n in tqdm(values(shipping_nodes)) - add_to_expression!(obj, n.disposal_cost, vars.dispose[n]) + for n in values(graph.plant_shipping_nodes) + add_to_expression!(obj, + n.location.disposal_cost[n.product], + vars.dispose[n]) end @objective(mip, Min, obj) - - return ReverseManufacturingModel(mip, - vars, - arcs, - shipping_nodes, - process_nodes) -end +end -function create_shipping_node_constraints!(mip, nodes, vars) - println(" Creating shipping-node constraints...") - for (id, n) in tqdm(nodes) + +function create_shipping_node_constraints!(model::ManufacturingModel) + mip, vars, graph = model.mip, model.vars, model.graph + + # 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) + n.balance == + sum(vars.flow[a] for a in n.incoming_arcs) == sum(vars.flow[a] for a in n.outgoing_arcs) + vars.dispose[n]) end end -function create_process_node_constraints!(mip, nodes, vars) - println(" Creating process-node constraints...") - for (id, n) in tqdm(nodes) + +function create_process_node_constraints!(model) + mip, vars, graph = model.mip, model.vars, model.graph + + for n in graph.process_nodes + # Output amount is implied by input amount input_sum = isempty(n.incoming_arcs) ? 0 : 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, capacity is zero. - @constraint(mip, vars.capacity[n] <= n.max_capacity * vars.open_plant[n]) + # If plant is closed, capacity is zero + @constraint(mip, vars.capacity[n] <= n.plant.max_capacity * vars.open_plant[n]) # Capacity is linked to expansion - @constraint(mip, vars.capacity[n] <= n.base_capacity + vars.expansion[n]) + @constraint(mip, vars.capacity[n] <= n.plant.base_capacity + vars.expansion[n]) # Input sum must be smaller than capacity @constraint(mip, input_sum <= vars.capacity[n]) end end -function create_nodes_and_arcs(instance) - println(" Creating nodes and arcs...") - arcs = Arc[] - shipping_nodes = Dict() - process_nodes = Dict() +function solve(filename::String; optimizer=Cbc.Optimizer) + println("Reading $filename...") + instance = ReverseManufacturing.load(filename) - # Create all nodes - for (product_name, product) in instance.products - - # Shipping nodes for initial amounts - if haskey(product, "initial amounts") - for location_name in keys(product["initial amounts"]) - balance = product["initial amounts"][location_name]["amount"] - n = ShippingNode(product_name, - "Origin", # plant_name - location_name, - [], # incoming_arcs - [], # outgoing_arcs - balance, - 0.0, # disposal_cost - 0.0, # disposal_limit - ) - shipping_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"] - base_capacity = 1e8 - max_capacity = 1e8 - expansion_cost = 0.0 - fixed_cost = location["opening cost"] + location["fixed operating cost"] - if "base capacity" in keys(location) - base_capacity = location["base capacity"] - end - if "max capacity" in keys(location) - max_capacity = location["max capacity"] - end - if "expansion cost" in keys(location) - expansion_cost = location["expansion cost"] - end - n = ProcessNode(product_name, - plant["name"], - location_name, - [], # incoming_arcs - [], # outgoing_arcs - fixed_cost, - expansion_cost, - base_capacity, - max_capacity) - process_nodes[n.product_name, n.plant_name, n.location_name] = n - end - end - - # Shipping nodes for each plant - for plant in product["output plants"] - for (location_name, location) in plant["locations"] - disposal_cost = 0.0 - disposal_limit = 0.0 - - if "disposal" in keys(location) && product_name in keys(location["disposal"]) - dict = location["disposal"][product_name] - disposal_cost = dict["cost"] - if "limit" in keys(dict) - disposal_limit = dict["limit"] - else - disposal_limit = 1e10 - end - end - - n = ShippingNode(product_name, - plant["name"], - location_name, - [], # incoming_arcs - [], # outgoing_arcs - 0.0, # balance - disposal_cost, - disposal_limit, - ) - shipping_nodes[n.product_name, n.plant_name, n.location_name] = n - end - end - end + println("Building graph...") + graph = ReverseManufacturing.build_graph(instance) - # 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 = shipping_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 (conversions within a plant) - source = process_nodes[source_plant["input"], source_plant["name"], source_location_name] - dest = shipping_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 = shipping_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 shipping_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 - -function solve(filename::String; - optimizer=Cbc.Optimizer) - println("Reading $filename") - instance = ReverseManufacturing.readfile(filename) - model = ReverseManufacturing.build_model(instance, optimizer) + println("Building optimization model...") + model = ReverseManufacturing.build_model(instance, graph, optimizer) println("Optimizing...") JuMP.optimize!(model.mip) - println("Extracting solution...") - return get_solution(instance, model) +# println("Extracting solution...") +# return get_solution(instance, model) end -function get_solution(instance::ReverseManufacturingInstance, - model::ReverseManufacturingModel) - vals = Dict() - for a in values(model.arcs) - vals[a] = JuMP.value(model.vars.flow[a]) - end - for n in values(model.process_nodes) - vals[n] = JuMP.value(model.vars.open_plant[n]) - end +# function get_solution(instance::ReverseManufacturingInstance, +# model::ReverseManufacturingModel) +# vals = Dict() +# for a in values(model.arcs) +# vals[a] = JuMP.value(model.vars.flow[a]) +# end +# for n in values(model.process_nodes) +# vals[n] = JuMP.value(model.vars.open_plant[n]) +# end - output = Dict( - "plants" => Dict(), - "costs" => Dict( - "fixed" => 0.0, - "variable" => 0.0, - "transportation" => 0.0, - "disposal" => 0.0, - "total" => 0.0, - "expansion" => 0.0, - ) - ) - - for (plant_name, plant) in instance.plants - skip_plant = true - plant_dict = Dict{Any, Any}() - input_product_name = plant["input"] +# output = Dict( +# "plants" => Dict(), +# "costs" => Dict( +# "fixed" => 0.0, +# "variable" => 0.0, +# "transportation" => 0.0, +# "disposal" => 0.0, +# "total" => 0.0, +# "expansion" => 0.0, +# ) +# ) + +# for (plant_name, plant) in instance.plants +# skip_plant = true +# plant_dict = Dict{Any, Any}() +# input_product_name = plant["input"] - for (location_name, location) in plant["locations"] - skip_location = true - process_node = model.process_nodes[input_product_name, plant_name, location_name] - - plant_loc_dict = Dict{Any, Any}( - "input" => Dict(), - "output" => Dict( - "send" => Dict(), - "dispose" => Dict(), - ), - "total input" => 0.0, - "total output" => Dict(), - "latitude" => location["latitude"], - "longitude" => location["longitude"], - "capacity" => round(JuMP.value(model.vars.capacity[process_node]), digits=2) - ) - - plant_loc_dict["fixed cost"] = round(vals[process_node] * process_node.fixed_cost, digits=5) - plant_loc_dict["expansion cost"] = round(JuMP.value(model.vars.expansion[process_node]) * process_node.expansion_cost, digits=5) - output["costs"]["fixed"] += plant_loc_dict["fixed cost"] - output["costs"]["expansion"] += plant_loc_dict["expansion cost"] - - # Inputs - for a in process_node.incoming_arcs - if vals[a] <= 1e-3 - continue - end - skip_plant = skip_location = false - val = round(vals[a], digits=5) - if !(a.source.plant_name in keys(plant_loc_dict["input"])) - plant_loc_dict["input"][a.source.plant_name] = Dict() - end - if a.source.plant_name == "Origin" - product = instance.products[a.source.product_name] - source_location = product["initial amounts"][a.source.location_name] - else - source_plant = instance.plants[a.source.plant_name] - source_location = source_plant["locations"][a.source.location_name] - end +# for (location_name, location) in plant["locations"] +# skip_location = true +# process_node = model.process_nodes[input_product_name, plant_name, location_name] + +# plant_loc_dict = Dict{Any, Any}( +# "input" => Dict(), +# "output" => Dict( +# "send" => Dict(), +# "dispose" => Dict(), +# ), +# "total input" => 0.0, +# "total output" => Dict(), +# "latitude" => location["latitude"], +# "longitude" => location["longitude"], +# "capacity" => round(JuMP.value(model.vars.capacity[process_node]), digits=2) +# ) + +# plant_loc_dict["fixed cost"] = round(vals[process_node] * process_node.fixed_cost, digits=5) +# plant_loc_dict["expansion cost"] = round(JuMP.value(model.vars.expansion[process_node]) * process_node.expansion_cost, digits=5) +# output["costs"]["fixed"] += plant_loc_dict["fixed cost"] +# output["costs"]["expansion"] += plant_loc_dict["expansion cost"] + +# # Inputs +# for a in process_node.incoming_arcs +# if vals[a] <= 1e-3 +# continue +# end +# skip_plant = skip_location = false +# val = round(vals[a], digits=5) +# if !(a.source.plant_name in keys(plant_loc_dict["input"])) +# plant_loc_dict["input"][a.source.plant_name] = Dict() +# end +# if a.source.plant_name == "Origin" +# product = instance.products[a.source.product_name] +# source_location = product["initial amounts"][a.source.location_name] +# else +# source_plant = instance.plants[a.source.plant_name] +# source_location = source_plant["locations"][a.source.location_name] +# end - # Input - cost_transportation = round(a.costs["transportation"] * val, digits=5) - plant_loc_dict["input"][a.source.plant_name][a.source.location_name] = dict = Dict() - cost_variable = round(a.costs["variable"] * val, digits=5) - dict["amount"] = val - dict["distance"] = a.values["distance"] - dict["transportation cost"] = cost_transportation - dict["variable operating cost"] = cost_variable - dict["latitude"] = source_location["latitude"] - dict["longitude"] = source_location["longitude"] - plant_loc_dict["total input"] += val +# # Input +# cost_transportation = round(a.costs["transportation"] * val, digits=5) +# plant_loc_dict["input"][a.source.plant_name][a.source.location_name] = dict = Dict() +# cost_variable = round(a.costs["variable"] * val, digits=5) +# dict["amount"] = val +# dict["distance"] = a.values["distance"] +# dict["transportation cost"] = cost_transportation +# dict["variable operating cost"] = cost_variable +# dict["latitude"] = source_location["latitude"] +# dict["longitude"] = source_location["longitude"] +# plant_loc_dict["total input"] += val - output["costs"]["transportation"] += cost_transportation - output["costs"]["variable"] += cost_variable - end - - # Outputs - for output_product_name in keys(plant["outputs"]) - plant_loc_dict["total output"][output_product_name] = 0.0 - plant_loc_dict["output"]["send"][output_product_name] = product_dict = Dict() - shipping_node = model.shipping_nodes[output_product_name, plant_name, location_name] - - disposal_amount = JuMP.value(model.vars.dispose[shipping_node]) - if disposal_amount > 1e-5 - plant_loc_dict["output"]["dispose"][output_product_name] = disposal_dict = Dict() - disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node]) - disposal_dict["cost"] = disposal_dict["amount"] * shipping_node.disposal_cost - plant_loc_dict["total output"][output_product_name] += disposal_amount - output["costs"]["disposal"] += disposal_dict["cost"] - end - - for a in shipping_node.outgoing_arcs - if vals[a] <= 1e-3 - continue - end - skip_plant = skip_location = false - if !(a.dest.plant_name in keys(product_dict)) - product_dict[a.dest.plant_name] = Dict{Any,Any}() - end - dest_location = instance.plants[a.dest.plant_name]["locations"][a.dest.location_name] - val = round(vals[a], digits=5) - plant_loc_dict["total output"][output_product_name] += val - product_dict[a.dest.plant_name][a.dest.location_name] = dict = Dict() - dict["amount"] = val - dict["distance"] = a.values["distance"] - dict["latitude"] = dest_location["latitude"] - dict["longitude"] = dest_location["longitude"] - end - end +# output["costs"]["transportation"] += cost_transportation +# output["costs"]["variable"] += cost_variable +# end + +# # Outputs +# for output_product_name in keys(plant["outputs"]) +# plant_loc_dict["total output"][output_product_name] = 0.0 +# plant_loc_dict["output"]["send"][output_product_name] = product_dict = Dict() +# shipping_node = model.shipping_nodes[output_product_name, plant_name, location_name] + +# disposal_amount = JuMP.value(model.vars.dispose[shipping_node]) +# if disposal_amount > 1e-5 +# plant_loc_dict["output"]["dispose"][output_product_name] = disposal_dict = Dict() +# disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node]) +# disposal_dict["cost"] = disposal_dict["amount"] * shipping_node.disposal_cost +# plant_loc_dict["total output"][output_product_name] += disposal_amount +# output["costs"]["disposal"] += disposal_dict["cost"] +# end + +# for a in shipping_node.outgoing_arcs +# if vals[a] <= 1e-3 +# continue +# end +# skip_plant = skip_location = false +# if !(a.dest.plant_name in keys(product_dict)) +# product_dict[a.dest.plant_name] = Dict{Any,Any}() +# end +# dest_location = instance.plants[a.dest.plant_name]["locations"][a.dest.location_name] +# val = round(vals[a], digits=5) +# plant_loc_dict["total output"][output_product_name] += val +# product_dict[a.dest.plant_name][a.dest.location_name] = dict = Dict() +# dict["amount"] = val +# dict["distance"] = a.values["distance"] +# dict["latitude"] = dest_location["latitude"] +# dict["longitude"] = dest_location["longitude"] +# end +# end - if !skip_location - plant_dict[location_name] = plant_loc_dict - end - end - if !skip_plant - output["plants"][plant_name] = plant_dict - end - end - - output["costs"]["total"] = sum(values(output["costs"])) - return output -end +# if !skip_location +# plant_dict[location_name] = plant_loc_dict +# end +# end +# if !skip_plant +# output["plants"][plant_name] = plant_dict +# end +# end + +# output["costs"]["total"] = sum(values(output["costs"])) +# return output +# end -export FlowArc diff --git a/test/graph_test.jl b/test/graph_test.jl new file mode 100644 index 0000000..fce0c40 --- /dev/null +++ b/test/graph_test.jl @@ -0,0 +1,42 @@ +# Copyright (C) 2020 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using ReverseManufacturing + +@testset "Graph" begin + @testset "build_graph" begin + 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 + for n in graph.process_nodes) + + @test length(graph.plant_shipping_nodes) == 8 + @test length(graph.collection_shipping_nodes) == 10 + @test length(graph.process_nodes) == 6 + + node = graph.collection_shipping_nodes[1] + @test node.location.name == "C1" + @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].values["distance"] == 1095.62 + + node = process_node_by_location_name["L1"] + @test node.plant.plant_name == "F1" + @test node.plant.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 length(node.incoming_arcs) == 2 + @test length(node.outgoing_arcs) == 2 + + @test length(graph.arcs) == 38 + end +end + diff --git a/test/instance_test.jl b/test/instance_test.jl index 3d5a134..9aecae5 100644 --- a/test/instance_test.jl +++ b/test/instance_test.jl @@ -12,11 +12,9 @@ using ReverseManufacturing plants = instance.plants products = instance.products - plant_name_to_plant = Dict(p.name => p for p in plants) + location_name_to_plant = Dict(p.location_name => p for p in plants) product_name_to_product = Dict(p.name => p for p in products) - p2 = product_name_to_product["P2"] - p3 = product_name_to_product["P3"] @test length(centers) == 10 @test centers[1].name == "C1" @@ -28,8 +26,9 @@ using ReverseManufacturing @test length(plants) == 6 - plant = plant_name_to_plant["L1"] - @test plant.name == "L1" + plant = location_name_to_plant["L1"] + @test plant.plant_name == "F1" + @test plant.location_name == "L1" @test plant.input.name == "P1" @test plant.latitude == 0 @test plant.longitude == 0 @@ -40,26 +39,33 @@ using ReverseManufacturing @test plant.max_capacity == 1000 @test plant.expansion_cost == 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 length(plant.disposal) == 2 - @test plant.disposal[1].product.name == "P2" - @test plant.disposal[1].cost == -10 - @test plant.disposal[1].limit == 1 - - plant = plant_name_to_plant["L3"] - @test plant.name == "L3" + 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.base_capacity == Inf - @test plant.max_capacity == Inf + @test plant.base_capacity == 1e8 + @test plant.max_capacity == 1e8 @test plant.expansion_cost == 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 end end diff --git a/test/model_test.jl b/test/model_test.jl index 84ac919..0f46cc2 100644 --- a/test/model_test.jl +++ b/test/model_test.jl @@ -4,61 +4,54 @@ using ReverseManufacturing, Cbc, JuMP, Printf, JSON @testset "Model" begin - instance = ReverseManufacturing.load("samples/s1") - model = ReverseManufacturing.build_model(instance, Cbc.Optimizer) - - # Verify nodes - @test ("P1", "Origin", "C1") in keys(model.shipping_nodes) - @test ("P1", "Origin", "C3") in keys(model.shipping_nodes) - @test ("P1", "Origin", "C8") in keys(model.shipping_nodes) - @test ("P2", "F1", "L1") in keys(model.shipping_nodes) - @test ("P2", "F1", "L2") in keys(model.shipping_nodes) - @test ("P3", "F1", "L1") in keys(model.shipping_nodes) - @test ("P3", "F1", "L2") in keys(model.shipping_nodes) - @test ("P3", "F2", "L3") in keys(model.shipping_nodes) - @test ("P3", "F2", "L4") in keys(model.shipping_nodes) - @test ("P4", "F2", "L3") in keys(model.shipping_nodes) - @test ("P4", "F2", "L4") in keys(model.shipping_nodes) - @test ("P1", "F1", "L1") in keys(model.process_nodes) - @test ("P1", "F1", "L2") in keys(model.process_nodes) - @test ("P2", "F2", "L3") in keys(model.process_nodes) - @test ("P2", "F2", "L4") in keys(model.process_nodes) - @test ("P3", "F4", "L6") in keys(model.process_nodes) - @test ("P4", "F3", "L5") in keys(model.process_nodes) - - # Verify some arcs - p1_orig_c1 = model.shipping_nodes["P1", "Origin", "C1"] - p1_f1_l1 = model.process_nodes["P1", "F1", "L1"] - @test length(p1_orig_c1.outgoing_arcs) == 2 - @test length(p1_f1_l1.incoming_arcs) == 10 - - arc = p1_orig_c1.outgoing_arcs[1] - @test arc.dest.location_name == "L1" - @test arc.values["distance"] == 1095.62 - @test round(arc.costs["transportation"], digits=2) == 16.43 - @test arc.costs["variable"] == 30.0 - - p2_f1_l1 = model.shipping_nodes["P2", "F1", "L1"] - p2_f2_l3 = model.process_nodes["P2", "F2", "L3"] - @test length(p2_f1_l1.incoming_arcs) == 1 - @test length(p2_f1_l1.outgoing_arcs) == 2 - - arc = p2_f1_l1.incoming_arcs[1] - @test arc.values["weight"] == 0.2 - @test isempty(arc.costs) -end + @testset "build" begin + basedir = dirname(@__FILE__) + instance = ReverseManufacturing.load("$basedir/../instances/samples/s1.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 + 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 + + l1 = process_node_by_location_name["L1"] + v = model.vars.capacity[l1] + @test lower_bound(v) == 0.0 + @test upper_bound(v) == 1000.0 + + v = model.vars.expansion[l1] + @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"]] + @test lower_bound(v) == 0.0 + @test upper_bound(v) == 1.0 -@testset "Solve" begin - solution = ReverseManufacturing.solve("$(pwd())/../instances/samples/s1.json") - println(JSON.print(solution, 2)) + end + + @testset "build" begin + solution = ReverseManufacturing.solve("$(pwd())/../instances/samples/s1.json") +# println(JSON.print(solution, 2)) + +# @test "plants" in keys(solution) +# @test "F1" in keys(solution["plants"]) +# @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 - @test "plants" in keys(solution) - @test "F1" in keys(solution["plants"]) - @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 \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 87113d7..4bb8cf1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,5 +5,6 @@ using Test @testset "ReverseManufacturing" begin include("instance_test.jl") - #include("model_test.jl") + include("graph_test.jl") + include("model_test.jl") end \ No newline at end of file