mirror of
https://github.com/ANL-CEEESA/RELOG.git
synced 2025-12-06 07:48:50 -06:00
Implement multi-period heuristic
This commit is contained in:
4
Makefile
4
Makefile
@@ -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
|
||||||
|
|||||||
48
src/model.jl
48
src/model.jl
@@ -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,6 +226,7 @@ function solve(instance::Instance;
|
|||||||
return OrderedDict()
|
return OrderedDict()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if marginal_costs
|
||||||
@info "Re-optimizing with integer variables fixed..."
|
@info "Re-optimizing with integer variables fixed..."
|
||||||
all_vars = JuMP.all_variables(model.mip)
|
all_vars = JuMP.all_variables(model.mip)
|
||||||
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
|
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
|
||||||
@@ -235,9 +238,10 @@ function solve(instance::Instance;
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
JuMP.optimize!(model.mip)
|
JuMP.optimize!(model.mip)
|
||||||
|
end
|
||||||
|
|
||||||
@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,16 +325,18 @@ function get_solution(model::ManufacturingModel)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Products
|
# Products
|
||||||
|
if marginal_costs
|
||||||
for n in graph.collection_shipping_nodes
|
for n in graph.collection_shipping_nodes
|
||||||
location_dict = OrderedDict{Any, Any}(
|
location_dict = OrderedDict{Any, Any}(
|
||||||
"Marginal cost (\$/tonne)" => [round(abs(JuMP.shadow_price(eqs.balance[n, t])), digits=2)
|
"Marginal cost (\$/tonne)" => [round(abs(JuMP.shadow_price(eqs.balance[n, t])), digits=2)
|
||||||
for t in 1:T],
|
for t in 1:T]
|
||||||
)
|
)
|
||||||
if n.product.name ∉ keys(output["Products"])
|
if n.product.name ∉ keys(output["Products"])
|
||||||
output["Products"][n.product.name] = OrderedDict()
|
output["Products"][n.product.name] = OrderedDict()
|
||||||
end
|
end
|
||||||
output["Products"][n.product.name][n.location.name] = location_dict
|
output["Products"][n.product.name][n.location.name] = location_dict
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Plants
|
# Plants
|
||||||
for plant in instance.plants
|
for plant in instance.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)
|
||||||
@@ -59,6 +59,12 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
|||||||
@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")
|
||||||
for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
|
for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user