From a6846be9ebd02c42282a5f721d73db5e689815d3 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Fri, 2 Oct 2020 11:25:40 -0500 Subject: [PATCH] Implement multi-period heuristic --- Makefile | 4 +-- Project.toml | 3 +- src/docs/usage.md | 17 +++++++++ src/instance.jl | 61 +++++++++++++++++++++++++++++++-- src/model.jl | 80 +++++++++++++++++++++++++++++++------------ test/instance_test.jl | 50 ++++++++++++++++++++++++++- test/model_test.jl | 9 +++-- 7 files changed, 194 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index 042e921..315699c 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,11 @@ VERSION := 0.4 all: docs test build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml + mkdir -p build $(JULIA) src/sysimage.jl build/test.log: $(SRC_FILES) build/sysimage.so - @echo Running tests... - cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl | tee ../build/test.log + cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl clean: rm -rf build/* diff --git a/Project.toml b/Project.toml index 50bf3c1..6870d06 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RELOG" uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008" authors = ["Alinson S Xavier "] -version = "0.4.0" +version = "0.4.1" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" @@ -19,6 +19,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] diff --git a/src/docs/usage.md b/src/docs/usage.md index 0dbf780..8b32d47 100644 --- a/src/docs/usage.md +++ b/src/docs/usage.md @@ -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) +``` diff --git a/src/instance.jl b/src/instance.jl index c07e72d..a82961d 100644 --- a/src/instance.jl +++ b/src/instance.jl @@ -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 diff --git a/src/model.jl b/src/model.jl index 97c5c84..0ea5366 100644 --- a/src/model.jl +++ b/src/model.jl @@ -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 diff --git a/test/instance_test.jl b/test/instance_test.jl index 2364e47..eb35bf3 100644 --- a/test/instance_test.jl +++ b/test/instance_test.jl @@ -11,7 +11,6 @@ using RELOG 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) @@ -75,5 +74,54 @@ using RELOG @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 diff --git a/test/model_test.jl b/test/model_test.jl index cf108ec..455beea 100644 --- a/test/model_test.jl +++ b/test/model_test.jl @@ -41,7 +41,7 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats #MOI.write_to_file(dest, "model.lp") end - @testset "solve" begin + @testset "solve (exact)" begin solution_filename = tempname() solution = RELOG.solve("$(pwd())/../instances/s1.json", output=solution_filename) @@ -58,6 +58,12 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats @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") @@ -66,7 +72,6 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats end RELOG.solve(RELOG.parse(json)) end - end