mirror of
https://github.com/ANL-CEEESA/RELOG.git
synced 2025-12-05 23:38:52 -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
|
||||
|
||||
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/*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "RELOG"
|
||||
uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008"
|
||||
authors = ["Alinson S Xavier <axavier@anl.gov>"]
|
||||
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]
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
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;
|
||||
optimizer=nothing,
|
||||
output=nothing)
|
||||
output=nothing,
|
||||
marginal_costs=true,
|
||||
)
|
||||
|
||||
milp_optimizer = lp_optimizer = optimizer
|
||||
if optimizer == nothing
|
||||
@@ -224,6 +226,7 @@ function solve(instance::Instance;
|
||||
return OrderedDict()
|
||||
end
|
||||
|
||||
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)
|
||||
@@ -235,9 +238,10 @@ function solve(instance::Instance;
|
||||
end
|
||||
end
|
||||
JuMP.optimize!(model.mip)
|
||||
end
|
||||
|
||||
@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,16 +325,18 @@ function get_solution(model::ManufacturingModel)
|
||||
end
|
||||
|
||||
# Products
|
||||
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],
|
||||
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
|
||||
end
|
||||
|
||||
# Plants
|
||||
for plant in instance.plants
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -59,6 +59,12 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
@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")
|
||||
for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
|
||||
@@ -66,7 +72,6 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
end
|
||||
RELOG.solve(RELOG.parse(json))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user