diff --git a/instances/s1.json b/instances/s1.json index e88e8c5..3cbee2d 100644 --- a/instances/s1.json +++ b/instances/s1.json @@ -4,73 +4,132 @@ }, "products": { "P1": { - "transportation cost ($/km/tonne)": [0.015, 0.015], - "transportation energy (J/km/tonne)": [0.12, 0.11], + "transportation cost ($/km/tonne)": [ + 0.015, + 0.015 + ], + "transportation energy (J/km/tonne)": [ + 0.12, + 0.11 + ], "transportation emissions (tonne/km/tonne)": { - "CO2": [0.052, 0.050], - "CH4": [0.003, 0.002] - }, + "CO2": [ + 0.052, + 0.050 + ], + "CH4": [ + 0.003, + 0.002 + ] + }, "initial amounts": { "C1": { "latitude (deg)": 7.0, "longitude (deg)": 7.0, - "amount (tonne)": [934.56, 934.56] + "amount (tonne)": [ + 934.56, + 934.56 + ] }, "C2": { "latitude (deg)": 7.0, "longitude (deg)": 19.0, - "amount (tonne)": [198.95, 198.95] + "amount (tonne)": [ + 198.95, + 198.95 + ] }, "C3": { "latitude (deg)": 84.0, "longitude (deg)": 76.0, - "amount (tonne)": [212.97, 212.97] + "amount (tonne)": [ + 212.97, + 212.97 + ] }, "C4": { "latitude (deg)": 21.0, "longitude (deg)": 16.0, - "amount (tonne)": [352.19, 352.19] + "amount (tonne)": [ + 352.19, + 352.19 + ] }, "C5": { "latitude (deg)": 32.0, "longitude (deg)": 92.0, - "amount (tonne)": [510.33, 510.33] + "amount (tonne)": [ + 510.33, + 510.33 + ] }, "C6": { "latitude (deg)": 14.0, "longitude (deg)": 62.0, - "amount (tonne)": [471.66, 471.66] + "amount (tonne)": [ + 471.66, + 471.66 + ] }, "C7": { "latitude (deg)": 30.0, "longitude (deg)": 83.0, - "amount (tonne)": [785.21, 785.21] + "amount (tonne)": [ + 785.21, + 785.21 + ] }, "C8": { "latitude (deg)": 35.0, "longitude (deg)": 40.0, - "amount (tonne)": [706.17, 706.17] + "amount (tonne)": [ + 706.17, + 706.17 + ] }, "C9": { "latitude (deg)": 74.0, "longitude (deg)": 52.0, - "amount (tonne)": [30.08, 30.08] + "amount (tonne)": [ + 30.08, + 30.08 + ] }, "C10": { "latitude (deg)": 22.0, "longitude (deg)": 54.0, - "amount (tonne)": [536.52, 536.52] + "amount (tonne)": [ + 536.52, + 536.52 + ] } - } + }, + "disposal limit (tonne)": [ + 1.0, + 1.0 + ], + "disposal cost ($/tonne)": [ + -1000, + -1000 + ] }, "P2": { - "transportation cost ($/km/tonne)": [0.02, 0.02] + "transportation cost ($/km/tonne)": [ + 0.02, + 0.02 + ] }, "P3": { - "transportation cost ($/km/tonne)": [0.0125, 0.0125] + "transportation cost ($/km/tonne)": [ + 0.0125, + 0.0125 + ] }, "P4": { - "transportation cost ($/km/tonne)": [0.0175, 0.0175] + "transportation cost ($/km/tonne)": [ + 0.0175, + 0.0175 + ] } }, "plants": { @@ -80,35 +139,74 @@ "P2": 0.2, "P3": 0.5 }, - "energy (GJ/tonne)": [0.12, 0.11], + "energy (GJ/tonne)": [ + 0.12, + 0.11 + ], "emissions (tonne/tonne)": { - "CO2": [0.052, 0.050], - "CH4": [0.003, 0.002] - }, + "CO2": [ + 0.052, + 0.050 + ], + "CH4": [ + 0.003, + 0.002 + ] + }, "locations": { "L1": { "latitude (deg)": 0.0, "longitude (deg)": 0.0, "disposal": { "P2": { - "cost ($/tonne)": [-10.0, -10.0], - "limit (tonne)": [1.0, 1.0] + "cost ($/tonne)": [ + -10.0, + -10.0 + ], + "limit (tonne)": [ + 1.0, + 1.0 + ] }, "P3": { - "cost ($/tonne)": [-10.0, -10.0], - "limit (tonne)": [1.0, 1.0] + "cost ($/tonne)": [ + -10.0, + -10.0 + ], + "limit (tonne)": [ + 1.0, + 1.0 + ] } }, "capacities (tonne)": { "250.0": { - "opening cost ($)": [500.0, 500.0], - "fixed operating cost ($)": [30.0, 30.0], - "variable operating cost ($/tonne)": [30.0, 30.0] + "opening cost ($)": [ + 500.0, + 500.0 + ], + "fixed operating cost ($)": [ + 30.0, + 30.0 + ], + "variable operating cost ($/tonne)": [ + 30.0, + 30.0 + ] }, "1000.0": { - "opening cost ($)": [1250.0, 1250.0], - "fixed operating cost ($)": [30.0, 30.0], - "variable operating cost ($/tonne)": [30.0, 30.0] + "opening cost ($)": [ + 1250.0, + 1250.0 + ], + "fixed operating cost ($)": [ + 30.0, + 30.0 + ], + "variable operating cost ($/tonne)": [ + 30.0, + 30.0 + ] } } }, @@ -117,17 +215,35 @@ "longitude (deg)": 0.5, "capacities (tonne)": { "0.0": { - "opening cost ($)": [1000, 1000], - "fixed operating cost ($)": [50.0, 50.0], - "variable operating cost ($/tonne)": [50.0, 50.0] + "opening cost ($)": [ + 1000, + 1000 + ], + "fixed operating cost ($)": [ + 50.0, + 50.0 + ], + "variable operating cost ($/tonne)": [ + 50.0, + 50.0 + ] }, "10000.0": { - "opening cost ($)": [10000, 10000], - "fixed operating cost ($)": [50.0, 50.0], - "variable operating cost ($/tonne)": [50.0, 50.0] + "opening cost ($)": [ + 10000, + 10000 + ], + "fixed operating cost ($)": [ + 50.0, + 50.0 + ], + "variable operating cost ($/tonne)": [ + 50.0, + 50.0 + ] } } - } + } } }, "F2": { @@ -142,14 +258,26 @@ "longitude (deg)": 65.0, "disposal": { "P3": { - "cost ($/tonne)": [100.0, 100.0] + "cost ($/tonne)": [ + 100.0, + 100.0 + ] } }, "capacities (tonne)": { "1000.0": { - "opening cost ($)": [3000, 3000], - "fixed operating cost ($)": [50.0, 50.0], - "variable operating cost ($/tonne)": [50.0, 50.0] + "opening cost ($)": [ + 3000, + 3000 + ], + "fixed operating cost ($)": [ + 50.0, + 50.0 + ], + "variable operating cost ($/tonne)": [ + 50.0, + 50.0 + ] } } }, @@ -158,9 +286,18 @@ "longitude (deg)": 0.20, "capacities (tonne)": { "10000": { - "opening cost ($)": [3000, 3000], - "fixed operating cost ($)": [50.0, 50.0], - "variable operating cost ($/tonne)": [50.0, 50.0] + "opening cost ($)": [ + 3000, + 3000 + ], + "fixed operating cost ($)": [ + 50.0, + 50.0 + ], + "variable operating cost ($/tonne)": [ + 50.0, + 50.0 + ] } } } @@ -174,12 +311,21 @@ "longitude (deg)": 100.0, "capacities (tonne)": { "15000": { - "opening cost ($)": [0.0, 0.0], - "fixed operating cost ($)": [0.0, 0.0], - "variable operating cost ($/tonne)": [-15.0, -15.0] + "opening cost ($)": [ + 0.0, + 0.0 + ], + "fixed operating cost ($)": [ + 0.0, + 0.0 + ], + "variable operating cost ($/tonne)": [ + -15.0, + -15.0 + ] } } - } + } } }, "F4": { @@ -190,12 +336,21 @@ "longitude (deg)": 50.0, "capacities (tonne)": { "10000": { - "opening cost ($)": [0.0, 0.0], - "fixed operating cost ($)": [0.0, 0.0], - "variable operating cost ($/tonne)": [-15.0, -15.0] + "opening cost ($)": [ + 0.0, + 0.0 + ], + "fixed operating cost ($)": [ + 0.0, + 0.0 + ], + "variable operating cost ($/tonne)": [ + -15.0, + -15.0 + ] } } - } + } } } } diff --git a/src/docs/format.md b/src/docs/format.md index cef0537..45d4dd2 100644 --- a/src/docs/format.md +++ b/src/docs/format.md @@ -36,6 +36,8 @@ The **products** section describes all products and subproducts in the simulatio |`transportation energy (J/km/tonne)` | The energy required to transport this product. Must be a time series. Optional. |`transportation emissions (tonne/km/tonne)` | A dictionary mapping the name of each greenhouse gas, produced to transport one tonne of this product along one kilometer, to the amount of gas produced (in tonnes). Must be a time series. Optional. |`initial amounts` | A dictionary mapping the name of each location to its description (see below). If this product is not initially available, this key may be omitted. Must be a time series. +| `disposal limit (tonne)` | Total amount of product that can be disposed of across all collection centers. If omitted, all product must be processed. This parameter has no effect on product disposal at plants. +| `disposal cost ($/tonne)` | Cost of disposing one tonne of this product at a collection center. If omitted, defaults to zero. This parameter has no effect on product disposal costs at plants. Each product may have some amount available at the beginning of each time period. In this case, the key `initial amounts` maps to a dictionary with the following keys: @@ -73,7 +75,9 @@ Each product may have some amount available at the beginning of each time period "transportation emissions (tonne/km/tonne)": { "CO2": [0.052, 0.050], "CH4": [0.003, 0.002] - } + }, + "disposal cost ($/tonne)": [-10.0, -12.0], + "disposal limit (tonne)": [1.0, 1.0], }, "P2": { "transportation cost ($/km/tonne)": [0.022, 0.020] diff --git a/src/docs/reports.md b/src/docs/reports.md index 8f7090d..2b28490 100644 --- a/src/docs/reports.md +++ b/src/docs/reports.md @@ -147,6 +147,7 @@ Report showing primary product amounts, locations and marginal costs. Generated | `longitude (deg)` | Longitude of the collection center. | `year` | What year this row corresponds to. This reports includes one row for each year. | `amount (tonne)` | Amount of product available at this collection center. +| `amount disposed (tonne)` | Amount of product disposed of at this collection center. | `marginal cost ($/tonne)` | Cost to process one additional tonne of this product coming from this collection center. diff --git a/src/graph/build.jl b/src/graph/build.jl index ee28413..27a323e 100644 --- a/src/graph/build.jl +++ b/src/graph/build.jl @@ -18,6 +18,7 @@ function build_graph(instance::Instance)::Graph collection_shipping_nodes = ShippingNode[] name_to_process_node_map = Dict{Tuple{AbstractString,AbstractString},ProcessNode}() + collection_center_to_node = Dict() process_nodes_by_input_product = Dict(product => ProcessNode[] for product in instance.products) @@ -27,6 +28,7 @@ function build_graph(instance::Instance)::Graph for center in instance.collection_centers node = ShippingNode(next_index, center, center.product, [], []) next_index += 1 + collection_center_to_node[center] = node push!(collection_shipping_nodes, node) end @@ -83,6 +85,7 @@ function build_graph(instance::Instance)::Graph collection_shipping_nodes, arcs, name_to_process_node_map, + collection_center_to_node, ) end diff --git a/src/graph/structs.jl b/src/graph/structs.jl index ba81527..e4f1149 100644 --- a/src/graph/structs.jl +++ b/src/graph/structs.jl @@ -33,6 +33,7 @@ mutable struct Graph collection_shipping_nodes::Vector{ShippingNode} arcs::Vector{Arc} name_to_process_node_map::Dict{Tuple{AbstractString,AbstractString},ProcessNode} + collection_center_to_node::Dict{CollectionCenter,ShippingNode} end function Base.show(io::IO, instance::Graph) diff --git a/src/instance/parse.jl b/src/instance/parse.jl index 5720393..c87faa8 100644 --- a/src/instance/parse.jl +++ b/src/instance/parse.jl @@ -37,6 +37,8 @@ function parse(json)::Instance cost = product_dict["transportation cost (\$/km/tonne)"] energy = zeros(T) emissions = Dict() + disposal_limit = zeros(T) + disposal_cost = zeros(T) if "transportation energy (J/km/tonne)" in keys(product_dict) energy = product_dict["transportation energy (J/km/tonne)"] @@ -46,7 +48,25 @@ function parse(json)::Instance emissions = product_dict["transportation emissions (tonne/km/tonne)"] end - product = Product(product_name, cost, energy, emissions) + if "disposal limit (tonne)" in keys(product_dict) + disposal_limit = product_dict["disposal limit (tonne)"] + end + + if "disposal cost (\$/tonne)" in keys(product_dict) + disposal_cost = product_dict["disposal cost (\$/tonne)"] + end + + prod_centers = [] + + product = Product( + product_name, + cost, + energy, + emissions, + disposal_limit, + disposal_cost, + prod_centers, + ) push!(products, product) prod_name_to_product[product_name] = product @@ -66,6 +86,7 @@ function parse(json)::Instance product, center_dict["amount (tonne)"], ) + push!(prod_centers, center) push!(collection_centers, center) end end diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 190ae9b..d42ace8 100644 --- a/src/instance/structs.jl +++ b/src/instance/structs.jl @@ -13,6 +13,9 @@ mutable struct Product transportation_cost::Vector{Float64} transportation_energy::Vector{Float64} transportation_emissions::Dict{String,Vector{Float64}} + disposal_limit::Vector{Float64} + disposal_cost::Vector{Float64} + collection_centers::Vector end mutable struct CollectionCenter diff --git a/src/model/build.jl b/src/model/build.jl index 48ae7c0..1dec9d2 100644 --- a/src/model/build.jl +++ b/src/model/build.jl @@ -20,13 +20,17 @@ function create_vars!(model::JuMP.Model) graph, T = model[:graph], model[:instance].time model[:flow] = Dict((a, t) => @variable(model, lower_bound = 0) for a in graph.arcs, t = 1:T) - model[:dispose] = Dict( + model[:plant_dispose] = Dict( (n, t) => @variable( model, lower_bound = 0, 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 in values(graph.collection_shipping_nodes), t = 1:T + ) model[:store] = Dict( (n, t) => @variable(model, lower_bound = 0, upper_bound = n.location.storage_limit) @@ -131,14 +135,25 @@ function create_objective_function!(model::JuMP.Model) end end - # Shipping node costs + # Plant shipping node costs for n in values(graph.plant_shipping_nodes), t = 1:T # Disposal costs add_to_expression!( obj, n.location.disposal_cost[n.product][t], - model[:dispose][n, t], + model[:plant_dispose][n, t], + ) + end + + # Collection shipping node costs + for n in values(graph.collection_shipping_nodes), t = 1:T + + # Disposal costs + add_to_expression!( + obj, + n.location.product.disposal_cost[t], + model[:collection_dispose][n, t], ) end @@ -154,16 +169,29 @@ function create_shipping_node_constraints!(model::JuMP.Model) for n in graph.collection_shipping_nodes model[:eq_balance][n, t] = @constraint( model, - sum(model[:flow][a, t] for a in n.outgoing_arcs) == n.location.amount[t] + sum(model[:flow][a, t] for a in n.outgoing_arcs) == + n.location.amount[t] + model[:collection_dispose][n, t] ) end + for prod in model[:instance].products + if isempty(prod.collection_centers) + continue + end + expr = AffExpr() + for center in prod.collection_centers + n = graph.collection_center_to_node[center] + add_to_expression!(expr, model[:collection_dispose][n, t]) + end + @constraint(model, expr <= prod.disposal_limit[t]) + end # Plants for n in graph.plant_shipping_nodes @constraint( model, sum(model[:flow][a, t] for a in n.incoming_arcs) == - sum(model[:flow][a, t] for a in n.outgoing_arcs) + model[:dispose][n, t] + sum(model[:flow][a, t] for a in n.outgoing_arcs) + + model[:plant_dispose][n, t] ) end end diff --git a/src/model/getsol.jl b/src/model/getsol.jl index a368c47..f5f3e91 100644 --- a/src/model/getsol.jl +++ b/src/model/getsol.jl @@ -39,21 +39,24 @@ function get_solution(model::JuMP.Model; marginal_costs = true) 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(model[:eq_balance][n, t])), digits = 2) for t = 1:T - ], - "Latitude (deg)" => n.location.latitude, - "Longitude (deg)" => n.location.longitude, - "Amount (tonne)" => n.location.amount, - ) - if n.product.name ∉ keys(output["Products"]) - output["Products"][n.product.name] = OrderedDict() - end - output["Products"][n.product.name][n.location.name] = location_dict + for n in graph.collection_shipping_nodes + location_dict = OrderedDict{Any,Any}( + "Latitude (deg)" => n.location.latitude, + "Longitude (deg)" => n.location.longitude, + "Amount (tonne)" => n.location.amount, + "Dispose (tonne)" => + [JuMP.value(model[:collection_dispose][n, t]) for t = 1:T], + ) + if marginal_costs + location_dict["Marginal cost (\$/tonne)"] = [ + round(abs(JuMP.shadow_price(model[:eq_balance][n, t])), digits = 2) for + t = 1:T + ] + end + 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 # Plants @@ -178,13 +181,14 @@ function get_solution(model::JuMP.Model; marginal_costs = true) plant_dict["Total output"][product_name] = zeros(T) plant_dict["Output"]["Send"][product_name] = product_dict = OrderedDict() - disposal_amount = [JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T] + disposal_amount = + [JuMP.value(model[:plant_dispose][shipping_node, t]) for t = 1:T] if sum(disposal_amount) > 1e-5 skip_plant = false plant_dict["Output"]["Dispose"][product_name] = disposal_dict = OrderedDict() disposal_dict["Amount (tonne)"] = - [JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T] + [JuMP.value(model[:plant_dispose][shipping_node, t]) for t = 1:T] disposal_dict["Cost (\$)"] = [ disposal_dict["Amount (tonne)"][t] * plant.disposal_cost[shipping_node.product][t] for t = 1:T diff --git a/src/reports/products.jl b/src/reports/products.jl index 0c5f322..1d99b2c 100644 --- a/src/reports/products.jl +++ b/src/reports/products.jl @@ -13,6 +13,7 @@ function products_report(solution; marginal_costs = true)::DataFrame df."longitude (deg)" = Float64[] df."year" = Int[] df."amount (tonne)" = Float64[] + df."amount disposed (tonne)" = Float64[] df."marginal cost (\$/tonne)" = Float64[] T = length(solution["Energy"]["Plants (GJ)"]) for (prod_name, prod_dict) in solution["Products"] @@ -22,6 +23,7 @@ function products_report(solution; marginal_costs = true)::DataFrame latitude = round(location_dict["Latitude (deg)"], digits = 6) longitude = round(location_dict["Longitude (deg)"], digits = 6) amount = location_dict["Amount (tonne)"][year] + amount_disposed = location_dict["Dispose (tonne)"][year] push!( df, [ @@ -32,6 +34,7 @@ function products_report(solution; marginal_costs = true)::DataFrame year, amount, marginal_cost, + amount_disposed, ], ) end diff --git a/src/schemas/input.json b/src/schemas/input.json index fd46ec0..6ab7094 100644 --- a/src/schemas/input.json +++ b/src/schemas/input.json @@ -169,6 +169,12 @@ }, "initial amounts": { "$ref": "#/definitions/InitialAmount" + }, + "disposal limit (tonne)": { + "$ref": "#/definitions/TimeSeries" + }, + "disposal cost ($/tonne)": { + "$ref": "#/definitions/TimeSeries" } }, "required": [ diff --git a/src/sysimage.jl b/src/sysimage.jl index 8cdeecf..cb27eb0 100644 --- a/src/sysimage.jl +++ b/src/sysimage.jl @@ -6,23 +6,23 @@ Logging.disable_logging(Logging.Info) mkpath("build") -printstyled("Generating precompilation statements...\n", color=:light_green) +printstyled("Generating precompilation statements...\n", color = :light_green) run(`julia --project=. --trace-compile=build/precompile.jl $ARGS`) -printstyled("Finding dependencies...\n", color=:light_green) +printstyled("Finding dependencies...\n", color = :light_green) project = TOML.parsefile("Project.toml") manifest = TOML.parsefile("Manifest.toml") deps = Symbol[] for dep in keys(project["deps"]) if "path" in keys(manifest[dep][1]) - printstyled(" skip $(dep)\n", color=:light_black) + printstyled(" skip $(dep)\n", color = :light_black) else println(" add $(dep)") push!(deps, Symbol(dep)) end end -printstyled("Building system image...\n", color=:light_green) +printstyled("Building system image...\n", color = :light_green) create_sysimage( deps, precompile_statements_file = "build/precompile.jl", diff --git a/test/instance/parse_test.jl b/test/instance/parse_test.jl index 8c29914..f4d0527 100644 --- a/test/instance/parse_test.jl +++ b/test/instance/parse_test.jl @@ -40,7 +40,14 @@ using RELOG @test plant.sizes[2].fixed_operating_cost == [30, 30] @test plant.sizes[2].variable_operating_cost == [30, 30] + p1 = product_name_to_product["P1"] + @test p1.disposal_limit == [1.0, 1.0] + @test p1.disposal_cost == [-1000.0, -1000.0] + p2 = product_name_to_product["P2"] + @test p2.disposal_limit == [0.0, 0.0] + @test p2.disposal_cost == [0.0, 0.0] + p3 = product_name_to_product["P3"] @test length(plant.output) == 2 @test plant.output[p2] == 0.2 diff --git a/test/model/build_test.jl b/test/model/build_test.jl index 27d0e7f..5dc554a 100644 --- a/test/model/build_test.jl +++ b/test/model/build_test.jl @@ -18,7 +18,7 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats ) @test length(model[:flow]) == 76 - @test length(model[:dispose]) == 16 + @test length(model[:plant_dispose]) == 16 @test length(model[:open_plant]) == 12 @test length(model[:capacity]) == 12 @test length(model[:expansion]) == 12 @@ -32,7 +32,7 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats @test lower_bound(v) == 0.0 @test upper_bound(v) == 750.0 - v = model[:dispose][shipping_node_by_loc_and_prod_names["L1", "P2"], 1] + v = model[:plant_dispose][shipping_node_by_loc_and_prod_names["L1", "P2"], 1] @test lower_bound(v) == 0.0 @test upper_bound(v) == 1.0 end diff --git a/test/model/solve_test.jl b/test/model/solve_test.jl index d3b3d0d..0f03bd4 100644 --- a/test/model/solve_test.jl +++ b/test/model/solve_test.jl @@ -26,6 +26,15 @@ basedir = dirname(@__FILE__) @test "F2" in keys(solution["Plants"]) @test "F3" in keys(solution["Plants"]) @test "F4" in keys(solution["Plants"]) + + @test "Products" in keys(solution) + @test "P1" in keys(solution["Products"]) + @test "C1" in keys(solution["Products"]["P1"]) + @test "Dispose (tonne)" in keys(solution["Products"]["P1"]["C1"]) + + total_disposal = + sum([loc["Dispose (tonne)"] for loc in values(solution["Products"]["P1"])]) + @test total_disposal == [1.0, 1.0] end @testset "solve (heuristic)" begin