# RELOG: Reverse Logistics Optimization # Copyright (C) 2020-2024, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. using CSV using DataFrames using JSON using JuMP using OrderedCollections using Random using TimerOutputs using Printf using Gurobi include("jumpext.jl") include("dist.jl") dict = OrderedDict Time = Int Random.seed!(42) # Structs # ========================================================================= Base.@kwdef mutable struct Component name::String end Base.@kwdef mutable struct Product name::String comp::Vector{Component} end Base.@kwdef mutable struct Center name::String latitude::Float64 longitude::Float64 prod_out::Vector{Product} end Base.@kwdef mutable struct Plant name::String #county::String latitude::Float64 longitude::Float64 prod_out::Vector{Product} prod_in::Product end Base.@kwdef mutable struct Emission name::String end Base.show(io::IO, p::Union{Component,Product,Center,Plant,Emission}) = print(io, p.name) Base.@kwdef mutable struct Instance T::UnitRange{Int} centers::Vector{Center} plants::Vector{Plant} products::Vector{Product} emissions::Vector{Emission} alpha_mix::dict{Tuple{Plant,Product,Component,Component},Float64} alpha_plant_emission::dict{Tuple{Plant,Emission,Time},Float64} alpha_tr_emission::dict{Tuple{Product,Emission,Time},Float64} c_acq::dict{Tuple{Center,Product,Time},Float64} #c_interv::dict{Tuple{Center,Product,Time},Float64} c_center_disp::dict{Tuple{Center,Product,Time},Float64} c_emission::dict{Tuple{Emission,Time},Float64} c_fix::dict{Tuple{Plant,Time},Float64} c_open::dict{Tuple{Plant,Time},Float64} c_plant_disp::dict{Tuple{Plant,Product,Time},Float64} c_store::dict{Tuple{Center,Product,Time},Float64} c_tr::dict{Tuple{Product,Time},Float64} c_var::dict{Tuple{Plant,Time},Float64} m_cap::dict{Plant,Float64} m_center_disp::dict{Tuple{Product,Time},Float64} m_dist::dict{Tuple{Union{Center,Plant},Plant},Float64} m_emission::dict{Tuple{Emission,Time},Float64} m_init::dict{Tuple{Center,Product,Component,Time},Float64} m_plant_disp::dict{Tuple{Plant,Product,Time},Float64} m_store::dict{Tuple{Center,Product,Time},Float64} selected_edges::Union{Nothing,Set} = nothing max_cap::dict{Plant,Float64} max_OpC::dict{Plant,Float64} max_OpenC::dict{Plant,Float64} min_cap::dict{Plant,Float64} min_OpC::dict{Plant,Float64} min_OpenC::dict{Plant,Float64} unit_open::dict{Plant,Float64} unit_OpC::dict{Plant,Float64} end # Generate # ============================================================================== function generate_data() # Time window T = 1:2 # Cities cities_a = dict( "Chicago" => [41.881832, -87.623177], "New York City" => [40.712776, -74.005974], "Los Angeles" => [34.052235, -118.243683], "Houston" => [29.760427, -95.369804], "Phoenix" => [33.448376, -112.074036], "Philadelphia" => [39.952583, -75.165222], "San Antonio" => [29.424122, -98.493629], "San Diego" => [32.715736, -117.161087], "Dallas" => [32.776664, -96.796988], "San Jose" => [37.338208, -121.886329], ) cities_b = dict( "Chicago" => [41.881832, -87.623177], "Phoenix" => [33.448376, -112.074036], "Dallas" => [32.776664, -96.796988], ) # Components film = Component("Film") paper = Component("Paper") cardboard = Component("Cardboard") # Products waste = Product(name="Waste", comp=[film, paper, cardboard]) film_bale = Product(name="Film bale", comp=[film, paper, cardboard]) cardboard_bale = Product(name="Cardboard bale", comp=[paper, cardboard]) cardboard_sheets = Product(name="Cardboard sheets", comp=[cardboard]) products = [waste, film_bale, cardboard_bale, cardboard_sheets] # Centers centers = [ Center( name="Collection ($city_name)", latitude=city_lat, longitude=city_lon, prod_out=[waste], ) for (city_name, (city_lat, city_lon)) in cities_a ] # Plants plants_a = [ Plant( name="MRF ($city_name)", latitude=city_lat, longitude=city_lon, prod_in=waste, prod_out=[film_bale, cardboard_bale], ) for (city_name, (city_lat, city_lon)) in cities_b ] plants_b = [ Plant( name="Paper Mill ($city_name)", latitude=city_lat, longitude=city_lon, prod_in=cardboard_bale, prod_out=[cardboard_sheets], ) for (city_name, (city_lat, city_lon)) in cities_b ] plants = [plants_a; plants_b] # Emissions emissions = [Emission("CO2")] alpha_mix = dict( (p, r, cin, cout) => 0.0 for p in plants for cin in p.prod_in.comp for r in p.prod_out for cout in r.comp ) for p in plants_a alpha_mix[p, film_bale, film, film] = 0.98 alpha_mix[p, film_bale, paper, paper] = 0.02 alpha_mix[p, film_bale, cardboard, cardboard] = 0.02 alpha_mix[p, cardboard_bale, paper, paper] = 0.02 alpha_mix[p, cardboard_bale, cardboard, cardboard] = 0.75 end for p in plants_b alpha_mix[p, cardboard_sheets, cardboard, cardboard] = 0.95 end alpha_plant_emission = dict((p, s, t) => 0.01 for p in plants, s in emissions, t in T) alpha_tr_emission = dict((r, s, t) => 0.01 for r in products, s in emissions, t in T) c_acq = dict((q, r, t) => 1.0 for q in centers for r in q.prod_out, t in T) c_center_disp = dict((q, r, t) => 0 for q in centers for r in q.prod_out for t in T) c_emission = dict((s, t) => 0.01 for s in emissions, t in T) c_fix = dict((p, t) => 1_000.0 for p in plants, t in T) c_open = dict((p, t) => 10_000.0 for p in plants, t in T) c_plant_disp = dict( (p, r, t) => (r == cardboard_sheets ? -100.0 : -10.0) for p in plants for r in p.prod_out, t in T ) c_store = dict((q, r, t) => 1.0 for q in centers for r in q.prod_out, t in T) c_tr = dict((r, t) => 0.05 for r in products, t in T) c_var = dict((p, t) => 1.0 for p in plants, t in T) m_cap = dict(p => 50000.0 for p in plants) m_center_disp = dict((r, t) => 0.0 for r in products, t in T) metric = KnnDrivingDistance() m_dist = dict( (p, q) => _calculate_distance( p.latitude, p.longitude, q.latitude, q.longitude, metric, ) for p in [plants; centers], q in plants ) m_emission = dict((s, t) => 1_000_000 for s in emissions, t in T) m_init = dict() for q in centers, r in q.prod_out ratio = dict(c => rand(1:10) for c in r.comp) total = dict(t => rand(1:1000) for t in T) for c in r.comp, t in T m_init[q, r, c, t] = ratio[c] * total[t] end end m_plant_disp = dict((p, r, t) => 1_000_000 for p in plants for r in p.prod_out for t in T) m_store = dict((q, r, t) => 1_000 for q in centers for r in q.prod_out, t in T) return Instance(; T, centers, plants, products, emissions, alpha_mix, alpha_plant_emission, alpha_tr_emission, c_acq, c_center_disp, c_emission, c_fix, c_open, c_plant_disp, c_store, c_tr, c_var, m_cap, m_center_disp, m_dist, m_emission, m_init, m_plant_disp, m_store, ) end # Write # ============================================================================== function write_json(data, filename) json = dict() json["parameters"] = dict("time horizon (years)" => data.T.stop) json["products"] = dict( r.name => dict( "components" => [c.name for c in r.comp], "disposal limit (tonne)" => [data.m_center_disp[r, t] for t in data.T], "transportation cost (\$/km/tonne)" => [data.c_tr[r, t] for t in data.T], "transportation emissions (tonne/km/tonne)" => dict( s => [data.alpha_tr_emission[r, s, t] for t in data.T] for s in data.emissions ), ) for r in data.products ) json["centers"] = dict( q.name => dict( "latitude" => q.latitude, "longitude" => q.longitude, "output" => dict( r.name => dict( "initial amount (tonne)" => [ data.m_init[q, r, c, t] for c in r.comp, t in data.T ], "disposal cost (\$/tonne)" => [data.c_center_disp[q, r, t] for t in data.T], "storage cost (\$/tonne)" => [data.c_store[q, r, t] for t in data.T], "storage limit (tonne)" => [data.m_store[q, r, t] for t in data.T], "acquisition cost (\$/tonne)" => [data.c_acq[q, r, t] for t in data.T], ) for r in q.prod_out ), ) for q in data.centers ) json["plants"] = dict( p.name => dict( "latitude" => p.latitude, "longitude" => p.longitude, "input" => p.prod_in.name, "output" => dict( r.name => dict( "output matrix" => [ data.alpha_mix[p, r, c_in, c_out] for c_in in p.prod_in.comp, c_out in r.comp ], "disposal limit (tonne)" => [data.m_plant_disp[p, r, t] for t in data.T], "disposal cost (\$/tonne)" => [data.c_plant_disp[p, r, t] for t in data.T], ) for r in p.prod_out ), "Min_capacity" => data.min_cap[p], "Min_FixOperatingCost" => data.min_OpC[p], "Min_OpeningCost" => data.min_OpenC[p], "Max_capacity" => data.max_cap[p], "Max_FixOperatingCost" => data.max_OpC[p], "Max_OpeningCost" => data.max_OpenC[p], "fixed operating cost (\$)" => [data.c_fix[p, t] for t in data.T], "variable operating cost (\$/tonne)" => [data.c_var[p, t] for t in data.T], "opening cost (\$)" => [data.c_open[p, t] for t in data.T], "capacity (tonne)" => data.m_cap[p], "emissions (tonne/tonne)" => dict( s => [data.alpha_plant_emission[p, s, t] for t in data.T] for s in data.emissions ), ) for p in data.plants ) json["emissions"] = dict( s.name => dict( "penalty (\$/tonne)" => [data.c_emission[s, t] for t in data.T], "limit (tonne)" => [data.m_emission[s, t] for t in data.T], ) for s in data.emissions ) open(filename, "w") do io JSON.print(io, json, 2) end end # Read # ============================================================================== function read_json(filename; max_centers=Inf, max_plants=Inf)::Instance json = JSON.parsefile(filename) T = 1:json["parameters"]["time horizon (years)"] centers = [] components_by_name = dict() emissions = [] emissions_by_name = dict() plants = [] products = [] products_by_name = dict() alpha_mix = dict() alpha_plant_emission = dict() alpha_tr_emission = dict() c_acq = dict() #c_interv = dict() c_center_disp = dict() c_emission = dict() c_fix = dict() c_open = dict() c_plant_disp = dict() c_store = dict() c_tr = dict() c_var = dict() m_cap = dict() m_center_disp = dict() m_emission = dict() m_init = dict() m_plant_disp = dict() m_store = dict() max_cap = dict() max_OpC = dict() max_OpenC = dict() min_cap = dict() min_OpC = dict() min_OpenC = dict() unit_open = dict() unit_OpC = dict() @timeit "Read: Emissions" begin for (emission_name, emission_data) in json["emissions"] s = Emission(emission_name) emissions_by_name[emission_name] = s push!(emissions, s) for t in T c_emission[s, t] = emission_data["penalty (\$/tonne)"][t] m_emission[s, t] = emission_data["limit (tonne)"][t] end end end @timeit "Read: Products" begin for (name, prod_data) in json["products"] print(name) comp = [] for (comp_name) in prod_data["components"] if comp_name ∉ keys(components_by_name) print(comp_name) components_by_name[comp_name] = Component(comp_name) end push!(comp, components_by_name[comp_name]) end r = Product(name, comp) products_by_name[name] = r push!(products, r) for t in T m_center_disp[r, t] = prod_data["disposal limit (tonne)"][t] c_tr[r, t] = prod_data["transportation cost (\$/km/tonne)"][t] end for (s_name, s_data) in prod_data["transportation emissions (tonne/km/tonne)"] s = emissions_by_name[s_name] for t in T alpha_tr_emission[r, s, t] = s_data[t] end end end end @timeit "Read: Centers" begin for (name, center_data) in json["centers"] if length(centers) >= max_centers @warn "Maximum number of centers reached. Skipping remaining ones." break end latitude = center_data["latitude"] longitude = center_data["longitude"] prod_out = [products_by_name[r] for r in keys(center_data["output"])] q = Center(; name, latitude, longitude, prod_out) push!(centers, q) for r in prod_out, t in T c_acq[q, r, t] = center_data["output"][r.name]["acquisition cost (\$/tonne)"][t] #c_interv[q, r, t] = # center_data["output"][r.name]["intervention costs ($)"][t] c_center_disp[q, r, t] = center_data["output"][r.name]["disposal cost (\$/tonne)"][t] c_store[q, r, t] = center_data["output"][r.name]["storage cost (\$/tonne)"][t] m_store[q, r, t] = center_data["output"][r.name]["storage limit (tonne)"][t] for (c_idx, c) in enumerate(r.comp) m_init[q, r, c, t] = center_data["output"][r.name]["initial amount (tonne)"][t][c_idx] end end end end @timeit "Read: Plants" begin for (plant_name, plant_data) in json["plants"] if length(plants) >= max_plants @warn "Maximum number of plants reached. Skipping remaining ones." break end #county = plant_data["county"] latitude = plant_data["latitude"] longitude = plant_data["longitude"] prod_in = products_by_name[plant_data["input"]] prod_out = [products_by_name[r] for r in keys(plant_data["output"])] p = Plant(plant_name, latitude, longitude, prod_out, prod_in) push!(plants, p) m_cap[p] = plant_data["capacity (tonne)"] min_cap[p] = plant_data["Min_capacity"] min_OpC[p] = plant_data["Min_FixOperatingCost"] min_OpenC[p] = plant_data["Min_OpeningCost"] max_cap[p] = plant_data["Max_capacity"] max_OpC[p] = plant_data["Max_FixOperatingCost"] max_OpenC[p] = plant_data["Max_OpeningCost"] #Compute unit cost for expansion calculations---------------------- #unit opening cost if max_cap[p] > min_cap[p] unit_open[p] = (max_OpenC[p] - min_OpenC[p])/(max_cap[p] - min_cap[p]) else unit_open[p] = (max_OpenC[p]/max_cap[p]) end #unit operating cost if max_cap[p] > min_cap[p] unit_OpC[p] = (max_OpC[p] - min_OpC[p])/(max_cap[p] - min_cap[p]) else unit_OpC[p] = (max_OpC[p]/max_cap[p]) end #------------------------------------------------------------------ for t in T c_fix[p, t] = plant_data["fixed operating cost (\$)"][t] c_var[p, t] = plant_data["variable operating cost (\$/tonne)"][t] c_open[p, t] = plant_data["opening cost (\$)"][t] end for r in prod_out, (cin_idx, c_in) in enumerate(prod_in.comp), (cout_idx, c_out) in enumerate(r.comp) alpha_mix[p, r, c_in, c_out] = plant_data["output"][r.name]["output matrix"][cout_idx][cin_idx] end for r in prod_out, t in T c_plant_disp[p, r, t] = plant_data["output"][r.name]["disposal cost (\$/tonne)"][t] m_plant_disp[p, r, t] = plant_data["output"][r.name]["disposal limit (tonne)"][t] end for (s_name, s_data) in plant_data["emissions (tonne/tonne)"] s = emissions_by_name[s_name] for t in T alpha_plant_emission[p, s, t] = s_data[t] end end end end @timeit "Calculate distances" begin metric = KnnDrivingDistance() m_dist = dict( (p, q) => _calculate_distance( p.latitude, p.longitude, q.latitude, q.longitude, metric, ) for p in [plants; centers], q in plants ) end return Instance(; T, centers, plants, products, emissions, alpha_mix, alpha_plant_emission, alpha_tr_emission, c_acq, #c_interv, c_center_disp, c_emission, c_fix, c_open, c_plant_disp, c_store, c_tr, c_var, m_cap, m_center_disp, m_dist, m_emission, m_init, m_plant_disp, m_store, max_cap, max_OpC, max_OpenC, min_cap, min_OpC, min_OpenC, unit_open, unit_OpC ) end # Multi-period Heuristic # ============================================================================== function compress(original::Instance)::Instance T = original.T alpha_plant_emission = Dict( (p, s, 1) => mean([original.alpha_plant_emission[p, s, t] for t in T]) for p in original.plants, s in original.emissions ) alpha_tr_emission = Dict( (r, s, 1) => mean([original.alpha_tr_emission[r, s, t] for t in T]) for r in original.products, s in original.emissions ) c_acq = Dict( (q, r, 1) => mean([original.c_acq[q, r, t] for t in T]) for q in original.centers for r in q.prod_out ) #c_interv = Dict( # (q, r, 1) => mean([original.c_interv[q, r, t] for t in T]) for # q in original.centers for r in q.prod_out #) c_center_disp = Dict( (q, r, 1) => mean([original.c_center_disp[q, r, t] for t in T]) for q in original.centers for r in q.prod_out ) c_emission = Dict( (s, 1) => mean([original.c_emission[s, t] for t in T]) for s in original.emissions ) c_fix = Dict((p, 1) => sum([original.c_fix[p, t] for t in T]) for p in original.plants) c_open = Dict((p, 1) => mean([original.c_open[p, t] for t in T]) for p in original.plants) c_plant_disp = Dict( (p, r, 1) => mean([original.c_plant_disp[p, r, t] for t in T]) for p in original.plants for r in p.prod_out ) c_store = Dict( (q, r, 1) => mean([original.c_store[q, r, t] for t in T]) for q in original.centers for r in q.prod_out ) c_tr = Dict((r, 1) => mean([original.c_tr[r, t] for t in T]) for r in original.products) c_var = Dict((p, 1) => mean([original.c_var[p, t] for t in T]) for p in original.plants) m_cap = Dict(p => original.m_cap[p] * length(T) for p in original.plants) max_cap = Dict(p => original.max_cap[p] * length(T) for p in original.plants) min_cap = Dict(p => original.min_cap[p] * length(T) for p in original.plants) max_OpC = Dict(p => original.max_OpC[p] * length(T) for p in original.plants) min_OpC = Dict(p => original.min_OpC[p] * length(T) for p in original.plants) max_OpenC = Dict(p => original.max_OpenC[p] * length(T) for p in original.plants) min_OpenC = Dict(p => original.min_OpenC[p] * length(T) for p in original.plants) unit_open = Dict(p => original.unit_open[p] * length(T) for p in original.plants) #unit_open = Dict(p => ((max_OpenC[p] - min_OpenC[p])/(max_cap[p] - min_cap[p])) for p in original.plants) #unit_open = Dict(p => original.unit_open[p] for p in original.plants) unit_OpC = Dict(p => original.unit_OpC[p] * length(T) for p in original.plants) #unit_OpC = Dict(p => ((max_OpC[p] - min_OpC[p])/(max_cap[p] - min_cap[p])) for p in original.plants) #unit_OpC = Dict(p => original.unit_OpC[p] for p in original.plants) m_center_disp = Dict( (r, 1) => sum([original.m_center_disp[r, t] for t in T]) for r in original.products ) m_emission = Dict( (s, 1) => sum([original.m_emission[s, t] for t in T]) for s in original.emissions ) m_init = Dict( (q, r, c, 1) => length(T) * maximum([original.m_init[q, r, c, t] for t in T]) for q in original.centers for r in q.prod_out for c in r.comp ) m_plant_disp = Dict( (p, r, 1) => length(T) * minimum([original.m_plant_disp[p, r, t] for t in T]) for p in original.plants for r in p.prod_out ) m_store = Dict( (q, r, 1) => length(T) * minimum([original.m_store[q, r, t] for t in T]) for q in original.centers for r in q.prod_out ) #= m_init = Dict( (q, r, c, 1) => sum([original.m_init[q, r, c, t] for t in T]) for q in original.centers for r in q.prod_out for c in r.comp ) m_plant_disp = Dict( (p, r, 1) => sum([original.m_plant_disp[p, r, t] for t in T]) for p in original.plants for r in p.prod_out ) m_store = Dict( (q, r, 1) => sum([original.m_store[q, r, t] for t in T]) for q in original.centers for r in q.prod_out ) =# return Instance(; T=1:1, centers=original.centers, plants=original.plants, products=original.products, emissions=original.emissions, alpha_mix=original.alpha_mix, alpha_plant_emission, alpha_tr_emission, c_acq, #c_interv, c_center_disp, c_emission, c_fix, c_open, c_plant_disp, c_store, c_tr, c_var, m_cap, m_center_disp, m_dist=original.m_dist, m_emission, m_init, m_plant_disp, m_store, max_cap, max_OpC, max_OpenC, min_cap, min_OpC, min_OpenC, unit_open, unit_OpC ) end function benchmark_compress(filename, optimizer; max_centers=[Inf], max_plants=[Inf]) stats = [] for mc in max_centers, mp in max_plants # Solve original orig = read_json(filename; max_centers=mc, max_plants=mp) reset_timer!() _, stats_orig = solve(orig; optimizer) stats_orig["Filename"] = filename stats_orig["Method"] = "Original" push!(stats, stats_orig) # Solve compressed compressed = compress(orig) reset_timer!() model_comp, stats_comp = solve(compressed; optimizer) stats_comp["Filename"] = filename stats_comp["Method"] = "Compressed" push!(stats, stats_comp) end return DataFrame(stats) end # Run # ============================================================================== function generate_json() @info "Generating data" data = generate_data() @info "Writing JSON file" write_json(data, "output-3/case.json") end function solve(filename, optimizer; max_centers=Inf, max_plants=Inf, heuristic=false) reset_timer!() @timeit "Read JSON" begin data = read_json(filename; max_centers, max_plants) write_json(data, "output-3/BaselinePlastics_capExTestCase10C7P.json") end return solve(data; optimizer=optimizer, output_dir=dirname(filename), heuristic) print_timer() end function solve(data::Instance; optimizer, output_dir=nothing, heuristic=false) if heuristic @info "Solving compressed instance..." comp_data = compress(data) #print("data compressed") comp_model, comp_stats = solve(comp_data; optimizer, heuristic=false) #print("compressed model solved") # Filter plants selected_plants = Plant[] for p in comp_data.plants if value(comp_model[:x_open][p, 1]) > 0.5 push!(selected_plants, p) end end @info "Selected $(length(selected_plants)) out of $(length(comp_data.plants)) plants" # Filter edges selected_edges = Set() for (src, dst, r) in comp_model[:E] if value(comp_model[:y_total][src, dst, r, 1]) > 0.5 push!(selected_edges, (src, dst, r)) end end @info "Selected $(length(selected_edges)) out of $(length(comp_model[:E])) transportation edges" data = Instance(; data.T, data.centers, plants=selected_plants, data.products, data.emissions, data.alpha_mix, data.alpha_plant_emission, data.alpha_tr_emission, data.c_acq, #data.c_interv, data.c_center_disp, data.c_emission, data.c_fix, data.c_open, data.c_plant_disp, data.c_store, data.c_tr, data.c_var, data.m_cap, data.m_center_disp, data.m_dist, data.m_emission, data.m_init, data.m_plant_disp, data.m_store, selected_edges, data.max_cap, data.max_OpC, data.max_OpenC, data.min_cap, data.min_OpC, data.min_OpenC, data.unit_open, data.unit_OpC ) end T = data.T centers = data.centers plants = data.plants products = data.products emissions = data.emissions #model = Model(optimizer) model = Model(Gurobi.Optimizer) set_optimizer_attribute(model, "MIPGap", 0.005) init_time = time() # Graph # ------------------------------------------------------------------------- @timeit "Build graph" begin E = [] E_in = dict(src => [] for src in plants) E_out = dict(src => [] for src in plants ∪ centers) function push_edge!(src, dst, r) e = (src, dst, r) if data.selected_edges !== nothing && e ∉ data.selected_edges return end push!(E, e) push!(E_out[src], (dst, r)) push!(E_in[dst], (src, r)) end for r in products # Plant to plant for p1 in plants r ∈ p1.prod_out || continue for p2 in plants p1 != p2 || continue r == p2.prod_in || continue push_edge!(p1, p2, r) end end # Center to plant for q in centers r ∈ q.prod_out || continue for p in plants r == p.prod_in || continue push_edge!(q, p, r) end end end end model[:E] = E model[:E_in] = E_in model[:E_out] = E_out @printf("Building optimization problem with:\n") @printf(" %8d plants\n", length(plants)) @printf(" %8d centers\n", length(centers)) @printf(" %8d products\n", length(products)) @printf(" %8d time periods\n", length(T)) @printf(" %8d transportation edges\n", length(E)) available = Dict((r, t) => 0.0 for r in data.products, t in T) for q in centers, t in T for r in q.prod_out, s in r.comp available[r, t] += data.m_init[q, r, s, t] end end capacity = Dict(r => 0.0 for r in data.products) for p in plants capacity[p.prod_in] += data.max_cap[p] end for r in products, t in T if available[r, t] > capacity[r] @warn "Not enough capacity to process $(r.name) at time $t: $(available[r,t]) > $(capacity[r])" end end # Decision variables # ------------------------------------------------------------------------- @timeit "Model: Add variables" begin @timeit "y" begin y = _init(model, :y) for (q, p, r) in E, c in r.comp, t in T y[q, p, r, c ,t] = @variable(model, lower_bound = 0) end end @timeit "y_total" begin y_total = _init(model, :y_total) for (q, p, r) in E, t in T y_total[q, p, r, t] = @variable(model, lower_bound = 0) end end @timeit "z_center_disp" begin z_center_disp = _init(model, :z_center_disp) for q in centers, r in q.prod_out, c in r.comp, t in T z_center_disp[q, r, c, t] = @variable(model, lower_bound = 0) end end @timeit "z_center_disp_total" begin z_center_disp_total = _init(model, :z_center_disp_total) for q in centers, r in q.prod_out, t in T z_center_disp_total[q, r, t] = @variable(model, lower_bound = 0) end end @timeit "z_store" begin z_store = _init(model, :z_store) for q in centers, r in q.prod_out, c in r.comp, t in T if t == T.stop z_store[q, r, c, t] = 0.0 else z_store[q, r, c, t] = @variable(model, lower_bound = 0) end end end @timeit "z_store_total" begin z_store_total = _init(model, :z_store_total) for q in centers, r in q.prod_out z_store_total[q, r, 0] = 0.0 for t in T if t == T.stop z_store_total[q, r, t] = 0.0 else z_store_total[q, r, t] = @variable(model, lower_bound = 0) end end end end #= @timeit "x_open" begin x_open = _init(model, :x_open) for p in plants x_open[p, 0] = 0 for t in T x_open[p, t] = @variable(model, binary = true) end end end =# @timeit "x_open" begin x_open = _init(model, :x_open) for p in plants if data.m_cap[p]>0 x_open[p,0] = 1 else x_open[p,0] = 0 end for t in T x_open[p, t] = @variable(model, binary = true) end end end @timeit "x_send" begin x_send = _init(model, :x_send) for p in plants, (q, r) in E_out[p], t in T x_send[p, q, r, t] = @variable(model, binary = true) end end @timeit "x_disp" begin x_disp = _init(model, :x_disp) for p in plants, r in p.prod_out, t in T x_disp[p, r, t] = @variable(model, binary = true) end end @timeit "z_prod" begin z_prod = _init(model, :z_prod) for p in plants, r in p.prod_out, c in r.comp, t in T z_prod[p, r, c, t] = @variable(model, lower_bound = 0) end end @timeit "z_plant_disp" begin z_plant_disp = _init(model, :z_plant_disp) for p in plants, r in p.prod_out, c in r.comp, t in T z_plant_disp[p, r, c, t] = @variable(model, lower_bound = 0) end end @timeit "z_tr_emissions" begin z_tr_emissions = _init(model, :z_tr_emissions) for (q, p, r) in E, s in emissions, t in T z_tr_emissions[q, p, r, s, t] = @variable(model, lower_bound = 0) end end @timeit "z_plant_emissions" begin z_plant_emissions = _init(model, :z_plant_emissions) for p in plants, s in emissions, t in T z_plant_emissions[p, s, t] = @variable(model, lower_bound = 0) end end ########################## FOR CAPACITY EXPANSION ############################################################## #Binary variable - if a new plant is built @timeit "x_build" begin x_build = _init(model, :x_build) for p in plants, t in T x_build[p, t] = @variable(model, binary = true) end end #Plant capacity expansion Variable @timeit "z_capExp" begin z_capExp = _init(model, :z_capExp) for p in plants, t in T z_capExp[p, t] = @variable(model, lower_bound = 0, upper_bound = (data.max_cap[p] - data.m_cap[p])) end end #Total plant capacity Variable @timeit "z_capTot" begin z_capTot = _init(model, :z_capTot) for p in plants z_capTot[p, 0] = data.m_cap[p] for t in T z_capTot[p, t] = @variable(model, lower_bound = 0, upper_bound = data.max_cap[p]) end end end ############################################################################################################## end # Objective function # ------------------------------------------------------------------------- @timeit "Model: Objective function" begin obj = AffExpr() # Center disposal @timeit "c_center_disp" begin for q in centers, r in q.prod_out, t in T add_to_expression!( obj, data.c_center_disp[q, r, t], z_center_disp_total[q, r, t], ) end end # Center acquisition @timeit "c_acq" begin for q in centers, r in q.prod_out, c in r.comp, t in T add_to_expression!(obj, data.m_init[q, r, c, t] * data.c_acq[q, r, t]) #+data.c_interv[q,r,t]) end end # Center storage @timeit "c_store" begin for q in centers, r in q.prod_out, t in T add_to_expression!(obj, data.c_store[q, r, t], z_store_total[q, r, t]) end end # Transportation, variable operating cost @timeit "c_tr, c_var" begin for (q, p, r) in E dist = data.m_dist[q, p] #y_qpr = y[q, p, r] for c in r.comp, t in T add_to_expression!(obj, dist * data.c_tr[r, t], y[q, p, r,c, t]) add_to_expression!(obj, data.c_var[p, t], y[q, p, r, c, t]) end end end # Transportation emissions @timeit "c_emission" begin for (q, p, r) in E, s in emissions, t in T add_to_expression!( obj, data.c_emission[s, t], z_tr_emissions[q, p, r, s, t], ) end end # Fixed cost @timeit "c_fix" begin for p in plants, t in T add_to_expression!(obj, data.c_fix[p, t], x_open[p, t]) end end #= # Opening cost @timeit "c_open" begin for p in plants, t in T add_to_expression!(obj, data.c_open[p, t], x_open[p, t] - x_open[p, t-1]) end end =# # Plant disposal @timeit "c_plant_disp" begin for p in plants, r in p.prod_out, c in r.comp, t in T add_to_expression!( obj, data.c_plant_disp[p, r, t], z_plant_disp[p, r, c, t], ) end end # Plant emissions @timeit "c_emission" begin for p in plants, s in emissions, t in T add_to_expression!(obj, data.c_emission[s, t], z_plant_emissions[p, s, t]) end end ######################## FOR CAPACITY EXPANSION #################################################### #Plant opening cost @timeit "c_open" begin for p in plants, t in T add_to_expression!( obj, data.min_OpenC[p], x_build[p, t] ) end end #Plant Expansion cost @timeit "c_exp" begin for p in plants, t in T add_to_expression!( obj, data.unit_open[p], (z_capExp[p, t]) ) end end #Plant additional fixed operating cost due to expansion @timeit "c_fix_exp" begin for p in plants, t in T add_to_expression!( obj, data.unit_OpC[p], sum(z_capExp[p, i] for i = 1:t) ) end end #################################################################################################### @objective(model, Min, obj) end # Constraints # ------------------------------------------------------------------------- @timeit "Model: Constraints" begin @timeit "eq_balance" begin eq_balance = _init(model, :eq_balance) for q in centers, r in q.prod_out, t in T eq_balance[q, r, t] = @constraint( model, sum(y_total[q, p, r2, t] for (p, r2) in E_out[q] if r == r2) + z_store_total[q, r, t] + sum(z_center_disp[q, r, c, t] for c in r.comp) == sum(data.m_init[q, r, c, t] for c in r.comp) + z_store_total[q, r, t-1] ) end end ratio(q, r, c, t) = (data.m_init[q, r, c, t] / sum(data.m_init[q, r, d, t] for d in r.comp)) @timeit "eq_y_total" begin eq_y_total = _init(model, :eq_y_total) for (q, p, r) in E, t in T eq_y_total[q, p, r, t] = @constraint( model, y_total[q, p, r, t] == sum(y[q, p, r, c, t] for c in r.comp) ) end end @timeit "eq_split_y" begin eq_split_y = _init(model, :eq_split_y) for (q, p, r) in E, c in r.comp, t in T if q ∉ centers continue end eq_split_y[q, p, r, c, t] = @constraint( model, y[q, p, r, c, t] == ratio(q, r, c, t) * y_total[q, p, r, t] ) end end @timeit "eq_split_center_disp" begin eq_split_center_disp = _init(model, :eq_split_center_disp) for q in centers, r in q.prod_out, c in r.comp, t in T eq_split_center_disp[q, r, c, t] = @constraint( model, z_center_disp[q, r, c, t] == ratio(q, r, c, t) * z_center_disp_total[q, r, t] ) end end @timeit "eq_split_store" begin eq_split_store = _init(model, :eq_split_store) for q in centers, r in q.prod_out, c in r.comp, t in T eq_split_store[q, r, c, t] = @constraint( model, z_store[q, r, c, t] == ratio(q, r, c, t) * z_store_total[q, r, t] ) end end @timeit "eq_center_disposal" begin eq_center_disposal = _init(model, :eq_center_disposal) for r in products, t in T centers_r = [q for q in centers if r ∈ q.prod_out] if isempty(centers_r) continue end eq_center_disposal[r, t] = @constraint( model, sum(z_center_disp_total[q, r, t] for q in centers_r) <= data.m_center_disp[r, t] ) end end @timeit "eq_center_storage" begin eq_center_storage = _init(model, :eq_center_storage) for q in centers, r in q.prod_out, t in T eq_center_storage[q, r, t] = @constraint(model, z_store_total[q, r, t] <= data.m_store[q, r, t]) end end @timeit "eq_tr_emissions" begin eq_tr_emissions = _init(model, :eq_tr_emissions) for (q, p, r) in E, s in emissions, t in T eq_tr_emissions[q, p, r, s, t] = @constraint( model, z_tr_emissions[q, p, r, s, t] == data.m_dist[q, p] * data.alpha_tr_emission[r, s, t] * y_total[q, p, r, t] ) end end #= @timeit "eq_plant_capacity" begin eq_plant_capacity = _init(model, :eq_plant_capacity) for p in plants, t in T eq_plant_capacity[p, t] = @constraint( model, sum(y_total[q, p, r][t] for (q, r) in E_in[p]) <= data.m_cap[p] * x_open[p, t] ) end end =# @timeit "eq_plant_prod" begin eq_plant_prod = _init(model, :eq_plant_prod) for p in plants, r_out in p.prod_out, c_out in r_out.comp, t in T eq_plant_prod[p, r_out, c_out, t] = @constraint( model, z_prod[p, r_out, c_out, t] == sum( data.alpha_mix[p, r_out, c_in, c_out] * y[q, p, r_in, c_in, t] for (q, r_in) in E_in[p] for c_in in r_in.comp if data.alpha_mix[p, r_out, c_in, c_out] > 0 ) ) end end @timeit "eq_plant_disp" begin eq_plant_disp = _init(model, :eq_plant_disp) for p in plants, r in p.prod_out, t in T eq_plant_disp[p, r, t] = @constraint( model, sum(z_plant_disp[p, r, c, t] for c in r.comp) <= data.m_plant_disp[p, r, t] * x_disp[p, r, t] ) end end @timeit "eq_plant_emissions" begin eq_plant_emissions = _init(model, :eq_plant_emissions) for p in plants, s in emissions, t in T eq_plant_emissions[p, s, t] = @constraint( model, z_plant_emissions[p, s, t] == sum(y_total[q, p, r, t] for (q, r) in E_in[p]) * data.alpha_plant_emission[p, s, t] ) end end @timeit "eq_emissions_limit" begin eq_emissions_limit = _init(model, :eq_emissions_limit) for s in emissions, t in T eq_emissions_limit[s, t] = @constraint( model, sum(z_plant_emissions[p, s, t] for p in plants) + sum(z_tr_emissions[q, p, r, s, t] for (q, p, r) in E) <= data.m_emission[s, t] ) end end @timeit "eq_plant_remains_open" begin eq_plant_remains_open = _init(model, :eq_plant_remains_open) for p in plants, t in T eq_plant_remains_open[p, t] = @constraint(model, x_open[p, t] >= x_open[p, t-1]) end end #= @timeit "eq_plant_single_dest" begin eq_plant_single_dest = _init(model, :eq_plant_single_dest) for p in plants, r in p.prod_out, t in T eq_plant_single_dest[p, r, t] = @constraint( model, sum(x_send[p, q, r, t] for (q, r2) in E_out[p] if r == r2) + x_disp[p, r, t] <= 1 ) end end @timeit "eq_plant_send_limit" begin eq_plant_send_limit = _init(model, :eq_plant_send_limit) for p in plants, (q, r) in E_out[p], t in T eq_plant_send_limit[p, q, r, t] = @constraint( model, y_total[p, q, r][t] <= data.m_cap[q] * x_send[p, q, r, t] ) end end =# @timeit "eq_plant_balance" begin eq_plant_balance = _init(model, :eq_plant_balance) for p in plants, r in p.prod_out, c in r.comp, t in T eq_plant_balance[p, r, c, t] = @constraint( model, z_prod[p, r, c, t] == z_plant_disp[p, r, c, t] + sum(y[p, q, r, c, t] for (q, r2) in E_out[p] if r == r2) ) end end ############################################### FOR CAPACITY EXPANSION ########################################### #Total amount send to a plant should satisfy the capacity constraints of the plant @timeit "eq_plant_capacity" begin eq_plant_capacity = _init(model, :eq_plant_capacity) for p in plants, t in T eq_plant_capacity[p, t] = @constraint( model, sum(y_total[q, p, r, t] for (q, r) in E_in[p]) <= z_capTot[p, t] ) end end # total plant capacity @timeit "eq_plant_totalcapacity" begin eq_plant_totalcapacity = _init(model, :eq_plant_totalcapacity) for p in plants, t in T eq_plant_totalcapacity[p, t] = @constraint( model, z_capTot[p, t] == z_capTot[p, t-1] + z_capExp[p, t] + (data.min_cap[p] * x_build[p, t]) ) end end #Expansion capacity limitation constraints for plants @timeit "eq_plant_Expcapacity" begin eq_plant_ExpCapacity = _init(model, :eq_plant_Expcapacity) for p in plants, t in T eq_plant_ExpCapacity[p, t] = @constraint( model, z_capExp[p, t] <= (data.max_cap[p] - data.m_cap[p]) * x_open[p, t] ) end end #Plant Feasibility constraint - A plant is open iff it was open at t-1or new plant is built at t @timeit "eq_plant_feasibility" begin eq_plant_feasibility = _init(model, :eq_plant_feasibility) for p in plants, t in T eq_plant_feasibility[p, t] = @constraint( model, x_open[p, t] == x_open[p, t-1] + x_build[p, t] ) end end #Plant Initial opening condition Constraints @timeit "eq_plant_opencondition" begin eq_plant_opencondition = _init(model, :eq_plant_opencondition) for p in plants, t in T eq_plant_opencondition[p, t] = @constraint( model, x_build[p, t] <= 1 - x_open[p, t-1] ) end end #Total capacity can only increase or stay the same @timeit "eq_plant_totalcap_continuity" begin eq_plant_totalcap_continuity = _init(model, :eq_plant_totalcap_continuity) for p in plants, t in T #if t>1 eq_plant_totalcap_continuity[p, t] = @constraint( model, z_capTot[p, t] >= z_capTot[p, t-1] ) #end end end ################################################################################################################ end # Optimize # ------------------------------------------------------------------------- _set_names!(model) model_build_time = time() - init_time #write_to_file(model, "model.lp") #return #error("STOP") optimize!(model) stats = OrderedDict{String,Any}( "Plants" => length(plants), "Centers" => length(centers), "Products" => length(products), "Time periods" => length(T), "Model build time (s)" => model_build_time, "Variables" => num_variables(model), "Constraints" => num_constraints(model, count_variable_in_set_constraints=false), "Objective Value" => objective_value(model), "Solve time (s)" => solve_time(model), ) write_to_file(model, "model.lp") #open("model.lp", "w") do f #print(f, model) #end #write_to_file(model, "model.ilp") #= #Add check for composition consistency #----------------------------------------------------------------------------- pdt_ratio = dict((p, r, c, t) => 0.0 for p in plants, r in p.prod_out, c in r.comp, t in data.T) transp_ratio = dict((p, q, r, c, t) => 0.0 for p in plants, (q,r) in E_out[p], c in r.comp, t in T) #compute the proportion of components produced in the plants for p in plants, r in p.prod_out, c in r.comp, t in T pdt_ratio[p,r,c,t] = value(z_prod[p,r,c,t])/sum(value(z_prod[p,r,d,t]) for d in r.comp) end #compute the proportion of components transported from a plant to another plant for p in plants, (q,r) in E_out[p], c in r.comp, t in T transp_ratio[p,q,r,c,t] = y[q,p,r][c,t]/sum(value(y[q,p,r][d,t]) for d in r.comp) end for p in plants, r in p.prod_out, c in r.comp, t in T if (pdt_ratio[p,r,c,t] - transp_ratio[q,p,r,c,t] for (q, r2) in E_out[p] if r == r2) >0.1 @warn "composition inconsistency detected for $r.name transported from $p.name to $q.name at time $t" end end =# if output_dir === nothing return model, stats end # Report: Transportation # ------------------------------------------------------------------------- df = DataFrame() df."source" = String[] df."source longitude (deg)" = Float64[] df."source latitude (deg)" = Float64[] df."destination" = String[] df."destination longitude (deg)" = Float64[] df."destination latitude (deg)" = Float64[] df."product" = String[] df."component" = String[] df."time" = Int[] df."distance (km)" = Float64[] df."amount sent (tonne)" = Float64[] df."transportation cost (\$)" = Float64[] df."variable operating cost (\$)" = Float64[] for (q, p, r) in E, c in r.comp, t in T if value(y[q, p, r, c, t]) ≈ 0 continue end push!( df, [ q.name, q.longitude, q.latitude, p.name, p.longitude, p.latitude, r.name, c.name, t, round(data.m_dist[q, p], digits=2), round(value(y[q, p, r, c, t]), digits=2), round( data.m_dist[q, p] * data.c_tr[r, t] * value(y[q, p, r, c, t]), digits=2, ), round(data.c_var[p, t] * value(y[q, p, r, c, t]), digits=2), ], ) end CSV.write("$output_dir/transp.csv", df) # Report: Centers # ------------------------------------------------------------------------- df = DataFrame() df."center" = String[] df."product" = String[] df."component" = String[] df."time" = Int[] df."amount available (tonne)" = Float64[] df."amount sent (tonne)" = Float64[] df."amount stored (tonne)" = Float64[] df."amount disposed (tonne)" = Float64[] df."acquisition cost (\$)" = Float64[] df."storage cost (\$)" = Float64[] df."disposal cost (\$)" = Float64[] for q in centers, r in q.prod_out, c in r.comp, t in T push!( df, [ q.name, r.name, c.name, t, round(data.m_init[q, r, c, t], digits=2), round( sum(value(y[q, p, r, c, t]) for (p, r2) in E_out[q] if r == r2; init=0), digits=2, ), round(value(z_store[q, r, c, t]), digits=2), round(value(z_center_disp[q, r, c, t]), digits=2), round(data.m_init[q, r, c, t] * data.c_acq[q, r, t], digits=2), round(data.c_store[q, r, t] * value(z_store[q, r, c, t]), digits=2), round( data.c_center_disp[q, r, t] * value(z_center_disp[q, r, c, t]), digits=2, ), ], ) end CSV.write("$output_dir/centers.csv", df) # Report: Plants # ------------------------------------------------------------------------- df = DataFrame() df."plant" = String[] df."Plant latitude (deg)" = Float64[] df."Plant longitude (deg)" = Float64[] df."Maximum Capacity" = Float64[] df."time(year)" = Int[] df."is open?" = Float64[] df."is newly built" = Float64[] df."Initial Capacity" = Float64[] df."Expanded Capacity" = Float64[] df."Total Capacity" = Float64[] df."opening cost (\$)" = Float64[] df."Expansion Cost" = Float64[] df."fixed operating cost (\$)" = Float64[] df."Exp fixed op cost" = Float64[] df."Total fixed operating cost" = Float64[] for p in plants, t in T push!( df, [ p.name, p.latitude, p.longitude, data.max_cap[p], t, value(x_open[p, t]), value(x_build[p, t]), data.m_cap[p], value(z_capExp[p, t]), value(z_capTot[p, t]), #data.c_open[p, t] * (value(x_open[p, t]) - value(x_open[p, t-1])), data.c_open[p, t] * value(x_build[p, t]), data.unit_open[p] * value(z_capExp[p, t]), data.c_fix[p, t] * value(x_open[p, t]), data.unit_OpC[p] * sum(value(z_capExp[p, i]) for i in 1:t), ((data.c_fix[p, t] * value(x_open[p, t])) + (data.unit_OpC[p] * sum(value(z_capExp[p, i]) for i in 1:t))), ] ) end CSV.write("$output_dir/plants.csv", df) # Report: Plant Outputs # ------------------------------------------------------------------------- df = DataFrame() df."plant" = String[] #df."county" = String[] df."Plant Longitude (deg)" = Float64[] df."Plant Latitude (deg)" = Float64[] df."product" = String[] df."component" = String[] df."time" = Int[] df."amount produced (tonne)" = Float64[] df."amount disposed (tonne)" = Float64[] df."amount sent (tonne)" = Float64[] df."disposal cost (\$)" = Float64[] for p in plants, r in p.prod_out, c in r.comp, t in T if value(z_prod[p, r, c, t]) ≈ 0 continue end push!( df, [ p.name, #p.county, p.longitude, p.latitude, r.name, c.name, t, round(value(z_prod[p, r, c, t]), digits=2), round(value(z_plant_disp[p, r, c, t]), digits=2), round( sum( value(y[p, q, r, c, t]) for (q, r2) in E_out[p] if r == r2; init=0.0, ), digits=2, ), round( data.c_plant_disp[p, r, t] * value(z_plant_disp[p, r, c, t]), digits=2, ), ], ) end CSV.write("$output_dir/plant-outputs.csv", df) # Report: Plant Emissions # ------------------------------------------------------------------------- df = DataFrame() df."plant" = String[] df."emission" = String[] df."time" = Int[] df."amount emitted (tonne)" = Float64[] df."emission cost (\$)" = Float64[] for p in plants, s in emissions, t in T push!( df, [ p.name, s.name, t, round(value(z_plant_emissions[p, s, t]), digits=2), round( data.c_emission[s, t] * value(z_plant_emissions[p, s, t]), digits=2, ), ], ) end CSV.write("$output_dir/plant-emissions.csv", df) # Report: Transportation Emissions # ------------------------------------------------------------------------- df = DataFrame() df."source" = String[] df."destination" = String[] df."product" = String[] df."emission" = String[] df."time" = Int[] df."distance (km)" = Float64[] df."amount sent (tonne)" = Float64[] df."amount emitted (tonne)" = Float64[] df."emission cost (\$)" = Float64[] for (q, p, r) in E, s in emissions, t in T if value(y_total[q, p, r, t]) ≈ 0 continue end push!( df, [ q.name, p.name, r.name, s.name, t, round(data.m_dist[q, p], digits=2), round(value(y_total[q, p, r, t]), digits=2), round(value(z_tr_emissions[q, p, r, s, t]), digits=2), round( data.c_emission[s, t] * value(z_tr_emissions[q, p, r, s, t]), digits=2, ), ], ) end CSV.write("$output_dir/transp-emissions.csv", df) return model, stats end