Implement multi-period heuristic

gh-actions
Alinson S. Xavier 5 years ago
parent fe9cacef24
commit a6846be9eb

@ -5,11 +5,11 @@ VERSION := 0.4
all: docs test all: docs test
build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml
mkdir -p build
$(JULIA) src/sysimage.jl $(JULIA) src/sysimage.jl
build/test.log: $(SRC_FILES) build/sysimage.so build/test.log: $(SRC_FILES) build/sysimage.so
@echo Running tests... cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl
cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl | tee ../build/test.log
clean: clean:
rm -rf build/* rm -rf build/*

@ -1,7 +1,7 @@
name = "RELOG" name = "RELOG"
uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008" uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008"
authors = ["Alinson S Xavier <axavier@anl.gov>"] authors = ["Alinson S Xavier <axavier@anl.gov>"]
version = "0.4.0" version = "0.4.1"
[deps] [deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
@ -19,6 +19,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[compat] [compat]

@ -79,3 +79,20 @@ RELOG.solve("instance.json",
output="solution.json", output="solution.json",
optimizer=gurobi) 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)
```

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

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

@ -11,7 +11,6 @@ using RELOG
centers = instance.collection_centers centers = instance.collection_centers
plants = instance.plants plants = instance.plants
products = instance.products products = instance.products
location_name_to_plant = Dict(p.location_name => p for p in plants) location_name_to_plant = Dict(p.location_name => p for p in plants)
product_name_to_product = Dict(p.name => p for p in products) product_name_to_product = Dict(p.name => p for p in products)
@ -75,5 +74,54 @@ using RELOG
@testset "validate timeseries" begin @testset "validate timeseries" begin
@test_throws String RELOG.parsefile("fixtures/s1-wrong-length.json") @test_throws String RELOG.parsefile("fixtures/s1-wrong-length.json")
end 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 end

@ -41,7 +41,7 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
#MOI.write_to_file(dest, "model.lp") #MOI.write_to_file(dest, "model.lp")
end end
@testset "solve" begin @testset "solve (exact)" begin
solution_filename = tempname() solution_filename = tempname()
solution = RELOG.solve("$(pwd())/../instances/s1.json", solution = RELOG.solve("$(pwd())/../instances/s1.json",
output=solution_filename) output=solution_filename)
@ -58,6 +58,12 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
@test "F3" in keys(solution["Plants"]) @test "F3" in keys(solution["Plants"])
@test "F4" in keys(solution["Plants"]) @test "F4" in keys(solution["Plants"])
end end
@testset "solve (heuristic)" begin
# Should not crash
solution = RELOG.solve("$(pwd())/../instances/s1.json", heuristic=true)
end
@testset "infeasible solve" begin @testset "infeasible solve" begin
json = JSON.parsefile("$(pwd())/../instances/s1.json") json = JSON.parsefile("$(pwd())/../instances/s1.json")
@ -66,7 +72,6 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
end end
RELOG.solve(RELOG.parse(json)) RELOG.solve(RELOG.parse(json))
end end
end end

Loading…
Cancel
Save