diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b71acf6..327c0ef 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,10 +14,10 @@ jobs: shell: julia --color=yes {0} run: | using Pkg - Pkg.add(PackageSpec(name="JuliaFormatter", version="0.14.4")) + Pkg.add(PackageSpec(name="JuliaFormatter", version="1")) using JuliaFormatter format("src", verbose=true) - format("test", verbose=true) + format("test/src", verbose=true) out = String(read(Cmd(`git diff`))) if isempty(out) exit(0) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5aa8f86..e2c31cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,5 +21,15 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 + - name: Run tests + shell: julia --color=yes --project=test {0} + run: | + using Pkg + Pkg.develop(path=".") + Pkg.update() + using RELOGT + try + runtests() + catch + exit(1) + end diff --git a/CHANGELOG.md b/CHANGELOG.md index f3776ff..a257a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ 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 +## [0.6.0] -- 2022-12-15 +### Added +- Allow RELOG to calculate approximate driving distances, instead of just straight-line distances between points. + +### Fixed +- Fix bug that caused building period parameter to be ignored + ## [0.5.2] -- 2022-08-26 ### Changed - Update to JuMP 1.x diff --git a/Makefile b/Makefile index 96072d9..0239ef3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION := 0.5 +VERSION := 0.6 PKG := ghcr.io/anl-ceeesa/relog-web clean: diff --git a/Project.toml b/Project.toml index 7bf1664..6556fcc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RELOG" uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008" authors = ["Alinson S Xavier "] -version = "0.5.2" +version = "0.6.0" [deps] CRC = "44b605c4-b955-5f2b-9b6d-d2bd01d3d205" @@ -19,7 +19,9 @@ JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -40,6 +42,7 @@ JSON = "0.21" JSONSchema = "1" JuMP = "1" MathOptInterface = "1" +NearestNeighbors = "0.4" OrderedCollections = "1" ProgressBars = "1" Shapefile = "0.8" diff --git a/README.md b/README.md index 0ab3362..a581688 100644 --- a/README.md +++ b/README.md @@ -13,22 +13,24 @@ **RELOG** is a supply chain optimization package focusing on reverse logistics and reverse manufacturing. For example, the package can be used to determine where to build recycling plants, what sizes should they have and which customers should be served by which plants. The package supports customized reverse logistics pipelines, with multiple types of plants, multiple types of product and multiple time periods. - + + + ### Documentation -- [Usage](https://anl-ceeesa.github.io/RELOG/0.5/usage) -- [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.5/format) -- [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.5/reports) -- [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.5/model) + * [Usage](https://anl-ceeesa.github.io/RELOG/0.6/usage) + * [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.6/format) + * [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.6/reports) + * [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.6/model) ### Authors -- **Alinson S. Xavier** <> -- **Nwike Iloeje** <> -- **John Atkins** -- **Kyle Sun** -- **Audrey Gallier** +* **Alinson S. Xavier** <> +* **Nwike Iloeje** <> +* **John Atkins** +* **Kyle Sun** +* **Audrey Gallier** ### License diff --git a/deps/formatter/Project.toml b/deps/formatter/Project.toml deleted file mode 100644 index 4bc5f25..0000000 --- a/deps/formatter/Project.toml +++ /dev/null @@ -1,5 +0,0 @@ -[deps] -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" - -[compat] -JuliaFormatter = "0.14.4" diff --git a/deps/formatter/format.jl b/deps/formatter/format.jl deleted file mode 100644 index 9f9d965..0000000 --- a/deps/formatter/format.jl +++ /dev/null @@ -1,8 +0,0 @@ -using JuliaFormatter -format( - [ - "../../src", - "../../test", - ], - verbose=true, -) diff --git a/docs/src/format.md b/docs/src/format.md index 01fdc7d..af55391 100644 --- a/docs/src/format.md +++ b/docs/src/format.md @@ -14,6 +14,7 @@ The **parameters** section describes details about the simulation itself. |:--------------------------|:---------------| |`time horizon (years)` | Number of years in the simulation. |`building period (years)` | List of years in which we are allowed to open new plants. For example, if this parameter is set to `[1,2,3]`, we can only open plants during the first three years. By default, this equals `[1]`; that is, plants can only be opened during the first year. | +|`distance metric` | Metric used to compute distances between pairs of locations. Valid options are: `"Euclidean"`, for the straight-line distance between points; or `"driving"` for an approximated driving distance. If not specified, defaults to `"Euclidean"`. #### Example @@ -21,7 +22,8 @@ The **parameters** section describes details about the simulation itself. { "parameters": { "time horizon (years)": 2, - "building period (years)": [1] + "building period (years)": [1], + "distance metric": "driving", } } ``` @@ -36,6 +38,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 +77,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] @@ -220,6 +226,7 @@ Database | Description | Examples * Plants can be expanded at any time, even long after they are open. * All material available at the beginning of a time period must be entirely processed by the end of that time period. It is not possible to store unprocessed materials from one time period to the next. * Up to two plant sizes are currently supported. Variable operating costs must be the same for all plant sizes. +* Accurate driving distances are only available for the continental United States. ## Output Data Format (JSON) diff --git a/docs/src/reports.md b/docs/src/reports.md index 4cda24d..85a902e 100644 --- a/docs/src/reports.md +++ b/docs/src/reports.md @@ -154,6 +154,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/docs/src/usage.md b/docs/src/usage.md index 82d7edd..19cea94 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -7,13 +7,7 @@ To use RELOG, the first step is to install the [Julia programming language](http ```julia using Pkg -Pkg.add(name="RELOG", version="0.5") -``` - -After the package and all its dependencies have been installed, please run the RELOG test suite, as shown below, to make sure that the package has been correctly installed: - -```julia -Pkg.test("RELOG") +Pkg.add(name="RELOG", version="0.6") ``` ## 2. Modeling the problem diff --git a/instances/s1.json b/instances/s1.json deleted file mode 100644 index e88e8c5..0000000 --- a/instances/s1.json +++ /dev/null @@ -1,202 +0,0 @@ -{ - "parameters": { - "time horizon (years)": 2 - }, - "products": { - "P1": { - "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] - }, - "initial amounts": { - "C1": { - "latitude (deg)": 7.0, - "longitude (deg)": 7.0, - "amount (tonne)": [934.56, 934.56] - }, - "C2": { - "latitude (deg)": 7.0, - "longitude (deg)": 19.0, - "amount (tonne)": [198.95, 198.95] - }, - "C3": { - "latitude (deg)": 84.0, - "longitude (deg)": 76.0, - "amount (tonne)": [212.97, 212.97] - }, - "C4": { - "latitude (deg)": 21.0, - "longitude (deg)": 16.0, - "amount (tonne)": [352.19, 352.19] - }, - "C5": { - "latitude (deg)": 32.0, - "longitude (deg)": 92.0, - "amount (tonne)": [510.33, 510.33] - }, - "C6": { - "latitude (deg)": 14.0, - "longitude (deg)": 62.0, - "amount (tonne)": [471.66, 471.66] - }, - "C7": { - "latitude (deg)": 30.0, - "longitude (deg)": 83.0, - "amount (tonne)": [785.21, 785.21] - }, - "C8": { - "latitude (deg)": 35.0, - "longitude (deg)": 40.0, - "amount (tonne)": [706.17, 706.17] - }, - "C9": { - "latitude (deg)": 74.0, - "longitude (deg)": 52.0, - "amount (tonne)": [30.08, 30.08] - }, - "C10": { - "latitude (deg)": 22.0, - "longitude (deg)": 54.0, - "amount (tonne)": [536.52, 536.52] - } - } - }, - "P2": { - "transportation cost ($/km/tonne)": [0.02, 0.02] - }, - "P3": { - "transportation cost ($/km/tonne)": [0.0125, 0.0125] - }, - "P4": { - "transportation cost ($/km/tonne)": [0.0175, 0.0175] - } - }, - "plants": { - "F1": { - "input": "P1", - "outputs (tonne/tonne)": { - "P2": 0.2, - "P3": 0.5 - }, - "energy (GJ/tonne)": [0.12, 0.11], - "emissions (tonne/tonne)": { - "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] - }, - "P3": { - "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] - }, - "1000.0": { - "opening cost ($)": [1250.0, 1250.0], - "fixed operating cost ($)": [30.0, 30.0], - "variable operating cost ($/tonne)": [30.0, 30.0] - } - } - }, - "L2": { - "latitude (deg)": 0.5, - "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] - }, - "10000.0": { - "opening cost ($)": [10000, 10000], - "fixed operating cost ($)": [50.0, 50.0], - "variable operating cost ($/tonne)": [50.0, 50.0] - } - } - } - } - }, - "F2": { - "input": "P2", - "outputs (tonne/tonne)": { - "P3": 0.05, - "P4": 0.80 - }, - "locations": { - "L3": { - "latitude (deg)": 25.0, - "longitude (deg)": 65.0, - "disposal": { - "P3": { - "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] - } - } - }, - "L4": { - "latitude (deg)": 0.75, - "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] - } - } - } - } - }, - "F3": { - "input": "P4", - "locations": { - "L5": { - "latitude (deg)": 100.0, - "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] - } - } - } - } - }, - "F4": { - "input": "P3", - "locations": { - "L6": { - "latitude (deg)": 50.0, - "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] - } - } - } - } - } - } -} \ No newline at end of file diff --git a/juliaw b/juliaw deleted file mode 100755 index b78bc72..0000000 --- a/juliaw +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment -# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -if [ ! -e Project.toml ]; then - echo "juliaw: Project.toml not found" - exit 1 -fi - -if [ ! -e Manifest.toml ]; then - julia --project=. -e 'using Pkg; Pkg.instantiate()' || exit 1 -fi - -if [ ! -e build/sysimage.so -o Project.toml -nt build/sysimage.so ]; then - echo "juliaw: rebuilding system image..." - - # Generate temporary project folder - rm -rf $HOME/.juliaw - mkdir -p $HOME/.juliaw/src - cp Project.toml Manifest.toml $HOME/.juliaw - NAME=$(julia -e 'using TOML; toml = TOML.parsefile("Project.toml"); "name" in keys(toml) && print(toml["name"])') - if [ ! -z $NAME ]; then - cat > $HOME/.juliaw/src/$NAME.jl << EOF -module $NAME -end -EOF - fi - - # Add PackageCompiler dependencies to temporary project - julia --project=$HOME/.juliaw -e 'using Pkg; Pkg.add(["PackageCompiler", "TOML", "Logging"])' - - # Generate system image scripts - cat > $HOME/.juliaw/sysimage.jl << EOF -using PackageCompiler -using TOML -using Logging - -Logging.disable_logging(Logging.Info) -mkpath("$PWD/build") - -println("juliaw: generating precompilation statements...") -run(\`julia --project="$PWD" --trace-compile="$PWD"/build/precompile.jl \$(ARGS)\`) - -println("juliaw: finding dependencies...") -project = TOML.parsefile("Project.toml") -manifest = TOML.parsefile("Manifest.toml") -deps = Symbol[] -for dep in keys(project["deps"]) - if dep in keys(manifest) - # Up to Julia 1.6 - dep_entry = manifest[dep][1] - else - # Julia 1.7+ - dep_entry = manifest["deps"][dep][1] - end - if "path" in keys(dep_entry) - println(" - \$(dep) [skip]") - else - println(" - \$(dep)") - push!(deps, Symbol(dep)) - end -end - -println("juliaw: building system image...") -create_sysimage( - deps, - precompile_statements_file = "$PWD/build/precompile.jl", - sysimage_path = "$PWD/build/sysimage.so", -) -EOF - julia --project=$HOME/.juliaw $HOME/.juliaw/sysimage.jl $* -else - julia --project=. --sysimage build/sysimage.so $* -fi diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 551535f..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,23 +0,0 @@ -site_name: RELOG -theme: cinder -copyright: "Copyright © 2020, UChicago Argonne, LLC. All Rights Reserved." -repo_url: https://github.com/ANL-CEEESA/RELOG -edit_uri: edit/master/src/docs/ -nav: - - Home: index.md - - Usage: usage.md - - Data Format: format.md - - Reports: reports.md - - Optimization Model: model.md -plugins: - - search -markdown_extensions: - - admonition - - mdx_math -extra_javascript: - - https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML - - js/mathjax.js -docs_dir: src/docs -site_dir: docs -extra_css: - - "css/custom.css" diff --git a/src/RELOG.jl b/src/RELOG.jl index f27a4e6..9646112 100644 --- a/src/RELOG.jl +++ b/src/RELOG.jl @@ -4,20 +4,24 @@ module RELOG -include("instance/structs.jl") +using Pkg + +version() = Pkg.dependencies()[Base.UUID("a2afcdf7-cf04-4913-85f9-c0d81ddf2008")].version +include("instance/structs.jl") include("graph/structs.jl") +include("instance/geodb.jl") +include("graph/dist.jl") include("graph/build.jl") include("graph/csv.jl") include("instance/compress.jl") -include("instance/geodb.jl") include("instance/parse.jl") include("instance/validate.jl") include("model/build.jl") include("model/getsol.jl") -include("model/solve.jl") include("model/resolve.jl") +include("model/solve.jl") include("reports/plant_emissions.jl") include("reports/plant_outputs.jl") include("reports/plants.jl") diff --git a/src/graph/build.jl b/src/graph/build.jl index dc274e7..076be2a 100644 --- a/src/graph/build.jl +++ b/src/graph/build.jl @@ -2,14 +2,6 @@ # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. -using Geodesy - -function calculate_distance(source_lat, source_lon, dest_lat, dest_lon)::Float64 - x = LLA(source_lat, source_lon, 0.0) - y = LLA(dest_lat, dest_lon, 0.0) - return round(euclidean_distance(x, y) / 1000.0, digits = 2) -end - function build_graph(instance::Instance)::Graph arcs = [] next_index = 0 @@ -18,6 +10,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 +20,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 @@ -50,11 +44,12 @@ function build_graph(instance::Instance)::Graph # Build arcs from collection centers to plants, and from one plant to another for source in [collection_shipping_nodes; plant_shipping_nodes] for dest in process_nodes_by_input_product[source.product] - distance = calculate_distance( + distance = _calculate_distance( source.location.latitude, source.location.longitude, dest.location.latitude, dest.location.longitude, + instance.distance_metric, ) values = Dict("distance" => distance) arc = Arc(source, dest, values) @@ -83,6 +78,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/dist.jl b/src/graph/dist.jl new file mode 100644 index 0000000..05fe687 --- /dev/null +++ b/src/graph/dist.jl @@ -0,0 +1,60 @@ +# RELOG: Reverse Logistics Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using Geodesy +using NearestNeighbors +using DataFrames + +function _calculate_distance( + source_lat, + source_lon, + dest_lat, + dest_lon, + ::EuclideanDistance, +)::Float64 + x = LLA(source_lat, source_lon, 0.0) + y = LLA(dest_lat, dest_lon, 0.0) + return round(euclidean_distance(x, y) / 1000.0, digits = 3) +end + +function _calculate_distance( + source_lat, + source_lon, + dest_lat, + dest_lon, + metric::KnnDrivingDistance, +)::Float64 + if metric.tree === nothing + basedir = joinpath(dirname(@__FILE__), "..", "..", "data") + csv_filename = joinpath(basedir, "dist_driving.csv") + + # Download pre-computed driving data + if !isfile(csv_filename) + _download_zip( + "https://axavier.org/RELOG/0.6/data/dist_driving_0b9a6ad6.zip", + basedir, + csv_filename, + 0x0b9a6ad6, + ) + end + + # Fit kNN model + df = DataFrame(CSV.File(csv_filename, missingstring = "NaN")) + dropmissing!(df) + coords = Matrix(df[!, [:source_lat, :source_lon, :dest_lat, :dest_lon]])' + metric.ratios = Matrix(df[!, [:ratio]]) + metric.tree = KDTree(coords) + end + + # Compute Euclidean distance + dist_euclidean = + _calculate_distance(source_lat, source_lon, dest_lat, dest_lon, EuclideanDistance()) + + # Predict ratio + idxs, _ = knn(metric.tree, [source_lat, source_lon, dest_lat, dest_lon], 5) + ratio_pred = mean(metric.ratios[idxs]) + dist_pred = round(dist_euclidean * ratio_pred, digits = 3) + isfinite(dist_pred) || error("non-finite distance detected: $dist_pred") + return dist_pred +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..5a1eabd 100644 --- a/src/instance/parse.jl +++ b/src/instance/parse.jl @@ -23,8 +23,20 @@ function parse(json)::Instance validate(json, Schema(json_schema)) building_period = [1] - if "building period (years)" in keys(json) - building_period = json["building period (years)"] + if "building period (years)" in keys(json["parameters"]) + building_period = json["parameters"]["building period (years)"] + end + + distance_metric = EuclideanDistance() + if "distance metric" in keys(json["parameters"]) + metric_name = json["parameters"]["distance metric"] + if metric_name == "driving" + distance_metric = KnnDrivingDistance() + elseif metric_name == "Euclidean" + # nop + else + error("Unknown distance metric: $metric_name") + end end plants = Plant[] @@ -37,6 +49,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 +60,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 +98,7 @@ function parse(json)::Instance product, center_dict["amount (tonne)"], ) + push!(prod_centers, center) push!(collection_centers, center) end end @@ -176,5 +209,12 @@ function parse(json)::Instance @info @sprintf("%12d collection centers", length(collection_centers)) @info @sprintf("%12d candidate plant locations", length(plants)) - return Instance(T, products, collection_centers, plants, building_period) + return Instance( + T, + products, + collection_centers, + plants, + building_period, + distance_metric, + ) end diff --git a/src/instance/structs.jl b/src/instance/structs.jl index 190ae9b..8f69470 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 @@ -48,10 +51,21 @@ mutable struct Plant storage_cost::Vector{Float64} end + +abstract type DistanceMetric end + +Base.@kwdef mutable struct KnnDrivingDistance <: DistanceMetric + tree = nothing + ratios = nothing +end + +mutable struct EuclideanDistance <: DistanceMetric end + mutable struct Instance time::Int64 products::Vector{Product} collection_centers::Vector{CollectionCenter} plants::Vector{Plant} building_period::Vector{Int64} + distance_metric::DistanceMetric end diff --git a/src/model/build.jl b/src/model/build.jl index 2d33900..cda7c64 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 b991f92..60891d7 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/model/solve.jl b/src/model/solve.jl index fe4de6e..7da7a7c 100644 --- a/src/model/solve.jl +++ b/src/model/solve.jl @@ -14,14 +14,14 @@ end function _print_graph_stats(instance::Instance, graph::Graph)::Nothing - @info @sprintf(" %12d time periods", instance.time) - @info @sprintf(" %12d process nodes", length(graph.process_nodes)) - @info @sprintf(" %12d shipping nodes (plant)", length(graph.plant_shipping_nodes)) + @info @sprintf("%12d time periods", instance.time) + @info @sprintf("%12d process nodes", length(graph.process_nodes)) + @info @sprintf("%12d shipping nodes (plant)", length(graph.plant_shipping_nodes)) @info @sprintf( - " %12d shipping nodes (collection)", + "%12d shipping nodes (collection)", length(graph.collection_shipping_nodes) ) - @info @sprintf(" %12d arcs", length(graph.arcs)) + @info @sprintf("%12d arcs", length(graph.arcs)) return end 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..d3cc9aa 100644 --- a/src/schemas/input.json +++ b/src/schemas/input.json @@ -14,6 +14,9 @@ "properties": { "time horizon (years)": { "type": "number" + }, + "distance metric": { + "type": "string" } }, "required": [ @@ -169,6 +172,12 @@ }, "initial amounts": { "$ref": "#/definitions/InitialAmount" + }, + "disposal limit (tonne)": { + "$ref": "#/definitions/TimeSeries" + }, + "disposal cost ($/tonne)": { + "$ref": "#/definitions/TimeSeries" } }, "required": [ diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..bcf66c4 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,19 @@ +name = "RELOGT" +uuid = "a6dae211-05d8-42ed-9081-b88c982fc90a" +authors = ["Alinson S. Xavier "] +version = "0.1.0" + +[deps] +Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" +GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +RELOG = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +JuliaFormatter = "1" \ No newline at end of file diff --git a/test/fixtures/s1.json b/test/fixtures/s1.json new file mode 100644 index 0000000..f4c243d --- /dev/null +++ b/test/fixtures/s1.json @@ -0,0 +1,358 @@ +{ + "parameters": { + "time horizon (years)": 2, + "distance metric": "driving" + }, + "products": { + "P1": { + "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 + ] + }, + "initial amounts": { + "C1": { + "latitude (deg)": 7.0, + "longitude (deg)": 7.0, + "amount (tonne)": [ + 934.56, + 934.56 + ] + }, + "C2": { + "latitude (deg)": 7.0, + "longitude (deg)": 19.0, + "amount (tonne)": [ + 198.95, + 198.95 + ] + }, + "C3": { + "latitude (deg)": 84.0, + "longitude (deg)": 76.0, + "amount (tonne)": [ + 212.97, + 212.97 + ] + }, + "C4": { + "latitude (deg)": 21.0, + "longitude (deg)": 16.0, + "amount (tonne)": [ + 352.19, + 352.19 + ] + }, + "C5": { + "latitude (deg)": 32.0, + "longitude (deg)": 92.0, + "amount (tonne)": [ + 510.33, + 510.33 + ] + }, + "C6": { + "latitude (deg)": 14.0, + "longitude (deg)": 62.0, + "amount (tonne)": [ + 471.66, + 471.66 + ] + }, + "C7": { + "latitude (deg)": 30.0, + "longitude (deg)": 83.0, + "amount (tonne)": [ + 785.21, + 785.21 + ] + }, + "C8": { + "latitude (deg)": 35.0, + "longitude (deg)": 40.0, + "amount (tonne)": [ + 706.17, + 706.17 + ] + }, + "C9": { + "latitude (deg)": 74.0, + "longitude (deg)": 52.0, + "amount (tonne)": [ + 30.08, + 30.08 + ] + }, + "C10": { + "latitude (deg)": 22.0, + "longitude (deg)": 54.0, + "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 + ] + }, + "P3": { + "transportation cost ($/km/tonne)": [ + 0.0125, + 0.0125 + ] + }, + "P4": { + "transportation cost ($/km/tonne)": [ + 0.0175, + 0.0175 + ] + } + }, + "plants": { + "F1": { + "input": "P1", + "outputs (tonne/tonne)": { + "P2": 0.2, + "P3": 0.5 + }, + "energy (GJ/tonne)": [ + 0.12, + 0.11 + ], + "emissions (tonne/tonne)": { + "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 + ] + }, + "P3": { + "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 + ] + }, + "1000.0": { + "opening cost ($)": [ + 1250.0, + 1250.0 + ], + "fixed operating cost ($)": [ + 30.0, + 30.0 + ], + "variable operating cost ($/tonne)": [ + 30.0, + 30.0 + ] + } + } + }, + "L2": { + "latitude (deg)": 0.5, + "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 + ] + }, + "10000.0": { + "opening cost ($)": [ + 10000, + 10000 + ], + "fixed operating cost ($)": [ + 50.0, + 50.0 + ], + "variable operating cost ($/tonne)": [ + 50.0, + 50.0 + ] + } + } + } + } + }, + "F2": { + "input": "P2", + "outputs (tonne/tonne)": { + "P3": 0.05, + "P4": 0.80 + }, + "locations": { + "L3": { + "latitude (deg)": 25.0, + "longitude (deg)": 65.0, + "disposal": { + "P3": { + "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 + ] + } + } + }, + "L4": { + "latitude (deg)": 0.75, + "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 + ] + } + } + } + } + }, + "F3": { + "input": "P4", + "locations": { + "L5": { + "latitude (deg)": 100.0, + "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 + ] + } + } + } + } + }, + "F4": { + "input": "P3", + "locations": { + "L6": { + "latitude (deg)": 50.0, + "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 + ] + } + } + } + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/s1.zip b/test/fixtures/s1.zip new file mode 100644 index 0000000..2d09afb Binary files /dev/null and b/test/fixtures/s1.zip differ diff --git a/instances/s2.json b/test/fixtures/s2.json similarity index 100% rename from instances/s2.json rename to test/fixtures/s2.json diff --git a/instances/solutions/s1.json b/test/fixtures/solutions/s1.json similarity index 100% rename from instances/solutions/s1.json rename to test/fixtures/solutions/s1.json diff --git a/instances/solutions/s1.log b/test/fixtures/solutions/s1.log similarity index 100% rename from instances/solutions/s1.log rename to test/fixtures/solutions/s1.log diff --git a/test/graph/build_test.jl b/test/graph/build_test.jl deleted file mode 100644 index 1b52f58..0000000 --- a/test/graph/build_test.jl +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 2020 Argonne National Laboratory -# Written by Alinson Santos Xavier - -using RELOG - -@testset "build_graph" begin - basedir = dirname(@__FILE__) - instance = RELOG.parsefile("$basedir/../../instances/s1.json") - graph = RELOG.build_graph(instance) - process_node_by_location_name = - Dict(n.location.location_name => n for n in graph.process_nodes) - - @test length(graph.plant_shipping_nodes) == 8 - @test length(graph.collection_shipping_nodes) == 10 - @test length(graph.process_nodes) == 6 - - node = graph.collection_shipping_nodes[1] - @test node.location.name == "C1" - @test length(node.incoming_arcs) == 0 - @test length(node.outgoing_arcs) == 2 - @test node.outgoing_arcs[1].source.location.name == "C1" - @test node.outgoing_arcs[1].dest.location.plant_name == "F1" - @test node.outgoing_arcs[1].dest.location.location_name == "L1" - @test node.outgoing_arcs[1].values["distance"] == 1095.62 - - node = process_node_by_location_name["L1"] - @test node.location.plant_name == "F1" - @test node.location.location_name == "L1" - @test length(node.incoming_arcs) == 10 - @test length(node.outgoing_arcs) == 2 - - node = process_node_by_location_name["L3"] - @test node.location.plant_name == "F2" - @test node.location.location_name == "L3" - @test length(node.incoming_arcs) == 2 - @test length(node.outgoing_arcs) == 2 - - @test length(graph.arcs) == 38 -end diff --git a/test/instance/compress_test.jl b/test/instance/compress_test.jl deleted file mode 100644 index 87d2a98..0000000 --- a/test/instance/compress_test.jl +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2020 Argonne National Laboratory -# Written by Alinson Santos Xavier - -using RELOG - -@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 diff --git a/test/instance/geodb_test.jl b/test/instance/geodb_test.jl deleted file mode 100644 index e0009f0..0000000 --- a/test/instance/geodb_test.jl +++ /dev/null @@ -1,25 +0,0 @@ -# RELOG: Reverse Logistics Optimization -# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -using RELOG - -@testset "geodb_query (2018-us-county)" begin - region = RELOG.geodb_query("2018-us-county:17043") - @test region.centroid.lat == 41.83956 - @test region.centroid.lon == -88.08857 - @test region.population == 922_921 -end - -# @testset "geodb_query (2018-us-zcta)" begin -# region = RELOG.geodb_query("2018-us-zcta:60439") -# @test region.centroid.lat == 41.68241 -# @test region.centroid.lon == -87.98954 -# end - -@testset "geodb_query (us-state)" begin - region = RELOG.geodb_query("us-state:IL") - @test region.centroid.lat == 39.73939 - @test region.centroid.lon == -89.50414 - @test region.population == 12_671_821 -end diff --git a/test/instance/parse_test.jl b/test/instance/parse_test.jl deleted file mode 100644 index 8c29914..0000000 --- a/test/instance/parse_test.jl +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (C) 2020 Argonne National Laboratory -# Written by Alinson Santos Xavier - -using RELOG - -@testset "parse" begin - basedir = dirname(@__FILE__) - instance = RELOG.parsefile("$basedir/../../instances/s1.json") - - 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) - - @test length(centers) == 10 - @test centers[1].name == "C1" - @test centers[1].latitude == 7 - @test centers[1].latitude == 7 - @test centers[1].longitude == 7 - @test centers[1].amount == [934.56, 934.56] - @test centers[1].product.name == "P1" - - @test length(plants) == 6 - - plant = location_name_to_plant["L1"] - @test plant.plant_name == "F1" - @test plant.location_name == "L1" - @test plant.input.name == "P1" - @test plant.latitude == 0 - @test plant.longitude == 0 - - @test length(plant.sizes) == 2 - @test plant.sizes[1].capacity == 250 - @test plant.sizes[1].opening_cost == [500, 500] - @test plant.sizes[1].fixed_operating_cost == [30, 30] - @test plant.sizes[1].variable_operating_cost == [30, 30] - @test plant.sizes[2].capacity == 1000 - @test plant.sizes[2].opening_cost == [1250, 1250] - @test plant.sizes[2].fixed_operating_cost == [30, 30] - @test plant.sizes[2].variable_operating_cost == [30, 30] - - p2 = product_name_to_product["P2"] - p3 = product_name_to_product["P3"] - @test length(plant.output) == 2 - @test plant.output[p2] == 0.2 - @test plant.output[p3] == 0.5 - @test plant.disposal_limit[p2] == [1, 1] - @test plant.disposal_limit[p3] == [1, 1] - @test plant.disposal_cost[p2] == [-10, -10] - @test plant.disposal_cost[p3] == [-10, -10] - - plant = location_name_to_plant["L3"] - @test plant.location_name == "L3" - @test plant.input.name == "P2" - @test plant.latitude == 25 - @test plant.longitude == 65 - - @test length(plant.sizes) == 2 - @test plant.sizes[1].capacity == 1000.0 - @test plant.sizes[1].opening_cost == [3000, 3000] - @test plant.sizes[1].fixed_operating_cost == [50, 50] - @test plant.sizes[1].variable_operating_cost == [50, 50] - @test plant.sizes[1] == plant.sizes[2] - - p4 = product_name_to_product["P4"] - @test plant.output[p3] == 0.05 - @test plant.output[p4] == 0.8 - @test plant.disposal_limit[p3] == [1e8, 1e8] - @test plant.disposal_limit[p4] == [0, 0] -end - -@testset "parse (geodb)" begin - basedir = dirname(@__FILE__) - instance = RELOG.parsefile("$basedir/../../instances/s2.json") - - centers = instance.collection_centers - @test centers[1].name == "C1" - @test centers[1].latitude == 41.83956 - @test centers[1].longitude == -88.08857 -end - -# @testset "parse (invalid)" begin -# basedir = dirname(@__FILE__) -# @test_throws ErrorException RELOG.parsefile("$basedir/../fixtures/s1-wrong-length.json") -# end diff --git a/test/model/build_test.jl b/test/model/build_test.jl deleted file mode 100644 index 15c75b2..0000000 --- a/test/model/build_test.jl +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (C) 2020 Argonne National Laboratory -# Written by Alinson Santos Xavier - -using RELOG, HiGHS, JuMP, Printf, JSON, MathOptInterface.FileFormats - -@testset "build" begin - basedir = dirname(@__FILE__) - instance = RELOG.parsefile("$basedir/../../instances/s1.json") - graph = RELOG.build_graph(instance) - model = RELOG.build_model(instance, graph, HiGHS.Optimizer) - - process_node_by_location_name = - Dict(n.location.location_name => n for n in graph.process_nodes) - - shipping_node_by_loc_and_prod_names = Dict( - (n.location.location_name, n.product.name) => n for n in graph.plant_shipping_nodes - ) - - @test length(model[:flow]) == 76 - @test length(model[:dispose]) == 16 - @test length(model[:open_plant]) == 12 - @test length(model[:capacity]) == 12 - @test length(model[:expansion]) == 12 - - l1 = process_node_by_location_name["L1"] - v = model[:capacity][l1, 1] - @test lower_bound(v) == 0.0 - @test upper_bound(v) == 1000.0 - - v = model[:expansion][l1, 1] - @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] - @test lower_bound(v) == 0.0 - @test upper_bound(v) == 1.0 -end diff --git a/test/model/resolve_test.jl b/test/model/resolve_test.jl deleted file mode 100644 index 216c592..0000000 --- a/test/model/resolve_test.jl +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) 2020 Argonne National Laboratory -# Written by Alinson Santos Xavier - -using RELOG - -BASEDIR = dirname(@__FILE__) - -@testset "Resolve" begin - # Shoud not crash - filename = joinpath(BASEDIR, "..", "..", "instances", "s1.json") - solution_old, model_old = RELOG.solve(filename, return_model = true) - solution_new = RELOG.resolve(model_old, filename) -end diff --git a/test/model/solve_test.jl b/test/model/solve_test.jl deleted file mode 100644 index 003af57..0000000 --- a/test/model/solve_test.jl +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (C) 2020 Argonne National Laboratory -# Written by Alinson Santos Xavier - -using RELOG, JuMP, Printf, JSON, MathOptInterface.FileFormats - -basedir = dirname(@__FILE__) - -@testset "solve (exact)" begin - solution_filename_a = tempname() - solution_filename_b = tempname() - solution = RELOG.solve("$basedir/../../instances/s1.json", output = solution_filename_a) - - @test isfile(solution_filename_a) - - RELOG.write(solution, solution_filename_b) - @test isfile(solution_filename_b) - - @test "Costs" in keys(solution) - @test "Fixed operating (\$)" in keys(solution["Costs"]) - @test "Transportation (\$)" in keys(solution["Costs"]) - @test "Variable operating (\$)" in keys(solution["Costs"]) - @test "Total (\$)" in keys(solution["Costs"]) - - @test "Plants" in keys(solution) - @test "F1" in keys(solution["Plants"]) - @test "F2" in keys(solution["Plants"]) - @test "F3" in keys(solution["Plants"]) - @test "F4" in keys(solution["Plants"]) -end - -@testset "solve (heuristic)" begin - # Should not crash - solution = RELOG.solve("$basedir/../../instances/s1.json", heuristic = true) -end - -@testset "solve (infeasible)" begin - json = JSON.parsefile("$basedir/../../instances/s1.json") - for (location_name, location_dict) in json["products"]["P1"]["initial amounts"] - location_dict["amount (tonne)"] *= 1000 - end - @test_throws ErrorException("No solution available") RELOG.solve(RELOG.parse(json)) -end - -@testset "solve (with storage)" begin - basedir = dirname(@__FILE__) - filename = "$basedir/../fixtures/storage.json" - instance = RELOG.parsefile(filename) - @test instance.plants[1].storage_limit == 50.0 - @test instance.plants[1].storage_cost == [2.0, 1.5, 1.0] - - solution = RELOG.solve(filename) - plant_dict = solution["Plants"]["mega plant"]["Chicago"] - @test plant_dict["Variable operating cost (\$)"] == [500.0, 0.0, 100.0] - @test plant_dict["Process (tonne)"] == [50.0, 0.0, 50.0] - @test plant_dict["Storage (tonne)"] == [50.0, 50.0, 0.0] - @test plant_dict["Storage cost (\$)"] == [100.0, 75.0, 0.0] - - @test solution["Costs"]["Variable operating (\$)"] == [500.0, 0.0, 100.0] - @test solution["Costs"]["Storage (\$)"] == [100.0, 75.0, 0.0] - @test solution["Costs"]["Total (\$)"] == [600.0, 75.0, 100.0] -end diff --git a/test/reports_test.jl b/test/reports_test.jl deleted file mode 100644 index dbf4314..0000000 --- a/test/reports_test.jl +++ /dev/null @@ -1,21 +0,0 @@ -# RELOG: Reverse Logistics Optimization -# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -using RELOG, JSON, GZip - -BASEDIR = dirname(@__FILE__) - -@testset "Reports" begin - @testset "from solve" begin - solution = RELOG.solve(joinpath(BASEDIR, "..", "instances", "s1.json")) - tmp_filename = tempname() - # The following should not crash - RELOG.write_plant_emissions_report(solution, tmp_filename) - RELOG.write_plant_outputs_report(solution, tmp_filename) - RELOG.write_plants_report(solution, tmp_filename) - RELOG.write_products_report(solution, tmp_filename) - RELOG.write_transportation_emissions_report(solution, tmp_filename) - RELOG.write_transportation_report(solution, tmp_filename) - end -end diff --git a/test/runtests.jl b/test/runtests.jl deleted file mode 100644 index 3fcf070..0000000 --- a/test/runtests.jl +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2020 Argonne National Laboratory -# Written by Alinson Santos Xavier - -using Test - -@testset "RELOG" begin - @testset "Instance" begin - include("instance/compress_test.jl") - include("instance/geodb_test.jl") - include("instance/parse_test.jl") - end - @testset "Graph" begin - include("graph/build_test.jl") - end - @testset "Model" begin - include("model/build_test.jl") - include("model/solve_test.jl") - include("model/resolve_test.jl") - end - include("reports_test.jl") -end diff --git a/test/src/RELOGT.jl b/test/src/RELOGT.jl new file mode 100644 index 0000000..2691480 --- /dev/null +++ b/test/src/RELOGT.jl @@ -0,0 +1,51 @@ +module RELOGT + +using Test +using JuliaFormatter + +include("instance/compress_test.jl") +include("instance/geodb_test.jl") +include("instance/parse_test.jl") +include("graph/build_test.jl") +include("graph/dist_test.jl") +include("model/build_test.jl") +include("model/solve_test.jl") +include("model/resolve_test.jl") +include("reports_test.jl") + +basedir = dirname(@__FILE__) + +function fixture(path::String)::String + return "$basedir/../fixtures/$path" +end + +function runtests() + @testset "RELOG" begin + @testset "instance" begin + instance_compress_test() + instance_geodb_test() + instance_parse_test() + end + @testset "graph" begin + graph_build_test() + graph_dist_test() + end + @testset "model" begin + model_build_test() + model_solve_test() + model_resolve_test() + end + reports_test() + end + return +end + +function format() + JuliaFormatter.format(basedir, verbose = true) + JuliaFormatter.format("$basedir/../../src", verbose = true) + return +end + +export runtests, format + +end # module RELOGT diff --git a/test/src/graph/build_test.jl b/test/src/graph/build_test.jl new file mode 100644 index 0000000..070f899 --- /dev/null +++ b/test/src/graph/build_test.jl @@ -0,0 +1,40 @@ +# Copyright (C) 2020 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using RELOG + +function graph_build_test() + @testset "build_graph" begin + instance = RELOG.parsefile(fixture("s1.json")) + graph = RELOG.build_graph(instance) + process_node_by_location_name = + Dict(n.location.location_name => n for n in graph.process_nodes) + + @test length(graph.plant_shipping_nodes) == 8 + @test length(graph.collection_shipping_nodes) == 10 + @test length(graph.process_nodes) == 6 + + node = graph.collection_shipping_nodes[1] + @test node.location.name == "C1" + @test length(node.incoming_arcs) == 0 + @test length(node.outgoing_arcs) == 2 + @test node.outgoing_arcs[1].source.location.name == "C1" + @test node.outgoing_arcs[1].dest.location.plant_name == "F1" + @test node.outgoing_arcs[1].dest.location.location_name == "L1" + @test node.outgoing_arcs[1].values["distance"] == 1695.364 + + node = process_node_by_location_name["L1"] + @test node.location.plant_name == "F1" + @test node.location.location_name == "L1" + @test length(node.incoming_arcs) == 10 + @test length(node.outgoing_arcs) == 2 + + node = process_node_by_location_name["L3"] + @test node.location.plant_name == "F2" + @test node.location.location_name == "L3" + @test length(node.incoming_arcs) == 2 + @test length(node.outgoing_arcs) == 2 + + @test length(graph.arcs) == 38 + end +end diff --git a/test/src/graph/dist_test.jl b/test/src/graph/dist_test.jl new file mode 100644 index 0000000..8dd084a --- /dev/null +++ b/test/src/graph/dist_test.jl @@ -0,0 +1,27 @@ +# RELOG: Reverse Logistics Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using RELOG + +function graph_dist_test() + @testset "KnnDrivingDistance" begin + # Euclidean distance between Chicago and Indianapolis + @test RELOG._calculate_distance( + 41.866, + -87.656, + 39.764, + -86.148, + RELOG.EuclideanDistance(), + ) == 265.818 + + # Approximate driving distance between Chicago and Indianapolis + @test RELOG._calculate_distance( + 41.866, + -87.656, + 39.764, + -86.148, + RELOG.KnnDrivingDistance(), + ) == 316.43 + end +end diff --git a/test/src/instance/compress_test.jl b/test/src/instance/compress_test.jl new file mode 100644 index 0000000..6652c63 --- /dev/null +++ b/test/src/instance/compress_test.jl @@ -0,0 +1,54 @@ +# Copyright (C) 2020 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using RELOG + +function instance_compress_test() + @testset "compress" begin + instance = RELOG.parsefile(fixture("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 diff --git a/test/src/instance/geodb_test.jl b/test/src/instance/geodb_test.jl new file mode 100644 index 0000000..f128ccc --- /dev/null +++ b/test/src/instance/geodb_test.jl @@ -0,0 +1,27 @@ +# RELOG: Reverse Logistics Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using RELOG + +function instance_geodb_test() + @testset "geodb_query (2018-us-county)" begin + region = RELOG.geodb_query("2018-us-county:17043") + @test region.centroid.lat == 41.83956 + @test region.centroid.lon == -88.08857 + @test region.population == 922_921 + end + + # @testset "geodb_query (2018-us-zcta)" begin + # region = RELOG.geodb_query("2018-us-zcta:60439") + # @test region.centroid.lat == 41.68241 + # @test region.centroid.lon == -87.98954 + # end + + @testset "geodb_query (us-state)" begin + region = RELOG.geodb_query("us-state:IL") + @test region.centroid.lat == 39.73939 + @test region.centroid.lon == -89.50414 + @test region.population == 12_671_821 + end +end diff --git a/test/src/instance/parse_test.jl b/test/src/instance/parse_test.jl new file mode 100644 index 0000000..16288e1 --- /dev/null +++ b/test/src/instance/parse_test.jl @@ -0,0 +1,87 @@ +# Copyright (C) 2020 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using RELOG + +function instance_parse_test() + @testset "parse" begin + instance = RELOG.parsefile(fixture("s1.json")) + + 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) + + @test length(centers) == 10 + @test centers[1].name == "C1" + @test centers[1].latitude == 7 + @test centers[1].latitude == 7 + @test centers[1].longitude == 7 + @test centers[1].amount == [934.56, 934.56] + @test centers[1].product.name == "P1" + + @test length(plants) == 6 + + plant = location_name_to_plant["L1"] + @test plant.plant_name == "F1" + @test plant.location_name == "L1" + @test plant.input.name == "P1" + @test plant.latitude == 0 + @test plant.longitude == 0 + + @test length(plant.sizes) == 2 + @test plant.sizes[1].capacity == 250 + @test plant.sizes[1].opening_cost == [500, 500] + @test plant.sizes[1].fixed_operating_cost == [30, 30] + @test plant.sizes[1].variable_operating_cost == [30, 30] + @test plant.sizes[2].capacity == 1000 + @test plant.sizes[2].opening_cost == [1250, 1250] + @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 + @test plant.output[p3] == 0.5 + @test plant.disposal_limit[p2] == [1, 1] + @test plant.disposal_limit[p3] == [1, 1] + @test plant.disposal_cost[p2] == [-10, -10] + @test plant.disposal_cost[p3] == [-10, -10] + + plant = location_name_to_plant["L3"] + @test plant.location_name == "L3" + @test plant.input.name == "P2" + @test plant.latitude == 25 + @test plant.longitude == 65 + + @test length(plant.sizes) == 2 + @test plant.sizes[1].capacity == 1000.0 + @test plant.sizes[1].opening_cost == [3000, 3000] + @test plant.sizes[1].fixed_operating_cost == [50, 50] + @test plant.sizes[1].variable_operating_cost == [50, 50] + @test plant.sizes[1] == plant.sizes[2] + + p4 = product_name_to_product["P4"] + @test plant.output[p3] == 0.05 + @test plant.output[p4] == 0.8 + @test plant.disposal_limit[p3] == [1e8, 1e8] + @test plant.disposal_limit[p4] == [0, 0] + end + + @testset "parse (geodb)" begin + instance = RELOG.parsefile(fixture("s2.json")) + centers = instance.collection_centers + @test centers[1].name == "C1" + @test centers[1].latitude == 41.83956 + @test centers[1].longitude == -88.08857 + end +end diff --git a/test/src/model/build_test.jl b/test/src/model/build_test.jl new file mode 100644 index 0000000..3c111ea --- /dev/null +++ b/test/src/model/build_test.jl @@ -0,0 +1,40 @@ +# Copyright (C) 2020 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats + +function model_build_test() + @testset "build" begin + instance = RELOG.parsefile(fixture("s1.json")) + graph = RELOG.build_graph(instance) + model = RELOG.build_model(instance, graph, Cbc.Optimizer) + set_optimizer_attribute(model, "logLevel", 0) + + process_node_by_location_name = + Dict(n.location.location_name => n for n in graph.process_nodes) + + shipping_node_by_loc_and_prod_names = Dict( + (n.location.location_name, n.product.name) => n for + n in graph.plant_shipping_nodes + ) + + @test length(model[:flow]) == 76 + @test length(model[:plant_dispose]) == 16 + @test length(model[:open_plant]) == 12 + @test length(model[:capacity]) == 12 + @test length(model[:expansion]) == 12 + + l1 = process_node_by_location_name["L1"] + v = model[:capacity][l1, 1] + @test lower_bound(v) == 0.0 + @test upper_bound(v) == 1000.0 + + v = model[:expansion][l1, 1] + @test lower_bound(v) == 0.0 + @test upper_bound(v) == 750.0 + + 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 +end diff --git a/test/src/model/resolve_test.jl b/test/src/model/resolve_test.jl new file mode 100644 index 0000000..32e3e23 --- /dev/null +++ b/test/src/model/resolve_test.jl @@ -0,0 +1,13 @@ +# Copyright (C) 2020 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using RELOG + +function model_resolve_test() + @testset "Resolve" begin + # Shoud not crash + filename = fixture("s1.json") + solution_old, model_old = RELOG.solve(filename, return_model = true) + solution_new = RELOG.resolve(model_old, filename) + end +end diff --git a/test/src/model/solve_test.jl b/test/src/model/solve_test.jl new file mode 100644 index 0000000..ecb948a --- /dev/null +++ b/test/src/model/solve_test.jl @@ -0,0 +1,70 @@ +# Copyright (C) 2020 Argonne National Laboratory +# Written by Alinson Santos Xavier + +using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats + + +function model_solve_test() + @testset "solve (exact)" begin + solution_filename_a = tempname() + solution_filename_b = tempname() + solution = RELOG.solve(fixture("s1.json"), output = solution_filename_a) + + @test isfile(solution_filename_a) + + RELOG.write(solution, solution_filename_b) + @test isfile(solution_filename_b) + + @test "Costs" in keys(solution) + @test "Fixed operating (\$)" in keys(solution["Costs"]) + @test "Transportation (\$)" in keys(solution["Costs"]) + @test "Variable operating (\$)" in keys(solution["Costs"]) + @test "Total (\$)" in keys(solution["Costs"]) + + @test "Plants" in keys(solution) + @test "F1" in keys(solution["Plants"]) + @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 + # Should not crash + solution = RELOG.solve(fixture("s1.json"), heuristic = true) + end + + @testset "solve (infeasible)" begin + json = JSON.parsefile(fixture("s1.json")) + for (location_name, location_dict) in json["products"]["P1"]["initial amounts"] + location_dict["amount (tonne)"] *= 1000 + end + @test_throws ErrorException("No solution available") RELOG.solve(RELOG.parse(json)) + end + + @testset "solve (with storage)" begin + filename = fixture("storage.json") + instance = RELOG.parsefile(filename) + @test instance.plants[1].storage_limit == 50.0 + @test instance.plants[1].storage_cost == [2.0, 1.5, 1.0] + + solution = RELOG.solve(filename) + plant_dict = solution["Plants"]["mega plant"]["Chicago"] + @test plant_dict["Variable operating cost (\$)"] == [500.0, 0.0, 100.0] + @test plant_dict["Process (tonne)"] == [50.0, 0.0, 50.0] + @test plant_dict["Storage (tonne)"] == [50.0, 50.0, 0.0] + @test plant_dict["Storage cost (\$)"] == [100.0, 75.0, 0.0] + + @test solution["Costs"]["Variable operating (\$)"] == [500.0, 0.0, 100.0] + @test solution["Costs"]["Storage (\$)"] == [100.0, 75.0, 0.0] + @test solution["Costs"]["Total (\$)"] == [600.0, 75.0, 100.0] + end +end diff --git a/test/src/reports_test.jl b/test/src/reports_test.jl new file mode 100644 index 0000000..3f3971b --- /dev/null +++ b/test/src/reports_test.jl @@ -0,0 +1,21 @@ +# RELOG: Reverse Logistics Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using RELOG, JSON, GZip + +function reports_test() + @testset "reports" begin + @testset "from solve" begin + solution = RELOG.solve(fixture("s1.json")) + tmp_filename = tempname() + # The following should not crash + RELOG.write_plant_emissions_report(solution, tmp_filename) + RELOG.write_plant_outputs_report(solution, tmp_filename) + RELOG.write_plants_report(solution, tmp_filename) + RELOG.write_products_report(solution, tmp_filename) + RELOG.write_transportation_emissions_report(solution, tmp_filename) + RELOG.write_transportation_report(solution, tmp_filename) + end + end +end