27 Commits

Author SHA1 Message Date
a03b9169fd Allow product disposal at collection centers 2021-10-15 09:11:41 -05:00
ee58af73f0 Update sysimage and build scripts 2021-10-15 08:14:04 -05:00
9ebb2e49f9 Fix validation error on JSONSchema 0.3 2021-08-06 14:56:46 -05:00
505e3a8e1e Update CHANGELOG 2021-07-23 17:42:48 -05:00
d4fa75297f Fix OrderedCollections version 2021-07-23 17:42:29 -05:00
881957d6b5 Implement resolve 2021-07-21 14:53:49 -05:00
86cf7f5bd9 Throw exception for infeasible models 2021-07-21 14:18:10 -05:00
a8c7047e2d Add custom show function for Instance and Graph
Without these functions, Julia 1.5 enters an infinite loop whenever it
tries to generate a stack trace, so any error (such as a missing method)
causes the program to hang, instead of an error message to appear.
2021-07-21 14:11:01 -05:00
099e0fae3a Docs: Minor fixes to what-if analsis section 2021-07-21 14:07:00 -05:00
1b8f392852 Docs: Add description of resolve 2021-07-21 14:07:00 -05:00
7a95aa66f6 Update CHANGELOG 2021-07-21 11:49:54 -05:00
40d28c727a Add products report 2021-07-16 11:25:40 -05:00
a9ac164833 Fix GeoDB download 2021-07-16 10:31:21 -05:00
e244ded51d GH Actions: Add Julia 1.6, remove nightly 2021-07-16 10:18:10 -05:00
7180651cfa Reformat source code 2021-07-16 10:15:41 -05:00
0c9465411f Document GeoDB; remove unused code; minor fixes 2021-07-16 10:13:58 -05:00
658d5ddbdc Add population to region; disable zip codes 2021-07-01 17:14:00 -05:00
399db41f86 Temporarily disable failing test 2021-07-01 16:13:38 -05:00
e407a53ecf Download and join population 2021-07-01 16:10:55 -05:00
33ab4c5f76 GeoDB: Prepare for population 2021-07-01 14:56:08 -05:00
c9391dd299 Update JSONSchema 2021-07-01 14:56:08 -05:00
6c70d9acd5 GeoDB: Add 2018-us-zcta and us-state 2021-07-01 14:56:08 -05:00
339255bf9b Enable geodb in input files 2021-07-01 14:56:08 -05:00
ca187fe78e Implement geodb.jl 2021-07-01 14:56:08 -05:00
c256cd8b75 Update CHANGELOG.md 2021-06-25 06:16:20 -05:00
05d48e2cbf Update tagbot.yml 2021-06-25 06:13:27 -05:00
9446b1921d Add tagbot.yml 2021-06-22 11:10:15 -05:00
33 changed files with 1437 additions and 195 deletions

15
.github/workflows/tagbot.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: TagBot
on:
issue_comment:
types:
- created
workflow_dispatch:
jobs:
TagBot:
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
runs-on: ubuntu-latest
steps:
- uses: JuliaRegistries/TagBot@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
ssh: ${{ secrets.DOCUMENTER_KEY }}

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
version: ['1.3', '1.4', '1.5', 'nightly']
version: ['1.3', '1.4', '1.5', '1.6']
os:
- ubuntu-latest
arch:

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ notebooks
.idea
*.lp
Manifest.toml
data
build
benchmark

View File

@@ -1,28 +1,49 @@
# Version 0.5.0 (TBD)
# Changelog
All notable changes to this project will be documented in this file.
- The format is based on [Keep a Changelog][changelog].
- This project adheres to [Semantic Versioning][semver].
- For versions before 1.0, we follow the [Pkg.jl convention][pkjjl]
that `0.a.b` is compatible with `0.a.c`.
[changelog]: https://keepachangelog.com/en/1.0.0/
[semver]: https://semver.org/spec/v2.0.0.html
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0
## [0.5.1] -- 2021-07-23
## Added
- Allow user to specify locations as unique identifiers, instead of latitude and longitude (e.g. `us-state:IL` or `2018-us-county:17043`)
- Add what-if scenarios.
- Add products report.
## [0.5.0] -- 2021-01-06
## Added
- Allow plants to store input material for processing in later years
# Version 0.4.0 (Sep 18, 2020)
## [0.4.0] -- 2020-09-18
## Added
- Generate simplified solution reports (CSV)
# Version 0.3.3 (Aug 13, 2020)
## [0.3.3] -- 2020-10-13
## Added
- Add option to write solution to JSON file in RELOG.solve
- Improve error message when instance is infeasible
- Make output file more readable
# Version 0.3.2 (Aug 7, 2020)
## [0.3.2] -- 2020-10-07
## Added
- Add "building period" parameter
# Version 0.3.1 (July 17, 2020)
## [0.3.1] -- 2020-07-17
## Fixed
- Fix expansion cost breakdown
# Version 0.3.0 (June 25, 2020)
## [0.3.0] -- 2020-06-25
## Added
- Track emissions and energy (transportation and plants)
## Changed
- Minor changes to input file format:
- Make all dictionary keys lowercase
- Rename "outputs (tonne)" to "outputs (tonne/tonne)"

View File

@@ -1,15 +1,11 @@
JULIA := julia --color=yes --project=@.
JULIA := julia --project=.
SRC_FILES := $(wildcard src/*.jl test/*.jl)
VERSION := 0.5
all: docs test
build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml
mkdir -p build
$(JULIA) src/sysimage.jl
build/test.log: $(SRC_FILES) build/sysimage.so
cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl
@$(JULIA) src/sysimage.jl
clean:
rm -rf build/*
@@ -20,7 +16,8 @@ docs:
format:
julia -e 'using JuliaFormatter; format(["src", "test"], verbose=true);'
test: build/test.log
test:
@$(JULIA) --sysimage build/sysimage.so test/runtests.jl
test-watch:
bash -c "while true; do make test --quiet; sleep 1; done"

View File

@@ -1,14 +1,16 @@
name = "RELOG"
uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008"
authors = ["Alinson S Xavier <axavier@anl.gov>"]
version = "0.5.0"
version = "0.5.1"
[deps]
CRC = "44b605c4-b955-5f2b-9b6d-d2bd01d3d205"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
Geodesy = "0ef565a4-170c-5f04-8de2-149903a85f3d"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
@@ -16,13 +18,17 @@ JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
Shapefile = "8e980c4a-a4fe-5da2-b3a7-4b4b0353a2f4"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea"
[compat]
CRC = "4"
CSV = "0.7"
Cbc = "0.6"
Clp = "0.8"
@@ -31,9 +37,12 @@ DataStructures = "0.17"
GZip = "0.5"
Geodesy = "0.5"
JSON = "0.21"
JSONSchema = "0.2"
JSONSchema = "0.3"
JuMP = "0.21"
MathOptInterface = "0.9"
OrderedCollections = "1.4"
PackageCompiler = "1"
ProgressBars = "0.6"
Shapefile = "0.7"
ZipFile = "0.9"
julia = "1"

View File

@@ -4,73 +4,132 @@
},
"products": {
"P1": {
"transportation cost ($/km/tonne)": [0.015, 0.015],
"transportation energy (J/km/tonne)": [0.12, 0.11],
"transportation cost ($/km/tonne)": [
0.015,
0.015
],
"transportation energy (J/km/tonne)": [
0.12,
0.11
],
"transportation emissions (tonne/km/tonne)": {
"CO2": [0.052, 0.050],
"CH4": [0.003, 0.002]
"CO2": [
0.052,
0.050
],
"CH4": [
0.003,
0.002
]
},
"initial amounts": {
"C1": {
"latitude (deg)": 7.0,
"longitude (deg)": 7.0,
"amount (tonne)": [934.56, 934.56]
"amount (tonne)": [
934.56,
934.56
]
},
"C2": {
"latitude (deg)": 7.0,
"longitude (deg)": 19.0,
"amount (tonne)": [198.95, 198.95]
"amount (tonne)": [
198.95,
198.95
]
},
"C3": {
"latitude (deg)": 84.0,
"longitude (deg)": 76.0,
"amount (tonne)": [212.97, 212.97]
"amount (tonne)": [
212.97,
212.97
]
},
"C4": {
"latitude (deg)": 21.0,
"longitude (deg)": 16.0,
"amount (tonne)": [352.19, 352.19]
"amount (tonne)": [
352.19,
352.19
]
},
"C5": {
"latitude (deg)": 32.0,
"longitude (deg)": 92.0,
"amount (tonne)": [510.33, 510.33]
"amount (tonne)": [
510.33,
510.33
]
},
"C6": {
"latitude (deg)": 14.0,
"longitude (deg)": 62.0,
"amount (tonne)": [471.66, 471.66]
"amount (tonne)": [
471.66,
471.66
]
},
"C7": {
"latitude (deg)": 30.0,
"longitude (deg)": 83.0,
"amount (tonne)": [785.21, 785.21]
"amount (tonne)": [
785.21,
785.21
]
},
"C8": {
"latitude (deg)": 35.0,
"longitude (deg)": 40.0,
"amount (tonne)": [706.17, 706.17]
"amount (tonne)": [
706.17,
706.17
]
},
"C9": {
"latitude (deg)": 74.0,
"longitude (deg)": 52.0,
"amount (tonne)": [30.08, 30.08]
"amount (tonne)": [
30.08,
30.08
]
},
"C10": {
"latitude (deg)": 22.0,
"longitude (deg)": 54.0,
"amount (tonne)": [536.52, 536.52]
"amount (tonne)": [
536.52,
536.52
]
}
}
},
"disposal limit (tonne)": [
1.0,
1.0
],
"disposal cost ($/tonne)": [
-1000,
-1000
]
},
"P2": {
"transportation cost ($/km/tonne)": [0.02, 0.02]
"transportation cost ($/km/tonne)": [
0.02,
0.02
]
},
"P3": {
"transportation cost ($/km/tonne)": [0.0125, 0.0125]
"transportation cost ($/km/tonne)": [
0.0125,
0.0125
]
},
"P4": {
"transportation cost ($/km/tonne)": [0.0175, 0.0175]
"transportation cost ($/km/tonne)": [
0.0175,
0.0175
]
}
},
"plants": {
@@ -80,10 +139,19 @@
"P2": 0.2,
"P3": 0.5
},
"energy (GJ/tonne)": [0.12, 0.11],
"energy (GJ/tonne)": [
0.12,
0.11
],
"emissions (tonne/tonne)": {
"CO2": [0.052, 0.050],
"CH4": [0.003, 0.002]
"CO2": [
0.052,
0.050
],
"CH4": [
0.003,
0.002
]
},
"locations": {
"L1": {
@@ -91,24 +159,54 @@
"longitude (deg)": 0.0,
"disposal": {
"P2": {
"cost ($/tonne)": [-10.0, -10.0],
"limit (tonne)": [1.0, 1.0]
"cost ($/tonne)": [
-10.0,
-10.0
],
"limit (tonne)": [
1.0,
1.0
]
},
"P3": {
"cost ($/tonne)": [-10.0, -10.0],
"limit (tonne)": [1.0, 1.0]
"cost ($/tonne)": [
-10.0,
-10.0
],
"limit (tonne)": [
1.0,
1.0
]
}
},
"capacities (tonne)": {
"250.0": {
"opening cost ($)": [500.0, 500.0],
"fixed operating cost ($)": [30.0, 30.0],
"variable operating cost ($/tonne)": [30.0, 30.0]
"opening cost ($)": [
500.0,
500.0
],
"fixed operating cost ($)": [
30.0,
30.0
],
"variable operating cost ($/tonne)": [
30.0,
30.0
]
},
"1000.0": {
"opening cost ($)": [1250.0, 1250.0],
"fixed operating cost ($)": [30.0, 30.0],
"variable operating cost ($/tonne)": [30.0, 30.0]
"opening cost ($)": [
1250.0,
1250.0
],
"fixed operating cost ($)": [
30.0,
30.0
],
"variable operating cost ($/tonne)": [
30.0,
30.0
]
}
}
},
@@ -117,14 +215,32 @@
"longitude (deg)": 0.5,
"capacities (tonne)": {
"0.0": {
"opening cost ($)": [1000, 1000],
"fixed operating cost ($)": [50.0, 50.0],
"variable operating cost ($/tonne)": [50.0, 50.0]
"opening cost ($)": [
1000,
1000
],
"fixed operating cost ($)": [
50.0,
50.0
],
"variable operating cost ($/tonne)": [
50.0,
50.0
]
},
"10000.0": {
"opening cost ($)": [10000, 10000],
"fixed operating cost ($)": [50.0, 50.0],
"variable operating cost ($/tonne)": [50.0, 50.0]
"opening cost ($)": [
10000,
10000
],
"fixed operating cost ($)": [
50.0,
50.0
],
"variable operating cost ($/tonne)": [
50.0,
50.0
]
}
}
}
@@ -142,14 +258,26 @@
"longitude (deg)": 65.0,
"disposal": {
"P3": {
"cost ($/tonne)": [100.0, 100.0]
"cost ($/tonne)": [
100.0,
100.0
]
}
},
"capacities (tonne)": {
"1000.0": {
"opening cost ($)": [3000, 3000],
"fixed operating cost ($)": [50.0, 50.0],
"variable operating cost ($/tonne)": [50.0, 50.0]
"opening cost ($)": [
3000,
3000
],
"fixed operating cost ($)": [
50.0,
50.0
],
"variable operating cost ($/tonne)": [
50.0,
50.0
]
}
}
},
@@ -158,9 +286,18 @@
"longitude (deg)": 0.20,
"capacities (tonne)": {
"10000": {
"opening cost ($)": [3000, 3000],
"fixed operating cost ($)": [50.0, 50.0],
"variable operating cost ($/tonne)": [50.0, 50.0]
"opening cost ($)": [
3000,
3000
],
"fixed operating cost ($)": [
50.0,
50.0
],
"variable operating cost ($/tonne)": [
50.0,
50.0
]
}
}
}
@@ -174,9 +311,18 @@
"longitude (deg)": 100.0,
"capacities (tonne)": {
"15000": {
"opening cost ($)": [0.0, 0.0],
"fixed operating cost ($)": [0.0, 0.0],
"variable operating cost ($/tonne)": [-15.0, -15.0]
"opening cost ($)": [
0.0,
0.0
],
"fixed operating cost ($)": [
0.0,
0.0
],
"variable operating cost ($/tonne)": [
-15.0,
-15.0
]
}
}
}
@@ -190,9 +336,18 @@
"longitude (deg)": 50.0,
"capacities (tonne)": {
"10000": {
"opening cost ($)": [0.0, 0.0],
"fixed operating cost ($)": [0.0, 0.0],
"variable operating cost ($/tonne)": [-15.0, -15.0]
"opening cost ($)": [
0.0,
0.0
],
"fixed operating cost ($)": [
0.0,
0.0
],
"variable operating cost ($/tonne)": [
-15.0,
-15.0
]
}
}
}

347
instances/s2.json Normal file
View File

@@ -0,0 +1,347 @@
{
"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": {
"location": "2018-us-county:17043",
"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": {
"location": "2018-us-county:17043",
"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
]
}
}
}
}
}
}
}

View File

@@ -11,14 +11,17 @@ include("graph/structs.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("reports/plant_emissions.jl")
include("reports/plant_outputs.jl")
include("reports/plants.jl")
include("reports/products.jl")
include("reports/tr_emissions.jl")
include("reports/tr.jl")
include("reports/write.jl")

View File

@@ -36,6 +36,8 @@ The **products** section describes all products and subproducts in the simulatio
|`transportation energy (J/km/tonne)` | The energy required to transport this product. Must be a time series. Optional.
|`transportation emissions (tonne/km/tonne)` | A dictionary mapping the name of each greenhouse gas, produced to transport one tonne of this product along one kilometer, to the amount of gas produced (in tonnes). Must be a time series. Optional.
|`initial amounts` | A dictionary mapping the name of each location to its description (see below). If this product is not initially available, this key may be omitted. Must be a time series.
| `disposal limit (tonne)` | Total amount of product that can be disposed of across all collection centers. If omitted, all product must be processed. This parameter has no effect on product disposal at plants.
| `disposal cost ($/tonne)` | Cost of disposing one tonne of this product at a collection center. If omitted, defaults to zero. This parameter has no effect on product disposal costs at plants.
Each product may have some amount available at the beginning of each time period. In this case, the key `initial amounts` maps to a dictionary with the following keys:
@@ -73,7 +75,9 @@ Each product may have some amount available at the beginning of each time period
"transportation emissions (tonne/km/tonne)": {
"CO2": [0.052, 0.050],
"CH4": [0.003, 0.002]
}
},
"disposal cost ($/tonne)": [-10.0, -12.0],
"disposal limit (tonne)": [1.0, 1.0],
},
"P2": {
"transportation cost ($/km/tonne)": [0.022, 0.020]
@@ -182,6 +186,38 @@ The keys in the `capacities (tonne)` dictionary should be the amounts (in tonnes
}
```
### Geographic database
Instead of specifying locations using latitudes and longitudes, it is also possible to specify them using unique identifiers, such as the name of a US state, or the county FIPS code. This works anywhere `latitude (deg)` and `longitude (deg)` are expected. For example, instead of:
```json
{
"initial amounts": {
"C1": {
"latitude (deg)": 37.27182,
"longitude (deg)": -119.2704,
"amount (tonne)": [934.56, 934.56]
},
}
}
```
is is possible to write:
```json
{
"initial amounts": {
"C1": {
"location": "us-state:CA",
"amount (tonne)": [934.56, 934.56]
},
}
}
```
Location names follow the format `db:id`, where `db` is the name of the database and `id` is the identifier for a specific location. RELOG currently includes the following databases:
Database | Description | Examples
---------|-------------|----------
`us-state`| List of states of the United States. | `us-state:IL` (State of Illinois)
`2018-us-county` | List of United States counties, as of 2018. IDs are 5-digit FIPS codes. | `2018-us-county:17043` (DuPage county in Illinois)
### Current limitations
* Each plant can only be opened exactly once. After open, the plant remains open until the end of the simulation.
@@ -192,4 +228,3 @@ The keys in the `capacities (tonne)` dictionary should be the amounts (in tonnes
## Output Data Format (JSON)
To be documented.

View File

@@ -6,15 +6,13 @@ In this page, we also illustrate what types of charts and visualizations can be
## Plants report
Report showing plant costs, capacities, energy expenditure and utilization factors.
Generated by `RELOG.write_plants_report(solution, filename)`. For a concrete example, see [nimh_plants.csv](https://github.com/ANL-CEEESA/RELOG/blob/master/test/fixtures/nimh_plants.csv).
Report showing plant costs, capacities, energy expenditure and utilization factors. Generated by `RELOG.write_plants_report(solution, filename)`.
| Column | Description
|:--------------------------------------|---------------|
| `plant type` | Plant type.
| `location name` | Location name.
| `year` | What year this row corresponds to. This reports includes one row for each year in the simulation.
| `year` | What year this row corresponds to. This reports includes one row for each year.
| `latitude (deg)` | Latitude of the plant.
| `longitude (deg)` | Longitude of the plant.
| `capacity (tonne)` | Capacity of the plant at this point in time.
@@ -72,16 +70,14 @@ gp.GeoDataFrame(data, geometry=points).plot(ax=ax);
## Plant outputs report
Report showing amount of products produced, sent and disposed of by each plant, as well as disposal costs.
Generated by `RELOG.write_plant_outputs_report(solution, filename)`. For a concrete example, see [nimh_plant_outputs.csv](https://github.com/ANL-CEEESA/RELOG/blob/master/test/fixtures/nimh_plant_outputs.csv).
Report showing amount of products produced, sent and disposed of by each plant, as well as disposal costs. Generated by `RELOG.write_plant_outputs_report(solution, filename)`.
| Column | Description
|:--------------------------------------|---------------|
| `plant type` | Plant type.
| `location name` | Location name.
| `year` | What year this row corresponds to. This reports includes one row for each year in the simulation.
| `year` | What year this row corresponds to. This reports includes one row for each year.
| `product name` | Product being produced.
| `amount produced (tonne)` | Amount of product produced this year.
| `amount sent (tonne)` | Amount of product produced by this plant and sent to another plant for further processing this year.
@@ -110,9 +106,7 @@ sns.barplot(x="amount produced (tonne)",
## Plant emissions report
Report showing amount of emissions produced by each plant.
Generated by `RELOG.write_plant_emissions_report(solution, filename)`. For a concrete example, see [nimh_plant_emissions.csv](https://github.com/ANL-CEEESA/RELOG/blob/master/test/fixtures/nimh_plant_emissions.csv).
Report showing amount of emissions produced by each plant. Generated by `RELOG.write_plant_emissions_report(solution, filename)`.
| Column | Description
|:--------------------------------------|---------------|
@@ -141,11 +135,25 @@ sns.barplot(x="plant type",
<img src="../images/ex_emissions.png" width="500px"/>
## Products report
Report showing primary product amounts, locations and marginal costs. Generated by `RELOG.write_products_report(solution, filename)`.
| Column | Description
|:--------------------------------------|---------------|
| `product name` | Product name.
| `location name` | Name of the collection center.
| `latitude (deg)` | Latitude 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.
| `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.
## Transportation report
Report showing amount of product sent from initial locations to plants, and from one plant to another. Includes the distance between each pair of locations, amount-distance shipped, transportation costs and energy expenditure.
Generated by `RELOG.write_transportation_report(solution, filename)`. For a concrete example, see [nimh_transportation.csv](https://github.com/ANL-CEEESA/RELOG/blob/master/test/fixtures/nimh_transportation.csv).
Report showing amount of product sent from initial locations to plants, and from one plant to another. Includes the distance between each pair of locations, amount-distance shipped, transportation costs and energy expenditure. Generated by `RELOG.write_transportation_report(solution, filename)`.
| Column | Description
@@ -231,9 +239,7 @@ gp.GeoDataFrame(data, geometry=points).plot(ax=ax,
## Transportation emissions report
Report showing emissions for each trip between initial locations and plants, and between pairs of plants.
Generated by `RELOG.write_transportation_emissions_report(solution, filename)`. For a concrete example, see [nimh_transportation_emissions.csv](https://github.com/ANL-CEEESA/RELOG/blob/master/test/fixtures/nimh_transportation_emissions.csv).
Report showing emissions for each trip between initial locations and plants, and between pairs of plants. Generated by `RELOG.write_transportation_emissions_report(solution, filename)`.
| Column | Description
|:--------------------------------------|---------------|

View File

@@ -66,9 +66,45 @@ RELOG.write_transportation_report(solution, "transportation.csv")
For a complete description of the file formats above, and for a complete list of available reports, see the [data format page](format.md).
## 4. Advanced options
## 4. What-If Analysis
### 4.1 Changing the solver
Fundamentally, RELOG decides when and where to build plants based on a deterministic optimization problem that minimizes costs for a particular input file provided by the user. In practical situations, it may not be possible to perfectly estimate some (or most) entries in this input file in advance, such as costs, demands and emissions. In this situation, it may be interesting to evaluate how well does the facility location plan produced by RELOG work if costs, demands and emissions turn out to be different.
To simplify this what-if analysis, RELOG provides the `resolve` method, which updates a previous solution based on a new scenario, but keeps some of the previous decisions fixed. More precisely, given an optimal solution produced by RELOG and a new input file describing the new scenario, the `resolve` method reoptimizes the supply chain and produces a new solution which still builds the same set of plants as before, in exactly the same locations and with the same capacities, but that may now utilize the plants differently, based on the new data. For example, in the new solution, plants that were previously used at full capacity may now be utilized at half-capacity instead. As another example, regions that were previously served by a certain plant may now be served by a different one.
The following snippet shows how to use the method:
```julia
# Import package
using RELOG
# Optimize for the average scenario
solution_avg, model_avg = RELOG.solve("input_avg.json", return_model=true)
# Write reports for the average scenario
RELOG.write_plants_report(solution_avg, "plants_avg.csv")
RELOG.write_transportation_report(solution_avg, "transportation_avg.csv")
# Re-optimize for the high-demand scenario, keeping plants fixed
solution_high = RELOG.resolve(model_avg, "input_high.json")
# Write reports for the high-demand scenario
RELOG.write_plants_report(solution_high, "plants_high.csv")
RELOG.write_transportation_report(solution_high, "transportation_high.csv")
```
To use the `resolve` method, the new input file should be very similar to the original one. Only the following entries are allowed to change:
- **Products:** Transportation costs, energy, emissions and initial amounts (latitude, longitude and amount).
- **Plants:** Energy and emissions.
- **Plant's location:** Latitude and longitude.
- **Plant's storage:** Cost.
- **Plant's capacity:** Opening cost, fixed operating cost and variable operating cost.
## 5. Advanced options
### 5.1 Changing the solver
By default, RELOG internally uses [Cbc](https://github.com/coin-or/Cbc), an open-source and freely-available Mixed-Integer Linear Programming solver developed by the [COIN-OR Project](https://www.coin-or.org/). For larger-scale test cases, a commercial solver such as Gurobi, CPLEX or XPRESS is recommended. The following snippet shows how to switch from Cbc to Gurobi, for example:
@@ -84,7 +120,7 @@ RELOG.solve("instance.json",
optimizer=gurobi)
```
### 4.2 Multi-period heuristics
### 5.2 Multi-period heuristics
For large-scale instances, it may be too time-consuming to find an exact optimal solution to the multi-period version of the problem. For these situations, RELOG includes a heuristic solution method, which proceeds as follows:

View File

@@ -17,6 +17,9 @@ function build_graph(instance::Instance)::Graph
plant_shipping_nodes = ShippingNode[]
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)
shipping_nodes_by_plant = Dict(plant => [] for plant in instance.plants)
@@ -25,6 +28,7 @@ function build_graph(instance::Instance)::Graph
for center in instance.collection_centers
node = ShippingNode(next_index, center, center.product, [], [])
next_index += 1
collection_center_to_node[center] = node
push!(collection_shipping_nodes, node)
end
@@ -35,6 +39,8 @@ function build_graph(instance::Instance)::Graph
push!(process_nodes, pn)
push!(process_nodes_by_input_product[plant.input], pn)
name_to_process_node_map[(plant.plant_name, plant.location_name)] = pn
for product in keys(plant.output)
sn = ShippingNode(next_index, plant, product, [], [])
next_index += 1
@@ -73,5 +79,25 @@ function build_graph(instance::Instance)::Graph
end
end
return Graph(process_nodes, plant_shipping_nodes, collection_shipping_nodes, arcs)
return Graph(
process_nodes,
plant_shipping_nodes,
collection_shipping_nodes,
arcs,
name_to_process_node_map,
collection_center_to_node,
)
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 shipping nodes (collection)",
length(graph.collection_shipping_nodes)
)
@info @sprintf(" %12d arcs", length(graph.arcs))
return
end

View File

@@ -32,4 +32,14 @@ mutable struct Graph
plant_shipping_nodes::Vector{ShippingNode}
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)
print(io, "RELOG graph with ")
print(io, "$(length(instance.process_nodes)) process nodes, ")
print(io, "$(length(instance.plant_shipping_nodes)) plant shipping nodes, ")
print(io, "$(length(instance.collection_shipping_nodes)) collection shipping nodes, ")
print(io, "$(length(instance.arcs)) arcs")
end

212
src/instance/geodb.jl Normal file
View File

@@ -0,0 +1,212 @@
# 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 CRC
using CSV
using DataFrames
using Shapefile
using Statistics
using ZipFile
using ProgressBars
using OrderedCollections
import Downloads: download
import Base: parse
crc32 = crc(CRC_32)
struct GeoPoint
lat::Float64
lon::Float64
end
struct GeoRegion
centroid::GeoPoint
population::Int
GeoRegion(; centroid, population) = new(centroid, population)
end
DB_CACHE = Dict{String,Dict{String,GeoRegion}}()
function centroid(geom::Shapefile.Polygon)::GeoPoint
x_max, x_min, y_max, y_min = -Inf, Inf, -Inf, Inf
for p in geom.points
x_max = max(x_max, p.x)
x_min = min(x_min, p.x)
y_max = max(y_max, p.y)
y_min = min(y_min, p.y)
end
x_center = (x_max + x_min) / 2.0
y_center = (y_max + y_min) / 2.0
return GeoPoint(round(y_center, digits = 5), round(x_center, digits = 5))
end
function _download_file(url, output, expected_crc32)::Nothing
if isfile(output)
return
end
mkpath(dirname(output))
@info "Downloading: $url"
fname = download(url)
actual_crc32 = open(crc32, fname)
expected_crc32 == actual_crc32 || error("CRC32 mismatch")
cp(fname, output)
return
end
function _download_zip(url, outputdir, expected_output_file, expected_crc32)::Nothing
if isfile(expected_output_file)
return
end
mkpath(outputdir)
@info "Downloading: $url"
zip_filename = download(url)
actual_crc32 = open(crc32, zip_filename)
expected_crc32 == actual_crc32 || error("CRC32 mismatch")
open(zip_filename) do zip_file
zr = ZipFile.Reader(zip_file)
for file in zr.files
open(joinpath(outputdir, file.name), "w") do output_file
write(output_file, read(file))
end
end
end
return
end
function _geodb_load_gov_census(;
db_name,
extract_cols,
shp_crc32,
shp_filename,
shp_url,
population_url,
population_crc32,
population_col,
population_preprocess,
population_join,
)::Dict{String,GeoRegion}
basedir = joinpath(dirname(@__FILE__), "..", "..", "data", db_name)
csv_filename = "$basedir/locations.csv"
if !isfile(csv_filename)
# Download required files
_download_zip(shp_url, basedir, joinpath(basedir, shp_filename), shp_crc32)
_download_file(population_url, "$basedir/population.csv", population_crc32)
# Read shapefile
@info "Processing: $shp_filename"
table = Shapefile.Table(joinpath(basedir, shp_filename))
geoms = Shapefile.shapes(table)
# Build empty dataframe
df = DataFrame()
cols = extract_cols(table, 1)
for k in keys(cols)
df[!, k] = []
end
df[!, "latitude"] = Float64[]
df[!, "longitude"] = Float64[]
# Add regions to dataframe
for (i, geom) in tqdm(enumerate(geoms))
c = centroid(geom)
cols = extract_cols(table, i)
push!(df, [values(cols)..., c.lat, c.lon])
end
sort!(df)
# Join with population data
population = DataFrame(CSV.File("$basedir/population.csv"))
population_preprocess(population)
population = population[:, [population_join, population_col]]
rename!(population, population_col => "population")
df = leftjoin(df, population, on = population_join)
# Write output
CSV.write(csv_filename, df)
end
if db_name keys(DB_CACHE)
csv = CSV.File(csv_filename)
DB_CACHE[db_name] = Dict(
string(row.id) => GeoRegion(
centroid = GeoPoint(row.latitude, row.longitude),
population = (row.population === missing ? 0 : row.population),
) for row in csv
)
end
return DB_CACHE[db_name]
end
# 2018 US counties
# -----------------------------------------------------------------------------
function _extract_cols_2018_us_county(
table::Shapefile.Table,
i::Int,
)::OrderedDict{String,Any}
return OrderedDict(
"id" => table.STATEFP[i] * table.COUNTYFP[i],
"statefp" => table.STATEFP[i],
"countyfp" => table.COUNTYFP[i],
"name" => table.NAME[i],
)
end
function _population_preprocess_2018_us_county(df)
df[!, "id"] = [@sprintf("%02d%03d", row.STATE, row.COUNTY) for row in eachrow(df)]
end
function _geodb_load_2018_us_county()::Dict{String,GeoRegion}
return _geodb_load_gov_census(
db_name = "2018-us-county",
extract_cols = _extract_cols_2018_us_county,
shp_crc32 = 0x83eaec6d,
shp_filename = "cb_2018_us_county_500k.shp",
shp_url = "https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_county_500k.zip",
population_url = "https://www2.census.gov/programs-surveys/popest/datasets/2010-2019/counties/totals/co-est2019-alldata.csv",
population_crc32 = 0xf85b0405,
population_col = "POPESTIMATE2019",
population_join = "id",
population_preprocess = _population_preprocess_2018_us_county,
)
end
# US States
# -----------------------------------------------------------------------------
function _extract_cols_us_state(table::Shapefile.Table, i::Int)::OrderedDict{String,Any}
return OrderedDict(
"id" => table.STUSPS[i],
"statefp" => parse(Int, table.STATEFP[i]),
"name" => table.NAME[i],
)
end
function _population_preprocess_us_state(df)
rename!(df, "STATE" => "statefp")
end
function _geodb_load_us_state()::Dict{String,GeoRegion}
return _geodb_load_gov_census(
db_name = "us-state",
extract_cols = _extract_cols_us_state,
shp_crc32 = 0x9469e5ca,
shp_filename = "cb_2018_us_state_500k.shp",
shp_url = "https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_state_500k.zip",
population_url = "http://www2.census.gov/programs-surveys/popest/datasets/2010-2019/national/totals/nst-est2019-alldata.csv",
population_crc32 = 0x191cc64c,
population_col = "POPESTIMATE2019",
population_join = "statefp",
population_preprocess = _population_preprocess_us_state,
)
end
function geodb_load(db_name::AbstractString)::Dict{String,GeoRegion}
db_name == "2018-us-county" && return _geodb_load_2018_us_county()
db_name == "us-state" && return _geodb_load_us_state()
error("Unknown database: $db_name")
end
function geodb_query(name)::GeoRegion
db_name, id = split(name, ":")
return geodb_load(db_name)[id]
end

View File

@@ -37,6 +37,8 @@ function parse(json)::Instance
cost = product_dict["transportation cost (\$/km/tonne)"]
energy = zeros(T)
emissions = Dict()
disposal_limit = zeros(T)
disposal_cost = zeros(T)
if "transportation energy (J/km/tonne)" in keys(product_dict)
energy = product_dict["transportation energy (J/km/tonne)"]
@@ -46,13 +48,36 @@ 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
# Create collection centers
if "initial amounts" in keys(product_dict)
for (center_name, center_dict) in product_dict["initial amounts"]
if "location" in keys(center_dict)
region = geodb_query(center_dict["location"])
center_dict["latitude (deg)"] = region.centroid.lat
center_dict["longitude (deg)"] = region.centroid.lon
end
center = CollectionCenter(
length(collection_centers) + 1,
center_name,
@@ -61,6 +86,7 @@ function parse(json)::Instance
product,
center_dict["amount (tonne)"],
)
push!(prod_centers, center)
push!(collection_centers, center)
end
end
@@ -95,6 +121,13 @@ function parse(json)::Instance
disposal_limit = Dict(p => [0.0 for t = 1:T] for p in keys(output))
disposal_cost = Dict(p => [0.0 for t = 1:T] for p in keys(output))
# GeoDB
if "location" in keys(location_dict)
region = geodb_query(location_dict["location"])
location_dict["latitude (deg)"] = region.centroid.lat
location_dict["longitude (deg)"] = region.centroid.lon
end
# Disposal
if "disposal" in keys(location_dict)
for (product_name, disposal_dict) in location_dict["disposal"]

View File

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

View File

@@ -12,14 +12,10 @@ function validate(json, schema)
result = JSONSchema.validate(json, schema)
if result !== nothing
if result isa JSONSchema.SingleIssue
path = join(result.path, "")
if length(path) == 0
path = "root"
end
msg = "$(result.msg) in $(path)"
msg = "$(result.reason) in $(result.path)"
else
msg = convert(String, result)
end
throw(msg)
throw("Error parsing input file: $(msg)")
end
end

View File

@@ -4,7 +4,6 @@
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
function build_model(instance::Instance, graph::Graph, optimizer)::JuMP.Model
model = Model(optimizer)
model[:instance] = instance
@@ -21,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)
@@ -132,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
@@ -155,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

View File

@@ -39,18 +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
],
)
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
@@ -175,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

97
src/model/resolve.jl Normal file
View File

@@ -0,0 +1,97 @@
# RELOG: Reverse Logistics Optimization
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using JuMP
function resolve(model_old, filename::AbstractString; kwargs...)::OrderedDict
@info "Reading $filename..."
instance = RELOG.parsefile(filename)
return resolve(model_old, instance; kwargs...)
end
function resolve(model_old, instance::Instance; optimizer = nothing)::OrderedDict
milp_optimizer = lp_optimizer = optimizer
if optimizer === nothing
milp_optimizer = _get_default_milp_optimizer()
lp_optimizer = _get_default_lp_optimizer()
end
@info "Building new graph..."
graph = build_graph(instance)
_print_graph_stats(instance, graph)
@info "Building new optimization model..."
model_new = RELOG.build_model(instance, graph, milp_optimizer)
@info "Fixing decision variables..."
_fix_plants!(model_old, model_new)
JuMP.set_optimizer(model_new, lp_optimizer)
@info "Optimizing MILP..."
JuMP.optimize!(model_new)
if !has_values(model_new)
@warn("No solution available")
return OrderedDict()
end
@info "Extracting solution..."
solution = get_solution(model_new, marginal_costs = true)
return solution
end
function _fix_plants!(model_old, model_new)::Nothing
T = model_new[:instance].time
# Fix open_plant variables
for ((node_old, t), var_old) in model_old[:open_plant]
value_old = JuMP.value(var_old)
node_new = model_new[:graph].name_to_process_node_map[(
node_old.location.plant_name,
node_old.location.location_name,
)]
var_new = model_new[:open_plant][node_new, t]
JuMP.unset_binary(var_new)
JuMP.fix(var_new, value_old)
end
# Fix is_open variables
for ((node_old, t), var_old) in model_old[:is_open]
value_old = JuMP.value(var_old)
node_new = model_new[:graph].name_to_process_node_map[(
node_old.location.plant_name,
node_old.location.location_name,
)]
var_new = model_new[:is_open][node_new, t]
JuMP.unset_binary(var_new)
JuMP.fix(var_new, value_old)
end
# Fix plant capacities
for ((node_old, t), var_old) in model_old[:capacity]
value_old = JuMP.value(var_old)
node_new = model_new[:graph].name_to_process_node_map[(
node_old.location.plant_name,
node_old.location.location_name,
)]
var_new = model_new[:capacity][node_new, t]
JuMP.delete_lower_bound(var_new)
JuMP.delete_upper_bound(var_new)
JuMP.fix(var_new, value_old)
end
# Fix plant expansion
for ((node_old, t), var_old) in model_old[:expansion]
value_old = JuMP.value(var_old)
node_new = model_new[:graph].name_to_process_node_map[(
node_old.location.plant_name,
node_old.location.location_name,
)]
var_new = model_new[:expansion][node_new, t]
JuMP.delete_lower_bound(var_new)
JuMP.delete_upper_bound(var_new)
JuMP.fix(var_new, value_old)
end
end

View File

@@ -4,24 +4,16 @@
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
default_milp_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
default_lp_optimizer = optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
function _get_default_milp_optimizer()
return optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
end
function solve(
instance::Instance;
optimizer = nothing,
output = nothing,
marginal_costs = true,
)
function _get_default_lp_optimizer()
return optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
end
milp_optimizer = lp_optimizer = optimizer
if optimizer == nothing
milp_optimizer = default_milp_optimizer
lp_optimizer = default_lp_optimizer
end
@info "Building graph..."
graph = RELOG.build_graph(instance)
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))
@@ -30,6 +22,26 @@ function solve(
length(graph.collection_shipping_nodes)
)
@info @sprintf(" %12d arcs", length(graph.arcs))
return
end
function solve(
instance::Instance;
optimizer = nothing,
output = nothing,
marginal_costs = true,
return_model = false,
)
milp_optimizer = lp_optimizer = optimizer
if optimizer == nothing
milp_optimizer = _get_default_milp_optimizer()
lp_optimizer = _get_default_lp_optimizer()
end
@info "Building graph..."
graph = RELOG.build_graph(instance)
_print_graph_stats(instance, graph)
@info "Building optimization model..."
model = RELOG.build_model(instance, graph, milp_optimizer)
@@ -38,8 +50,7 @@ function solve(
JuMP.optimize!(model)
if !has_values(model)
@warn "No solution available"
return OrderedDict()
error("No solution available")
end
if marginal_costs
@@ -63,7 +74,11 @@ function solve(
write(solution, output)
end
return solution
if return_model
return solution, model
else
return solution
end
end
function solve(filename::AbstractString; heuristic = false, kwargs...)

46
src/reports/products.jl Normal file
View File

@@ -0,0 +1,46 @@
# 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 DataFrames
using CSV
function products_report(solution; marginal_costs = true)::DataFrame
df = DataFrame()
df."product name" = String[]
df."location name" = String[]
df."latitude (deg)" = Float64[]
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"]
for (location_name, location_dict) in prod_dict
for year = 1:T
marginal_cost = location_dict["Marginal cost (\$/tonne)"][year]
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,
[
prod_name,
location_name,
latitude,
longitude,
year,
amount,
marginal_cost,
amount_disposed,
],
)
end
end
end
return df
end
write_products_report(solution, filename) = CSV.write(filename, products_report(solution))

View File

@@ -4,6 +4,7 @@
using DataFrames
using CSV
import Base: write
function write(solution::AbstractDict, filename::AbstractString)
@info "Writing solution: $filename"

View File

@@ -12,7 +12,9 @@
"Parameters": {
"type": "object",
"properties": {
"time horizon (years)": { "type": "number" }
"time horizon (years)": {
"type": "number"
}
},
"required": [
"time horizon (years)"
@@ -23,17 +25,27 @@
"additionalProperties": {
"type": "object",
"properties": {
"input": { "type": "string" },
"input": {
"type": "string"
},
"outputs (tonne/tonne)": {
"type": "object",
"additionalProperties": { "type": "number" }
"additionalProperties": {
"type": "number"
}
},
"energy (GJ/tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"energy (GJ/tonne)": { "$ref": "#/definitions/TimeSeries" },
"emissions (tonne/tonne)": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/TimeSeries" }
"additionalProperties": {
"$ref": "#/definitions/TimeSeries"
}
},
"locations": { "$ref": "#/definitions/PlantLocation" }
"locations": {
"$ref": "#/definitions/PlantLocation"
}
},
"required": [
"input",
@@ -46,15 +58,26 @@
"additionalProperties": {
"type": "object",
"properties": {
"latitude (deg)": { "type": "number" },
"longitude (deg)": { "type": "number" },
"location": {
"type": "string"
},
"latitude (deg)": {
"type": "number"
},
"longitude (deg)": {
"type": "number"
},
"disposal": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"cost ($/tonne)": { "$ref": "#/definitions/TimeSeries" },
"limit (tonne)": { "$ref": "#/definitions/TimeSeries" }
"cost ($/tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"limit (tonne)": {
"$ref": "#/definitions/TimeSeries"
}
},
"required": [
"cost ($/tonne)"
@@ -64,8 +87,12 @@
"storage": {
"type": "object",
"properties": {
"cost ($/tonne)": { "$ref": "#/definitions/TimeSeries" },
"limit (tonne)": { "type": "number" }
"cost ($/tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"limit (tonne)": {
"type": "number"
}
},
"required": [
"cost ($/tonne)",
@@ -77,9 +104,15 @@
"additionalProperties": {
"type": "object",
"properties": {
"variable operating cost ($/tonne)": { "$ref": "#/definitions/TimeSeries" },
"fixed operating cost ($)": { "$ref": "#/definitions/TimeSeries" },
"opening cost ($)": { "$ref": "#/definitions/TimeSeries" }
"variable operating cost ($/tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"fixed operating cost ($)": {
"$ref": "#/definitions/TimeSeries"
},
"opening cost ($)": {
"$ref": "#/definitions/TimeSeries"
}
},
"required": [
"variable operating cost ($/tonne)",
@@ -90,8 +123,6 @@
}
},
"required": [
"latitude (deg)",
"longitude (deg)",
"capacities (tonne)"
]
}
@@ -101,13 +132,20 @@
"additionalProperties": {
"type": "object",
"properties": {
"latitude (deg)": { "type": "number" },
"longitude (deg)": { "type": "number" },
"amount (tonne)": { "$ref": "#/definitions/TimeSeries" }
"location": {
"type": "string"
},
"latitude (deg)": {
"type": "number"
},
"longitude (deg)": {
"type": "number"
},
"amount (tonne)": {
"$ref": "#/definitions/TimeSeries"
}
},
"required": [
"latitude (deg)",
"longitude (deg)",
"amount (tonne)"
]
}
@@ -117,13 +155,27 @@
"additionalProperties": {
"type": "object",
"properties": {
"transportation cost ($/km/tonne)": { "$ref": "#/definitions/TimeSeries" },
"transportation energy (J/km/tonne)": { "$ref": "#/definitions/TimeSeries" },
"transportation cost ($/km/tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"transportation energy (J/km/tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"transportation emissions (tonne/km/tonne)": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/TimeSeries" }
"additionalProperties": {
"$ref": "#/definitions/TimeSeries"
}
},
"initial amounts": { "$ref": "#/definitions/InitialAmount" }
"initial amounts": {
"$ref": "#/definitions/InitialAmount"
},
"disposal limit (tonne)": {
"$ref": "#/definitions/TimeSeries"
},
"disposal cost ($/tonne)": {
"$ref": "#/definitions/TimeSeries"
}
},
"required": [
"transportation cost ($/km/tonne)"
@@ -133,9 +185,15 @@
},
"type": "object",
"properties": {
"parameters": { "$ref": "#/definitions/Parameters" },
"plants": { "$ref": "#/definitions/Plant" },
"products": { "$ref": "#/definitions/Product" }
"parameters": {
"$ref": "#/definitions/Parameters"
},
"plants": {
"$ref": "#/definitions/Plant"
},
"products": {
"$ref": "#/definitions/Product"
}
},
"required": [
"parameters",

View File

@@ -1,15 +1,30 @@
using PackageCompiler
using TOML
using Logging
using Cbc
using Clp
using Geodesy
using JSON
using JSONSchema
using JuMP
using MathOptInterface
using ProgressBars
Logging.disable_logging(Logging.Info)
pkg = [:Cbc, :Clp, :Geodesy, :JSON, :JSONSchema, :JuMP, :MathOptInterface, :ProgressBars]
mkpath("build")
@info "Building system image..."
create_sysimage(pkg, sysimage_path = "build/sysimage.so")
printstyled("Generating precompilation statements...\n", color = :light_green)
run(`julia --project=. --trace-compile=build/precompile.jl $ARGS`)
printstyled("Finding dependencies...\n", color = :light_green)
project = TOML.parsefile("Project.toml")
manifest = TOML.parsefile("Manifest.toml")
deps = Symbol[]
for dep in keys(project["deps"])
if "path" in keys(manifest[dep][1])
printstyled(" skip $(dep)\n", color = :light_black)
else
println(" add $(dep)")
push!(deps, Symbol(dep))
end
end
printstyled("Building system image...\n", color = :light_green)
create_sysimage(
deps,
precompile_statements_file = "build/precompile.jl",
sysimage_path = "build/sysimage.so",
)

View File

@@ -0,0 +1,25 @@
# 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

View File

@@ -40,7 +40,14 @@ using RELOG
@test plant.sizes[2].fixed_operating_cost == [30, 30]
@test plant.sizes[2].variable_operating_cost == [30, 30]
p1 = product_name_to_product["P1"]
@test p1.disposal_limit == [1.0, 1.0]
@test p1.disposal_cost == [-1000.0, -1000.0]
p2 = product_name_to_product["P2"]
@test p2.disposal_limit == [0.0, 0.0]
@test p2.disposal_cost == [0.0, 0.0]
p3 = product_name_to_product["P3"]
@test length(plant.output) == 2
@test plant.output[p2] == 0.2
@@ -70,7 +77,17 @@ using RELOG
@test plant.disposal_limit[p4] == [0, 0]
end
@testset "parse (invalid)" begin
@testset "parse (geodb)" begin
basedir = dirname(@__FILE__)
@test_throws String RELOG.parsefile("$basedir/../fixtures/s1-wrong-length.json")
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

View File

@@ -18,7 +18,7 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
)
@test length(model[:flow]) == 76
@test length(model[:dispose]) == 16
@test length(model[:plant_dispose]) == 16
@test length(model[:open_plant]) == 12
@test length(model[:capacity]) == 12
@test length(model[:expansion]) == 12
@@ -32,7 +32,7 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
@test lower_bound(v) == 0.0
@test upper_bound(v) == 750.0
v = model[:dispose][shipping_node_by_loc_and_prod_names["L1", "P2"], 1]
v = model[:plant_dispose][shipping_node_by_loc_and_prod_names["L1", "P2"], 1]
@test lower_bound(v) == 0.0
@test upper_bound(v) == 1.0
end

View File

@@ -0,0 +1,13 @@
# Copyright (C) 2020 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov>
using RELOG
basedir = @__DIR__
@testset "Resolve" begin
# Shoud not crash
filename = "$basedir/../../instances/s1.json"
solution_old, model_old = RELOG.solve(filename, return_model = true)
solution_new = RELOG.resolve(model_old, filename)
end

View File

@@ -26,6 +26,15 @@ basedir = dirname(@__FILE__)
@test "F2" in keys(solution["Plants"])
@test "F3" in keys(solution["Plants"])
@test "F4" in keys(solution["Plants"])
@test "Products" in keys(solution)
@test "P1" in keys(solution["Products"])
@test "C1" in keys(solution["Products"]["P1"])
@test "Dispose (tonne)" in keys(solution["Products"]["P1"]["C1"])
total_disposal =
sum([loc["Dispose (tonne)"] for loc in values(solution["Products"]["P1"])])
@test total_disposal == [1.0, 1.0]
end
@testset "solve (heuristic)" begin
@@ -38,7 +47,7 @@ end
for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
location_dict["amount (tonne)"] *= 1000
end
RELOG.solve(RELOG.parse(json))
@test_throws ErrorException("No solution available") RELOG.solve(RELOG.parse(json))
end
@testset "solve (with storage)" begin

View File

@@ -4,15 +4,18 @@
using RELOG, JSON, GZip
basedir = @__DIR__
@testset "Reports" begin
@testset "from solve" begin
solution = RELOG.solve("$(pwd())/../instances/s1.json")
solution = RELOG.solve("$basedir/../instances/s1.json")
tmp_filename = tempname()
# The following should not crash
RELOG.write_plants_report(solution, tmp_filename)
RELOG.write_plant_outputs_report(solution, tmp_filename)
RELOG.write_plant_emissions_report(solution, tmp_filename)
RELOG.write_transportation_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

View File

@@ -6,6 +6,7 @@ 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
@@ -14,6 +15,7 @@ using Test
@testset "Model" begin
include("model/build_test.jl")
include("model/solve_test.jl")
include("model/resolve_test.jl")
end
include("reports_test.jl")
end