Implement multi-period heuristic

This commit is contained in:
2020-10-02 11:25:40 -05:00
parent fe9cacef24
commit a6846be9eb
7 changed files with 194 additions and 30 deletions

View File

@@ -79,3 +79,20 @@ RELOG.solve("instance.json",
output="solution.json",
optimizer=gurobi)
```
### 4.2 Multi-period heuristics
For large-scale instances, it may be too time-consuming to find an exact optimal solution to the multi-period version of the problem. For these situations, RELOG includes a heuristic solution method, which proceeds as follows:
1. First, RELOG creates a single-period version of the problem, in which most values are replaced by their averages. This single-period problem is typically much easier to solve.
2. After solving the simplified problem, RELOG resolves the multi-period version of the problem, but considering only candidate plant locations that were selected by the optimal solution to the single-period version of the problem. All remaining candidate plant locations are removed.
To solve an instance using this heuristic, use the option `heuristic=true`, as shown below.
```julia
using RELOG
solution = RELOG.solve("/home/user/instance.json",
heuristic=true)
```

View File

@@ -2,7 +2,11 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using JSON, JSONSchema, Printf
using DataStructures
using JSON
using JSONSchema
using Printf
using Statistics
mutable struct Product
@@ -55,6 +59,7 @@ mutable struct Instance
building_period::Array{Int64}
end
function validate(json, schema)
result = JSONSchema.validate(json, schema)
if result !== nothing
@@ -77,7 +82,7 @@ function parsefile(path::String)::Instance
end
function parse(json::Dict)::Instance
function parse(json)::Instance
basedir = dirname(@__FILE__)
json_schema = JSON.parsefile("$basedir/schemas/input.json")
validate(json, Schema(json_schema))
@@ -209,3 +214,55 @@ function parse(json::Dict)::Instance
return Instance(T, products, collection_centers, plants, building_period)
end
"""
_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

View File

@@ -197,7 +197,9 @@ default_lp_optimizer = optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
function solve(instance::Instance;
optimizer=nothing,
output=nothing)
output=nothing,
marginal_costs=true,
)
milp_optimizer = lp_optimizer = optimizer
if optimizer == nothing
@@ -224,20 +226,22 @@ function solve(instance::Instance;
return OrderedDict()
end
@info "Re-optimizing with integer variables fixed..."
all_vars = JuMP.all_variables(model.mip)
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
JuMP.set_optimizer(model.mip, lp_optimizer)
for var in all_vars
if JuMP.is_binary(var)
JuMP.unset_binary(var)
JuMP.fix(var, vals[var])
if marginal_costs
@info "Re-optimizing with integer variables fixed..."
all_vars = JuMP.all_variables(model.mip)
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
JuMP.set_optimizer(model.mip, 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.mip)
end
JuMP.optimize!(model.mip)
@info "Extracting solution..."
solution = get_solution(model)
solution = get_solution(model, marginal_costs=marginal_costs)
if output != nothing
@info "Writing solution: $output"
@@ -249,13 +253,43 @@ function solve(instance::Instance;
return solution
end
function solve(filename::String; kwargs...)
function solve(filename::AbstractString;
heuristic=false,
kwargs...,
)
@info "Reading $filename..."
instance = RELOG.parsefile(filename)
return solve(instance; kwargs...)
if heuristic
@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::ManufacturingModel)
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
@@ -291,15 +325,17 @@ function get_solution(model::ManufacturingModel)
end
# Products
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],
)
if n.product.name keys(output["Products"])
output["Products"][n.product.name] = OrderedDict()
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]
)
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
output["Products"][n.product.name][n.location.name] = location_dict
end
# Plants