Reformat source code; set up lint GH Action

This commit is contained in:
2021-06-22 09:47:40 -05:00
parent 823db2838b
commit b00b24ffbc
14 changed files with 549 additions and 458 deletions

View File

@@ -3,9 +3,9 @@
# Released under the modified BSD license. See COPYING.md for more details.
module RELOG
include("dotdict.jl")
include("instance.jl")
include("graph.jl")
include("model.jl")
include("reports.jl")
include("dotdict.jl")
include("instance.jl")
include("graph.jl")
include("model.jl")
include("reports.jl")
end

View File

@@ -58,11 +58,11 @@ function Base.show(io::IO, d::DotDict)
end
function recursive_to_dot_dict(el)
if typeof(el) == Dict{String, Any}
if typeof(el) == Dict{String,Any}
return DotDict(Dict(Symbol(k) => recursive_to_dot_dict(el[k]) for k in keys(el)))
else
return el
end
end
export recursive_to_dot_dict
export recursive_to_dot_dict

View File

@@ -5,14 +5,13 @@
using Geodesy
abstract type Node
end
abstract type Node end
mutable struct Arc
source::Node
dest::Node
values::Dict{String, Float64}
values::Dict{String,Float64}
end
@@ -26,7 +25,7 @@ end
mutable struct ShippingNode <: Node
index::Int
location::Union{Plant, CollectionCenter}
location::Union{Plant,CollectionCenter}
product::Product
incoming_arcs::Array{Arc}
outgoing_arcs::Array{Arc}
@@ -47,26 +46,25 @@ function build_graph(instance::Instance)::Graph
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)
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
@@ -74,14 +72,16 @@ function build_graph(instance::Instance)::Graph
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.location.latitude,
dest.location.longitude)
distance = calculate_distance(
source.location.latitude,
source.location.longitude,
dest.location.latitude,
dest.location.longitude,
)
values = Dict("distance" => distance)
arc = Arc(source, dest, values)
push!(source.outgoing_arcs, arc)
@@ -89,7 +89,7 @@ function build_graph(instance::Instance)::Graph
push!(arcs, arc)
end
end
# Build arcs from process nodes to shipping nodes within a plant
for source in process_nodes
plant = source.location
@@ -102,11 +102,8 @@ function build_graph(instance::Instance)::Graph
push!(arcs, arc)
end
end
return Graph(process_nodes,
plant_shipping_nodes,
collection_shipping_nodes,
arcs)
return Graph(process_nodes, plant_shipping_nodes, collection_shipping_nodes, arcs)
end
@@ -122,5 +119,5 @@ 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)
return round(distance(x, y) / 1000.0, digits = 2)
end

View File

@@ -13,7 +13,7 @@ mutable struct Product
name::String
transportation_cost::Array{Float64}
transportation_energy::Array{Float64}
transportation_emissions::Dict{String, Array{Float64}}
transportation_emissions::Dict{String,Array{Float64}}
end
@@ -40,14 +40,14 @@ mutable struct Plant
plant_name::String
location_name::String
input::Product
output::Dict{Product, Float64}
output::Dict{Product,Float64}
latitude::Float64
longitude::Float64
disposal_limit::Dict{Product, Array{Float64}}
disposal_cost::Dict{Product, Array{Float64}}
disposal_limit::Dict{Product,Array{Float64}}
disposal_cost::Dict{Product,Array{Float64}}
sizes::Array{PlantSize}
energy::Array{Float64}
emissions::Dict{String, Array{Float64}}
emissions::Dict{String,Array{Float64}}
storage_limit::Float64
storage_cost::Array{Float64}
end
@@ -55,9 +55,9 @@ end
mutable struct Instance
time::Int64
products::Array{Product, 1}
collection_centers::Array{CollectionCenter, 1}
plants::Array{Plant, 1}
products::Array{Product,1}
collection_centers::Array{CollectionCenter,1}
plants::Array{Plant,1}
building_period::Array{Int64}
end
@@ -88,104 +88,113 @@ function parse(json)::Instance
basedir = dirname(@__FILE__)
json_schema = JSON.parsefile("$basedir/schemas/input.json")
validate(json, Schema(json_schema))
T = json["parameters"]["time horizon (years)"]
json_schema["definitions"]["TimeSeries"]["minItems"] = T
json_schema["definitions"]["TimeSeries"]["maxItems"] = T
validate(json, Schema(json_schema))
building_period = [1]
if "building period (years)" in keys(json)
building_period = json["building period (years)"]
end
plants = Plant[]
products = Product[]
collection_centers = CollectionCenter[]
prod_name_to_product = Dict{String, Product}()
prod_name_to_product = Dict{String,Product}()
# Create products
for (product_name, product_dict) in json["products"]
cost = product_dict["transportation cost (\$/km/tonne)"]
energy = zeros(T)
emissions = Dict()
if "transportation energy (J/km/tonne)" in keys(product_dict)
energy = product_dict["transportation energy (J/km/tonne)"]
end
if "transportation emissions (tonne/km/tonne)" in keys(product_dict)
emissions = product_dict["transportation emissions (tonne/km/tonne)"]
end
product = Product(product_name, cost, energy, emissions)
push!(products, product)
prod_name_to_product[product_name] = product
# Create collection centers
if "initial amounts" in keys(product_dict)
for (center_name, center_dict) in product_dict["initial amounts"]
center = CollectionCenter(length(collection_centers) + 1,
center_name,
center_dict["latitude (deg)"],
center_dict["longitude (deg)"],
product,
center_dict["amount (tonne)"])
center = CollectionCenter(
length(collection_centers) + 1,
center_name,
center_dict["latitude (deg)"],
center_dict["longitude (deg)"],
product,
center_dict["amount (tonne)"],
)
push!(collection_centers, center)
end
end
end
# Create plants
for (plant_name, plant_dict) in json["plants"]
input = prod_name_to_product[plant_dict["input"]]
output = Dict()
# Plant outputs
if "outputs (tonne/tonne)" in keys(plant_dict)
output = Dict(prod_name_to_product[key] => value
for (key, value) in plant_dict["outputs (tonne/tonne)"]
if value > 0)
output = Dict(
prod_name_to_product[key] => value for
(key, value) in plant_dict["outputs (tonne/tonne)"] if value > 0
)
end
energy = zeros(T)
emissions = Dict()
if "energy (GJ/tonne)" in keys(plant_dict)
energy = plant_dict["energy (GJ/tonne)"]
end
if "emissions (tonne/tonne)" in keys(plant_dict)
emissions = plant_dict["emissions (tonne/tonne)"]
end
for (location_name, location_dict) in plant_dict["locations"]
sizes = PlantSize[]
disposal_limit = Dict(p => [0.0 for t in 1:T] for p in keys(output))
disposal_cost = Dict(p => [0.0 for t in 1:T] for p in keys(output))
disposal_limit = Dict(p => [0.0 for t = 1:T] for p in keys(output))
disposal_cost = Dict(p => [0.0 for t = 1:T] for p in keys(output))
# Disposal
if "disposal" in keys(location_dict)
for (product_name, disposal_dict) in location_dict["disposal"]
limit = [1e8 for t in 1:T]
limit = [1e8 for t = 1:T]
if "limit (tonne)" in keys(disposal_dict)
limit = disposal_dict["limit (tonne)"]
limit = disposal_dict["limit (tonne)"]
end
disposal_limit[prod_name_to_product[product_name]] = limit
disposal_cost[prod_name_to_product[product_name]] = disposal_dict["cost (\$/tonne)"]
disposal_cost[prod_name_to_product[product_name]] =
disposal_dict["cost (\$/tonne)"]
end
end
# Capacities
for (capacity_name, capacity_dict) in location_dict["capacities (tonne)"]
push!(sizes, PlantSize(Base.parse(Float64, capacity_name),
capacity_dict["variable operating cost (\$/tonne)"],
capacity_dict["fixed operating cost (\$)"],
capacity_dict["opening cost (\$)"]))
push!(
sizes,
PlantSize(
Base.parse(Float64, capacity_name),
capacity_dict["variable operating cost (\$/tonne)"],
capacity_dict["fixed operating cost (\$)"],
capacity_dict["opening cost (\$)"],
),
)
end
length(sizes) > 1 || push!(sizes, sizes[1])
length(sizes) > 1 || push!(sizes, sizes[1])
sort!(sizes, by = x -> x.capacity)
# Storage
storage_limit = 0
storage_cost = zeros(T)
@@ -194,7 +203,7 @@ function parse(json)::Instance
storage_limit = storage_dict["limit (tonne)"]
storage_cost = storage_dict["cost (\$/tonne)"]
end
# Validation: Capacities
if length(sizes) != 2
throw("At most two capacities are supported")
@@ -203,28 +212,30 @@ function parse(json)::Instance
throw("Variable operating costs must be the same for all capacities")
end
plant = Plant(length(plants) + 1,
plant_name,
location_name,
input,
output,
location_dict["latitude (deg)"],
location_dict["longitude (deg)"],
disposal_limit,
disposal_cost,
sizes,
energy,
emissions,
storage_limit,
storage_cost)
plant = Plant(
length(plants) + 1,
plant_name,
location_name,
input,
output,
location_dict["latitude (deg)"],
location_dict["longitude (deg)"],
disposal_limit,
disposal_cost,
sizes,
energy,
emissions,
storage_limit,
storage_cost,
)
push!(plants, plant)
end
end
@info @sprintf("%12d collection centers", length(collection_centers))
@info @sprintf("%12d candidate plant locations", length(plants))
return Instance(T, products, collection_centers, plants, building_period)
end
@@ -242,7 +253,7 @@ function _compress(instance::Instance)::Instance
compressed = deepcopy(instance)
compressed.time = 1
compressed.building_period = [1]
# Compress products
for p in compressed.products
p.transportation_cost = [mean(p.transportation_cost)]
@@ -251,12 +262,12 @@ function _compress(instance::Instance)::Instance
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)]
@@ -276,6 +287,6 @@ function _compress(instance::Instance)::Instance
plant.disposal_cost[prod_name] = [mean(disp_cost)]
end
end
return compressed
end

View File

@@ -26,40 +26,51 @@ end
function create_vars!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
vars.flow = Dict((a, t) => @variable(mip, lower_bound=0)
for a in graph.arcs, t in 1:T)
vars.dispose = Dict((n, t) => @variable(mip,
lower_bound=0,
upper_bound=n.location.disposal_limit[n.product][t])
for n in values(graph.plant_shipping_nodes), t in 1:T)
vars.store = Dict((n, t) => @variable(mip,
lower_bound=0,
upper_bound=n.location.storage_limit)
for n in values(graph.process_nodes), t in 1:T)
vars.process = Dict((n, t) => @variable(mip,
lower_bound = 0)
for n in values(graph.process_nodes), t in 1:T)
vars.open_plant = Dict((n, t) => @variable(mip, binary=true)
for n in values(graph.process_nodes), t in 1:T)
vars.is_open = Dict((n, t) => @variable(mip, binary=true)
for n in values(graph.process_nodes), t in 1:T)
vars.flow = Dict((a, t) => @variable(mip, lower_bound = 0) for a in graph.arcs, t = 1:T)
vars.capacity = Dict((n, t) => @variable(mip,
lower_bound = 0,
upper_bound = n.location.sizes[2].capacity)
for n in values(graph.process_nodes), t in 1:T)
vars.expansion = Dict((n, t) => @variable(mip,
lower_bound = 0,
upper_bound = n.location.sizes[2].capacity -
n.location.sizes[1].capacity)
for n in values(graph.process_nodes), t in 1:T)
vars.dispose = Dict(
(n, t) => @variable(
mip,
lower_bound = 0,
upper_bound = n.location.disposal_limit[n.product][t]
) for n in values(graph.plant_shipping_nodes), t = 1:T
)
vars.store = Dict(
(n, t) =>
@variable(mip, lower_bound = 0, upper_bound = n.location.storage_limit) for
n in values(graph.process_nodes), t = 1:T
)
vars.process = Dict(
(n, t) => @variable(mip, lower_bound = 0) for n in values(graph.process_nodes),
t = 1:T
)
vars.open_plant = Dict(
(n, t) => @variable(mip, binary = true) for n in values(graph.process_nodes),
t = 1:T
)
vars.is_open = Dict(
(n, t) => @variable(mip, binary = true) for n in values(graph.process_nodes),
t = 1:T
)
vars.capacity = Dict(
(n, t) =>
@variable(mip, lower_bound = 0, upper_bound = n.location.sizes[2].capacity)
for n in values(graph.process_nodes), t = 1:T
)
vars.expansion = Dict(
(n, t) => @variable(
mip,
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
@@ -68,7 +79,7 @@ function slope_open(plant, t)
0.0
else
(plant.sizes[2].opening_cost[t] - plant.sizes[1].opening_cost[t]) /
(plant.sizes[2].capacity - plant.sizes[1].capacity)
(plant.sizes[2].capacity - plant.sizes[1].capacity)
end
end
@@ -77,7 +88,7 @@ function slope_fix_oper_cost(plant, t)
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)
(plant.sizes[2].capacity - plant.sizes[1].capacity)
end
end
@@ -86,111 +97,119 @@ function create_objective_function!(model::ManufacturingModel)
obj = AffExpr(0.0)
# Process node costs
for n in values(graph.process_nodes), t in 1:T
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, vars.flow[a, t])
end
# Opening costs
add_to_expression!(obj,
n.location.sizes[1].opening_cost[t],
vars.open_plant[n, t])
add_to_expression!(obj, n.location.sizes[1].opening_cost[t], vars.open_plant[n, t])
# Fixed operating costs (base)
add_to_expression!(obj,
n.location.sizes[1].fixed_operating_cost[t],
vars.is_open[n, t])
add_to_expression!(
obj,
n.location.sizes[1].fixed_operating_cost[t],
vars.is_open[n, t],
)
# Fixed operating costs (expansion)
add_to_expression!(obj,
slope_fix_oper_cost(n.location, t),
vars.expansion[n, t])
add_to_expression!(obj, slope_fix_oper_cost(n.location, t), vars.expansion[n, t])
# Processing costs
add_to_expression!(obj,
n.location.sizes[1].variable_operating_cost[t],
vars.process[n, t])
add_to_expression!(
obj,
n.location.sizes[1].variable_operating_cost[t],
vars.process[n, t],
)
# Storage costs
add_to_expression!(obj,
n.location.storage_cost[t],
vars.store[n, t])
add_to_expression!(obj, n.location.storage_cost[t], vars.store[n, t])
# Expansion costs
if t < T
add_to_expression!(obj,
slope_open(n.location, t) - slope_open(n.location, t + 1),
vars.expansion[n, t])
add_to_expression!(
obj,
slope_open(n.location, t) - slope_open(n.location, t + 1),
vars.expansion[n, t],
)
else
add_to_expression!(obj,
slope_open(n.location, t),
vars.expansion[n, t])
add_to_expression!(obj, slope_open(n.location, t), vars.expansion[n, t])
end
end
# Shipping node costs
for n in values(graph.plant_shipping_nodes), t in 1:T
for n in values(graph.plant_shipping_nodes), t = 1:T
# Disposal costs
add_to_expression!(obj,
n.location.disposal_cost[n.product][t],
vars.dispose[n, t])
add_to_expression!(obj, n.location.disposal_cost[n.product][t], vars.dispose[n, t])
end
@objective(mip, Min, obj)
end
end
function create_shipping_node_constraints!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
eqs = model.eqs
eqs.balance = OrderedDict()
for t in 1:T
for t = 1:T
# Collection centers
for n in graph.collection_shipping_nodes
eqs.balance[n, t] = @constraint(mip,
sum(vars.flow[a, t] for a in n.outgoing_arcs)
== n.location.amount[t])
eqs.balance[n, t] = @constraint(
mip,
sum(vars.flow[a, t] for a in n.outgoing_arcs) == n.location.amount[t]
)
end
# Plants
for n in graph.plant_shipping_nodes
@constraint(mip,
@constraint(
mip,
sum(vars.flow[a, t] for a in n.incoming_arcs) ==
sum(vars.flow[a, t] for a in n.outgoing_arcs) + vars.dispose[n, t])
sum(vars.flow[a, t] for a in n.outgoing_arcs) + vars.dispose[n, t]
)
end
end
end
function create_process_node_constraints!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
for t in 1:T, n in graph.process_nodes
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, vars.flow[a, t])
end
# Output amount is implied by amount processed
for a in n.outgoing_arcs
@constraint(mip, vars.flow[a, t] == a.values["weight"] * vars.process[n, t])
end
# If plant is closed, capacity is zero
@constraint(mip, vars.capacity[n, t] <= n.location.sizes[2].capacity * vars.is_open[n, t])
@constraint(
mip,
vars.capacity[n, t] <= n.location.sizes[2].capacity * vars.is_open[n, t]
)
# If plant is open, capacity is greater than base
@constraint(mip, vars.capacity[n, t] >= n.location.sizes[1].capacity * vars.is_open[n, t])
@constraint(
mip,
vars.capacity[n, t] >= n.location.sizes[1].capacity * vars.is_open[n, t]
)
# Capacity is linked to expansion
@constraint(mip, vars.capacity[n, t] <= n.location.sizes[1].capacity + vars.expansion[n, t])
@constraint(
mip,
vars.capacity[n, t] <= n.location.sizes[1].capacity + vars.expansion[n, t]
)
# Can only process up to capacity
@constraint(mip, vars.process[n, t] <= vars.capacity[n, t])
@@ -200,7 +219,7 @@ function create_process_node_constraints!(model::ManufacturingModel)
@constraint(mip, vars.capacity[n, t] >= vars.capacity[n, t-1])
@constraint(mip, vars.expansion[n, t] >= vars.expansion[n, t-1])
end
# Amount received equals amount processed plus stored
store_in = 0
if t > 1
@@ -209,18 +228,20 @@ function create_process_node_constraints!(model::ManufacturingModel)
if t == T
@constraint(mip, vars.store[n, t] == 0)
end
@constraint(mip,
input_sum + store_in == vars.store[n, t] + vars.process[n, t])
@constraint(mip, input_sum + store_in == vars.store[n, t] + vars.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(mip, vars.is_open[n, t] == vars.is_open[n, t-1] + vars.open_plant[n, t])
@constraint(
mip,
vars.is_open[n, t] == vars.is_open[n, t-1] + vars.open_plant[n, t]
)
else
@constraint(mip, vars.is_open[n, t] == vars.open_plant[n, t])
end
# Plant can only be opened during building period
if t model.instance.building_period
@constraint(mip, vars.open_plant[n, t] == 0)
@@ -231,37 +252,41 @@ 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,
)
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 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.mip)
if !has_values(model.mip)
@warn "No solution available"
return OrderedDict()
end
if marginal_costs
@info "Re-optimizing with integer variables fixed..."
all_vars = JuMP.all_variables(model.mip)
@@ -275,30 +300,24 @@ function solve(instance::Instance;
end
JuMP.optimize!(model.mip)
end
@info "Extracting solution..."
solution = get_solution(model, marginal_costs=marginal_costs)
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...,
)
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...)
csol = solve(compressed; output = nothing, marginal_costs = false, kwargs...)
@info "Filtering candidate locations..."
selected_pairs = []
for (plant_name, plant_dict) in csol["Plants"]
@@ -320,12 +339,11 @@ function solve(filename::AbstractString;
end
function get_solution(model::ManufacturingModel;
marginal_costs=true,
)
mip, vars, eqs, graph, instance = model.mip, model.vars, model.eqs, model.graph, model.instance
function get_solution(model::ManufacturingModel; marginal_costs = true)
mip, vars, eqs, graph, instance =
model.mip, model.vars, model.eqs, model.graph, model.instance
T = instance.time
output = OrderedDict(
"Plants" => OrderedDict(),
"Products" => OrderedDict(),
@@ -339,16 +357,14 @@ function get_solution(model::ManufacturingModel;
"Storage (\$)" => zeros(T),
"Total (\$)" => zeros(T),
),
"Energy" => OrderedDict(
"Plants (GJ)" => zeros(T),
"Transportation (GJ)" => 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
@@ -357,13 +373,15 @@ function get_solution(model::ManufacturingModel;
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(eqs.balance[n, t])), digits=2)
for t in 1:T]
location_dict = OrderedDict{Any,Any}(
"Marginal cost (\$/tonne)" => [
round(abs(JuMP.shadow_price(eqs.balance[n, t])), digits = 2) for
t = 1:T
],
)
if n.product.name keys(output["Products"])
output["Products"][n.product.name] = OrderedDict()
@@ -371,83 +389,83 @@ function get_solution(model::ManufacturingModel;
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}(
plant_dict = OrderedDict{Any,Any}(
"Input" => OrderedDict(),
"Output" => OrderedDict(
"Send" => OrderedDict(),
"Dispose" => OrderedDict(),
),
"Output" =>
OrderedDict("Send" => OrderedDict(), "Dispose" => OrderedDict()),
"Input product" => plant.input.name,
"Total input (tonne)" => [0.0 for t in 1:T],
"Total input (tonne)" => [0.0 for t = 1:T],
"Total output" => OrderedDict(),
"Latitude (deg)" => plant.latitude,
"Longitude (deg)" => plant.longitude,
"Capacity (tonne)" => [JuMP.value(vars.capacity[process_node, t])
for t in 1:T],
"Opening cost (\$)" => [JuMP.value(vars.open_plant[process_node, t]) *
plant.sizes[1].opening_cost[t]
for t in 1:T],
"Fixed operating cost (\$)" => [JuMP.value(vars.is_open[process_node, t]) *
plant.sizes[1].fixed_operating_cost[t] +
JuMP.value(vars.expansion[process_node, t]) *
slope_fix_oper_cost(plant, t)
for t in 1:T],
"Expansion cost (\$)" => [(if t == 1
slope_open(plant, t) * JuMP.value(vars.expansion[process_node, t])
else
slope_open(plant, t) * (
JuMP.value(vars.expansion[process_node, t]) -
JuMP.value(vars.expansion[process_node, t - 1])
)
end)
for t in 1:T],
"Process (tonne)" => [JuMP.value(vars.process[process_node, t])
for t in 1:T],
"Variable operating cost (\$)" => [JuMP.value(vars.process[process_node, t]) *
plant.sizes[1].variable_operating_cost[t]
for t in 1:T],
"Storage (tonne)" => [JuMP.value(vars.store[process_node, t])
for t in 1:T],
"Storage cost (\$)" => [JuMP.value(vars.store[process_node, t]) *
plant.storage_cost[t]
for t in 1:T],
"Capacity (tonne)" =>
[JuMP.value(vars.capacity[process_node, t]) for t = 1:T],
"Opening cost (\$)" => [
JuMP.value(vars.open_plant[process_node, t]) *
plant.sizes[1].opening_cost[t] for t = 1:T
],
"Fixed operating cost (\$)" => [
JuMP.value(vars.is_open[process_node, t]) *
plant.sizes[1].fixed_operating_cost[t] +
JuMP.value(vars.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(vars.expansion[process_node, t])
else
slope_open(plant, t) * (
JuMP.value(vars.expansion[process_node, t]) -
JuMP.value(vars.expansion[process_node, t-1])
)
end
) for t = 1:T
],
"Process (tonne)" =>
[JuMP.value(vars.process[process_node, t]) for t = 1:T],
"Variable operating cost (\$)" => [
JuMP.value(vars.process[process_node, t]) *
plant.sizes[1].variable_operating_cost[t] for t = 1:T
],
"Storage (tonne)" => [JuMP.value(vars.store[process_node, t]) for t = 1:T],
"Storage cost (\$)" => [
JuMP.value(vars.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"]["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(vars.flow[a, t]) for t in 1:T]
vals = [JuMP.value(vars.flow[a, t]) for t = 1:T]
if sum(vals) <= 1e-3
continue
end
skip_plant = false
dict = OrderedDict{Any, Any}(
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,
"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"]
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
@@ -460,23 +478,25 @@ function get_solution(model::ManufacturingModel;
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
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)"]
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
@@ -489,21 +509,23 @@ function get_solution(model::ManufacturingModel;
plant_dict["Total output"][product_name] = zeros(T)
plant_dict["Output"]["Send"][product_name] = product_dict = OrderedDict()
disposal_amount = [JuMP.value(vars.dispose[shipping_node, t]) for t in 1:T]
disposal_amount = [JuMP.value(vars.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.vars.dispose[shipping_node, t])
for t in 1:T]
disposal_dict["Cost (\$)"] = [disposal_dict["Amount (tonne)"][t] *
plant.disposal_cost[shipping_node.product][t]
for t in 1:T]
plant_dict["Output"]["Dispose"][product_name] =
disposal_dict = OrderedDict()
disposal_dict["Amount (tonne)"] =
[JuMP.value(model.vars.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(vars.flow[a, t]) for t in 1:T]
vals = [JuMP.value(vars.flow[a, t]) for t = 1:T]
if sum(vals) <= 1e-3
continue
end
@@ -517,11 +539,12 @@ function get_solution(model::ManufacturingModel;
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
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()

View File

@@ -27,41 +27,49 @@ function plants_report(solution)::DataFrame
T = length(solution["Energy"]["Plants (GJ)"])
for (plant_name, plant_dict) in solution["Plants"]
for (location_name, location_dict) in plant_dict
for year in 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,
])
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
@@ -84,7 +92,7 @@ function plant_outputs_report(solution)::DataFrame
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]
@@ -93,28 +101,31 @@ function plant_outputs_report(solution)::DataFrame
end
end
end
sent = round.(sent, digits=2)
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 in 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],
])
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
@@ -134,14 +145,17 @@ function plant_emissions_report(solution)::DataFrame
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 in 1:T
push!(df, [
plant_name,
location_name,
year,
emission_name,
round(emission_amount[year], digits=2),
])
for year = 1:T
push!(
df,
[
plant_name,
location_name,
year,
emission_name,
round(emission_amount[year], digits = 2),
],
)
end
end
end
@@ -165,34 +179,49 @@ function transportation_report(solution)::DataFrame
df."distance (km)" = Float64[]
df."amount (tonne)" = Float64[]
df."amount-distance (tonne-km)" = Float64[]
df."transportation cost (\$)" = Float64[]
df."transportation energy (GJ)" = 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 in 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),
])
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
@@ -217,35 +246,44 @@ function transportation_emissions_report(solution)::DataFrame
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[]
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 in 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),
])
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
@@ -262,8 +300,7 @@ function write(solution::AbstractDict, filename::AbstractString)
end
end
write_plants_report(solution, filename) =
CSV.write(filename, plants_report(solution))
write_plants_report(solution, filename) = CSV.write(filename, plants_report(solution))
write_plant_outputs_report(solution, filename) =
CSV.write(filename, plant_outputs_report(solution))

View File

@@ -9,14 +9,7 @@ using JuMP
using MathOptInterface
using ProgressBars
pkg = [:Cbc,
:Clp,
:Geodesy,
:JSON,
:JSONSchema,
:JuMP,
:MathOptInterface,
:ProgressBars]
pkg = [:Cbc, :Clp, :Geodesy, :JSON, :JSONSchema, :JuMP, :MathOptInterface, :ProgressBars]
@info "Building system image..."
create_sysimage(pkg, sysimage_path="build/sysimage.so")
create_sysimage(pkg, sysimage_path = "build/sysimage.so")