Implement first version of multi-period simulations

This commit is contained in:
2020-05-05 19:08:09 -05:00
parent f970dca68d
commit 9f6bfef327
10 changed files with 334 additions and 237 deletions

View File

@@ -17,7 +17,7 @@ end
mutable struct ProcessNode <: Node
index::Int
plant::Plant
location::Plant
incoming_arcs::Array{Arc}
outgoing_arcs::Array{Arc}
end
@@ -79,8 +79,8 @@ function build_graph(instance::Instance)::Graph
for dest in process_nodes_by_input_product[source.product]
distance = calculate_distance(source.location.latitude,
source.location.longitude,
dest.plant.latitude,
dest.plant.longitude)
dest.location.latitude,
dest.location.longitude)
values = Dict("distance" => distance)
arc = Arc(source, dest, values)
push!(source.outgoing_arcs, arc)
@@ -91,7 +91,7 @@ function build_graph(instance::Instance)::Graph
# Build arcs from process nodes to shipping nodes within a plant
for source in process_nodes
plant = source.plant
plant = source.location
for dest in shipping_nodes_by_plant[plant]
weight = plant.output[dest.product]
values = Dict("weight" => weight)

View File

@@ -6,38 +6,41 @@ using JSON, JSONSchema
mutable struct Product
name::String
transportation_cost::Float64
transportation_cost::Array{Float64}
end
mutable struct CollectionCenter
index::Int64
name::String
latitude::Float64
longitude::Float64
product::Product
amount::Float64
amount::Array{Float64}
end
mutable struct Plant
index::Int64
plant_name::String
location_name::String
input::Product
output::Dict{Product, Float64}
latitude::Float64
longitude::Float64
variable_operating_cost::Float64
fixed_operating_cost::Float64
opening_cost::Float64
variable_operating_cost::Array{Float64}
fixed_operating_cost::Array{Float64}
opening_cost::Array{Float64}
base_capacity::Float64
max_capacity::Float64
expansion_cost::Float64
disposal_limit::Dict{Product, Float64}
disposal_cost::Dict{Product, Float64}
expansion_cost::Array{Float64}
disposal_limit::Dict{Product, Array{Float64}}
disposal_cost::Dict{Product, Array{Float64}}
end
mutable struct Instance
time::Int64
products::Array{Product, 1}
collection_centers::Array{CollectionCenter, 1}
plants::Array{Plant, 1}
@@ -49,16 +52,21 @@ function load(path::String)::Instance
json = JSON.parsefile(path)
schema = Schema(JSON.parsefile("$basedir/schemas/input.json"))
validation_results = JSONSchema.validate(json, schema)
if validation_results !== nothing
println(validation_results)
throw("Invalid input file")
result = JSONSchema.validate(json, schema)
if result !== nothing
if result isa JSONSchema.SingleIssue
path = join(result.path, "")
msg = "$(result.x) $(result.msg) in $(path)"
else
msg = convert(String, result)
end
throw(msg)
end
T = json["parameters"]["time periods"]
plants = Plant[]
products = Product[]
collection_centers = CollectionCenter[]
plants = Plant[]
product_name_to_product = Dict{String, Product}()
# Create products
@@ -70,7 +78,8 @@ function load(path::String)::Instance
# Create collection centers
if "initial amounts" in keys(product_dict)
for (center_name, center_dict) in product_dict["initial amounts"]
center = CollectionCenter(center_name,
center = CollectionCenter(length(collection_centers) + 1,
center_name,
center_dict["latitude"],
center_dict["longitude"],
product,
@@ -93,8 +102,8 @@ function load(path::String)::Instance
end
for (location_name, location_dict) in plant_dict["locations"]
disposal_limit = Dict(p => 0.0 for p in keys(output))
disposal_cost = Dict(p => 0.0 for p in keys(output))
disposal_limit = Dict(p => [0.0 for t in 1:T] for p in keys(output))
disposal_cost = Dict(p => [0.0 for t in 1:T] for p in keys(output))
# Plant disposal
if "disposal" in keys(location_dict)
@@ -106,7 +115,7 @@ function load(path::String)::Instance
base_capacity = 1e8
max_capacity = 1e8
expansion_cost = 0
expansion_cost = [0.0 for t in 1:T]
if "base capacity" in keys(location_dict)
base_capacity = location_dict["base capacity"]
@@ -120,7 +129,8 @@ function load(path::String)::Instance
expansion_cost = location_dict["expansion cost"]
end
plant = Plant(plant_name,
plant = Plant(length(plants) + 1,
plant_name,
location_name,
input,
output,
@@ -138,5 +148,5 @@ function load(path::String)::Instance
end
end
return Instance(products, collection_centers, plants)
return Instance(T, products, collection_centers, plants)
end

View File

@@ -23,60 +23,76 @@ end
function create_vars!(model::ManufacturingModel)
mip, vars, graph = model.mip, model.vars, model.graph
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
vars.flow = Dict(a => @variable(mip, lower_bound=0)
for a in graph.arcs)
vars.flow = Dict((a, t) => @variable(mip,
lower_bound=0,
base_name="flow($(a.source.location.index),$(a.dest.location.index),$t)")
for a in graph.arcs, t in 1:T)
vars.dispose = Dict(n => @variable(mip,
lower_bound = 0,
upper_bound = n.location.disposal_limit[n.product])
for n in values(graph.plant_shipping_nodes))
vars.dispose = Dict((n, t) => @variable(mip,
lower_bound=0,
upper_bound=n.location.disposal_limit[n.product][t],
base_name="dispose($(n.location.index),$(n.product.name),$t)")
for n in values(graph.plant_shipping_nodes), t in 1:T)
vars.open_plant = Dict(n => @variable(mip, binary=true)
for n in values(graph.process_nodes))
vars.open_plant = Dict((n, t) => @variable(mip,
binary=true,
base_name="open_plant($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
vars.is_open = Dict((n, t) => @variable(mip,
binary=true,
base_name="is_open($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
vars.capacity = Dict((n, t) => @variable(mip,
lower_bound = 0,
upper_bound = n.location.max_capacity,
base_name="capacity($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
vars.capacity = Dict(n => @variable(mip,
lower_bound = 0,
upper_bound = n.plant.max_capacity)
for n in values(graph.process_nodes))
vars.expansion = Dict(n => @variable(mip,
lower_bound = 0,
upper_bound = (n.plant.max_capacity - n.plant.base_capacity))
for n in values(graph.process_nodes))
vars.expansion = Dict((n, t) => @variable(mip,
lower_bound = 0,
upper_bound = (n.location.max_capacity - n.location.base_capacity),
base_name="expansion($(n.location.index),$t)")
for n in values(graph.process_nodes), t in 1:T)
end
function create_objective_function!(model::ManufacturingModel)
mip, vars, graph = model.mip, model.vars, model.graph
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
obj = @expression(mip, 0 * @variable(mip))
# Process node costs
for n in values(graph.process_nodes)
for n in values(graph.process_nodes), t in 1:T
# Transportation and variable operating costs
for a in n.incoming_arcs
c = n.plant.input.transportation_cost * a.values["distance"]
c += n.plant.variable_operating_cost
add_to_expression!(obj, c, vars.flow[a])
c = n.location.input.transportation_cost[t] * a.values["distance"]
c += n.location.variable_operating_cost[t]
add_to_expression!(obj, c, vars.flow[a, t])
end
# Fixed and opening costss
add_to_expression!(obj,
n.plant.fixed_operating_cost + n.plant.opening_cost,
vars.open_plant[n])
# Opening costs
add_to_expression!(obj, n.location.opening_cost[t], vars.open_plant[n, t])
# Fixed operating costss
add_to_expression!(obj, n.location.fixed_operating_cost[t], vars.is_open[n, t])
# Expansion costs
add_to_expression!(obj, n.plant.expansion_cost,
vars.expansion[n])
if t < T
add_to_expression!(obj,
n.location.expansion_cost[t] - n.location.expansion_cost[t + 1],
vars.expansion[n, t])
else
add_to_expression!(obj, n.location.expansion_cost[t], vars.expansion[n, t])
end
end
# Disposal costs
for n in values(graph.plant_shipping_nodes)
add_to_expression!(obj,
n.location.disposal_cost[n.product],
vars.dispose[n])
for n in values(graph.plant_shipping_nodes), t in 1:T
add_to_expression!(obj, n.location.disposal_cost[n.product][t], vars.dispose[n, t])
end
@objective(mip, Min, obj)
@@ -84,41 +100,56 @@ end
function create_shipping_node_constraints!(model::ManufacturingModel)
mip, vars, graph = model.mip, model.vars, model.graph
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
# Collection centers
for n in graph.collection_shipping_nodes
@constraint(mip, sum(vars.flow[a] for a in n.outgoing_arcs) == n.location.amount)
end
# Plants
for n in graph.plant_shipping_nodes
@constraint(mip,
sum(vars.flow[a] for a in n.incoming_arcs) ==
sum(vars.flow[a] for a in n.outgoing_arcs) + vars.dispose[n])
for t in 1:T
# Collection centers
for n in graph.collection_shipping_nodes
@constraint(mip, sum(vars.flow[a, t] for a in n.outgoing_arcs) == n.location.amount[t])
end
# Plants
for n in graph.plant_shipping_nodes
@constraint(mip,
sum(vars.flow[a, t] for a in n.incoming_arcs) ==
sum(vars.flow[a, t] for a in n.outgoing_arcs) + vars.dispose[n, t])
end
end
end
function create_process_node_constraints!(model::ManufacturingModel)
mip, vars, graph = model.mip, model.vars, model.graph
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
for n in graph.process_nodes
for n in graph.process_nodes, t in 1:T
# Output amount is implied by input amount
input_sum = isempty(n.incoming_arcs) ? 0 : sum(vars.flow[a] for a in n.incoming_arcs)
input_sum = isempty(n.incoming_arcs) ? 0 : sum(vars.flow[a, t] for a in n.incoming_arcs)
for a in n.outgoing_arcs
@constraint(mip, vars.flow[a] == a.values["weight"] * input_sum)
@constraint(mip, vars.flow[a, t] == a.values["weight"] * input_sum)
end
# If plant is closed, capacity is zero
@constraint(mip, vars.capacity[n] <= n.plant.max_capacity * vars.open_plant[n])
@constraint(mip, vars.capacity[n, t] <= n.location.max_capacity * vars.is_open[n, t])
# Capacity is linked to expansion
@constraint(mip, vars.capacity[n] <= n.plant.base_capacity + vars.expansion[n])
@constraint(mip, vars.capacity[n, t] <= n.location.base_capacity + vars.expansion[n, t])
# Input sum must be smaller than capacity
@constraint(mip, input_sum <= vars.capacity[n])
@constraint(mip, input_sum <= vars.capacity[n, t])
if t > 1
# Plant capacity can only increase over time
@constraint(mip, vars.capacity[n, t] >= vars.capacity[n, t-1])
@constraint(mip, vars.expansion[n, t] >= vars.expansion[n, t-1])
end
# Plant is currently open if it was already open in the previous time period or
# if it was built just now
if t > 1
@constraint(mip, vars.is_open[n, t] == vars.is_open[n, t-1] + vars.open_plant[n, t])
else
@constraint(mip, vars.is_open[n, t] == vars.open_plant[n, t])
end
end
end
@@ -141,19 +172,22 @@ end
function get_solution(model::ManufacturingModel)
mip, vars, graph, instance = model.mip, model.vars, model.graph, model.instance
T = instance.time
output = Dict(
"plants" => Dict(),
"costs" => Dict(
"fixed" => 0.0,
"variable" => 0.0,
"transportation" => 0.0,
"disposal" => 0.0,
"total" => 0.0,
"expansion" => 0.0,
"fixed operating" => zeros(T),
"variable operating" => zeros(T),
"opening" => zeros(T),
"transportation" => zeros(T),
"disposal" => zeros(T),
"expansion" => zeros(T),
"total" => zeros(T),
)
)
plant_to_process_node = Dict(n.plant => n for n in graph.process_nodes)
plant_to_process_node = Dict(n.location => n for n in graph.process_nodes)
plant_to_shipping_nodes = Dict()
for p in instance.plants
plant_to_shipping_nodes[p] = []
@@ -163,7 +197,7 @@ function get_solution(model::ManufacturingModel)
end
for plant in instance.plants
skip_plant = true
skip_plant = false
process_node = plant_to_process_node[plant]
plant_dict = Dict{Any, Any}(
"input" => Dict(),
@@ -171,31 +205,44 @@ function get_solution(model::ManufacturingModel)
"send" => Dict(),
"dispose" => Dict(),
),
"total input" => 0.0,
"total input" => [0.0 for t in 1:T],
"total output" => Dict(),
"latitude" => plant.latitude,
"longitude" => plant.longitude,
"capacity" => JuMP.value(vars.capacity[process_node]),
"fixed cost" => JuMP.value(vars.open_plant[process_node]) * (plant.opening_cost + plant.fixed_operating_cost),
"expansion cost" => JuMP.value(vars.expansion[process_node]) * plant.expansion_cost,
"capacity" => [JuMP.value(vars.capacity[process_node, t])
for t in 1:T],
"opening cost" => [JuMP.value(vars.open_plant[process_node, t]) * plant.opening_cost[t]
for t in 1:T],
"fixed operating cost" => [JuMP.value(vars.is_open[process_node, t]) * plant.fixed_operating_cost[t]
for t in 1:T],
"expansion cost" => [plant.expansion_cost[t] *
(if t > 1
JuMP.value(vars.expansion[process_node, t]) - JuMP.value(vars.expansion[process_node, t-1])
else
JuMP.value(vars.expansion[process_node, t])
end)
for t in 1:T],
)
output["costs"]["fixed"] += plant_dict["fixed cost"]
output["costs"]["fixed operating"] += plant_dict["fixed operating cost"]
output["costs"]["opening"] += plant_dict["opening cost"]
output["costs"]["expansion"] += plant_dict["expansion cost"]
# Inputs
for a in process_node.incoming_arcs
val = JuMP.value(vars.flow[a])
if val <= 1e-3
vals = [JuMP.value(vars.flow[a, t]) for t in 1:T]
if sum(vals) <= 1e-3
continue
end
skip_plant = false
dict = Dict{Any, Any}(
"amount" => val,
"amount" => vals,
"distance" => a.values["distance"],
"latitude" => a.source.location.latitude,
"longitude" => a.source.location.longitude,
"transportation cost" => a.source.product.transportation_cost * val,
"variable operating cost" => plant.variable_operating_cost * val,
"transportation cost" => [a.source.product.transportation_cost[t] * vals[t]
for t in 1:T],
"variable operating cost" => [plant.variable_operating_cost[t] * vals[t]
for t in 1:T],
)
if a.source.location isa CollectionCenter
plant_name = "Origin"
@@ -209,45 +256,45 @@ function get_solution(model::ManufacturingModel)
plant_dict["input"][plant_name] = Dict()
end
plant_dict["input"][plant_name][location_name] = dict
plant_dict["total input"] += val
plant_dict["total input"] += vals
output["costs"]["transportation"] += dict["transportation cost"]
output["costs"]["variable"] += dict["variable operating cost"]
output["costs"]["variable operating"] += dict["variable operating cost"]
end
# Outputs
for shipping_node in plant_to_shipping_nodes[plant]
product_name = shipping_node.product.name
plant_dict["total output"][product_name] = 0.0
plant_dict["output"]["send"][product_name] = product_dict = Dict()
# # Outputs
# for shipping_node in plant_to_shipping_nodes[plant]
# product_name = shipping_node.product.name
# plant_dict["total output"][product_name] = 0.0
# plant_dict["output"]["send"][product_name] = product_dict = Dict()
disposal_amount = JuMP.value(vars.dispose[shipping_node])
if disposal_amount > 1e-5
plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict()
disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node])
disposal_dict["cost"] = disposal_dict["amount"] * plant.disposal_cost[shipping_node.product]
plant_dict["total output"][product_name] += disposal_amount
output["costs"]["disposal"] += disposal_dict["cost"]
end
# disposal_amount = JuMP.value(vars.dispose[shipping_node])
# if disposal_amount > 1e-5
# plant_dict["output"]["dispose"][product_name] = disposal_dict = Dict()
# disposal_dict["amount"] = JuMP.value(model.vars.dispose[shipping_node])
# disposal_dict["cost"] = disposal_dict["amount"] * plant.disposal_cost[shipping_node.product]
# plant_dict["total output"][product_name] += disposal_amount
# output["costs"]["disposal"] += disposal_dict["cost"]
# end
for a in shipping_node.outgoing_arcs
val = JuMP.value(vars.flow[a])
if val <= 1e-3
continue
end
skip_plant = false
dict = Dict(
"amount" => val,
"distance" => a.values["distance"],
"latitude" => a.dest.plant.latitude,
"longitude" => a.dest.plant.longitude,
)
if a.dest.plant.plant_name keys(product_dict)
product_dict[a.dest.plant.plant_name] = Dict()
end
product_dict[a.dest.plant.plant_name][a.dest.plant.location_name] = dict
plant_dict["total output"][product_name] += val
end
end
# for a in shipping_node.outgoing_arcs
# val = JuMP.value(vars.flow[a])
# if val <= 1e-3
# continue
# end
# skip_plant = false
# dict = Dict(
# "amount" => val,
# "distance" => a.values["distance"],
# "latitude" => a.dest.location.latitude,
# "longitude" => a.dest.location.longitude,
# )
# if a.dest.location.plant_name ∉ keys(product_dict)
# product_dict[a.dest.location.plant_name] = Dict()
# end
# product_dict[a.dest.location.plant_name][a.dest.location.location_name] = dict
# plant_dict["total output"][product_name] += val
# end
# end
if !skip_plant
if plant.plant_name keys(output["plants"])

View File

@@ -3,6 +3,21 @@
"$id": "https://axavier.org/ReverseManufacturing/input/schema",
"title": "Schema for ReverseManufacturing Input File",
"definitions": {
"TimeSeries": {
"type": "array",
"items": {
"type": "number"
}
},
"Parameters": {
"type": "object",
"properties": {
"time": { "type": "number" }
},
"required": [
"time periods"
]
},
"Plant": {
"type": "object",
"additionalProperties": {
@@ -28,19 +43,19 @@
"properties": {
"latitude": { "type": "number" },
"longitude": { "type": "number" },
"variable operating cost": { "type": "number" },
"fixed operating cost": { "type": "number" },
"opening cost": { "type": "number" },
"variable operating cost": { "$ref": "#/definitions/TimeSeries" },
"fixed operating cost": { "$ref": "#/definitions/TimeSeries" },
"opening cost": { "$ref": "#/definitions/TimeSeries" },
"base capacity": { "type": "number" },
"max capacity": { "type": "number" },
"expansion cost": { "type": "number" },
"expansion cost": { "$ref": "#/definitions/TimeSeries" },
"disposal": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"cost": { "type": "number" },
"limit": { "type": "number" }
"cost": { "$ref": "#/definitions/TimeSeries" },
"limit": { "$ref": "#/definitions/TimeSeries" }
},
"required": [
"cost"
@@ -64,7 +79,7 @@
"properties": {
"latitude": { "type": "number" },
"longitude": { "type": "number" },
"amount": { "type": "number" }
"amount": { "$ref": "#/definitions/TimeSeries" }
},
"required": [
"latitude",
@@ -78,7 +93,7 @@
"additionalProperties": {
"type": "object",
"properties": {
"transportation cost": { "type": "number" },
"transportation cost": { "$ref": "#/definitions/TimeSeries" },
"initial amounts": { "$ref": "#/definitions/InitialAmount" }
},
"required": [
@@ -89,6 +104,7 @@
},
"type": "object",
"properties": {
"parameters": { "$ref": "#/definitions/Parameters" },
"plants": { "$ref": "#/definitions/Plant" },
"products": { "$ref": "#/definitions/Product" }
},