mirror of https://github.com/ANL-CEEESA/RELOG.git
parent
849f902562
commit
9df416ed75
@ -0,0 +1,11 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function to_csv(graph::Graph)
|
||||
result = ""
|
||||
for a in graph.arcs
|
||||
result *= "$(a.source.index),$(a.dest.index)\n"
|
||||
end
|
||||
return result
|
||||
end
|
@ -0,0 +1,35 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using Geodesy
|
||||
|
||||
abstract type Node end
|
||||
|
||||
mutable struct Arc
|
||||
source::Node
|
||||
dest::Node
|
||||
values::Dict{String,Float64}
|
||||
end
|
||||
|
||||
mutable struct ProcessNode <: Node
|
||||
index::Int
|
||||
location::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
|
@ -0,0 +1,60 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataStructures
|
||||
using JSON
|
||||
using JSONSchema
|
||||
using Printf
|
||||
using Statistics
|
||||
|
||||
"""
|
||||
_compress(instance::Instance)
|
||||
|
||||
Create a single-period instance from a multi-period one. Specifically,
|
||||
replaces every time-dependent attribute, such as initial_amounts,
|
||||
by a list with a single element, which is either a sum, an average,
|
||||
or something else that makes sense to that specific attribute.
|
||||
"""
|
||||
function _compress(instance::Instance)::Instance
|
||||
T = instance.time
|
||||
compressed = deepcopy(instance)
|
||||
compressed.time = 1
|
||||
compressed.building_period = [1]
|
||||
|
||||
# Compress products
|
||||
for p in compressed.products
|
||||
p.transportation_cost = [mean(p.transportation_cost)]
|
||||
p.transportation_energy = [mean(p.transportation_energy)]
|
||||
for (emission_name, emission_value) in p.transportation_emissions
|
||||
p.transportation_emissions[emission_name] = [mean(emission_value)]
|
||||
end
|
||||
end
|
||||
|
||||
# Compress collection centers
|
||||
for c in compressed.collection_centers
|
||||
c.amount = [maximum(c.amount) * T]
|
||||
end
|
||||
|
||||
# Compress plants
|
||||
for plant in compressed.plants
|
||||
plant.energy = [mean(plant.energy)]
|
||||
for (emission_name, emission_value) in plant.emissions
|
||||
plant.emissions[emission_name] = [mean(emission_value)]
|
||||
end
|
||||
for s in plant.sizes
|
||||
s.capacity *= T
|
||||
s.variable_operating_cost = [mean(s.variable_operating_cost)]
|
||||
s.opening_cost = [s.opening_cost[1]]
|
||||
s.fixed_operating_cost = [sum(s.fixed_operating_cost)]
|
||||
end
|
||||
for (prod_name, disp_limit) in plant.disposal_limit
|
||||
plant.disposal_limit[prod_name] = [sum(disp_limit)]
|
||||
end
|
||||
for (prod_name, disp_cost) in plant.disposal_cost
|
||||
plant.disposal_cost[prod_name] = [mean(disp_cost)]
|
||||
end
|
||||
end
|
||||
|
||||
return compressed
|
||||
end
|
@ -0,0 +1,57 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataStructures
|
||||
using JSON
|
||||
using JSONSchema
|
||||
using Printf
|
||||
using Statistics
|
||||
|
||||
mutable struct Product
|
||||
name::String
|
||||
transportation_cost::Array{Float64}
|
||||
transportation_energy::Array{Float64}
|
||||
transportation_emissions::Dict{String,Array{Float64}}
|
||||
end
|
||||
|
||||
mutable struct CollectionCenter
|
||||
index::Int64
|
||||
name::String
|
||||
latitude::Float64
|
||||
longitude::Float64
|
||||
product::Product
|
||||
amount::Array{Float64}
|
||||
end
|
||||
|
||||
mutable struct PlantSize
|
||||
capacity::Float64
|
||||
variable_operating_cost::Array{Float64}
|
||||
fixed_operating_cost::Array{Float64}
|
||||
opening_cost::Array{Float64}
|
||||
end
|
||||
|
||||
mutable struct Plant
|
||||
index::Int64
|
||||
plant_name::String
|
||||
location_name::String
|
||||
input::Product
|
||||
output::Dict{Product,Float64}
|
||||
latitude::Float64
|
||||
longitude::Float64
|
||||
disposal_limit::Dict{Product,Array{Float64}}
|
||||
disposal_cost::Dict{Product,Array{Float64}}
|
||||
sizes::Array{PlantSize}
|
||||
energy::Array{Float64}
|
||||
emissions::Dict{String,Array{Float64}}
|
||||
storage_limit::Float64
|
||||
storage_cost::Array{Float64}
|
||||
end
|
||||
|
||||
mutable struct Instance
|
||||
time::Int64
|
||||
products::Array{Product,1}
|
||||
collection_centers::Array{CollectionCenter,1}
|
||||
plants::Array{Plant,1}
|
||||
building_period::Array{Int64}
|
||||
end
|
@ -0,0 +1,25 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataStructures
|
||||
using JSON
|
||||
using JSONSchema
|
||||
using Printf
|
||||
using Statistics
|
||||
|
||||
function validate(json, schema)
|
||||
result = JSONSchema.validate(json, schema)
|
||||
if result !== nothing
|
||||
if result isa JSONSchema.SingleIssue
|
||||
path = join(result.path, " → ")
|
||||
if length(path) == 0
|
||||
path = "root"
|
||||
end
|
||||
msg = "$(result.msg) in $(path)"
|
||||
else
|
||||
msg = convert(String, result)
|
||||
end
|
||||
throw(msg)
|
||||
end
|
||||
end
|
@ -1,559 +0,0 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
|
||||
|
||||
|
||||
function build_model(instance::Instance, graph::Graph, optimizer)::JuMP.Model
|
||||
model = Model(optimizer)
|
||||
model[:instance] = instance
|
||||
model[:graph] = graph
|
||||
create_vars!(model)
|
||||
create_objective_function!(model)
|
||||
create_shipping_node_constraints!(model)
|
||||
create_process_node_constraints!(model)
|
||||
return model
|
||||
end
|
||||
|
||||
|
||||
function create_vars!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
model[:flow] =
|
||||
Dict((a, t) => @variable(model, lower_bound = 0) for a in graph.arcs, t = 1:T)
|
||||
model[:dispose] = Dict(
|
||||
(n, t) => @variable(
|
||||
model,
|
||||
lower_bound = 0,
|
||||
upper_bound = n.location.disposal_limit[n.product][t]
|
||||
) for n in values(graph.plant_shipping_nodes), t = 1:T
|
||||
)
|
||||
model[:store] = Dict(
|
||||
(n, t) =>
|
||||
@variable(model, lower_bound = 0, upper_bound = n.location.storage_limit)
|
||||
for n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
model[:process] = Dict(
|
||||
(n, t) => @variable(model, lower_bound = 0) for
|
||||
n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
model[:open_plant] = Dict(
|
||||
(n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
|
||||
t = 1:T
|
||||
)
|
||||
model[:is_open] = Dict(
|
||||
(n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
|
||||
t = 1:T
|
||||
)
|
||||
model[:capacity] = Dict(
|
||||
(n, t) => @variable(
|
||||
model,
|
||||
lower_bound = 0,
|
||||
upper_bound = n.location.sizes[2].capacity
|
||||
) for n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
model[:expansion] = Dict(
|
||||
(n, t) => @variable(
|
||||
model,
|
||||
lower_bound = 0,
|
||||
upper_bound = n.location.sizes[2].capacity - n.location.sizes[1].capacity
|
||||
) for n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
function slope_open(plant, t)
|
||||
if plant.sizes[2].capacity <= plant.sizes[1].capacity
|
||||
0.0
|
||||
else
|
||||
(plant.sizes[2].opening_cost[t] - plant.sizes[1].opening_cost[t]) /
|
||||
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
||||
end
|
||||
end
|
||||
|
||||
function slope_fix_oper_cost(plant, t)
|
||||
if plant.sizes[2].capacity <= plant.sizes[1].capacity
|
||||
0.0
|
||||
else
|
||||
(plant.sizes[2].fixed_operating_cost[t] - plant.sizes[1].fixed_operating_cost[t]) /
|
||||
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
||||
end
|
||||
end
|
||||
|
||||
function create_objective_function!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
obj = AffExpr(0.0)
|
||||
|
||||
# Process node costs
|
||||
for n in values(graph.process_nodes), t = 1:T
|
||||
|
||||
# Transportation and variable operating costs
|
||||
for a in n.incoming_arcs
|
||||
c = n.location.input.transportation_cost[t] * a.values["distance"]
|
||||
add_to_expression!(obj, c, model[:flow][a, t])
|
||||
end
|
||||
|
||||
# Opening costs
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.sizes[1].opening_cost[t],
|
||||
model[:open_plant][n, t],
|
||||
)
|
||||
|
||||
# Fixed operating costs (base)
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.sizes[1].fixed_operating_cost[t],
|
||||
model[:is_open][n, t],
|
||||
)
|
||||
|
||||
# Fixed operating costs (expansion)
|
||||
add_to_expression!(obj, slope_fix_oper_cost(n.location, t), model[:expansion][n, t])
|
||||
|
||||
# Processing costs
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.sizes[1].variable_operating_cost[t],
|
||||
model[:process][n, t],
|
||||
)
|
||||
|
||||
# Storage costs
|
||||
add_to_expression!(obj, n.location.storage_cost[t], model[:store][n, t])
|
||||
|
||||
# Expansion costs
|
||||
if t < T
|
||||
add_to_expression!(
|
||||
obj,
|
||||
slope_open(n.location, t) - slope_open(n.location, t + 1),
|
||||
model[:expansion][n, t],
|
||||
)
|
||||
else
|
||||
add_to_expression!(obj, slope_open(n.location, t), model[:expansion][n, t])
|
||||
end
|
||||
end
|
||||
|
||||
# Shipping node costs
|
||||
for n in values(graph.plant_shipping_nodes), t = 1:T
|
||||
|
||||
# Disposal costs
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.disposal_cost[n.product][t],
|
||||
model[:dispose][n, t],
|
||||
)
|
||||
end
|
||||
|
||||
@objective(model, Min, obj)
|
||||
end
|
||||
|
||||
|
||||
function create_shipping_node_constraints!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
model[:eq_balance] = OrderedDict()
|
||||
for t = 1:T
|
||||
# Collection centers
|
||||
for n in graph.collection_shipping_nodes
|
||||
model[:eq_balance][n, t] = @constraint(
|
||||
model,
|
||||
sum(model[:flow][a, t] for a in n.outgoing_arcs) == n.location.amount[t]
|
||||
)
|
||||
end
|
||||
|
||||
# Plants
|
||||
for n in graph.plant_shipping_nodes
|
||||
@constraint(
|
||||
model,
|
||||
sum(model[:flow][a, t] for a in n.incoming_arcs) ==
|
||||
sum(model[:flow][a, t] for a in n.outgoing_arcs) + model[:dispose][n, t]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function create_process_node_constraints!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
|
||||
for t = 1:T, n in graph.process_nodes
|
||||
input_sum = AffExpr(0.0)
|
||||
for a in n.incoming_arcs
|
||||
add_to_expression!(input_sum, 1.0, model[:flow][a, t])
|
||||
end
|
||||
|
||||
# Output amount is implied by amount processed
|
||||
for a in n.outgoing_arcs
|
||||
@constraint(
|
||||
model,
|
||||
model[:flow][a, t] == a.values["weight"] * model[:process][n, t]
|
||||
)
|
||||
end
|
||||
|
||||
# If plant is closed, capacity is zero
|
||||
@constraint(
|
||||
model,
|
||||
model[:capacity][n, t] <= n.location.sizes[2].capacity * model[:is_open][n, t]
|
||||
)
|
||||
|
||||
# If plant is open, capacity is greater than base
|
||||
@constraint(
|
||||
model,
|
||||
model[:capacity][n, t] >= n.location.sizes[1].capacity * model[:is_open][n, t]
|
||||
)
|
||||
|
||||
# Capacity is linked to expansion
|
||||
@constraint(
|
||||
model,
|
||||
model[:capacity][n, t] <=
|
||||
n.location.sizes[1].capacity + model[:expansion][n, t]
|
||||
)
|
||||
|
||||
# Can only process up to capacity
|
||||
@constraint(model, model[:process][n, t] <= model[:capacity][n, t])
|
||||
|
||||
if t > 1
|
||||
# Plant capacity can only increase over time
|
||||
@constraint(model, model[:capacity][n, t] >= model[:capacity][n, t-1])
|
||||
@constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
|
||||
end
|
||||
|
||||
# Amount received equals amount processed plus stored
|
||||
store_in = 0
|
||||
if t > 1
|
||||
store_in = model[:store][n, t-1]
|
||||
end
|
||||
if t == T
|
||||
@constraint(model, model[:store][n, t] == 0)
|
||||
end
|
||||
@constraint(
|
||||
model,
|
||||
input_sum + store_in == model[:store][n, t] + model[:process][n, t]
|
||||
)
|
||||
|
||||
|
||||
# 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(
|
||||
model,
|
||||
model[:is_open][n, t] == model[:is_open][n, t-1] + model[:open_plant][n, t]
|
||||
)
|
||||
else
|
||||
@constraint(model, model[:is_open][n, t] == model[:open_plant][n, t])
|
||||
end
|
||||
|
||||
# Plant can only be opened during building period
|
||||
if t ∉ model[:instance].building_period
|
||||
@constraint(model, model[:open_plant][n, t] == 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
default_milp_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
default_lp_optimizer = optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
|
||||
|
||||
function solve(
|
||||
instance::Instance;
|
||||
optimizer = nothing,
|
||||
output = nothing,
|
||||
marginal_costs = true,
|
||||
)
|
||||
|
||||
milp_optimizer = lp_optimizer = optimizer
|
||||
if optimizer == nothing
|
||||
milp_optimizer = default_milp_optimizer
|
||||
lp_optimizer = default_lp_optimizer
|
||||
end
|
||||
|
||||
@info "Building graph..."
|
||||
graph = RELOG.build_graph(instance)
|
||||
@info @sprintf(" %12d time periods", instance.time)
|
||||
@info @sprintf(" %12d process nodes", length(graph.process_nodes))
|
||||
@info @sprintf(" %12d shipping nodes (plant)", length(graph.plant_shipping_nodes))
|
||||
@info @sprintf(
|
||||
" %12d shipping nodes (collection)",
|
||||
length(graph.collection_shipping_nodes)
|
||||
)
|
||||
@info @sprintf(" %12d arcs", length(graph.arcs))
|
||||
|
||||
@info "Building optimization model..."
|
||||
model = RELOG.build_model(instance, graph, milp_optimizer)
|
||||
|
||||
@info "Optimizing MILP..."
|
||||
JuMP.optimize!(model)
|
||||
|
||||
if !has_values(model)
|
||||
@warn "No solution available"
|
||||
return OrderedDict()
|
||||
end
|
||||
|
||||
if marginal_costs
|
||||
@info "Re-optimizing with integer variables fixed..."
|
||||
all_vars = JuMP.all_variables(model)
|
||||
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
|
||||
JuMP.set_optimizer(model, lp_optimizer)
|
||||
for var in all_vars
|
||||
if JuMP.is_binary(var)
|
||||
JuMP.unset_binary(var)
|
||||
JuMP.fix(var, vals[var])
|
||||
end
|
||||
end
|
||||
JuMP.optimize!(model)
|
||||
end
|
||||
|
||||
@info "Extracting solution..."
|
||||
solution = get_solution(model, marginal_costs = marginal_costs)
|
||||
|
||||
if output != nothing
|
||||
write(solution, output)
|
||||
end
|
||||
|
||||
return solution
|
||||
end
|
||||
|
||||
function solve(filename::AbstractString; heuristic = false, kwargs...)
|
||||
@info "Reading $filename..."
|
||||
instance = RELOG.parsefile(filename)
|
||||
if heuristic && instance.time > 1
|
||||
@info "Solving single-period version..."
|
||||
compressed = _compress(instance)
|
||||
csol = solve(compressed; output = nothing, marginal_costs = false, kwargs...)
|
||||
@info "Filtering candidate locations..."
|
||||
selected_pairs = []
|
||||
for (plant_name, plant_dict) in csol["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
push!(selected_pairs, (plant_name, location_name))
|
||||
end
|
||||
end
|
||||
filtered_plants = []
|
||||
for p in instance.plants
|
||||
if (p.plant_name, p.location_name) in selected_pairs
|
||||
push!(filtered_plants, p)
|
||||
end
|
||||
end
|
||||
instance.plants = filtered_plants
|
||||
@info "Solving original version..."
|
||||
end
|
||||
sol = solve(instance; kwargs...)
|
||||
return sol
|
||||
end
|
||||
|
||||
|
||||
function get_solution(model::JuMP.Model; marginal_costs = true)
|
||||
graph, instance = model[:graph], model[:instance]
|
||||
T = instance.time
|
||||
|
||||
output = OrderedDict(
|
||||
"Plants" => OrderedDict(),
|
||||
"Products" => OrderedDict(),
|
||||
"Costs" => OrderedDict(
|
||||
"Fixed operating (\$)" => zeros(T),
|
||||
"Variable operating (\$)" => zeros(T),
|
||||
"Opening (\$)" => zeros(T),
|
||||
"Transportation (\$)" => zeros(T),
|
||||
"Disposal (\$)" => zeros(T),
|
||||
"Expansion (\$)" => zeros(T),
|
||||
"Storage (\$)" => zeros(T),
|
||||
"Total (\$)" => zeros(T),
|
||||
),
|
||||
"Energy" =>
|
||||
OrderedDict("Plants (GJ)" => zeros(T), "Transportation (GJ)" => zeros(T)),
|
||||
"Emissions" => OrderedDict(
|
||||
"Plants (tonne)" => OrderedDict(),
|
||||
"Transportation (tonne)" => OrderedDict(),
|
||||
),
|
||||
)
|
||||
|
||||
plant_to_process_node = OrderedDict(n.location => n for n in graph.process_nodes)
|
||||
plant_to_shipping_nodes = OrderedDict()
|
||||
for p in instance.plants
|
||||
plant_to_shipping_nodes[p] = []
|
||||
for a in plant_to_process_node[p].outgoing_arcs
|
||||
push!(plant_to_shipping_nodes[p], a.dest)
|
||||
end
|
||||
end
|
||||
|
||||
# Products
|
||||
if marginal_costs
|
||||
for n in graph.collection_shipping_nodes
|
||||
location_dict = OrderedDict{Any,Any}(
|
||||
"Marginal cost (\$/tonne)" => [
|
||||
round(abs(JuMP.shadow_price(model[:eq_balance][n, t])), digits = 2) for t = 1:T
|
||||
],
|
||||
)
|
||||
if n.product.name ∉ keys(output["Products"])
|
||||
output["Products"][n.product.name] = OrderedDict()
|
||||
end
|
||||
output["Products"][n.product.name][n.location.name] = location_dict
|
||||
end
|
||||
end
|
||||
|
||||
# Plants
|
||||
for plant in instance.plants
|
||||
skip_plant = true
|
||||
process_node = plant_to_process_node[plant]
|
||||
plant_dict = OrderedDict{Any,Any}(
|
||||
"Input" => OrderedDict(),
|
||||
"Output" =>
|
||||
OrderedDict("Send" => OrderedDict(), "Dispose" => OrderedDict()),
|
||||
"Input product" => plant.input.name,
|
||||
"Total input (tonne)" => [0.0 for t = 1:T],
|
||||
"Total output" => OrderedDict(),
|
||||
"Latitude (deg)" => plant.latitude,
|
||||
"Longitude (deg)" => plant.longitude,
|
||||
"Capacity (tonne)" =>
|
||||
[JuMP.value(model[:capacity][process_node, t]) for t = 1:T],
|
||||
"Opening cost (\$)" => [
|
||||
JuMP.value(model[:open_plant][process_node, t]) *
|
||||
plant.sizes[1].opening_cost[t] for t = 1:T
|
||||
],
|
||||
"Fixed operating cost (\$)" => [
|
||||
JuMP.value(model[:is_open][process_node, t]) *
|
||||
plant.sizes[1].fixed_operating_cost[t] +
|
||||
JuMP.value(model[:expansion][process_node, t]) *
|
||||
slope_fix_oper_cost(plant, t) for t = 1:T
|
||||
],
|
||||
"Expansion cost (\$)" => [
|
||||
(
|
||||
if t == 1
|
||||
slope_open(plant, t) * JuMP.value(model[:expansion][process_node, t])
|
||||
else
|
||||
slope_open(plant, t) * (
|
||||
JuMP.value(model[:expansion][process_node, t]) -
|
||||
JuMP.value(model[:expansion][process_node, t-1])
|
||||
)
|
||||
end
|
||||
) for t = 1:T
|
||||
],
|
||||
"Process (tonne)" =>
|
||||
[JuMP.value(model[:process][process_node, t]) for t = 1:T],
|
||||
"Variable operating cost (\$)" => [
|
||||
JuMP.value(model[:process][process_node, t]) *
|
||||
plant.sizes[1].variable_operating_cost[t] for t = 1:T
|
||||
],
|
||||
"Storage (tonne)" =>
|
||||
[JuMP.value(model[:store][process_node, t]) for t = 1:T],
|
||||
"Storage cost (\$)" => [
|
||||
JuMP.value(model[:store][process_node, t]) * plant.storage_cost[t]
|
||||
for t = 1:T
|
||||
],
|
||||
)
|
||||
output["Costs"]["Fixed operating (\$)"] += plant_dict["Fixed operating cost (\$)"]
|
||||
output["Costs"]["Variable operating (\$)"] +=
|
||||
plant_dict["Variable operating cost (\$)"]
|
||||
output["Costs"]["Opening (\$)"] += plant_dict["Opening cost (\$)"]
|
||||
output["Costs"]["Expansion (\$)"] += plant_dict["Expansion cost (\$)"]
|
||||
output["Costs"]["Storage (\$)"] += plant_dict["Storage cost (\$)"]
|
||||
|
||||
# Inputs
|
||||
for a in process_node.incoming_arcs
|
||||
vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
|
||||
if sum(vals) <= 1e-3
|
||||
continue
|
||||
end
|
||||
skip_plant = false
|
||||
dict = OrderedDict{Any,Any}(
|
||||
"Amount (tonne)" => vals,
|
||||
"Distance (km)" => a.values["distance"],
|
||||
"Latitude (deg)" => a.source.location.latitude,
|
||||
"Longitude (deg)" => a.source.location.longitude,
|
||||
"Transportation cost (\$)" =>
|
||||
a.source.product.transportation_cost .* vals .* a.values["distance"],
|
||||
"Transportation energy (J)" =>
|
||||
vals .* a.values["distance"] .* a.source.product.transportation_energy,
|
||||
"Emissions (tonne)" => OrderedDict(),
|
||||
)
|
||||
emissions_dict = output["Emissions"]["Transportation (tonne)"]
|
||||
for (em_name, em_values) in a.source.product.transportation_emissions
|
||||
dict["Emissions (tonne)"][em_name] =
|
||||
em_values .* dict["Amount (tonne)"] .* a.values["distance"]
|
||||
if em_name ∉ keys(emissions_dict)
|
||||
emissions_dict[em_name] = zeros(T)
|
||||
end
|
||||
emissions_dict[em_name] += dict["Emissions (tonne)"][em_name]
|
||||
end
|
||||
if a.source.location isa CollectionCenter
|
||||
plant_name = "Origin"
|
||||
location_name = a.source.location.name
|
||||
else
|
||||
plant_name = a.source.location.plant_name
|
||||
location_name = a.source.location.location_name
|
||||
end
|
||||
|
||||
if plant_name ∉ keys(plant_dict["Input"])
|
||||
plant_dict["Input"][plant_name] = OrderedDict()
|
||||
end
|
||||
plant_dict["Input"][plant_name][location_name] = dict
|
||||
plant_dict["Total input (tonne)"] += vals
|
||||
output["Costs"]["Transportation (\$)"] += dict["Transportation cost (\$)"]
|
||||
output["Energy"]["Transportation (GJ)"] +=
|
||||
dict["Transportation energy (J)"] / 1e9
|
||||
end
|
||||
|
||||
plant_dict["Energy (GJ)"] = plant_dict["Total input (tonne)"] .* plant.energy
|
||||
output["Energy"]["Plants (GJ)"] += plant_dict["Energy (GJ)"]
|
||||
|
||||
plant_dict["Emissions (tonne)"] = OrderedDict()
|
||||
emissions_dict = output["Emissions"]["Plants (tonne)"]
|
||||
for (em_name, em_values) in plant.emissions
|
||||
plant_dict["Emissions (tonne)"][em_name] =
|
||||
em_values .* plant_dict["Total input (tonne)"]
|
||||
if em_name ∉ keys(emissions_dict)
|
||||
emissions_dict[em_name] = zeros(T)
|
||||
end
|
||||
emissions_dict[em_name] += plant_dict["Emissions (tonne)"][em_name]
|
||||
end
|
||||
|
||||
# Outputs
|
||||
for shipping_node in plant_to_shipping_nodes[plant]
|
||||
product_name = shipping_node.product.name
|
||||
plant_dict["Total output"][product_name] = zeros(T)
|
||||
plant_dict["Output"]["Send"][product_name] = product_dict = OrderedDict()
|
||||
|
||||
disposal_amount = [JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T]
|
||||
if sum(disposal_amount) > 1e-5
|
||||
skip_plant = false
|
||||
plant_dict["Output"]["Dispose"][product_name] =
|
||||
disposal_dict = OrderedDict()
|
||||
disposal_dict["Amount (tonne)"] =
|
||||
[JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T]
|
||||
disposal_dict["Cost (\$)"] = [
|
||||
disposal_dict["Amount (tonne)"][t] *
|
||||
plant.disposal_cost[shipping_node.product][t] for t = 1:T
|
||||
]
|
||||
plant_dict["Total output"][product_name] += disposal_amount
|
||||
output["Costs"]["Disposal (\$)"] += disposal_dict["Cost (\$)"]
|
||||
end
|
||||
|
||||
for a in shipping_node.outgoing_arcs
|
||||
vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
|
||||
if sum(vals) <= 1e-3
|
||||
continue
|
||||
end
|
||||
skip_plant = false
|
||||
dict = OrderedDict(
|
||||
"Amount (tonne)" => vals,
|
||||
"Distance (km)" => a.values["distance"],
|
||||
"Latitude (deg)" => a.dest.location.latitude,
|
||||
"Longitude (deg)" => a.dest.location.longitude,
|
||||
)
|
||||
if a.dest.location.plant_name ∉ keys(product_dict)
|
||||
product_dict[a.dest.location.plant_name] = OrderedDict()
|
||||
end
|
||||
product_dict[a.dest.location.plant_name][a.dest.location.location_name] =
|
||||
dict
|
||||
plant_dict["Total output"][product_name] += vals
|
||||
end
|
||||
end
|
||||
|
||||
if !skip_plant
|
||||
if plant.plant_name ∉ keys(output["Plants"])
|
||||
output["Plants"][plant.plant_name] = OrderedDict()
|
||||
end
|
||||
output["Plants"][plant.plant_name][plant.location_name] = plant_dict
|
||||
end
|
||||
end
|
||||
|
||||
output["Costs"]["Total (\$)"] = sum(values(output["Costs"]))
|
||||
return output
|
||||
end
|
@ -0,0 +1,250 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
|
||||
|
||||
|
||||
function build_model(instance::Instance, graph::Graph, optimizer)::JuMP.Model
|
||||
model = Model(optimizer)
|
||||
model[:instance] = instance
|
||||
model[:graph] = graph
|
||||
create_vars!(model)
|
||||
create_objective_function!(model)
|
||||
create_shipping_node_constraints!(model)
|
||||
create_process_node_constraints!(model)
|
||||
return model
|
||||
end
|
||||
|
||||
|
||||
function create_vars!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
model[:flow] =
|
||||
Dict((a, t) => @variable(model, lower_bound = 0) for a in graph.arcs, t = 1:T)
|
||||
model[:dispose] = Dict(
|
||||
(n, t) => @variable(
|
||||
model,
|
||||
lower_bound = 0,
|
||||
upper_bound = n.location.disposal_limit[n.product][t]
|
||||
) for n in values(graph.plant_shipping_nodes), t = 1:T
|
||||
)
|
||||
model[:store] = Dict(
|
||||
(n, t) =>
|
||||
@variable(model, lower_bound = 0, upper_bound = n.location.storage_limit)
|
||||
for n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
model[:process] = Dict(
|
||||
(n, t) => @variable(model, lower_bound = 0) for
|
||||
n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
model[:open_plant] = Dict(
|
||||
(n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
|
||||
t = 1:T
|
||||
)
|
||||
model[:is_open] = Dict(
|
||||
(n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
|
||||
t = 1:T
|
||||
)
|
||||
model[:capacity] = Dict(
|
||||
(n, t) => @variable(
|
||||
model,
|
||||
lower_bound = 0,
|
||||
upper_bound = n.location.sizes[2].capacity
|
||||
) for n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
model[:expansion] = Dict(
|
||||
(n, t) => @variable(
|
||||
model,
|
||||
lower_bound = 0,
|
||||
upper_bound = n.location.sizes[2].capacity - n.location.sizes[1].capacity
|
||||
) for n in values(graph.process_nodes), t = 1:T
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
function slope_open(plant, t)
|
||||
if plant.sizes[2].capacity <= plant.sizes[1].capacity
|
||||
0.0
|
||||
else
|
||||
(plant.sizes[2].opening_cost[t] - plant.sizes[1].opening_cost[t]) /
|
||||
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
||||
end
|
||||
end
|
||||
|
||||
function slope_fix_oper_cost(plant, t)
|
||||
if plant.sizes[2].capacity <= plant.sizes[1].capacity
|
||||
0.0
|
||||
else
|
||||
(plant.sizes[2].fixed_operating_cost[t] - plant.sizes[1].fixed_operating_cost[t]) /
|
||||
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
||||
end
|
||||
end
|
||||
|
||||
function create_objective_function!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
obj = AffExpr(0.0)
|
||||
|
||||
# Process node costs
|
||||
for n in values(graph.process_nodes), t = 1:T
|
||||
|
||||
# Transportation and variable operating costs
|
||||
for a in n.incoming_arcs
|
||||
c = n.location.input.transportation_cost[t] * a.values["distance"]
|
||||
add_to_expression!(obj, c, model[:flow][a, t])
|
||||
end
|
||||
|
||||
# Opening costs
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.sizes[1].opening_cost[t],
|
||||
model[:open_plant][n, t],
|
||||
)
|
||||
|
||||
# Fixed operating costs (base)
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.sizes[1].fixed_operating_cost[t],
|
||||
model[:is_open][n, t],
|
||||
)
|
||||
|
||||
# Fixed operating costs (expansion)
|
||||
add_to_expression!(obj, slope_fix_oper_cost(n.location, t), model[:expansion][n, t])
|
||||
|
||||
# Processing costs
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.sizes[1].variable_operating_cost[t],
|
||||
model[:process][n, t],
|
||||
)
|
||||
|
||||
# Storage costs
|
||||
add_to_expression!(obj, n.location.storage_cost[t], model[:store][n, t])
|
||||
|
||||
# Expansion costs
|
||||
if t < T
|
||||
add_to_expression!(
|
||||
obj,
|
||||
slope_open(n.location, t) - slope_open(n.location, t + 1),
|
||||
model[:expansion][n, t],
|
||||
)
|
||||
else
|
||||
add_to_expression!(obj, slope_open(n.location, t), model[:expansion][n, t])
|
||||
end
|
||||
end
|
||||
|
||||
# Shipping node costs
|
||||
for n in values(graph.plant_shipping_nodes), t = 1:T
|
||||
|
||||
# Disposal costs
|
||||
add_to_expression!(
|
||||
obj,
|
||||
n.location.disposal_cost[n.product][t],
|
||||
model[:dispose][n, t],
|
||||
)
|
||||
end
|
||||
|
||||
@objective(model, Min, obj)
|
||||
end
|
||||
|
||||
|
||||
function create_shipping_node_constraints!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
model[:eq_balance] = OrderedDict()
|
||||
for t = 1:T
|
||||
# Collection centers
|
||||
for n in graph.collection_shipping_nodes
|
||||
model[:eq_balance][n, t] = @constraint(
|
||||
model,
|
||||
sum(model[:flow][a, t] for a in n.outgoing_arcs) == n.location.amount[t]
|
||||
)
|
||||
end
|
||||
|
||||
# Plants
|
||||
for n in graph.plant_shipping_nodes
|
||||
@constraint(
|
||||
model,
|
||||
sum(model[:flow][a, t] for a in n.incoming_arcs) ==
|
||||
sum(model[:flow][a, t] for a in n.outgoing_arcs) + model[:dispose][n, t]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function create_process_node_constraints!(model::JuMP.Model)
|
||||
graph, T = model[:graph], model[:instance].time
|
||||
|
||||
for t = 1:T, n in graph.process_nodes
|
||||
input_sum = AffExpr(0.0)
|
||||
for a in n.incoming_arcs
|
||||
add_to_expression!(input_sum, 1.0, model[:flow][a, t])
|
||||
end
|
||||
|
||||
# Output amount is implied by amount processed
|
||||
for a in n.outgoing_arcs
|
||||
@constraint(
|
||||
model,
|
||||
model[:flow][a, t] == a.values["weight"] * model[:process][n, t]
|
||||
)
|
||||
end
|
||||
|
||||
# If plant is closed, capacity is zero
|
||||
@constraint(
|
||||
model,
|
||||
model[:capacity][n, t] <= n.location.sizes[2].capacity * model[:is_open][n, t]
|
||||
)
|
||||
|
||||
# If plant is open, capacity is greater than base
|
||||
@constraint(
|
||||
model,
|
||||
model[:capacity][n, t] >= n.location.sizes[1].capacity * model[:is_open][n, t]
|
||||
)
|
||||
|
||||
# Capacity is linked to expansion
|
||||
@constraint(
|
||||
model,
|
||||
model[:capacity][n, t] <=
|
||||
n.location.sizes[1].capacity + model[:expansion][n, t]
|
||||
)
|
||||
|
||||
# Can only process up to capacity
|
||||
@constraint(model, model[:process][n, t] <= model[:capacity][n, t])
|
||||
|
||||
if t > 1
|
||||
# Plant capacity can only increase over time
|
||||
@constraint(model, model[:capacity][n, t] >= model[:capacity][n, t-1])
|
||||
@constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
|
||||
end
|
||||
|
||||
# Amount received equals amount processed plus stored
|
||||
store_in = 0
|
||||
if t > 1
|
||||
store_in = model[:store][n, t-1]
|
||||
end
|
||||
if t == T
|
||||
@constraint(model, model[:store][n, t] == 0)
|
||||
end
|
||||
@constraint(
|
||||
model,
|
||||
input_sum + store_in == model[:store][n, t] + model[:process][n, t]
|
||||
)
|
||||
|
||||
|
||||
# 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(
|
||||
model,
|
||||
model[:is_open][n, t] == model[:is_open][n, t-1] + model[:open_plant][n, t]
|
||||
)
|
||||
else
|
||||
@constraint(model, model[:is_open][n, t] == model[:open_plant][n, t])
|
||||
end
|
||||
|
||||
# Plant can only be opened during building period
|
||||
if t ∉ model[:instance].building_period
|
||||
@constraint(model, model[:open_plant][n, t] == 0)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,224 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
|
||||
|
||||
function get_solution(model::JuMP.Model; marginal_costs = true)
|
||||
graph, instance = model[:graph], model[:instance]
|
||||
T = instance.time
|
||||
|
||||
output = OrderedDict(
|
||||
"Plants" => OrderedDict(),
|
||||
"Products" => OrderedDict(),
|
||||
"Costs" => OrderedDict(
|
||||
"Fixed operating (\$)" => zeros(T),
|
||||
"Variable operating (\$)" => zeros(T),
|
||||
"Opening (\$)" => zeros(T),
|
||||
"Transportation (\$)" => zeros(T),
|
||||
"Disposal (\$)" => zeros(T),
|
||||
"Expansion (\$)" => zeros(T),
|
||||
"Storage (\$)" => zeros(T),
|
||||
"Total (\$)" => zeros(T),
|
||||
),
|
||||
"Energy" =>
|
||||
OrderedDict("Plants (GJ)" => zeros(T), "Transportation (GJ)" => zeros(T)),
|
||||
"Emissions" => OrderedDict(
|
||||
"Plants (tonne)" => OrderedDict(),
|
||||
"Transportation (tonne)" => OrderedDict(),
|
||||
),
|
||||
)
|
||||
|
||||
plant_to_process_node = OrderedDict(n.location => n for n in graph.process_nodes)
|
||||
plant_to_shipping_nodes = OrderedDict()
|
||||
for p in instance.plants
|
||||
plant_to_shipping_nodes[p] = []
|
||||
for a in plant_to_process_node[p].outgoing_arcs
|
||||
push!(plant_to_shipping_nodes[p], a.dest)
|
||||
end
|
||||
end
|
||||
|
||||
# Products
|
||||
if marginal_costs
|
||||
for n in graph.collection_shipping_nodes
|
||||
location_dict = OrderedDict{Any,Any}(
|
||||
"Marginal cost (\$/tonne)" => [
|
||||
round(abs(JuMP.shadow_price(model[:eq_balance][n, t])), digits = 2) for t = 1:T
|
||||
],
|
||||
)
|
||||
if n.product.name ∉ keys(output["Products"])
|
||||
output["Products"][n.product.name] = OrderedDict()
|
||||
end
|
||||
output["Products"][n.product.name][n.location.name] = location_dict
|
||||
end
|
||||
end
|
||||
|
||||
# Plants
|
||||
for plant in instance.plants
|
||||
skip_plant = true
|
||||
process_node = plant_to_process_node[plant]
|
||||
plant_dict = OrderedDict{Any,Any}(
|
||||
"Input" => OrderedDict(),
|
||||
"Output" =>
|
||||
OrderedDict("Send" => OrderedDict(), "Dispose" => OrderedDict()),
|
||||
"Input product" => plant.input.name,
|
||||
"Total input (tonne)" => [0.0 for t = 1:T],
|
||||
"Total output" => OrderedDict(),
|
||||
"Latitude (deg)" => plant.latitude,
|
||||
"Longitude (deg)" => plant.longitude,
|
||||
"Capacity (tonne)" =>
|
||||
[JuMP.value(model[:capacity][process_node, t]) for t = 1:T],
|
||||
"Opening cost (\$)" => [
|
||||
JuMP.value(model[:open_plant][process_node, t]) *
|
||||
plant.sizes[1].opening_cost[t] for t = 1:T
|
||||
],
|
||||
"Fixed operating cost (\$)" => [
|
||||
JuMP.value(model[:is_open][process_node, t]) *
|
||||
plant.sizes[1].fixed_operating_cost[t] +
|
||||
JuMP.value(model[:expansion][process_node, t]) *
|
||||
slope_fix_oper_cost(plant, t) for t = 1:T
|
||||
],
|
||||
"Expansion cost (\$)" => [
|
||||
(
|
||||
if t == 1
|
||||
slope_open(plant, t) * JuMP.value(model[:expansion][process_node, t])
|
||||
else
|
||||
slope_open(plant, t) * (
|
||||
JuMP.value(model[:expansion][process_node, t]) -
|
||||
JuMP.value(model[:expansion][process_node, t-1])
|
||||
)
|
||||
end
|
||||
) for t = 1:T
|
||||
],
|
||||
"Process (tonne)" =>
|
||||
[JuMP.value(model[:process][process_node, t]) for t = 1:T],
|
||||
"Variable operating cost (\$)" => [
|
||||
JuMP.value(model[:process][process_node, t]) *
|
||||
plant.sizes[1].variable_operating_cost[t] for t = 1:T
|
||||
],
|
||||
"Storage (tonne)" =>
|
||||
[JuMP.value(model[:store][process_node, t]) for t = 1:T],
|
||||
"Storage cost (\$)" => [
|
||||
JuMP.value(model[:store][process_node, t]) * plant.storage_cost[t]
|
||||
for t = 1:T
|
||||
],
|
||||
)
|
||||
output["Costs"]["Fixed operating (\$)"] += plant_dict["Fixed operating cost (\$)"]
|
||||
output["Costs"]["Variable operating (\$)"] +=
|
||||
plant_dict["Variable operating cost (\$)"]
|
||||
output["Costs"]["Opening (\$)"] += plant_dict["Opening cost (\$)"]
|
||||
output["Costs"]["Expansion (\$)"] += plant_dict["Expansion cost (\$)"]
|
||||
output["Costs"]["Storage (\$)"] += plant_dict["Storage cost (\$)"]
|
||||
|
||||
# Inputs
|
||||
for a in process_node.incoming_arcs
|
||||
vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
|
||||
if sum(vals) <= 1e-3
|
||||
continue
|
||||
end
|
||||
skip_plant = false
|
||||
dict = OrderedDict{Any,Any}(
|
||||
"Amount (tonne)" => vals,
|
||||
"Distance (km)" => a.values["distance"],
|
||||
"Latitude (deg)" => a.source.location.latitude,
|
||||
"Longitude (deg)" => a.source.location.longitude,
|
||||
"Transportation cost (\$)" =>
|
||||
a.source.product.transportation_cost .* vals .* a.values["distance"],
|
||||
"Transportation energy (J)" =>
|
||||
vals .* a.values["distance"] .* a.source.product.transportation_energy,
|
||||
"Emissions (tonne)" => OrderedDict(),
|
||||
)
|
||||
emissions_dict = output["Emissions"]["Transportation (tonne)"]
|
||||
for (em_name, em_values) in a.source.product.transportation_emissions
|
||||
dict["Emissions (tonne)"][em_name] =
|
||||
em_values .* dict["Amount (tonne)"] .* a.values["distance"]
|
||||
if em_name ∉ keys(emissions_dict)
|
||||
emissions_dict[em_name] = zeros(T)
|
||||
end
|
||||
emissions_dict[em_name] += dict["Emissions (tonne)"][em_name]
|
||||
end
|
||||
if a.source.location isa CollectionCenter
|
||||
plant_name = "Origin"
|
||||
location_name = a.source.location.name
|
||||
else
|
||||
plant_name = a.source.location.plant_name
|
||||
location_name = a.source.location.location_name
|
||||
end
|
||||
|
||||
if plant_name ∉ keys(plant_dict["Input"])
|
||||
plant_dict["Input"][plant_name] = OrderedDict()
|
||||
end
|
||||
plant_dict["Input"][plant_name][location_name] = dict
|
||||
plant_dict["Total input (tonne)"] += vals
|
||||
output["Costs"]["Transportation (\$)"] += dict["Transportation cost (\$)"]
|
||||
output["Energy"]["Transportation (GJ)"] +=
|
||||
dict["Transportation energy (J)"] / 1e9
|
||||
end
|
||||
|
||||
plant_dict["Energy (GJ)"] = plant_dict["Total input (tonne)"] .* plant.energy
|
||||
output["Energy"]["Plants (GJ)"] += plant_dict["Energy (GJ)"]
|
||||
|
||||
plant_dict["Emissions (tonne)"] = OrderedDict()
|
||||
emissions_dict = output["Emissions"]["Plants (tonne)"]
|
||||
for (em_name, em_values) in plant.emissions
|
||||
plant_dict["Emissions (tonne)"][em_name] =
|
||||
em_values .* plant_dict["Total input (tonne)"]
|
||||
if em_name ∉ keys(emissions_dict)
|
||||
emissions_dict[em_name] = zeros(T)
|
||||
end
|
||||
emissions_dict[em_name] += plant_dict["Emissions (tonne)"][em_name]
|
||||
end
|
||||
|
||||
# Outputs
|
||||
for shipping_node in plant_to_shipping_nodes[plant]
|
||||
product_name = shipping_node.product.name
|
||||
plant_dict["Total output"][product_name] = zeros(T)
|
||||
plant_dict["Output"]["Send"][product_name] = product_dict = OrderedDict()
|
||||
|
||||
disposal_amount = [JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T]
|
||||
if sum(disposal_amount) > 1e-5
|
||||
skip_plant = false
|
||||
plant_dict["Output"]["Dispose"][product_name] =
|
||||
disposal_dict = OrderedDict()
|
||||
disposal_dict["Amount (tonne)"] =
|
||||
[JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T]
|
||||
disposal_dict["Cost (\$)"] = [
|
||||
disposal_dict["Amount (tonne)"][t] *
|
||||
plant.disposal_cost[shipping_node.product][t] for t = 1:T
|
||||
]
|
||||
plant_dict["Total output"][product_name] += disposal_amount
|
||||
output["Costs"]["Disposal (\$)"] += disposal_dict["Cost (\$)"]
|
||||
end
|
||||
|
||||
for a in shipping_node.outgoing_arcs
|
||||
vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
|
||||
if sum(vals) <= 1e-3
|
||||
continue
|
||||
end
|
||||
skip_plant = false
|
||||
dict = OrderedDict(
|
||||
"Amount (tonne)" => vals,
|
||||
"Distance (km)" => a.values["distance"],
|
||||
"Latitude (deg)" => a.dest.location.latitude,
|
||||
"Longitude (deg)" => a.dest.location.longitude,
|
||||
)
|
||||
if a.dest.location.plant_name ∉ keys(product_dict)
|
||||
product_dict[a.dest.location.plant_name] = OrderedDict()
|
||||
end
|
||||
product_dict[a.dest.location.plant_name][a.dest.location.location_name] =
|
||||
dict
|
||||
plant_dict["Total output"][product_name] += vals
|
||||
end
|
||||
end
|
||||
|
||||
if !skip_plant
|
||||
if plant.plant_name ∉ keys(output["Plants"])
|
||||
output["Plants"][plant.plant_name] = OrderedDict()
|
||||
end
|
||||
output["Plants"][plant.plant_name][plant.location_name] = plant_dict
|
||||
end
|
||||
end
|
||||
|
||||
output["Costs"]["Total (\$)"] = sum(values(output["Costs"]))
|
||||
return output
|
||||
end
|
@ -0,0 +1,94 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
|
||||
|
||||
default_milp_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
default_lp_optimizer = optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
|
||||
|
||||
function solve(
|
||||
instance::Instance;
|
||||
optimizer = nothing,
|
||||
output = nothing,
|
||||
marginal_costs = true,
|
||||
)
|
||||
|
||||
milp_optimizer = lp_optimizer = optimizer
|
||||
if optimizer == nothing
|
||||
milp_optimizer = default_milp_optimizer
|
||||
lp_optimizer = default_lp_optimizer
|
||||
end
|
||||
|
||||
@info "Building graph..."
|
||||
graph = RELOG.build_graph(instance)
|
||||
@info @sprintf(" %12d time periods", instance.time)
|
||||
@info @sprintf(" %12d process nodes", length(graph.process_nodes))
|
||||
@info @sprintf(" %12d shipping nodes (plant)", length(graph.plant_shipping_nodes))
|
||||
@info @sprintf(
|
||||
" %12d shipping nodes (collection)",
|
||||
length(graph.collection_shipping_nodes)
|
||||
)
|
||||
@info @sprintf(" %12d arcs", length(graph.arcs))
|
||||
|
||||
@info "Building optimization model..."
|
||||
model = RELOG.build_model(instance, graph, milp_optimizer)
|
||||
|
||||
@info "Optimizing MILP..."
|
||||
JuMP.optimize!(model)
|
||||
|
||||
if !has_values(model)
|
||||
@warn "No solution available"
|
||||
return OrderedDict()
|
||||
end
|
||||
|
||||
if marginal_costs
|
||||
@info "Re-optimizing with integer variables fixed..."
|
||||
all_vars = JuMP.all_variables(model)
|
||||
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
|
||||
JuMP.set_optimizer(model, lp_optimizer)
|
||||
for var in all_vars
|
||||
if JuMP.is_binary(var)
|
||||
JuMP.unset_binary(var)
|
||||
JuMP.fix(var, vals[var])
|
||||
end
|
||||
end
|
||||
JuMP.optimize!(model)
|
||||
end
|
||||
|
||||
@info "Extracting solution..."
|
||||
solution = get_solution(model, marginal_costs = marginal_costs)
|
||||
|
||||
if output != nothing
|
||||
write(solution, output)
|
||||
end
|
||||
|
||||
return solution
|
||||
end
|
||||
|
||||
function solve(filename::AbstractString; heuristic = false, kwargs...)
|
||||
@info "Reading $filename..."
|
||||
instance = RELOG.parsefile(filename)
|
||||
if heuristic && instance.time > 1
|
||||
@info "Solving single-period version..."
|
||||
compressed = _compress(instance)
|
||||
csol = solve(compressed; output = nothing, marginal_costs = false, kwargs...)
|
||||
@info "Filtering candidate locations..."
|
||||
selected_pairs = []
|
||||
for (plant_name, plant_dict) in csol["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
push!(selected_pairs, (plant_name, location_name))
|
||||
end
|
||||
end
|
||||
filtered_plants = []
|
||||
for p in instance.plants
|
||||
if (p.plant_name, p.location_name) in selected_pairs
|
||||
push!(filtered_plants, p)
|
||||
end
|
||||
end
|
||||
instance.plants = filtered_plants
|
||||
@info "Solving original version..."
|
||||
end
|
||||
sol = solve(instance; kwargs...)
|
||||
return sol
|
||||
end
|
@ -1,315 +0,0 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function plants_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
df."year" = Int[]
|
||||
df."latitude (deg)" = Float64[]
|
||||
df."longitude (deg)" = Float64[]
|
||||
df."capacity (tonne)" = Float64[]
|
||||
df."amount processed (tonne)" = Float64[]
|
||||
df."amount received (tonne)" = Float64[]
|
||||
df."amount in storage (tonne)" = Float64[]
|
||||
df."utilization factor (%)" = Float64[]
|
||||
df."energy (GJ)" = Float64[]
|
||||
df."opening cost (\$)" = Float64[]
|
||||
df."expansion cost (\$)" = Float64[]
|
||||
df."fixed operating cost (\$)" = Float64[]
|
||||
df."variable operating cost (\$)" = Float64[]
|
||||
df."storage cost (\$)" = Float64[]
|
||||
df."total cost (\$)" = Float64[]
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (plant_name, plant_dict) in solution["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
for year = 1:T
|
||||
capacity = round(location_dict["Capacity (tonne)"][year], digits = 2)
|
||||
received = round(location_dict["Total input (tonne)"][year], digits = 2)
|
||||
processed = round(location_dict["Process (tonne)"][year], digits = 2)
|
||||
in_storage = round(location_dict["Storage (tonne)"][year], digits = 2)
|
||||
utilization_factor = round(processed / capacity * 100.0, digits = 2)
|
||||
energy = round(location_dict["Energy (GJ)"][year], digits = 2)
|
||||
latitude = round(location_dict["Latitude (deg)"], digits = 6)
|
||||
longitude = round(location_dict["Longitude (deg)"], digits = 6)
|
||||
opening_cost = round(location_dict["Opening cost (\$)"][year], digits = 2)
|
||||
expansion_cost =
|
||||
round(location_dict["Expansion cost (\$)"][year], digits = 2)
|
||||
fixed_cost =
|
||||
round(location_dict["Fixed operating cost (\$)"][year], digits = 2)
|
||||
var_cost =
|
||||
round(location_dict["Variable operating cost (\$)"][year], digits = 2)
|
||||
storage_cost = round(location_dict["Storage cost (\$)"][year], digits = 2)
|
||||
total_cost = round(
|
||||
opening_cost + expansion_cost + fixed_cost + var_cost + storage_cost,
|
||||
digits = 2,
|
||||
)
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
plant_name,
|
||||
location_name,
|
||||
year,
|
||||
latitude,
|
||||
longitude,
|
||||
capacity,
|
||||
processed,
|
||||
received,
|
||||
in_storage,
|
||||
utilization_factor,
|
||||
energy,
|
||||
opening_cost,
|
||||
expansion_cost,
|
||||
fixed_cost,
|
||||
var_cost,
|
||||
storage_cost,
|
||||
total_cost,
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
function plant_outputs_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
df."year" = Int[]
|
||||
df."product name" = String[]
|
||||
df."amount produced (tonne)" = Float64[]
|
||||
df."amount sent (tonne)" = Float64[]
|
||||
df."amount disposed (tonne)" = Float64[]
|
||||
df."disposal cost (\$)" = Float64[]
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (plant_name, plant_dict) in solution["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
for (product_name, amount_produced) in location_dict["Total output"]
|
||||
send_dict = location_dict["Output"]["Send"]
|
||||
disposal_dict = location_dict["Output"]["Dispose"]
|
||||
|
||||
sent = zeros(T)
|
||||
if product_name in keys(send_dict)
|
||||
for (dst_plant_name, dst_plant_dict) in send_dict[product_name]
|
||||
for (dst_location_name, dst_location_dict) in dst_plant_dict
|
||||
sent += dst_location_dict["Amount (tonne)"]
|
||||
end
|
||||
end
|
||||
end
|
||||
sent = round.(sent, digits = 2)
|
||||
|
||||
disposal_amount = zeros(T)
|
||||
disposal_cost = zeros(T)
|
||||
if product_name in keys(disposal_dict)
|
||||
disposal_amount += disposal_dict[product_name]["Amount (tonne)"]
|
||||
disposal_cost += disposal_dict[product_name]["Cost (\$)"]
|
||||
end
|
||||
disposal_amount = round.(disposal_amount, digits = 2)
|
||||
disposal_cost = round.(disposal_cost, digits = 2)
|
||||
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
plant_name,
|
||||
location_name,
|
||||
year,
|
||||
product_name,
|
||||
round(amount_produced[year], digits = 2),
|
||||
sent[year],
|
||||
disposal_amount[year],
|
||||
disposal_cost[year],
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
|
||||
function plant_emissions_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
df."year" = Int[]
|
||||
df."emission type" = String[]
|
||||
df."emission amount (tonne)" = Float64[]
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (plant_name, plant_dict) in solution["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
for (emission_name, emission_amount) in location_dict["Emissions (tonne)"]
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
plant_name,
|
||||
location_name,
|
||||
year,
|
||||
emission_name,
|
||||
round(emission_amount[year], digits = 2),
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
|
||||
function transportation_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."source type" = String[]
|
||||
df."source location name" = String[]
|
||||
df."source latitude (deg)" = Float64[]
|
||||
df."source longitude (deg)" = Float64[]
|
||||
df."destination type" = String[]
|
||||
df."destination location name" = String[]
|
||||
df."destination latitude (deg)" = Float64[]
|
||||
df."destination longitude (deg)" = Float64[]
|
||||
df."product" = String[]
|
||||
df."year" = Int[]
|
||||
df."distance (km)" = Float64[]
|
||||
df."amount (tonne)" = Float64[]
|
||||
df."amount-distance (tonne-km)" = Float64[]
|
||||
df."transportation cost (\$)" = Float64[]
|
||||
df."transportation energy (GJ)" = Float64[]
|
||||
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (dst_plant_name, dst_plant_dict) in solution["Plants"]
|
||||
for (dst_location_name, dst_location_dict) in dst_plant_dict
|
||||
for (src_plant_name, src_plant_dict) in dst_location_dict["Input"]
|
||||
for (src_location_name, src_location_dict) in src_plant_dict
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
src_plant_name,
|
||||
src_location_name,
|
||||
round(src_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(src_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_plant_name,
|
||||
dst_location_name,
|
||||
round(dst_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(dst_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_location_dict["Input product"],
|
||||
year,
|
||||
round(src_location_dict["Distance (km)"], digits = 2),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year] *
|
||||
src_location_dict["Distance (km)"],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Transportation cost (\$)"][year],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Transportation energy (J)"][year] /
|
||||
1e9,
|
||||
digits = 2,
|
||||
),
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
|
||||
function transportation_emissions_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."source type" = String[]
|
||||
df."source location name" = String[]
|
||||
df."source latitude (deg)" = Float64[]
|
||||
df."source longitude (deg)" = Float64[]
|
||||
df."destination type" = String[]
|
||||
df."destination location name" = String[]
|
||||
df."destination latitude (deg)" = Float64[]
|
||||
df."destination longitude (deg)" = Float64[]
|
||||
df."product" = String[]
|
||||
df."year" = Int[]
|
||||
df."distance (km)" = Float64[]
|
||||
df."shipped amount (tonne)" = Float64[]
|
||||
df."shipped amount-distance (tonne-km)" = Float64[]
|
||||
df."emission type" = String[]
|
||||
df."emission amount (tonne)" = Float64[]
|
||||
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (dst_plant_name, dst_plant_dict) in solution["Plants"]
|
||||
for (dst_location_name, dst_location_dict) in dst_plant_dict
|
||||
for (src_plant_name, src_plant_dict) in dst_location_dict["Input"]
|
||||
for (src_location_name, src_location_dict) in src_plant_dict
|
||||
for (emission_name, emission_amount) in
|
||||
src_location_dict["Emissions (tonne)"]
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
src_plant_name,
|
||||
src_location_name,
|
||||
round(src_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(src_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_plant_name,
|
||||
dst_location_name,
|
||||
round(dst_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(dst_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_location_dict["Input product"],
|
||||
year,
|
||||
round(src_location_dict["Distance (km)"], digits = 2),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year] *
|
||||
src_location_dict["Distance (km)"],
|
||||
digits = 2,
|
||||
),
|
||||
emission_name,
|
||||
round(emission_amount[year], digits = 2),
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
function write(solution::AbstractDict, filename::AbstractString)
|
||||
@info "Writing solution: $filename"
|
||||
open(filename, "w") do file
|
||||
JSON.print(file, solution, 2)
|
||||
end
|
||||
end
|
||||
|
||||
write_plants_report(solution, filename) = CSV.write(filename, plants_report(solution))
|
||||
|
||||
write_plant_outputs_report(solution, filename) =
|
||||
CSV.write(filename, plant_outputs_report(solution))
|
||||
|
||||
write_plant_emissions_report(solution, filename) =
|
||||
CSV.write(filename, plant_emissions_report(solution))
|
||||
|
||||
write_transportation_report(solution, filename) =
|
||||
CSV.write(filename, transportation_report(solution))
|
||||
|
||||
write_transportation_emissions_report(solution, filename) =
|
||||
CSV.write(filename, transportation_emissions_report(solution))
|
@ -0,0 +1,38 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function plant_emissions_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
df."year" = Int[]
|
||||
df."emission type" = String[]
|
||||
df."emission amount (tonne)" = Float64[]
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (plant_name, plant_dict) in solution["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
for (emission_name, emission_amount) in location_dict["Emissions (tonne)"]
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
plant_name,
|
||||
location_name,
|
||||
year,
|
||||
emission_name,
|
||||
round(emission_amount[year], digits = 2),
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
write_plant_emissions_report(solution, filename) =
|
||||
CSV.write(filename, plant_emissions_report(solution))
|
@ -0,0 +1,66 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function plant_outputs_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
df."year" = Int[]
|
||||
df."product name" = String[]
|
||||
df."amount produced (tonne)" = Float64[]
|
||||
df."amount sent (tonne)" = Float64[]
|
||||
df."amount disposed (tonne)" = Float64[]
|
||||
df."disposal cost (\$)" = Float64[]
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (plant_name, plant_dict) in solution["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
for (product_name, amount_produced) in location_dict["Total output"]
|
||||
send_dict = location_dict["Output"]["Send"]
|
||||
disposal_dict = location_dict["Output"]["Dispose"]
|
||||
|
||||
sent = zeros(T)
|
||||
if product_name in keys(send_dict)
|
||||
for (dst_plant_name, dst_plant_dict) in send_dict[product_name]
|
||||
for (dst_location_name, dst_location_dict) in dst_plant_dict
|
||||
sent += dst_location_dict["Amount (tonne)"]
|
||||
end
|
||||
end
|
||||
end
|
||||
sent = round.(sent, digits = 2)
|
||||
|
||||
disposal_amount = zeros(T)
|
||||
disposal_cost = zeros(T)
|
||||
if product_name in keys(disposal_dict)
|
||||
disposal_amount += disposal_dict[product_name]["Amount (tonne)"]
|
||||
disposal_cost += disposal_dict[product_name]["Cost (\$)"]
|
||||
end
|
||||
disposal_amount = round.(disposal_amount, digits = 2)
|
||||
disposal_cost = round.(disposal_cost, digits = 2)
|
||||
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
plant_name,
|
||||
location_name,
|
||||
year,
|
||||
product_name,
|
||||
round(amount_produced[year], digits = 2),
|
||||
sent[year],
|
||||
disposal_amount[year],
|
||||
disposal_cost[year],
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
write_plant_outputs_report(solution, filename) =
|
||||
CSV.write(filename, plant_outputs_report(solution))
|
@ -0,0 +1,79 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function plants_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
df."year" = Int[]
|
||||
df."latitude (deg)" = Float64[]
|
||||
df."longitude (deg)" = Float64[]
|
||||
df."capacity (tonne)" = Float64[]
|
||||
df."amount processed (tonne)" = Float64[]
|
||||
df."amount received (tonne)" = Float64[]
|
||||
df."amount in storage (tonne)" = Float64[]
|
||||
df."utilization factor (%)" = Float64[]
|
||||
df."energy (GJ)" = Float64[]
|
||||
df."opening cost (\$)" = Float64[]
|
||||
df."expansion cost (\$)" = Float64[]
|
||||
df."fixed operating cost (\$)" = Float64[]
|
||||
df."variable operating cost (\$)" = Float64[]
|
||||
df."storage cost (\$)" = Float64[]
|
||||
df."total cost (\$)" = Float64[]
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (plant_name, plant_dict) in solution["Plants"]
|
||||
for (location_name, location_dict) in plant_dict
|
||||
for year = 1:T
|
||||
capacity = round(location_dict["Capacity (tonne)"][year], digits = 2)
|
||||
received = round(location_dict["Total input (tonne)"][year], digits = 2)
|
||||
processed = round(location_dict["Process (tonne)"][year], digits = 2)
|
||||
in_storage = round(location_dict["Storage (tonne)"][year], digits = 2)
|
||||
utilization_factor = round(processed / capacity * 100.0, digits = 2)
|
||||
energy = round(location_dict["Energy (GJ)"][year], digits = 2)
|
||||
latitude = round(location_dict["Latitude (deg)"], digits = 6)
|
||||
longitude = round(location_dict["Longitude (deg)"], digits = 6)
|
||||
opening_cost = round(location_dict["Opening cost (\$)"][year], digits = 2)
|
||||
expansion_cost =
|
||||
round(location_dict["Expansion cost (\$)"][year], digits = 2)
|
||||
fixed_cost =
|
||||
round(location_dict["Fixed operating cost (\$)"][year], digits = 2)
|
||||
var_cost =
|
||||
round(location_dict["Variable operating cost (\$)"][year], digits = 2)
|
||||
storage_cost = round(location_dict["Storage cost (\$)"][year], digits = 2)
|
||||
total_cost = round(
|
||||
opening_cost + expansion_cost + fixed_cost + var_cost + storage_cost,
|
||||
digits = 2,
|
||||
)
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
plant_name,
|
||||
location_name,
|
||||
year,
|
||||
latitude,
|
||||
longitude,
|
||||
capacity,
|
||||
processed,
|
||||
received,
|
||||
in_storage,
|
||||
utilization_factor,
|
||||
energy,
|
||||
opening_cost,
|
||||
expansion_cost,
|
||||
fixed_cost,
|
||||
var_cost,
|
||||
storage_cost,
|
||||
total_cost,
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
write_plants_report(solution, filename) = CSV.write(filename, plants_report(solution))
|
@ -0,0 +1,75 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function transportation_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."source type" = String[]
|
||||
df."source location name" = String[]
|
||||
df."source latitude (deg)" = Float64[]
|
||||
df."source longitude (deg)" = Float64[]
|
||||
df."destination type" = String[]
|
||||
df."destination location name" = String[]
|
||||
df."destination latitude (deg)" = Float64[]
|
||||
df."destination longitude (deg)" = Float64[]
|
||||
df."product" = String[]
|
||||
df."year" = Int[]
|
||||
df."distance (km)" = Float64[]
|
||||
df."amount (tonne)" = Float64[]
|
||||
df."amount-distance (tonne-km)" = Float64[]
|
||||
df."transportation cost (\$)" = Float64[]
|
||||
df."transportation energy (GJ)" = Float64[]
|
||||
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (dst_plant_name, dst_plant_dict) in solution["Plants"]
|
||||
for (dst_location_name, dst_location_dict) in dst_plant_dict
|
||||
for (src_plant_name, src_plant_dict) in dst_location_dict["Input"]
|
||||
for (src_location_name, src_location_dict) in src_plant_dict
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
src_plant_name,
|
||||
src_location_name,
|
||||
round(src_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(src_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_plant_name,
|
||||
dst_location_name,
|
||||
round(dst_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(dst_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_location_dict["Input product"],
|
||||
year,
|
||||
round(src_location_dict["Distance (km)"], digits = 2),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year] *
|
||||
src_location_dict["Distance (km)"],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Transportation cost (\$)"][year],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Transportation energy (J)"][year] /
|
||||
1e9,
|
||||
digits = 2,
|
||||
),
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
write_transportation_report(solution, filename) =
|
||||
CSV.write(filename, transportation_report(solution))
|
@ -0,0 +1,71 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function transportation_emissions_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."source type" = String[]
|
||||
df."source location name" = String[]
|
||||
df."source latitude (deg)" = Float64[]
|
||||
df."source longitude (deg)" = Float64[]
|
||||
df."destination type" = String[]
|
||||
df."destination location name" = String[]
|
||||
df."destination latitude (deg)" = Float64[]
|
||||
df."destination longitude (deg)" = Float64[]
|
||||
df."product" = String[]
|
||||
df."year" = Int[]
|
||||
df."distance (km)" = Float64[]
|
||||
df."shipped amount (tonne)" = Float64[]
|
||||
df."shipped amount-distance (tonne-km)" = Float64[]
|
||||
df."emission type" = String[]
|
||||
df."emission amount (tonne)" = Float64[]
|
||||
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (dst_plant_name, dst_plant_dict) in solution["Plants"]
|
||||
for (dst_location_name, dst_location_dict) in dst_plant_dict
|
||||
for (src_plant_name, src_plant_dict) in dst_location_dict["Input"]
|
||||
for (src_location_name, src_location_dict) in src_plant_dict
|
||||
for (emission_name, emission_amount) in
|
||||
src_location_dict["Emissions (tonne)"]
|
||||
for year = 1:T
|
||||
push!(
|
||||
df,
|
||||
[
|
||||
src_plant_name,
|
||||
src_location_name,
|
||||
round(src_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(src_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_plant_name,
|
||||
dst_location_name,
|
||||
round(dst_location_dict["Latitude (deg)"], digits = 6),
|
||||
round(dst_location_dict["Longitude (deg)"], digits = 6),
|
||||
dst_location_dict["Input product"],
|
||||
year,
|
||||
round(src_location_dict["Distance (km)"], digits = 2),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year],
|
||||
digits = 2,
|
||||
),
|
||||
round(
|
||||
src_location_dict["Amount (tonne)"][year] *
|
||||
src_location_dict["Distance (km)"],
|
||||
digits = 2,
|
||||
),
|
||||
emission_name,
|
||||
round(emission_amount[year], digits = 2),
|
||||
],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return df
|
||||
end
|
||||
|
||||
write_transportation_emissions_report(solution, filename) =
|
||||
CSV.write(filename, transportation_emissions_report(solution))
|
@ -0,0 +1,13 @@
|
||||
# RELOG: Reverse Logistics Optimization
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function write(solution::AbstractDict, filename::AbstractString)
|
||||
@info "Writing solution: $filename"
|
||||
open(filename, "w") do file
|
||||
JSON.print(file, solution, 2)
|
||||
end
|
||||
end
|
@ -0,0 +1,39 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG
|
||||
|
||||
@testset "build_graph" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||
graph = RELOG.build_graph(instance)
|
||||
process_node_by_location_name =
|
||||
Dict(n.location.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.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.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.location.plant_name == "F2"
|
||||
@test node.location.location_name == "L3"
|
||||
@test length(node.incoming_arcs) == 2
|
||||
@test length(node.outgoing_arcs) == 2
|
||||
|
||||
@test length(graph.arcs) == 38
|
||||
end
|
@ -1,41 +0,0 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG
|
||||
|
||||
@testset "Graph" begin
|
||||
@testset "build_graph" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../instances/s1.json")
|
||||
graph = RELOG.build_graph(instance)
|
||||
process_node_by_location_name =
|
||||
Dict(n.location.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.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.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.location.plant_name == "F2"
|
||||
@test node.location.location_name == "L3"
|
||||
@test length(node.incoming_arcs) == 2
|
||||
@test length(node.outgoing_arcs) == 2
|
||||
|
||||
@test length(graph.arcs) == 38
|
||||
end
|
||||
end
|
@ -0,0 +1,53 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG
|
||||
|
||||
@testset "compress" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||
compressed = RELOG._compress(instance)
|
||||
|
||||
product_name_to_product = Dict(p.name => p for p in compressed.products)
|
||||
location_name_to_facility = Dict()
|
||||
for p in compressed.plants
|
||||
location_name_to_facility[p.location_name] = p
|
||||
end
|
||||
for c in compressed.collection_centers
|
||||
location_name_to_facility[c.name] = c
|
||||
end
|
||||
|
||||
p1 = product_name_to_product["P1"]
|
||||
p2 = product_name_to_product["P2"]
|
||||
p3 = product_name_to_product["P3"]
|
||||
c1 = location_name_to_facility["C1"]
|
||||
l1 = location_name_to_facility["L1"]
|
||||
|
||||
@test compressed.time == 1
|
||||
@test compressed.building_period == [1]
|
||||
|
||||
@test p1.name == "P1"
|
||||
@test p1.transportation_cost ≈ [0.015]
|
||||
@test p1.transportation_energy ≈ [0.115]
|
||||
@test p1.transportation_emissions["CO2"] ≈ [0.051]
|
||||
@test p1.transportation_emissions["CH4"] ≈ [0.0025]
|
||||
|
||||
@test c1.name == "C1"
|
||||
@test c1.amount ≈ [1869.12]
|
||||
|
||||
@test l1.plant_name == "F1"
|
||||
@test l1.location_name == "L1"
|
||||
@test l1.energy ≈ [0.115]
|
||||
@test l1.emissions["CO2"] ≈ [0.051]
|
||||
@test l1.emissions["CH4"] ≈ [0.0025]
|
||||
@test l1.sizes[1].opening_cost ≈ [500]
|
||||
@test l1.sizes[2].opening_cost ≈ [1250]
|
||||
@test l1.sizes[1].fixed_operating_cost ≈ [60]
|
||||
@test l1.sizes[2].fixed_operating_cost ≈ [60]
|
||||
@test l1.sizes[1].variable_operating_cost ≈ [30]
|
||||
@test l1.sizes[2].variable_operating_cost ≈ [30]
|
||||
@test l1.disposal_limit[p2] ≈ [2.0]
|
||||
@test l1.disposal_limit[p3] ≈ [2.0]
|
||||
@test l1.disposal_cost[p2] ≈ [-10.0]
|
||||
@test l1.disposal_cost[p3] ≈ [-10.0]
|
||||
end
|
@ -0,0 +1,76 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG
|
||||
|
||||
@testset "parse" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||
|
||||
centers = instance.collection_centers
|
||||
plants = instance.plants
|
||||
products = instance.products
|
||||
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)
|
||||
|
||||
@test length(centers) == 10
|
||||
@test centers[1].name == "C1"
|
||||
@test centers[1].latitude == 7
|
||||
@test centers[1].latitude == 7
|
||||
@test centers[1].longitude == 7
|
||||
@test centers[1].amount == [934.56, 934.56]
|
||||
@test centers[1].product.name == "P1"
|
||||
|
||||
@test length(plants) == 6
|
||||
|
||||
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
|
||||
|
||||
@test length(plant.sizes) == 2
|
||||
@test plant.sizes[1].capacity == 250
|
||||
@test plant.sizes[1].opening_cost == [500, 500]
|
||||
@test plant.sizes[1].fixed_operating_cost == [30, 30]
|
||||
@test plant.sizes[1].variable_operating_cost == [30, 30]
|
||||
@test plant.sizes[2].capacity == 1000
|
||||
@test plant.sizes[2].opening_cost == [1250, 1250]
|
||||
@test plant.sizes[2].fixed_operating_cost == [30, 30]
|
||||
@test plant.sizes[2].variable_operating_cost == [30, 30]
|
||||
|
||||
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, 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 length(plant.sizes) == 2
|
||||
@test plant.sizes[1].capacity == 1000.0
|
||||
@test plant.sizes[1].opening_cost == [3000, 3000]
|
||||
@test plant.sizes[1].fixed_operating_cost == [50, 50]
|
||||
@test plant.sizes[1].variable_operating_cost == [50, 50]
|
||||
@test plant.sizes[1] == plant.sizes[2]
|
||||
|
||||
p4 = product_name_to_product["P4"]
|
||||
@test plant.output[p3] == 0.05
|
||||
@test plant.output[p4] == 0.8
|
||||
@test plant.disposal_limit[p3] == [1e8, 1e8]
|
||||
@test plant.disposal_limit[p4] == [0, 0]
|
||||
end
|
||||
|
||||
@testset "parse (invalid)" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
@test_throws String RELOG.parsefile("$basedir/../fixtures/s1-wrong-length.json")
|
||||
end
|
@ -1,126 +0,0 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG
|
||||
|
||||
@testset "Instance" begin
|
||||
@testset "load" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../instances/s1.json")
|
||||
|
||||
centers = instance.collection_centers
|
||||
plants = instance.plants
|
||||
products = instance.products
|
||||
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)
|
||||
|
||||
@test length(centers) == 10
|
||||
@test centers[1].name == "C1"
|
||||
@test centers[1].latitude == 7
|
||||
@test centers[1].latitude == 7
|
||||
@test centers[1].longitude == 7
|
||||
@test centers[1].amount == [934.56, 934.56]
|
||||
@test centers[1].product.name == "P1"
|
||||
|
||||
@test length(plants) == 6
|
||||
|
||||
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
|
||||
|
||||
@test length(plant.sizes) == 2
|
||||
@test plant.sizes[1].capacity == 250
|
||||
@test plant.sizes[1].opening_cost == [500, 500]
|
||||
@test plant.sizes[1].fixed_operating_cost == [30, 30]
|
||||
@test plant.sizes[1].variable_operating_cost == [30, 30]
|
||||
@test plant.sizes[2].capacity == 1000
|
||||
@test plant.sizes[2].opening_cost == [1250, 1250]
|
||||
@test plant.sizes[2].fixed_operating_cost == [30, 30]
|
||||
@test plant.sizes[2].variable_operating_cost == [30, 30]
|
||||
|
||||
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, 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 length(plant.sizes) == 2
|
||||
@test plant.sizes[1].capacity == 1000.0
|
||||
@test plant.sizes[1].opening_cost == [3000, 3000]
|
||||
@test plant.sizes[1].fixed_operating_cost == [50, 50]
|
||||
@test plant.sizes[1].variable_operating_cost == [50, 50]
|
||||
@test plant.sizes[1] == plant.sizes[2]
|
||||
|
||||
p4 = product_name_to_product["P4"]
|
||||
@test plant.output[p3] == 0.05
|
||||
@test plant.output[p4] == 0.8
|
||||
@test plant.disposal_limit[p3] == [1e8, 1e8]
|
||||
@test plant.disposal_limit[p4] == [0, 0]
|
||||
end
|
||||
|
||||
@testset "validate timeseries" begin
|
||||
@test_throws String RELOG.parsefile("fixtures/s1-wrong-length.json")
|
||||
end
|
||||
|
||||
@testset "compress" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../instances/s1.json")
|
||||
compressed = RELOG._compress(instance)
|
||||
|
||||
product_name_to_product = Dict(p.name => p for p in compressed.products)
|
||||
location_name_to_facility = Dict()
|
||||
for p in compressed.plants
|
||||
location_name_to_facility[p.location_name] = p
|
||||
end
|
||||
for c in compressed.collection_centers
|
||||
location_name_to_facility[c.name] = c
|
||||
end
|
||||
|
||||
p1 = product_name_to_product["P1"]
|
||||
p2 = product_name_to_product["P2"]
|
||||
p3 = product_name_to_product["P3"]
|
||||
c1 = location_name_to_facility["C1"]
|
||||
l1 = location_name_to_facility["L1"]
|
||||
|
||||
@test compressed.time == 1
|
||||
@test compressed.building_period == [1]
|
||||
|
||||
@test p1.name == "P1"
|
||||
@test p1.transportation_cost ≈ [0.015]
|
||||
@test p1.transportation_energy ≈ [0.115]
|
||||
@test p1.transportation_emissions["CO2"] ≈ [0.051]
|
||||
@test p1.transportation_emissions["CH4"] ≈ [0.0025]
|
||||
|
||||
@test c1.name == "C1"
|
||||
@test c1.amount ≈ [1869.12]
|
||||
|
||||
@test l1.plant_name == "F1"
|
||||
@test l1.location_name == "L1"
|
||||
@test l1.energy ≈ [0.115]
|
||||
@test l1.emissions["CO2"] ≈ [0.051]
|
||||
@test l1.emissions["CH4"] ≈ [0.0025]
|
||||
@test l1.sizes[1].opening_cost ≈ [500]
|
||||
@test l1.sizes[2].opening_cost ≈ [1250]
|
||||
@test l1.sizes[1].fixed_operating_cost ≈ [60]
|
||||
@test l1.sizes[2].fixed_operating_cost ≈ [60]
|
||||
@test l1.sizes[1].variable_operating_cost ≈ [30]
|
||||
@test l1.sizes[2].variable_operating_cost ≈ [30]
|
||||
@test l1.disposal_limit[p2] ≈ [2.0]
|
||||
@test l1.disposal_limit[p3] ≈ [2.0]
|
||||
@test l1.disposal_cost[p2] ≈ [-10.0]
|
||||
@test l1.disposal_cost[p3] ≈ [-10.0]
|
||||
end
|
||||
end
|
@ -0,0 +1,38 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
|
||||
@testset "build" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||
graph = RELOG.build_graph(instance)
|
||||
model = RELOG.build_model(instance, graph, Cbc.Optimizer)
|
||||
set_optimizer_attribute(model, "logLevel", 0)
|
||||
|
||||
process_node_by_location_name =
|
||||
Dict(n.location.location_name => n for n in graph.process_nodes)
|
||||
|
||||
shipping_node_by_loc_and_prod_names = Dict(
|
||||
(n.location.location_name, n.product.name) => n for n in graph.plant_shipping_nodes
|
||||
)
|
||||
|
||||
@test length(model[:flow]) == 76
|
||||
@test length(model[:dispose]) == 16
|
||||
@test length(model[:open_plant]) == 12
|
||||
@test length(model[:capacity]) == 12
|
||||
@test length(model[:expansion]) == 12
|
||||
|
||||
l1 = process_node_by_location_name["L1"]
|
||||
v = model[:capacity][l1, 1]
|
||||
@test lower_bound(v) == 0.0
|
||||
@test upper_bound(v) == 1000.0
|
||||
|
||||
v = model[:expansion][l1, 1]
|
||||
@test lower_bound(v) == 0.0
|
||||
@test upper_bound(v) == 750.0
|
||||
|
||||
v = model[:dispose][shipping_node_by_loc_and_prod_names["L1", "P2"], 1]
|
||||
@test lower_bound(v) == 0.0
|
||||
@test upper_bound(v) == 1.0
|
||||
end
|
@ -0,0 +1,61 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
|
||||
basedir = dirname(@__FILE__)
|
||||
|
||||
@testset "solve (exact)" begin
|
||||
solution_filename_a = tempname()
|
||||
solution_filename_b = tempname()
|
||||
solution = RELOG.solve("$basedir/../../instances/s1.json", output = solution_filename_a)
|
||||
|
||||
@test isfile(solution_filename_a)
|
||||
|
||||
RELOG.write(solution, solution_filename_b)
|
||||
@test isfile(solution_filename_b)
|
||||
|
||||
@test "Costs" in keys(solution)
|
||||
@test "Fixed operating (\$)" in keys(solution["Costs"])
|
||||
@test "Transportation (\$)" in keys(solution["Costs"])
|
||||
@test "Variable operating (\$)" in keys(solution["Costs"])
|
||||
@test "Total (\$)" in keys(solution["Costs"])
|
||||
|
||||
@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"])
|
||||
end
|
||||
|
||||
@testset "solve (heuristic)" begin
|
||||
# Should not crash
|
||||
solution = RELOG.solve("$basedir/../../instances/s1.json", heuristic = true)
|
||||
end
|
||||
|
||||
@testset "solve (infeasible)" begin
|
||||
json = JSON.parsefile("$basedir/../../instances/s1.json")
|
||||
for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
|
||||
location_dict["amount (tonne)"] *= 1000
|
||||
end
|
||||
RELOG.solve(RELOG.parse(json))
|
||||
end
|
||||
|
||||
@testset "solve (with storage)" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
filename = "$basedir/../fixtures/storage.json"
|
||||
instance = RELOG.parsefile(filename)
|
||||
@test instance.plants[1].storage_limit == 50.0
|
||||
@test instance.plants[1].storage_cost == [2.0, 1.5, 1.0]
|
||||
|
||||
solution = RELOG.solve(filename)
|
||||
plant_dict = solution["Plants"]["mega plant"]["Chicago"]
|
||||
@test plant_dict["Variable operating cost (\$)"] == [500.0, 0.0, 100.0]
|
||||
@test plant_dict["Process (tonne)"] == [50.0, 0.0, 50.0]
|
||||
@test plant_dict["Storage (tonne)"] == [50.0, 50.0, 0.0]
|
||||
@test plant_dict["Storage cost (\$)"] == [100.0, 75.0, 0.0]
|
||||
|
||||
@test solution["Costs"]["Variable operating (\$)"] == [500.0, 0.0, 100.0]
|
||||
@test solution["Costs"]["Storage (\$)"] == [100.0, 75.0, 0.0]
|
||||
@test solution["Costs"]["Total (\$)"] == [600.0, 75.0, 100.0]
|
||||
end
|
@ -1,102 +0,0 @@
|
||||
# Copyright (C) 2020 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
|
||||
@testset "Model" begin
|
||||
@testset "build" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
instance = RELOG.parsefile("$basedir/../instances/s1.json")
|
||||
graph = RELOG.build_graph(instance)
|
||||
model = RELOG.build_model(instance, graph, Cbc.Optimizer)
|
||||
set_optimizer_attribute(model, "logLevel", 0)
|
||||
|
||||
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[:flow]) == 76
|
||||
@test length(model[:dispose]) == 16
|
||||
@test length(model[:open_plant]) == 12
|
||||
@test length(model[:capacity]) == 12
|
||||
@test length(model[:expansion]) == 12
|
||||
|
||||
l1 = process_node_by_location_name["L1"]
|
||||
v = model[:capacity][l1, 1]
|
||||
@test lower_bound(v) == 0.0
|
||||
@test upper_bound(v) == 1000.0
|
||||
|
||||
v = model[:expansion][l1, 1]
|
||||
@test lower_bound(v) == 0.0
|
||||
@test upper_bound(v) == 750.0
|
||||
|
||||
v = model[: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)
|
||||
# MOI.write_to_file(dest, "model.lp")
|
||||
end
|
||||
|
||||
@testset "solve (exact)" begin
|
||||
solution_filename_a = tempname()
|
||||
solution_filename_b = tempname()
|
||||
solution =
|
||||
RELOG.solve("$(pwd())/../instances/s1.json", output = solution_filename_a)
|
||||
|
||||
@test isfile(solution_filename_a)
|
||||
|
||||
RELOG.write(solution, solution_filename_b)
|
||||
@test isfile(solution_filename_b)
|
||||
|
||||
@test "Costs" in keys(solution)
|
||||
@test "Fixed operating (\$)" in keys(solution["Costs"])
|
||||
@test "Transportation (\$)" in keys(solution["Costs"])
|
||||
@test "Variable operating (\$)" in keys(solution["Costs"])
|
||||
@test "Total (\$)" in keys(solution["Costs"])
|
||||
|
||||
@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"])
|
||||
end
|
||||
|
||||
|
||||
@testset "solve (heuristic)" begin
|
||||
# Should not crash
|
||||
solution = RELOG.solve("$(pwd())/../instances/s1.json", heuristic = true)
|
||||
end
|
||||
|
||||
@testset "infeasible solve" begin
|
||||
json = JSON.parsefile("$(pwd())/../instances/s1.json")
|
||||
for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
|
||||
location_dict["amount (tonne)"] *= 1000
|
||||
end
|
||||
RELOG.solve(RELOG.parse(json))
|
||||
end
|
||||
|
||||
@testset "storage" begin
|
||||
basedir = dirname(@__FILE__)
|
||||
filename = "$basedir/fixtures/storage.json"
|
||||
instance = RELOG.parsefile(filename)
|
||||
@test instance.plants[1].storage_limit == 50.0
|
||||
@test instance.plants[1].storage_cost == [2.0, 1.5, 1.0]
|
||||
|
||||
solution = RELOG.solve(filename)
|
||||
plant_dict = solution["Plants"]["mega plant"]["Chicago"]
|
||||
@test plant_dict["Variable operating cost (\$)"] == [500.0, 0.0, 100.0]
|
||||
@test plant_dict["Process (tonne)"] == [50.0, 0.0, 50.0]
|
||||
@test plant_dict["Storage (tonne)"] == [50.0, 50.0, 0.0]
|
||||
@test plant_dict["Storage cost (\$)"] == [100.0, 75.0, 0.0]
|
||||
|
||||
@test solution["Costs"]["Variable operating (\$)"] == [500.0, 0.0, 100.0]
|
||||
@test solution["Costs"]["Storage (\$)"] == [100.0, 75.0, 0.0]
|
||||
@test solution["Costs"]["Total (\$)"] == [600.0, 75.0, 100.0]
|
||||
end
|
||||
end
|
Loading…
Reference in new issue