Merge branch 'master' into relog-web

relog-web
Alinson S. Xavier 3 years ago
commit 1aa01b7b2b
Signed by: isoron
GPG Key ID: 0DA8E4B9E1109DCA

@ -14,10 +14,10 @@ jobs:
shell: julia --color=yes {0} shell: julia --color=yes {0}
run: | run: |
using Pkg using Pkg
Pkg.add(PackageSpec(name="JuliaFormatter", version="0.14.4")) Pkg.add(PackageSpec(name="JuliaFormatter", version="1"))
using JuliaFormatter using JuliaFormatter
format("src", verbose=true) format("src", verbose=true)
format("test", verbose=true) format("test/src", verbose=true)
out = String(read(Cmd(`git diff`))) out = String(read(Cmd(`git diff`)))
if isempty(out) if isempty(out)
exit(0) exit(0)

@ -21,5 +21,15 @@ jobs:
with: with:
version: ${{ matrix.version }} version: ${{ matrix.version }}
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
- uses: julia-actions/julia-buildpkg@v1 - name: Run tests
- uses: julia-actions/julia-runtest@v1 shell: julia --color=yes --project=test {0}
run: |
using Pkg
Pkg.develop(path=".")
Pkg.update()
using RELOGT
try
runtests()
catch
exit(1)
end

@ -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 [semver]: https://semver.org/spec/v2.0.0.html
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0 [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 ## [0.5.2] -- 2022-08-26
### Changed ### Changed
- Update to JuMP 1.x - Update to JuMP 1.x

@ -1,4 +1,4 @@
VERSION := 0.5 VERSION := 0.6
PKG := ghcr.io/anl-ceeesa/relog-web PKG := ghcr.io/anl-ceeesa/relog-web
clean: clean:

@ -1,7 +1,7 @@
name = "RELOG" name = "RELOG"
uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008" uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008"
authors = ["Alinson S Xavier <axavier@anl.gov>"] authors = ["Alinson S Xavier <axavier@anl.gov>"]
version = "0.5.2" version = "0.6.0"
[deps] [deps]
CRC = "44b605c4-b955-5f2b-9b6d-d2bd01d3d205" CRC = "44b605c4-b955-5f2b-9b6d-d2bd01d3d205"
@ -19,7 +19,9 @@ JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572" JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
@ -40,6 +42,7 @@ JSON = "0.21"
JSONSchema = "1" JSONSchema = "1"
JuMP = "1" JuMP = "1"
MathOptInterface = "1" MathOptInterface = "1"
NearestNeighbors = "0.4"
OrderedCollections = "1" OrderedCollections = "1"
ProgressBars = "1" ProgressBars = "1"
Shapefile = "0.8" Shapefile = "0.8"

@ -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. **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.
<img src="https://anl-ceeesa.github.io/RELOG/0.5/assets/ex_transportation.png" width="1000px"/>
<img src="https://anl-ceeesa.github.io/RELOG/0.6/assets/ex_transportation.png" width="1000px"/>
### Documentation ### Documentation
- [Usage](https://anl-ceeesa.github.io/RELOG/0.5/usage) * [Usage](https://anl-ceeesa.github.io/RELOG/0.6/usage)
- [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.5/format) * [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.6/format)
- [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.5/reports) * [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.6/reports)
- [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.5/model) * [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.6/model)
### Authors ### Authors
- **Alinson S. Xavier** <<axavier@anl.gov>> * **Alinson S. Xavier** <<axavier@anl.gov>>
- **Nwike Iloeje** <<ciloeje@anl.gov>> * **Nwike Iloeje** <<ciloeje@anl.gov>>
- **John Atkins** * **John Atkins**
- **Kyle Sun** * **Kyle Sun**
- **Audrey Gallier** * **Audrey Gallier**
### License ### License

@ -1,5 +0,0 @@
[deps]
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
[compat]
JuliaFormatter = "0.14.4"

@ -1,8 +0,0 @@
using JuliaFormatter
format(
[
"../../src",
"../../test",
],
verbose=true,
)

@ -14,6 +14,7 @@ The **parameters** section describes details about the simulation itself.
|:--------------------------|:---------------| |:--------------------------|:---------------|
|`time horizon (years)` | Number of years in the simulation. |`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. | |`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 #### Example
@ -21,7 +22,8 @@ The **parameters** section describes details about the simulation itself.
{ {
"parameters": { "parameters": {
"time horizon (years)": 2, "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 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. |`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. |`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: 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)": { "transportation emissions (tonne/km/tonne)": {
"CO2": [0.052, 0.050], "CO2": [0.052, 0.050],
"CH4": [0.003, 0.002] "CH4": [0.003, 0.002]
} },
"disposal cost ($/tonne)": [-10.0, -12.0],
"disposal limit (tonne)": [1.0, 1.0],
}, },
"P2": { "P2": {
"transportation cost ($/km/tonne)": [0.022, 0.020] "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. * 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. * 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. * 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) ## Output Data Format (JSON)

@ -154,6 +154,7 @@ Report showing primary product amounts, locations and marginal costs. Generated
| `longitude (deg)` | Longitude of the collection center. | `longitude (deg)` | Longitude of the collection center.
| `year` | What year this row corresponds to. This reports includes one row for each year. | `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 (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. | `marginal cost ($/tonne)` | Cost to process one additional tonne of this product coming from this collection center.

@ -7,13 +7,7 @@ To use RELOG, the first step is to install the [Julia programming language](http
```julia ```julia
using Pkg using Pkg
Pkg.add(name="RELOG", version="0.5") Pkg.add(name="RELOG", version="0.6")
```
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")
``` ```
## 2. Modeling the problem ## 2. Modeling the problem

@ -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]
}
}
}
}
}
}
}

@ -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

@ -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"

@ -4,20 +4,24 @@
module RELOG 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("graph/structs.jl")
include("instance/geodb.jl")
include("graph/dist.jl")
include("graph/build.jl") include("graph/build.jl")
include("graph/csv.jl") include("graph/csv.jl")
include("instance/compress.jl") include("instance/compress.jl")
include("instance/geodb.jl")
include("instance/parse.jl") include("instance/parse.jl")
include("instance/validate.jl") include("instance/validate.jl")
include("model/build.jl") include("model/build.jl")
include("model/getsol.jl") include("model/getsol.jl")
include("model/solve.jl")
include("model/resolve.jl") include("model/resolve.jl")
include("model/solve.jl")
include("reports/plant_emissions.jl") include("reports/plant_emissions.jl")
include("reports/plant_outputs.jl") include("reports/plant_outputs.jl")
include("reports/plants.jl") include("reports/plants.jl")

@ -2,14 +2,6 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # 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 function build_graph(instance::Instance)::Graph
arcs = [] arcs = []
next_index = 0 next_index = 0
@ -18,6 +10,7 @@ function build_graph(instance::Instance)::Graph
collection_shipping_nodes = ShippingNode[] collection_shipping_nodes = ShippingNode[]
name_to_process_node_map = Dict{Tuple{AbstractString,AbstractString},ProcessNode}() name_to_process_node_map = Dict{Tuple{AbstractString,AbstractString},ProcessNode}()
collection_center_to_node = Dict()
process_nodes_by_input_product = process_nodes_by_input_product =
Dict(product => ProcessNode[] for product in instance.products) Dict(product => ProcessNode[] for product in instance.products)
@ -27,6 +20,7 @@ function build_graph(instance::Instance)::Graph
for center in instance.collection_centers for center in instance.collection_centers
node = ShippingNode(next_index, center, center.product, [], []) node = ShippingNode(next_index, center, center.product, [], [])
next_index += 1 next_index += 1
collection_center_to_node[center] = node
push!(collection_shipping_nodes, node) push!(collection_shipping_nodes, node)
end end
@ -50,11 +44,12 @@ function build_graph(instance::Instance)::Graph
# Build arcs from collection centers to plants, and from one plant to another # Build arcs from collection centers to plants, and from one plant to another
for source in [collection_shipping_nodes; plant_shipping_nodes] for source in [collection_shipping_nodes; plant_shipping_nodes]
for dest in process_nodes_by_input_product[source.product] for dest in process_nodes_by_input_product[source.product]
distance = calculate_distance( distance = _calculate_distance(
source.location.latitude, source.location.latitude,
source.location.longitude, source.location.longitude,
dest.location.latitude, dest.location.latitude,
dest.location.longitude, dest.location.longitude,
instance.distance_metric,
) )
values = Dict("distance" => distance) values = Dict("distance" => distance)
arc = Arc(source, dest, values) arc = Arc(source, dest, values)
@ -83,6 +78,7 @@ function build_graph(instance::Instance)::Graph
collection_shipping_nodes, collection_shipping_nodes,
arcs, arcs,
name_to_process_node_map, name_to_process_node_map,
collection_center_to_node,
) )
end end

@ -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

@ -33,6 +33,7 @@ mutable struct Graph
collection_shipping_nodes::Vector{ShippingNode} collection_shipping_nodes::Vector{ShippingNode}
arcs::Vector{Arc} arcs::Vector{Arc}
name_to_process_node_map::Dict{Tuple{AbstractString,AbstractString},ProcessNode} name_to_process_node_map::Dict{Tuple{AbstractString,AbstractString},ProcessNode}
collection_center_to_node::Dict{CollectionCenter,ShippingNode}
end end
function Base.show(io::IO, instance::Graph) function Base.show(io::IO, instance::Graph)

@ -23,8 +23,20 @@ function parse(json)::Instance
validate(json, Schema(json_schema)) validate(json, Schema(json_schema))
building_period = [1] building_period = [1]
if "building period (years)" in keys(json) if "building period (years)" in keys(json["parameters"])
building_period = json["building period (years)"] 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 end
plants = Plant[] plants = Plant[]
@ -37,6 +49,8 @@ function parse(json)::Instance
cost = product_dict["transportation cost (\$/km/tonne)"] cost = product_dict["transportation cost (\$/km/tonne)"]
energy = zeros(T) energy = zeros(T)
emissions = Dict() emissions = Dict()
disposal_limit = zeros(T)
disposal_cost = zeros(T)
if "transportation energy (J/km/tonne)" in keys(product_dict) if "transportation energy (J/km/tonne)" in keys(product_dict)
energy = product_dict["transportation energy (J/km/tonne)"] energy = product_dict["transportation energy (J/km/tonne)"]
@ -46,7 +60,25 @@ function parse(json)::Instance
emissions = product_dict["transportation emissions (tonne/km/tonne)"] emissions = product_dict["transportation emissions (tonne/km/tonne)"]
end 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) push!(products, product)
prod_name_to_product[product_name] = product prod_name_to_product[product_name] = product
@ -66,6 +98,7 @@ function parse(json)::Instance
product, product,
center_dict["amount (tonne)"], center_dict["amount (tonne)"],
) )
push!(prod_centers, center)
push!(collection_centers, center) push!(collection_centers, center)
end end
end end
@ -176,5 +209,12 @@ function parse(json)::Instance
@info @sprintf("%12d collection centers", length(collection_centers)) @info @sprintf("%12d collection centers", length(collection_centers))
@info @sprintf("%12d candidate plant locations", length(plants)) @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 end

@ -13,6 +13,9 @@ mutable struct Product
transportation_cost::Vector{Float64} transportation_cost::Vector{Float64}
transportation_energy::Vector{Float64} transportation_energy::Vector{Float64}
transportation_emissions::Dict{String,Vector{Float64}} transportation_emissions::Dict{String,Vector{Float64}}
disposal_limit::Vector{Float64}
disposal_cost::Vector{Float64}
collection_centers::Vector
end end
mutable struct CollectionCenter mutable struct CollectionCenter
@ -48,10 +51,21 @@ mutable struct Plant
storage_cost::Vector{Float64} storage_cost::Vector{Float64}
end end
abstract type DistanceMetric end
Base.@kwdef mutable struct KnnDrivingDistance <: DistanceMetric
tree = nothing
ratios = nothing
end
mutable struct EuclideanDistance <: DistanceMetric end
mutable struct Instance mutable struct Instance
time::Int64 time::Int64
products::Vector{Product} products::Vector{Product}
collection_centers::Vector{CollectionCenter} collection_centers::Vector{CollectionCenter}
plants::Vector{Plant} plants::Vector{Plant}
building_period::Vector{Int64} building_period::Vector{Int64}
distance_metric::DistanceMetric
end end

@ -20,13 +20,17 @@ function create_vars!(model::JuMP.Model)
graph, T = model[:graph], model[:instance].time graph, T = model[:graph], model[:instance].time
model[:flow] = model[:flow] =
Dict((a, t) => @variable(model, lower_bound = 0) for a in graph.arcs, t = 1:T) 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( (n, t) => @variable(
model, model,
lower_bound = 0, 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 ) 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( model[:store] = Dict(
(n, t) => (n, t) =>
@variable(model, lower_bound = 0, upper_bound = n.location.storage_limit) @variable(model, lower_bound = 0, upper_bound = n.location.storage_limit)
@ -131,14 +135,25 @@ function create_objective_function!(model::JuMP.Model)
end end
end end
# Shipping node costs # Plant shipping node costs
for n in values(graph.plant_shipping_nodes), t = 1:T for n in values(graph.plant_shipping_nodes), t = 1:T
# Disposal costs # Disposal costs
add_to_expression!( add_to_expression!(
obj, obj,
n.location.disposal_cost[n.product][t], 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 end
@ -154,16 +169,29 @@ function create_shipping_node_constraints!(model::JuMP.Model)
for n in graph.collection_shipping_nodes for n in graph.collection_shipping_nodes
model[:eq_balance][n, t] = @constraint( model[:eq_balance][n, t] = @constraint(
model, 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 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 # Plants
for n in graph.plant_shipping_nodes for n in graph.plant_shipping_nodes
@constraint( @constraint(
model, model,
sum(model[:flow][a, t] for a in n.incoming_arcs) == 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
end end

@ -39,22 +39,25 @@ function get_solution(model::JuMP.Model; marginal_costs = true)
end end
# Products # Products
if marginal_costs
for n in graph.collection_shipping_nodes for n in graph.collection_shipping_nodes
location_dict = OrderedDict{Any,Any}( 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, "Latitude (deg)" => n.location.latitude,
"Longitude (deg)" => n.location.longitude, "Longitude (deg)" => n.location.longitude,
"Amount (tonne)" => n.location.amount, "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"]) if n.product.name keys(output["Products"])
output["Products"][n.product.name] = OrderedDict() output["Products"][n.product.name] = OrderedDict()
end end
output["Products"][n.product.name][n.location.name] = location_dict output["Products"][n.product.name][n.location.name] = location_dict
end end
end
# Plants # Plants
for plant in instance.plants for plant in instance.plants
@ -178,13 +181,14 @@ function get_solution(model::JuMP.Model; marginal_costs = true)
plant_dict["Total output"][product_name] = zeros(T) plant_dict["Total output"][product_name] = zeros(T)
plant_dict["Output"]["Send"][product_name] = product_dict = OrderedDict() 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 if sum(disposal_amount) > 1e-5
skip_plant = false skip_plant = false
plant_dict["Output"]["Dispose"][product_name] = plant_dict["Output"]["Dispose"][product_name] =
disposal_dict = OrderedDict() disposal_dict = OrderedDict()
disposal_dict["Amount (tonne)"] = 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["Cost (\$)"] = [
disposal_dict["Amount (tonne)"][t] * disposal_dict["Amount (tonne)"][t] *
plant.disposal_cost[shipping_node.product][t] for t = 1:T plant.disposal_cost[shipping_node.product][t] for t = 1:T

@ -14,14 +14,14 @@ end
function _print_graph_stats(instance::Instance, graph::Graph)::Nothing function _print_graph_stats(instance::Instance, graph::Graph)::Nothing
@info @sprintf(" %12d time periods", instance.time) @info @sprintf("%12d time periods", instance.time)
@info @sprintf(" %12d process nodes", length(graph.process_nodes)) @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 (plant)", length(graph.plant_shipping_nodes))
@info @sprintf( @info @sprintf(
" %12d shipping nodes (collection)", "%12d shipping nodes (collection)",
length(graph.collection_shipping_nodes) length(graph.collection_shipping_nodes)
) )
@info @sprintf(" %12d arcs", length(graph.arcs)) @info @sprintf("%12d arcs", length(graph.arcs))
return return
end end

@ -13,6 +13,7 @@ function products_report(solution; marginal_costs = true)::DataFrame
df."longitude (deg)" = Float64[] df."longitude (deg)" = Float64[]
df."year" = Int[] df."year" = Int[]
df."amount (tonne)" = Float64[] df."amount (tonne)" = Float64[]
df."amount disposed (tonne)" = Float64[]
df."marginal cost (\$/tonne)" = Float64[] df."marginal cost (\$/tonne)" = Float64[]
T = length(solution["Energy"]["Plants (GJ)"]) T = length(solution["Energy"]["Plants (GJ)"])
for (prod_name, prod_dict) in solution["Products"] 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) latitude = round(location_dict["Latitude (deg)"], digits = 6)
longitude = round(location_dict["Longitude (deg)"], digits = 6) longitude = round(location_dict["Longitude (deg)"], digits = 6)
amount = location_dict["Amount (tonne)"][year] amount = location_dict["Amount (tonne)"][year]
amount_disposed = location_dict["Dispose (tonne)"][year]
push!( push!(
df, df,
[ [
@ -32,6 +34,7 @@ function products_report(solution; marginal_costs = true)::DataFrame
year, year,
amount, amount,
marginal_cost, marginal_cost,
amount_disposed,
], ],
) )
end end

@ -14,6 +14,9 @@
"properties": { "properties": {
"time horizon (years)": { "time horizon (years)": {
"type": "number" "type": "number"
},
"distance metric": {
"type": "string"
} }
}, },
"required": [ "required": [
@ -169,6 +172,12 @@
}, },
"initial amounts": { "initial amounts": {
"$ref": "#/definitions/InitialAmount" "$ref": "#/definitions/InitialAmount"
},
"disposal limit (tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"disposal cost ($/tonne)": {
"$ref": "#/definitions/TimeSeries"
} }
}, },
"required": [ "required": [

@ -0,0 +1,19 @@
name = "RELOGT"
uuid = "a6dae211-05d8-42ed-9081-b88c982fc90a"
authors = ["Alinson S. Xavier <git@axavier.org>"]
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"

@ -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
]
}
}
}
}
}
}
}

BIN
test/fixtures/s1.zip vendored

Binary file not shown.

@ -1,39 +0,0 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -1,53 +0,0 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -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

@ -1,86 +0,0 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -1,37 +0,0 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -1,13 +0,0 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -1,61 +0,0 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -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

@ -1,21 +0,0 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -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

@ -0,0 +1,40 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -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

@ -0,0 +1,54 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -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

@ -0,0 +1,87 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -0,0 +1,40 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -0,0 +1,13 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -0,0 +1,70 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
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

@ -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
Loading…
Cancel
Save