diff --git a/CHANGELOG.md b/CHANGELOG.md index f038847..8855d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ All notable changes to this project will be documented in this file. [semver]: https://semver.org/spec/v2.0.0.html [pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0 +## [Unreleased] + +- Allow product disposal at collection centers + ## [0.5.1] -- 2021-07-23 ## Added - Allow user to specify locations as unique identifiers, instead of latitude and longitude (e.g. `us-state:IL` or `2018-us-county:17043`) diff --git a/Makefile b/Makefile index d151b3a..89178a1 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ JULIA := julia --project=. SRC_FILES := $(wildcard src/*.jl test/*.jl) -VERSION := 0.5 +VERSION := dev all: docs test diff --git a/src/instance/compress.jl b/src/instance/compress.jl index 8051aac..ac6075e 100644 --- a/src/instance/compress.jl +++ b/src/instance/compress.jl @@ -29,6 +29,8 @@ function _compress(instance::Instance)::Instance for (emission_name, emission_value) in p.transportation_emissions p.transportation_emissions[emission_name] = [mean(emission_value)] end + p.disposal_limit = [maximum(p.disposal_limit) * T] + p.disposal_cost = [mean(p.disposal_cost)] end # Compress collection centers @@ -58,3 +60,42 @@ function _compress(instance::Instance)::Instance return compressed end + +function _slice(instance::Instance, T::UnitRange)::Instance + sliced = deepcopy(instance) + sliced.time = length(T) + + for p in sliced.products + p.transportation_cost = p.transportation_cost[T] + p.transportation_energy = p.transportation_energy[T] + for (emission_name, emission_value) in p.transportation_emissions + p.transportation_emissions[emission_name] = emission_value[T] + end + p.disposal_limit = p.disposal_limit[T] + p.disposal_cost = p.disposal_cost[T] + end + + for c in sliced.collection_centers + c.amount = c.amount[T] + end + + for plant in sliced.plants + plant.energy = plant.energy[T] + for (emission_name, emission_value) in plant.emissions + plant.emissions[emission_name] = emission_value[T] + end + for s in plant.sizes + s.variable_operating_cost = s.variable_operating_cost[T] + s.opening_cost = s.opening_cost[T] + s.fixed_operating_cost = s.fixed_operating_cost[T] + end + for (prod_name, disp_limit) in plant.disposal_limit + plant.disposal_limit[prod_name] = disp_limit[T] + end + for (prod_name, disp_cost) in plant.disposal_cost + plant.disposal_cost[prod_name] = disp_cost[T] + end + end + + return sliced +end \ No newline at end of file diff --git a/src/model/build.jl b/src/model/build.jl index 1dec9d2..1acda5f 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -24,11 +24,15 @@ function create_vars!(model::JuMP.Model) (n, t) => @variable( model, lower_bound = 0, - upper_bound = n.location.disposal_limit[n.product][t] + upper_bound = n.location.disposal_limit[n.product][t], ) for n in values(graph.plant_shipping_nodes), t = 1:T ) model[:collection_dispose] = Dict( - (n, t) => @variable(model, lower_bound = 0,) for + (n, t) => @variable( + model, + lower_bound = 0, + upper_bound = n.location.amount[t], + ) for n in values(graph.collection_shipping_nodes), t = 1:T ) model[:store] = Dict( @@ -90,7 +94,7 @@ function create_objective_function!(model::JuMP.Model) # Process node costs for n in values(graph.process_nodes), t = 1:T - # Transportation and variable operating costs + # Transportation costs for a in n.incoming_arcs c = n.location.input.transportation_cost[t] * a.values["distance"] add_to_expression!(obj, c, model[:flow][a, t]) @@ -137,7 +141,6 @@ function create_objective_function!(model::JuMP.Model) # Plant shipping node costs for n in values(graph.plant_shipping_nodes), t = 1:T - # Disposal costs add_to_expression!( obj, @@ -148,7 +151,6 @@ function create_objective_function!(model::JuMP.Model) # Collection shipping node costs for n in values(graph.collection_shipping_nodes), t = 1:T - # Disposal costs add_to_expression!( obj, @@ -170,7 +172,7 @@ function create_shipping_node_constraints!(model::JuMP.Model) model[:eq_balance][n, t] = @constraint( model, sum(model[:flow][a, t] for a in n.outgoing_arcs) == - n.location.amount[t] + model[:collection_dispose][n, t] + n.location.amount[t] - model[:collection_dispose][n, t], ) end for prod in model[:instance].products diff --git a/src/model/solve.jl b/src/model/solve.jl index d517c12..12fb30d 100644 --- a/src/model/solve.jl +++ b/src/model/solve.jl @@ -29,29 +29,32 @@ function solve( instance::Instance; optimizer = nothing, output = nothing, + graph = nothing, marginal_costs = true, return_model = false, ) milp_optimizer = lp_optimizer = optimizer - if optimizer == nothing + if optimizer === nothing milp_optimizer = _get_default_milp_optimizer() lp_optimizer = _get_default_lp_optimizer() end - @info "Building graph..." - graph = RELOG.build_graph(instance) - _print_graph_stats(instance, graph) + if graph === nothing + @info "Building graph..." + graph = RELOG.build_graph(instance) + _print_graph_stats(instance, graph) + end @info "Building optimization model..." model = RELOG.build_model(instance, graph, milp_optimizer) @info "Optimizing MILP..." JuMP.optimize!(model) - if !has_values(model) error("No solution available") end + solution = get_solution(model, marginal_costs = false) if marginal_costs @info "Re-optimizing with integer variables fixed..." @@ -65,12 +68,15 @@ function solve( end end JuMP.optimize!(model) + if has_values(model) + @info "Extracting solution..." + solution = get_solution(model, marginal_costs = true) + else + @warn "Error computing marginal costs. Ignoring." + end end - @info "Extracting solution..." - solution = get_solution(model, marginal_costs = marginal_costs) - - if output != nothing + if output !== nothing write(solution, output) end @@ -87,7 +93,7 @@ function solve(filename::AbstractString; heuristic = false, kwargs...) if heuristic && instance.time > 1 @info "Solving single-period version..." compressed = _compress(instance) - csol = solve(compressed; output = nothing, marginal_costs = false, kwargs...) + csol, model = solve(compressed; output = nothing, marginal_costs = false, return_model = true, kwargs...) @info "Filtering candidate locations..." selected_pairs = [] for (plant_name, plant_dict) in csol["Plants"]