mirror of
https://github.com/ANL-CEEESA/RELOG.git
synced 2025-12-07 08:08:51 -06:00
Compare commits
4 Commits
feature/st
...
feature/dr
| Author | SHA1 | Date | |
|---|---|---|---|
|
9191474df8
|
|||
|
841fbf16fb
|
|||
|
48bd3c403f
|
|||
| 23b3b33146 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,4 +12,3 @@ Manifest.toml
|
|||||||
data
|
data
|
||||||
build
|
build
|
||||||
benchmark
|
benchmark
|
||||||
**/*.log
|
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ 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
|
||||||
|
|
||||||
## [Unreleased]
|
# [0.6.0] -- 2022-12-15
|
||||||
|
### Added
|
||||||
|
- Allow RELOG to calculate approximate driving distances, instead of just straight-line distances between points.
|
||||||
|
|
||||||
- Allow product disposal at collection centers
|
### Fixed
|
||||||
- Implement stochastic optimization
|
- Fix bug that caused building period parameter to be ignored
|
||||||
|
|
||||||
## [0.5.2] -- 2022-08-26
|
## [0.5.2] -- 2022-08-26
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -1,4 +1,4 @@
|
|||||||
VERSION := 0.5
|
VERSION := 0.6
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rfv build Manifest.toml test/Manifest.toml deps/formatter/build deps/formatter/Manifest.toml
|
rm -rfv build Manifest.toml test/Manifest.toml deps/formatter/build deps/formatter/Manifest.toml
|
||||||
|
|||||||
10
Project.toml
10
Project.toml
@@ -1,35 +1,37 @@
|
|||||||
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"
|
||||||
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
|
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
|
||||||
|
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
||||||
|
Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d"
|
||||||
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
|
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
|
||||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||||
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
|
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
|
||||||
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
||||||
Geodesy = "0ef565a4-170c-5f04-8de2-149903a85f3d"
|
Geodesy = "0ef565a4-170c-5f04-8de2-149903a85f3d"
|
||||||
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
|
|
||||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||||
JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
|
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"
|
||||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||||
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
|
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
|
||||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
|
||||||
Shapefile = "8e980c4a-a4fe-5da2-b3a7-4b4b0353a2f4"
|
Shapefile = "8e980c4a-a4fe-5da2-b3a7-4b4b0353a2f4"
|
||||||
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
||||||
StochasticPrograms = "8b8459f2-c380-502b-8633-9aed2d6c2b35"
|
|
||||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||||
ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea"
|
ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea"
|
||||||
|
|
||||||
[compat]
|
[compat]
|
||||||
CRC = "4"
|
CRC = "4"
|
||||||
CSV = "0.10"
|
CSV = "0.10"
|
||||||
|
Cbc = "1"
|
||||||
|
Clp = "1"
|
||||||
DataFrames = "1"
|
DataFrames = "1"
|
||||||
DataStructures = "0.18"
|
DataStructures = "0.18"
|
||||||
GZip = "0.5"
|
GZip = "0.5"
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -15,14 +15,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<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
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
* **Nwike Iloeje** <<ciloeje@anl.gov>>
|
* **Nwike Iloeje** <<ciloeje@anl.gov>>
|
||||||
* **John Atkins**
|
* **John Atkins**
|
||||||
* **Kyle Sun**
|
* **Kyle Sun**
|
||||||
|
* **Audrey Gallier**
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +38,6 @@ 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:
|
||||||
|
|
||||||
@@ -75,9 +75,7 @@ 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]
|
||||||
@@ -224,6 +222,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,7 +154,6 @@ 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,7 +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:
|
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:
|
||||||
|
|||||||
203
instances/s1.json
Normal file
203
instances/s1.json
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
{
|
||||||
|
"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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
instances/solutions/s1.log
Normal file
11
instances/solutions/s1.log
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[ Info: Reading s1.json...
|
||||||
|
[ Info: Building graph...
|
||||||
|
[ Info: 2 time periods
|
||||||
|
[ Info: 6 process nodes
|
||||||
|
[ Info: 8 shipping nodes (plant)
|
||||||
|
[ Info: 10 shipping nodes (collection)
|
||||||
|
[ Info: 38 arcs
|
||||||
|
[ Info: Building optimization model...
|
||||||
|
[ Info: Optimizing MILP...
|
||||||
|
[ Info: Re-optimizing with integer variables fixed...
|
||||||
|
[ Info: Extracting solution...
|
||||||
@@ -5,17 +5,18 @@
|
|||||||
module RELOG
|
module RELOG
|
||||||
|
|
||||||
include("instance/structs.jl")
|
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/resolve.jl")
|
||||||
include("model/solve.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")
|
||||||
|
|||||||
@@ -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,7 +10,6 @@ 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)
|
||||||
@@ -28,7 +19,6 @@ 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
|
||||||
|
|
||||||
@@ -52,14 +42,15 @@ 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(length(arcs) + 1, source, dest, values)
|
arc = Arc(source, dest, values)
|
||||||
push!(source.outgoing_arcs, arc)
|
push!(source.outgoing_arcs, arc)
|
||||||
push!(dest.incoming_arcs, arc)
|
push!(dest.incoming_arcs, arc)
|
||||||
push!(arcs, arc)
|
push!(arcs, arc)
|
||||||
@@ -72,7 +63,7 @@ function build_graph(instance::Instance)::Graph
|
|||||||
for dest in shipping_nodes_by_plant[plant]
|
for dest in shipping_nodes_by_plant[plant]
|
||||||
weight = plant.output[dest.product]
|
weight = plant.output[dest.product]
|
||||||
values = Dict("weight" => weight)
|
values = Dict("weight" => weight)
|
||||||
arc = Arc(length(arcs) + 1, source, dest, values)
|
arc = Arc(source, dest, values)
|
||||||
push!(source.outgoing_arcs, arc)
|
push!(source.outgoing_arcs, arc)
|
||||||
push!(dest.incoming_arcs, arc)
|
push!(dest.incoming_arcs, arc)
|
||||||
push!(arcs, arc)
|
push!(arcs, arc)
|
||||||
@@ -85,7 +76,6 @@ 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
|
||||||
|
|
||||||
|
|||||||
57
src/graph/dist.jl
Normal file
57
src/graph/dist.jl
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 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))
|
||||||
|
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])
|
||||||
|
return round(dist_euclidean * ratio_pred, digits = 3)
|
||||||
|
end
|
||||||
@@ -7,7 +7,6 @@ using Geodesy
|
|||||||
abstract type Node end
|
abstract type Node end
|
||||||
|
|
||||||
mutable struct Arc
|
mutable struct Arc
|
||||||
index::Int
|
|
||||||
source::Node
|
source::Node
|
||||||
dest::Node
|
dest::Node
|
||||||
values::Dict{String,Float64}
|
values::Dict{String,Float64}
|
||||||
@@ -34,7 +33,6 @@ 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)
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ function _compress(instance::Instance)::Instance
|
|||||||
for (emission_name, emission_value) in p.transportation_emissions
|
for (emission_name, emission_value) in p.transportation_emissions
|
||||||
p.transportation_emissions[emission_name] = [mean(emission_value)]
|
p.transportation_emissions[emission_name] = [mean(emission_value)]
|
||||||
end
|
end
|
||||||
p.disposal_limit = [maximum(p.disposal_limit) * T]
|
|
||||||
p.disposal_cost = [mean(p.disposal_cost)]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Compress collection centers
|
# Compress collection centers
|
||||||
@@ -60,42 +58,3 @@ function _compress(instance::Instance)::Instance
|
|||||||
|
|
||||||
return compressed
|
return compressed
|
||||||
end
|
end
|
||||||
|
|
||||||
function _slice(instance::Instance, T::UnitRange)::Instance
|
|
||||||
sliced = deepcopy(instance)
|
|
||||||
sliced.time = length(T)
|
|
||||||
|
|
||||||
for p in sliced.products
|
|
||||||
p.transportation_cost = p.transportation_cost[T]
|
|
||||||
p.transportation_energy = p.transportation_energy[T]
|
|
||||||
for (emission_name, emission_value) in p.transportation_emissions
|
|
||||||
p.transportation_emissions[emission_name] = emission_value[T]
|
|
||||||
end
|
|
||||||
p.disposal_limit = p.disposal_limit[T]
|
|
||||||
p.disposal_cost = p.disposal_cost[T]
|
|
||||||
end
|
|
||||||
|
|
||||||
for c in sliced.collection_centers
|
|
||||||
c.amount = c.amount[T]
|
|
||||||
end
|
|
||||||
|
|
||||||
for plant in sliced.plants
|
|
||||||
plant.energy = plant.energy[T]
|
|
||||||
for (emission_name, emission_value) in plant.emissions
|
|
||||||
plant.emissions[emission_name] = emission_value[T]
|
|
||||||
end
|
|
||||||
for s in plant.sizes
|
|
||||||
s.variable_operating_cost = s.variable_operating_cost[T]
|
|
||||||
s.opening_cost = s.opening_cost[T]
|
|
||||||
s.fixed_operating_cost = s.fixed_operating_cost[T]
|
|
||||||
end
|
|
||||||
for (prod_name, disp_limit) in plant.disposal_limit
|
|
||||||
plant.disposal_limit[prod_name] = disp_limit[T]
|
|
||||||
end
|
|
||||||
for (prod_name, disp_cost) in plant.disposal_cost
|
|
||||||
plant.disposal_cost[prod_name] = disp_cost[T]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return sliced
|
|
||||||
end
|
|
||||||
@@ -23,10 +23,23 @@ 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
|
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
|
||||||
|
@show distance_metric
|
||||||
|
|
||||||
plants = Plant[]
|
plants = Plant[]
|
||||||
products = Product[]
|
products = Product[]
|
||||||
collection_centers = CollectionCenter[]
|
collection_centers = CollectionCenter[]
|
||||||
@@ -37,8 +50,6 @@ 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)"]
|
||||||
@@ -48,25 +59,7 @@ function parse(json)::Instance
|
|||||||
emissions = product_dict["transportation emissions (tonne/km/tonne)"]
|
emissions = product_dict["transportation emissions (tonne/km/tonne)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
if "disposal limit (tonne)" in keys(product_dict)
|
product = Product(product_name, cost, energy, emissions)
|
||||||
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
|
||||||
|
|
||||||
@@ -86,7 +79,6 @@ 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
|
||||||
@@ -197,5 +189,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,9 +13,6 @@ 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
|
||||||
@@ -51,10 +48,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
|
||||||
|
|||||||
@@ -2,346 +2,62 @@
|
|||||||
# 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 JuMP, LinearAlgebra, Geodesy, ProgressBars, Printf, DataStructures, StochasticPrograms
|
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
|
||||||
|
|
||||||
function build_model(
|
function build_model(instance::Instance, graph::Graph, optimizer)::JuMP.Model
|
||||||
instance::Instance,
|
model = Model(optimizer)
|
||||||
graph::Graph,
|
model[:instance] = instance
|
||||||
optimizer,
|
model[:graph] = graph
|
||||||
)
|
create_vars!(model)
|
||||||
return build_model(
|
create_objective_function!(model)
|
||||||
instance,
|
create_shipping_node_constraints!(model)
|
||||||
[graph],
|
create_process_node_constraints!(model)
|
||||||
[1.0],
|
return model
|
||||||
optimizer=optimizer,
|
|
||||||
method=:ef,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function build_model(
|
|
||||||
instance::Instance,
|
|
||||||
graphs::Vector{Graph},
|
|
||||||
probs::Vector{Float64};
|
|
||||||
optimizer,
|
|
||||||
method=:ef,
|
|
||||||
tol=0.1,
|
|
||||||
)
|
|
||||||
T = instance.time
|
|
||||||
|
|
||||||
@stochastic_model model begin
|
function create_vars!(model::JuMP.Model)
|
||||||
# Stage 1: Build plants
|
graph, T = model[:graph], model[:instance].time
|
||||||
# =====================================================================
|
model[:flow] =
|
||||||
@stage 1 begin
|
Dict((a, t) => @variable(model, lower_bound = 0) for a in graph.arcs, t = 1:T)
|
||||||
pn = graphs[1].process_nodes
|
model[:dispose] = Dict(
|
||||||
PN = length(pn)
|
(n, t) => @variable(
|
||||||
|
|
||||||
# Var: open_plant
|
|
||||||
@decision(
|
|
||||||
model,
|
model,
|
||||||
open_plant[n in 1:PN, t in 1:T],
|
|
||||||
binary = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Var: is_open
|
|
||||||
@decision(
|
|
||||||
model,
|
|
||||||
is_open[n in 1:PN, t in 1:T],
|
|
||||||
binary = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Objective function
|
|
||||||
@objective(
|
|
||||||
model,
|
|
||||||
Min,
|
|
||||||
|
|
||||||
# Opening, fixed operating costs
|
|
||||||
sum(
|
|
||||||
pn[n].location.sizes[1].opening_cost[t] * open_plant[n, t] +
|
|
||||||
pn[n].location.sizes[1].fixed_operating_cost[t] * is_open[n, t]
|
|
||||||
for n in 1:PN
|
|
||||||
for t in 1:T
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for t = 1:T, n in 1:PN
|
|
||||||
# Plant is currently open if it was already open in the previous time period or
|
|
||||||
# if it was built just now
|
|
||||||
if t > 1
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
is_open[n, t] == is_open[n, t-1] + open_plant[n, t]
|
|
||||||
)
|
|
||||||
else
|
|
||||||
@constraint(model, is_open[n, t] == open_plant[n, t])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Plant can only be opened during building period
|
|
||||||
if t ∉ instance.building_period
|
|
||||||
@constraint(model, open_plant[n, t] == 0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Stage 2: Flows, disposal, capacity & storage
|
|
||||||
# =====================================================================
|
|
||||||
@stage 2 begin
|
|
||||||
@uncertain graph
|
|
||||||
pn = graph.process_nodes
|
|
||||||
psn = graph.plant_shipping_nodes
|
|
||||||
csn = graph.collection_shipping_nodes
|
|
||||||
arcs = graph.arcs
|
|
||||||
|
|
||||||
A = length(arcs)
|
|
||||||
PN = length(pn)
|
|
||||||
CSN = length(csn)
|
|
||||||
PSN = length(psn)
|
|
||||||
|
|
||||||
# Var: flow
|
|
||||||
@recourse(
|
|
||||||
model,
|
|
||||||
flow[a in 1:A, t in 1:T],
|
|
||||||
lower_bound = 0,
|
lower_bound = 0,
|
||||||
|
upper_bound = n.location.disposal_limit[n.product][t]
|
||||||
|
) for n in values(graph.plant_shipping_nodes), t = 1:T
|
||||||
)
|
)
|
||||||
|
model[:store] = Dict(
|
||||||
# Var: plant_dispose
|
(n, t) =>
|
||||||
@recourse(
|
@variable(model, lower_bound = 0, upper_bound = n.location.storage_limit)
|
||||||
|
for n in values(graph.process_nodes), t = 1:T
|
||||||
|
)
|
||||||
|
model[:process] = Dict(
|
||||||
|
(n, t) => @variable(model, lower_bound = 0) for
|
||||||
|
n in values(graph.process_nodes), t = 1:T
|
||||||
|
)
|
||||||
|
model[:open_plant] = Dict(
|
||||||
|
(n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
|
||||||
|
t = 1:T
|
||||||
|
)
|
||||||
|
model[:is_open] = Dict(
|
||||||
|
(n, t) => @variable(model, binary = true) for n in values(graph.process_nodes),
|
||||||
|
t = 1:T
|
||||||
|
)
|
||||||
|
model[:capacity] = Dict(
|
||||||
|
(n, t) => @variable(
|
||||||
model,
|
model,
|
||||||
plant_dispose[n in 1:PSN, t in 1:T],
|
|
||||||
lower_bound = 0,
|
lower_bound = 0,
|
||||||
upper_bound = psn[n].location.disposal_limit[psn[n].product][t],
|
upper_bound = n.location.sizes[2].capacity
|
||||||
|
) for n in values(graph.process_nodes), t = 1:T
|
||||||
)
|
)
|
||||||
|
model[:expansion] = Dict(
|
||||||
# Var: collection_dispose
|
(n, t) => @variable(
|
||||||
@recourse(
|
|
||||||
model,
|
model,
|
||||||
collection_dispose[n in 1:CSN, t in 1:T],
|
|
||||||
lower_bound = 0,
|
lower_bound = 0,
|
||||||
upper_bound = graph.collection_shipping_nodes[n].location.amount[t],
|
upper_bound = n.location.sizes[2].capacity - n.location.sizes[1].capacity
|
||||||
|
) for n in values(graph.process_nodes), t = 1:T
|
||||||
)
|
)
|
||||||
|
|
||||||
# Var: collection_shortfall
|
|
||||||
@recourse(
|
|
||||||
model,
|
|
||||||
collection_shortfall[n in 1:CSN, t in 1:T],
|
|
||||||
lower_bound = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Var: store
|
|
||||||
@recourse(
|
|
||||||
model,
|
|
||||||
store[
|
|
||||||
n in 1:PN,
|
|
||||||
t in 1:T,
|
|
||||||
],
|
|
||||||
lower_bound = 0,
|
|
||||||
upper_bound = pn[n].location.storage_limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Var: process
|
|
||||||
@recourse(
|
|
||||||
model,
|
|
||||||
process[
|
|
||||||
n in 1:PN,
|
|
||||||
t in 1:T,
|
|
||||||
],
|
|
||||||
lower_bound = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Var: capacity
|
|
||||||
@recourse(
|
|
||||||
model,
|
|
||||||
capacity[
|
|
||||||
n in 1:PN,
|
|
||||||
t in 1:T,
|
|
||||||
],
|
|
||||||
lower_bound = 0,
|
|
||||||
upper_bound = pn[n].location.sizes[2].capacity,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Var: expansion
|
|
||||||
@recourse(
|
|
||||||
model,
|
|
||||||
expansion[
|
|
||||||
n in 1:PN,
|
|
||||||
t in 1:T,
|
|
||||||
],
|
|
||||||
lower_bound = 0,
|
|
||||||
upper_bound = (
|
|
||||||
pn[n].location.sizes[2].capacity -
|
|
||||||
pn[n].location.sizes[1].capacity
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Objective function
|
|
||||||
@objective(
|
|
||||||
model,
|
|
||||||
Min,
|
|
||||||
sum(
|
|
||||||
# Transportation costs
|
|
||||||
pn[n].location.input.transportation_cost[t] *
|
|
||||||
a.values["distance"] *
|
|
||||||
flow[a.index,t]
|
|
||||||
|
|
||||||
for n in 1:PN
|
|
||||||
for a in pn[n].incoming_arcs
|
|
||||||
for t in 1:T
|
|
||||||
) + sum(
|
|
||||||
# Fixed operating costs (expansion)
|
|
||||||
slope_fix_oper_cost(pn[n].location, t) * expansion[n, t] +
|
|
||||||
|
|
||||||
# Processing costs
|
|
||||||
pn[n].location.sizes[1].variable_operating_cost[t] * process[n, t] +
|
|
||||||
|
|
||||||
# Storage costs
|
|
||||||
pn[n].location.storage_cost[t] * store[n, t] +
|
|
||||||
|
|
||||||
# Expansion costs
|
|
||||||
(
|
|
||||||
t < T ? (
|
|
||||||
(
|
|
||||||
slope_open(pn[n].location, t) -
|
|
||||||
slope_open(pn[n].location, t + 1)
|
|
||||||
) * expansion[n, t]
|
|
||||||
) : slope_open(pn[n].location, t) * expansion[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
for n in 1:PN
|
|
||||||
for t in 1:T
|
|
||||||
) + sum(
|
|
||||||
# Disposal costs (plants)
|
|
||||||
psn[n].location.disposal_cost[psn[n].product][t] * plant_dispose[n, t]
|
|
||||||
for n in 1:PSN
|
|
||||||
for t in 1:T
|
|
||||||
) + sum(
|
|
||||||
# Disposal costs (collection centers)
|
|
||||||
csn[n].location.product.disposal_cost[t] * collection_dispose[n, t]
|
|
||||||
for n in 1:CSN
|
|
||||||
for t in 1:T
|
|
||||||
) + sum(
|
|
||||||
# Collection shortfall
|
|
||||||
1e4 * collection_shortfall[n, t]
|
|
||||||
for n in 1:CSN
|
|
||||||
for t in 1:T
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process node constraints
|
|
||||||
for t = 1:T, n in 1:PN
|
|
||||||
node = pn[n]
|
|
||||||
|
|
||||||
# Output amount is implied by amount processed
|
|
||||||
for arc in node.outgoing_arcs
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
flow[arc.index, t] == arc.values["weight"] * process[n, t]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# If plant is closed, capacity is zero
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
capacity[n, t] <= node.location.sizes[2].capacity * is_open[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
# If plant is open, capacity is greater than base
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
capacity[n, t] >= node.location.sizes[1].capacity * is_open[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Capacity is linked to expansion
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
capacity[n, t] <=
|
|
||||||
node.location.sizes[1].capacity + expansion[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Can only process up to capacity
|
|
||||||
@constraint(model, process[n, t] <= capacity[n, t])
|
|
||||||
|
|
||||||
if t > 1
|
|
||||||
# Plant capacity can only increase over time
|
|
||||||
@constraint(model, capacity[n, t] >= capacity[n, t-1])
|
|
||||||
@constraint(model, expansion[n, t] >= expansion[n, t-1])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Amount received equals amount processed plus stored
|
|
||||||
store_in = 0
|
|
||||||
if t > 1
|
|
||||||
store_in = store[n, t-1]
|
|
||||||
end
|
|
||||||
if t == T
|
|
||||||
@constraint(model, store[n, t] == 0)
|
|
||||||
end
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
sum(
|
|
||||||
flow[arc.index, t]
|
|
||||||
for arc in node.incoming_arcs
|
|
||||||
) + store_in == store[n, t] + process[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# Material flow at collection shipping nodes
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
eq_balance_centers[
|
|
||||||
n in 1:CSN,
|
|
||||||
t in 1:T,
|
|
||||||
],
|
|
||||||
sum(
|
|
||||||
flow[arc.index, t]
|
|
||||||
for arc in csn[n].outgoing_arcs
|
|
||||||
) == csn[n].location.amount[t] - collection_dispose[n, t] - collection_shortfall[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Material flow at plant shipping nodes
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
eq_balance_plant[
|
|
||||||
n in 1:PSN,
|
|
||||||
t in 1:T,
|
|
||||||
],
|
|
||||||
sum(flow[a.index, t] for a in psn[n].incoming_arcs) ==
|
|
||||||
sum(flow[a.index, t] for a in psn[n].outgoing_arcs) +
|
|
||||||
plant_dispose[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enforce product disposal limit at collection centers
|
|
||||||
for t in 1:T, prod in instance.products
|
|
||||||
if isempty(prod.collection_centers)
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
@constraint(
|
|
||||||
model,
|
|
||||||
sum(
|
|
||||||
collection_dispose[n, t]
|
|
||||||
for n in 1:CSN
|
|
||||||
if csn[n].product.name == prod.name
|
|
||||||
) <= prod.disposal_limit[t]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ξ = [
|
|
||||||
@scenario graph = graphs[i] probability = probs[i]
|
|
||||||
for i in 1:length(graphs)
|
|
||||||
]
|
|
||||||
|
|
||||||
if method == :ef
|
|
||||||
sp = instantiate(model, ξ; optimizer=optimizer)
|
|
||||||
elseif method == :lshaped
|
|
||||||
sp = instantiate(model, ξ; optimizer=LShaped.Optimizer)
|
|
||||||
set_optimizer_attribute(sp, MasterOptimizer(), optimizer)
|
|
||||||
set_optimizer_attribute(sp, SubProblemOptimizer(), optimizer)
|
|
||||||
set_optimizer_attribute(sp, RelativeTolerance(), tol)
|
|
||||||
else
|
|
||||||
error("unknown method: $method")
|
|
||||||
end
|
|
||||||
|
|
||||||
return sp
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -362,3 +78,172 @@ function slope_fix_oper_cost(plant, t)
|
|||||||
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
(plant.sizes[2].capacity - plant.sizes[1].capacity)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function create_objective_function!(model::JuMP.Model)
|
||||||
|
graph, T = model[:graph], model[:instance].time
|
||||||
|
obj = AffExpr(0.0)
|
||||||
|
|
||||||
|
# Process node costs
|
||||||
|
for n in values(graph.process_nodes), t = 1:T
|
||||||
|
|
||||||
|
# Transportation and variable operating costs
|
||||||
|
for a in n.incoming_arcs
|
||||||
|
c = n.location.input.transportation_cost[t] * a.values["distance"]
|
||||||
|
add_to_expression!(obj, c, model[:flow][a, t])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Opening costs
|
||||||
|
add_to_expression!(
|
||||||
|
obj,
|
||||||
|
n.location.sizes[1].opening_cost[t],
|
||||||
|
model[:open_plant][n, t],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fixed operating costs (base)
|
||||||
|
add_to_expression!(
|
||||||
|
obj,
|
||||||
|
n.location.sizes[1].fixed_operating_cost[t],
|
||||||
|
model[:is_open][n, t],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fixed operating costs (expansion)
|
||||||
|
add_to_expression!(obj, slope_fix_oper_cost(n.location, t), model[:expansion][n, t])
|
||||||
|
|
||||||
|
# Processing costs
|
||||||
|
add_to_expression!(
|
||||||
|
obj,
|
||||||
|
n.location.sizes[1].variable_operating_cost[t],
|
||||||
|
model[:process][n, t],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Storage costs
|
||||||
|
add_to_expression!(obj, n.location.storage_cost[t], model[:store][n, t])
|
||||||
|
|
||||||
|
# Expansion costs
|
||||||
|
if t < T
|
||||||
|
add_to_expression!(
|
||||||
|
obj,
|
||||||
|
slope_open(n.location, t) - slope_open(n.location, t + 1),
|
||||||
|
model[:expansion][n, t],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
add_to_expression!(obj, slope_open(n.location, t), model[:expansion][n, t])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@objective(model, Min, obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function create_shipping_node_constraints!(model::JuMP.Model)
|
||||||
|
graph, T = model[:graph], model[:instance].time
|
||||||
|
model[:eq_balance] = OrderedDict()
|
||||||
|
for t = 1:T
|
||||||
|
# Collection centers
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function create_process_node_constraints!(model::JuMP.Model)
|
||||||
|
graph, T = model[:graph], model[:instance].time
|
||||||
|
|
||||||
|
for t = 1:T, n in graph.process_nodes
|
||||||
|
input_sum = AffExpr(0.0)
|
||||||
|
for a in n.incoming_arcs
|
||||||
|
add_to_expression!(input_sum, 1.0, model[:flow][a, t])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Output amount is implied by amount processed
|
||||||
|
for a in n.outgoing_arcs
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
model[:flow][a, t] == a.values["weight"] * model[:process][n, t]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# If plant is closed, capacity is zero
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
model[:capacity][n, t] <= n.location.sizes[2].capacity * model[:is_open][n, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
# If plant is open, capacity is greater than base
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
model[:capacity][n, t] >= n.location.sizes[1].capacity * model[:is_open][n, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Capacity is linked to expansion
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
model[:capacity][n, t] <=
|
||||||
|
n.location.sizes[1].capacity + model[:expansion][n, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Can only process up to capacity
|
||||||
|
@constraint(model, model[:process][n, t] <= model[:capacity][n, t])
|
||||||
|
|
||||||
|
if t > 1
|
||||||
|
# Plant capacity can only increase over time
|
||||||
|
@constraint(model, model[:capacity][n, t] >= model[:capacity][n, t-1])
|
||||||
|
@constraint(model, model[:expansion][n, t] >= model[:expansion][n, t-1])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Amount received equals amount processed plus stored
|
||||||
|
store_in = 0
|
||||||
|
if t > 1
|
||||||
|
store_in = model[:store][n, t-1]
|
||||||
|
end
|
||||||
|
if t == T
|
||||||
|
@constraint(model, model[:store][n, t] == 0)
|
||||||
|
end
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
input_sum + store_in == model[:store][n, t] + model[:process][n, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Plant is currently open if it was already open in the previous time period or
|
||||||
|
# if it was built just now
|
||||||
|
if t > 1
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
model[:is_open][n, t] == model[:is_open][n, t-1] + model[:open_plant][n, t]
|
||||||
|
)
|
||||||
|
else
|
||||||
|
@constraint(model, model[:is_open][n, t] == model[:open_plant][n, t])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Plant can only be opened during building period
|
||||||
|
if t ∉ model[:instance].building_period
|
||||||
|
@constraint(model, model[:open_plant][n, t] == 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -2,33 +2,12 @@
|
|||||||
# 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 JuMP, LinearAlgebra, Geodesy, ProgressBars, Printf, DataStructures
|
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
|
||||||
|
|
||||||
function get_solution(
|
|
||||||
instance,
|
|
||||||
graph,
|
|
||||||
model,
|
|
||||||
scenario_index::Int=1;
|
|
||||||
marginal_costs=false,
|
|
||||||
)
|
|
||||||
value(x) = StochasticPrograms.value(x, scenario_index)
|
|
||||||
ivalue(x) = StochasticPrograms.value(x)
|
|
||||||
shadow_price(x) = StochasticPrograms.shadow_price(x, scenario_index)
|
|
||||||
|
|
||||||
|
function get_solution(model::JuMP.Model; marginal_costs = true)
|
||||||
|
graph, instance = model[:graph], model[:instance]
|
||||||
T = instance.time
|
T = instance.time
|
||||||
|
|
||||||
pn = graph.process_nodes
|
|
||||||
psn = graph.plant_shipping_nodes
|
|
||||||
csn = graph.collection_shipping_nodes
|
|
||||||
arcs = graph.arcs
|
|
||||||
|
|
||||||
A = length(arcs)
|
|
||||||
PN = length(pn)
|
|
||||||
CSN = length(csn)
|
|
||||||
PSN = length(psn)
|
|
||||||
|
|
||||||
flow = model[2, :flow]
|
|
||||||
|
|
||||||
output = OrderedDict(
|
output = OrderedDict(
|
||||||
"Plants" => OrderedDict(),
|
"Plants" => OrderedDict(),
|
||||||
"Products" => OrderedDict(),
|
"Products" => OrderedDict(),
|
||||||
@@ -50,52 +29,37 @@ function get_solution(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
pn = graph.process_nodes
|
plant_to_process_node = OrderedDict(n.location => n for n in graph.process_nodes)
|
||||||
psn = graph.plant_shipping_nodes
|
plant_to_shipping_nodes = OrderedDict()
|
||||||
|
for p in instance.plants
|
||||||
plant_to_process_node_index = OrderedDict(
|
plant_to_shipping_nodes[p] = []
|
||||||
pn[n].location => n
|
for a in plant_to_process_node[p].outgoing_arcs
|
||||||
for n in 1:length(pn)
|
push!(plant_to_shipping_nodes[p], a.dest)
|
||||||
)
|
end
|
||||||
|
|
||||||
plant_to_shipping_node_indices = OrderedDict(p => [] for p in instance.plants)
|
|
||||||
for n in 1:length(psn)
|
|
||||||
push!(plant_to_shipping_node_indices[psn[n].location], n)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Products
|
# Products
|
||||||
for n in 1:CSN
|
|
||||||
node = csn[n]
|
|
||||||
location_dict = OrderedDict{Any,Any}(
|
|
||||||
"Latitude (deg)" => node.location.latitude,
|
|
||||||
"Longitude (deg)" => node.location.longitude,
|
|
||||||
"Amount (tonne)" => node.location.amount,
|
|
||||||
"Dispose (tonne)" => [
|
|
||||||
value(model[2, :collection_dispose][n, t])
|
|
||||||
for t = 1:T
|
|
||||||
],
|
|
||||||
"Disposal cost (\$)" => [
|
|
||||||
value(model[2, :collection_dispose][n, t]) *
|
|
||||||
node.location.product.disposal_cost[t]
|
|
||||||
for t = 1:T
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if marginal_costs
|
if marginal_costs
|
||||||
location_dict["Marginal cost (\$/tonne)"] = [
|
for n in graph.collection_shipping_nodes
|
||||||
round(abs(shadow_price(model[2, :eq_balance_centers][n, t])), digits=2) for t = 1:T
|
location_dict = OrderedDict{Any,Any}(
|
||||||
]
|
"Marginal cost (\$/tonne)" => [
|
||||||
|
round(abs(JuMP.shadow_price(model[:eq_balance][n, t])), digits = 2) for t = 1:T
|
||||||
|
],
|
||||||
|
"Latitude (deg)" => n.location.latitude,
|
||||||
|
"Longitude (deg)" => n.location.longitude,
|
||||||
|
"Amount (tonne)" => n.location.amount,
|
||||||
|
)
|
||||||
|
if n.product.name ∉ keys(output["Products"])
|
||||||
|
output["Products"][n.product.name] = OrderedDict()
|
||||||
end
|
end
|
||||||
if node.product.name ∉ keys(output["Products"])
|
output["Products"][n.product.name][n.location.name] = location_dict
|
||||||
output["Products"][node.product.name] = OrderedDict()
|
|
||||||
end
|
end
|
||||||
output["Products"][node.product.name][node.location.name] = location_dict
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Plants
|
# Plants
|
||||||
for plant in instance.plants
|
for plant in instance.plants
|
||||||
skip_plant = true
|
skip_plant = true
|
||||||
n = plant_to_process_node_index[plant]
|
process_node = plant_to_process_node[plant]
|
||||||
process_node = pn[n]
|
|
||||||
plant_dict = OrderedDict{Any,Any}(
|
plant_dict = OrderedDict{Any,Any}(
|
||||||
"Input" => OrderedDict(),
|
"Input" => OrderedDict(),
|
||||||
"Output" =>
|
"Output" =>
|
||||||
@@ -106,39 +70,39 @@ function get_solution(
|
|||||||
"Latitude (deg)" => plant.latitude,
|
"Latitude (deg)" => plant.latitude,
|
||||||
"Longitude (deg)" => plant.longitude,
|
"Longitude (deg)" => plant.longitude,
|
||||||
"Capacity (tonne)" =>
|
"Capacity (tonne)" =>
|
||||||
[value(model[2, :capacity][n, t]) for t = 1:T],
|
[JuMP.value(model[:capacity][process_node, t]) for t = 1:T],
|
||||||
"Opening cost (\$)" => [
|
"Opening cost (\$)" => [
|
||||||
ivalue(model[1, :open_plant][n, t]) *
|
JuMP.value(model[:open_plant][process_node, t]) *
|
||||||
plant.sizes[1].opening_cost[t] for t = 1:T
|
plant.sizes[1].opening_cost[t] for t = 1:T
|
||||||
],
|
],
|
||||||
"Fixed operating cost (\$)" => [
|
"Fixed operating cost (\$)" => [
|
||||||
ivalue(model[1, :is_open][n, t]) *
|
JuMP.value(model[:is_open][process_node, t]) *
|
||||||
plant.sizes[1].fixed_operating_cost[t] +
|
plant.sizes[1].fixed_operating_cost[t] +
|
||||||
value(model[2, :expansion][n, t]) *
|
JuMP.value(model[:expansion][process_node, t]) *
|
||||||
slope_fix_oper_cost(plant, t) for t = 1:T
|
slope_fix_oper_cost(plant, t) for t = 1:T
|
||||||
],
|
],
|
||||||
"Expansion cost (\$)" => [
|
"Expansion cost (\$)" => [
|
||||||
(
|
(
|
||||||
if t == 1
|
if t == 1
|
||||||
slope_open(plant, t) * value(model[2, :expansion][n, t])
|
slope_open(plant, t) * JuMP.value(model[:expansion][process_node, t])
|
||||||
else
|
else
|
||||||
slope_open(plant, t) * (
|
slope_open(plant, t) * (
|
||||||
value(model[2, :expansion][n, t]) -
|
JuMP.value(model[:expansion][process_node, t]) -
|
||||||
value(model[2, :expansion][n, t-1])
|
JuMP.value(model[:expansion][process_node, t-1])
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
) for t = 1:T
|
) for t = 1:T
|
||||||
],
|
],
|
||||||
"Process (tonne)" =>
|
"Process (tonne)" =>
|
||||||
[value(model[2, :process][n, t]) for t = 1:T],
|
[JuMP.value(model[:process][process_node, t]) for t = 1:T],
|
||||||
"Variable operating cost (\$)" => [
|
"Variable operating cost (\$)" => [
|
||||||
value(model[2, :process][n, t]) *
|
JuMP.value(model[:process][process_node, t]) *
|
||||||
plant.sizes[1].variable_operating_cost[t] for t = 1:T
|
plant.sizes[1].variable_operating_cost[t] for t = 1:T
|
||||||
],
|
],
|
||||||
"Storage (tonne)" =>
|
"Storage (tonne)" =>
|
||||||
[value(model[2, :store][n, t]) for t = 1:T],
|
[JuMP.value(model[:store][process_node, t]) for t = 1:T],
|
||||||
"Storage cost (\$)" => [
|
"Storage cost (\$)" => [
|
||||||
value(model[2, :store][n, t]) * plant.storage_cost[t]
|
JuMP.value(model[:store][process_node, t]) * plant.storage_cost[t]
|
||||||
for t = 1:T
|
for t = 1:T
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -151,7 +115,7 @@ function get_solution(
|
|||||||
|
|
||||||
# Inputs
|
# Inputs
|
||||||
for a in process_node.incoming_arcs
|
for a in process_node.incoming_arcs
|
||||||
vals = [value(flow[a.index, t]) for t = 1:T]
|
vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
|
||||||
if sum(vals) <= 1e-3
|
if sum(vals) <= 1e-3
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
@@ -209,20 +173,18 @@ function get_solution(
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Outputs
|
# Outputs
|
||||||
for n2 in plant_to_shipping_node_indices[plant]
|
for shipping_node in plant_to_shipping_nodes[plant]
|
||||||
shipping_node = psn[n2]
|
|
||||||
product_name = shipping_node.product.name
|
product_name = shipping_node.product.name
|
||||||
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 =
|
disposal_amount = [JuMP.value(model[:dispose][shipping_node, t]) for t = 1:T]
|
||||||
[value(model[2, :plant_dispose][n2, 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)"] =
|
||||||
[value(model[2, :plant_dispose][n2, t]) for t = 1:T]
|
[JuMP.value(model[: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
|
||||||
@@ -232,7 +194,7 @@ function get_solution(
|
|||||||
end
|
end
|
||||||
|
|
||||||
for a in shipping_node.outgoing_arcs
|
for a in shipping_node.outgoing_arcs
|
||||||
vals = [value(flow[a.index, t]) for t = 1:T]
|
vals = [JuMP.value(model[:flow][a, t]) for t = 1:T]
|
||||||
if sum(vals) <= 1e-3
|
if sum(vals) <= 1e-3
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|||||||
97
src/model/resolve.jl
Normal file
97
src/model/resolve.jl
Normal 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
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
# 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 JuMP, LinearAlgebra, Geodesy, HiGHS, ProgressBars, Printf, DataStructures
|
using JuMP, LinearAlgebra, Geodesy, Cbc, Clp, ProgressBars, Printf, DataStructures
|
||||||
|
|
||||||
function _get_default_milp_optimizer()
|
function _get_default_milp_optimizer()
|
||||||
return optimizer_with_attributes(HiGHS.Optimizer)
|
return optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
function _get_default_lp_optimizer()
|
function _get_default_lp_optimizer()
|
||||||
return optimizer_with_attributes(HiGHS.Optimizer)
|
return optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -25,81 +25,53 @@ function _print_graph_stats(instance::Instance, graph::Graph)::Nothing
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
function solve_stochastic(;
|
|
||||||
scenarios::Vector{String},
|
|
||||||
probs::Vector{Float64},
|
|
||||||
optimizer,
|
|
||||||
method=:ef,
|
|
||||||
tol=0.1,
|
|
||||||
)
|
|
||||||
@info "Reading instance files..."
|
|
||||||
instances = [parsefile(sc) for sc in scenarios]
|
|
||||||
|
|
||||||
@info "Building graphs..."
|
|
||||||
graphs = [build_graph(inst) for inst in instances]
|
|
||||||
|
|
||||||
@info "Building stochastic model..."
|
|
||||||
sp = RELOG.build_model(instances[1], graphs, probs; optimizer, method, tol)
|
|
||||||
|
|
||||||
@info "Optimizing stochastic model..."
|
|
||||||
optimize!(sp)
|
|
||||||
|
|
||||||
@info "Extracting solution..."
|
|
||||||
solutions = [
|
|
||||||
get_solution(instances[i], graphs[i], sp, i)
|
|
||||||
for i in 1:length(instances)
|
|
||||||
]
|
|
||||||
|
|
||||||
return solutions
|
|
||||||
end
|
|
||||||
|
|
||||||
function solve(
|
function solve(
|
||||||
instance::Instance;
|
instance::Instance;
|
||||||
optimizer=HiGHS.Optimizer,
|
optimizer = nothing,
|
||||||
|
output = nothing,
|
||||||
marginal_costs = true,
|
marginal_costs = true,
|
||||||
return_model=false
|
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..."
|
@info "Building graph..."
|
||||||
graph = RELOG.build_graph(instance)
|
graph = RELOG.build_graph(instance)
|
||||||
_print_graph_stats(instance, graph)
|
_print_graph_stats(instance, graph)
|
||||||
|
|
||||||
@info "Building model..."
|
@info "Building optimization model..."
|
||||||
model = RELOG.build_model(instance, [graph], [1.0]; optimizer)
|
model = RELOG.build_model(instance, graph, milp_optimizer)
|
||||||
|
|
||||||
|
@info "Optimizing MILP..."
|
||||||
|
JuMP.optimize!(model)
|
||||||
|
|
||||||
@info "Optimizing model..."
|
|
||||||
optimize!(model)
|
|
||||||
if !has_values(model)
|
if !has_values(model)
|
||||||
error("No solution available")
|
error("No solution available")
|
||||||
end
|
end
|
||||||
|
|
||||||
@info "Extracting solution..."
|
|
||||||
solution = get_solution(instance, graph, model, 1)
|
|
||||||
|
|
||||||
if marginal_costs
|
if marginal_costs
|
||||||
@info "Re-optimizing with integer variables fixed..."
|
@info "Re-optimizing with integer variables fixed..."
|
||||||
open_plant_vals = value.(model[1, :open_plant])
|
all_vars = JuMP.all_variables(model)
|
||||||
is_open_vals = value.(model[1, :is_open])
|
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
|
||||||
|
JuMP.set_optimizer(model, lp_optimizer)
|
||||||
for n in 1:length(graph.process_nodes), t in 1:instance.time
|
for var in all_vars
|
||||||
unset_binary(model[1, :open_plant][n, t])
|
if JuMP.is_binary(var)
|
||||||
unset_binary(model[1, :is_open][n, t])
|
JuMP.unset_binary(var)
|
||||||
fix(
|
JuMP.fix(var, vals[var])
|
||||||
model[1, :open_plant][n, t],
|
|
||||||
open_plant_vals[n, t]
|
|
||||||
)
|
|
||||||
fix(
|
|
||||||
model[1, :is_open][n, t],
|
|
||||||
is_open_vals[n, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
end
|
end
|
||||||
optimize!(model)
|
end
|
||||||
if has_values(model)
|
JuMP.optimize!(model)
|
||||||
|
end
|
||||||
|
|
||||||
@info "Extracting solution..."
|
@info "Extracting solution..."
|
||||||
solution = get_solution(instance, graph, model, 1, marginal_costs=true)
|
solution = get_solution(model, marginal_costs = marginal_costs)
|
||||||
else
|
|
||||||
@warn "Error computing marginal costs. Ignoring."
|
if output != nothing
|
||||||
end
|
write(solution, output)
|
||||||
end
|
end
|
||||||
|
|
||||||
if return_model
|
if return_model
|
||||||
@@ -115,7 +87,7 @@ function solve(filename::AbstractString; heuristic=false, kwargs...)
|
|||||||
if heuristic && instance.time > 1
|
if heuristic && instance.time > 1
|
||||||
@info "Solving single-period version..."
|
@info "Solving single-period version..."
|
||||||
compressed = _compress(instance)
|
compressed = _compress(instance)
|
||||||
csol, model = solve(compressed; marginal_costs=false, return_model=true, kwargs...)
|
csol = solve(compressed; output = nothing, marginal_costs = false, kwargs...)
|
||||||
@info "Filtering candidate locations..."
|
@info "Filtering candidate locations..."
|
||||||
selected_pairs = []
|
selected_pairs = []
|
||||||
for (plant_name, plant_dict) in csol["Plants"]
|
for (plant_name, plant_dict) in csol["Plants"]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
using DataFrames
|
using DataFrames
|
||||||
using CSV
|
using CSV
|
||||||
|
|
||||||
function products_report(solution)::DataFrame
|
function products_report(solution; marginal_costs = true)::DataFrame
|
||||||
df = DataFrame()
|
df = DataFrame()
|
||||||
df."product name" = String[]
|
df."product name" = String[]
|
||||||
df."location name" = String[]
|
df."location name" = String[]
|
||||||
@@ -14,21 +14,14 @@ function products_report(solution)::DataFrame
|
|||||||
df."year" = Int[]
|
df."year" = Int[]
|
||||||
df."amount (tonne)" = Float64[]
|
df."amount (tonne)" = Float64[]
|
||||||
df."marginal cost (\$/tonne)" = Float64[]
|
df."marginal cost (\$/tonne)" = Float64[]
|
||||||
df."amount disposed (tonne)" = Float64[]
|
|
||||||
df."disposal cost (\$)" = 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"]
|
||||||
for (location_name, location_dict) in prod_dict
|
for (location_name, location_dict) in prod_dict
|
||||||
for year = 1:T
|
for year = 1:T
|
||||||
marginal_cost = NaN
|
|
||||||
if "Marginal cost (\$/tonne)" in keys(location_dict)
|
|
||||||
marginal_cost = location_dict["Marginal cost (\$/tonne)"][year]
|
marginal_cost = location_dict["Marginal cost (\$/tonne)"][year]
|
||||||
end
|
|
||||||
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]
|
|
||||||
disposal_cost = location_dict["Disposal cost (\$)"][year]
|
|
||||||
push!(
|
push!(
|
||||||
df,
|
df,
|
||||||
[
|
[
|
||||||
@@ -39,8 +32,6 @@ function products_report(solution)::DataFrame
|
|||||||
year,
|
year,
|
||||||
amount,
|
amount,
|
||||||
marginal_cost,
|
marginal_cost,
|
||||||
amount_disposed,
|
|
||||||
disposal_cost,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
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,12 +172,6 @@
|
|||||||
},
|
},
|
||||||
"initial amounts": {
|
"initial amounts": {
|
||||||
"$ref": "#/definitions/InitialAmount"
|
"$ref": "#/definitions/InitialAmount"
|
||||||
},
|
|
||||||
"disposal limit (tonne)": {
|
|
||||||
"$ref": "#/definitions/TimeSeries"
|
|
||||||
},
|
|
||||||
"disposal cost ($/tonne)": {
|
|
||||||
"$ref": "#/definitions/TimeSeries"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -1,30 +1,15 @@
|
|||||||
using PackageCompiler
|
using PackageCompiler
|
||||||
using TOML
|
|
||||||
using Logging
|
|
||||||
|
|
||||||
Logging.disable_logging(Logging.Info)
|
using Cbc
|
||||||
|
using Clp
|
||||||
|
using Geodesy
|
||||||
|
using JSON
|
||||||
|
using JSONSchema
|
||||||
|
using JuMP
|
||||||
|
using MathOptInterface
|
||||||
|
using ProgressBars
|
||||||
|
|
||||||
mkpath("build")
|
pkg = [:Cbc, :Clp, :Geodesy, :JSON, :JSONSchema, :JuMP, :MathOptInterface, :ProgressBars]
|
||||||
|
|
||||||
printstyled("Generating precompilation statements...\n", color = :light_green)
|
@info "Building system image..."
|
||||||
run(`julia --project=. --trace-compile=build/precompile.jl $ARGS`)
|
create_sysimage(pkg, sysimage_path = "build/sysimage.so")
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|||||||
4406
test/fixtures/instances/case3_p010_s1.00.json
vendored
4406
test/fixtures/instances/case3_p010_s1.00.json
vendored
File diff suppressed because it is too large
Load Diff
4406
test/fixtures/instances/case3_p010_s1.25.json
vendored
4406
test/fixtures/instances/case3_p010_s1.25.json
vendored
File diff suppressed because it is too large
Load Diff
357
test/fixtures/instances/s1.json
vendored
357
test/fixtures/instances/s1.json
vendored
@@ -1,357 +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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
using RELOG
|
using RELOG
|
||||||
|
|
||||||
function graph_build_test()
|
|
||||||
@testset "build_graph" begin
|
@testset "build_graph" begin
|
||||||
instance = RELOG.parsefile(fixture("instances/s1.json"))
|
basedir = dirname(@__FILE__)
|
||||||
|
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||||
graph = RELOG.build_graph(instance)
|
graph = RELOG.build_graph(instance)
|
||||||
process_node_by_location_name =
|
process_node_by_location_name =
|
||||||
Dict(n.location.location_name => n for n in graph.process_nodes)
|
Dict(n.location.location_name => n for n in graph.process_nodes)
|
||||||
@@ -21,7 +21,7 @@ function graph_build_test()
|
|||||||
@test node.outgoing_arcs[1].source.location.name == "C1"
|
@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.plant_name == "F1"
|
||||||
@test node.outgoing_arcs[1].dest.location.location_name == "L1"
|
@test node.outgoing_arcs[1].dest.location.location_name == "L1"
|
||||||
@test node.outgoing_arcs[1].values["distance"] == 1095.62
|
@test node.outgoing_arcs[1].values["distance"] == 1695.364
|
||||||
|
|
||||||
node = process_node_by_location_name["L1"]
|
node = process_node_by_location_name["L1"]
|
||||||
@test node.location.plant_name == "F1"
|
@test node.location.plant_name == "F1"
|
||||||
@@ -37,4 +37,3 @@ function graph_build_test()
|
|||||||
|
|
||||||
@test length(graph.arcs) == 38
|
@test length(graph.arcs) == 38
|
||||||
end
|
end
|
||||||
end
|
|
||||||
25
test/graph/dist_test.jl
Normal file
25
test/graph/dist_test.jl
Normal 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 "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
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
using RELOG
|
using RELOG
|
||||||
|
|
||||||
function compress_test()
|
|
||||||
@testset "compress" begin
|
@testset "compress" begin
|
||||||
instance = RELOG.parsefile(fixture("instances/s1.json"))
|
basedir = dirname(@__FILE__)
|
||||||
|
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||||
compressed = RELOG._compress(instance)
|
compressed = RELOG._compress(instance)
|
||||||
|
|
||||||
product_name_to_product = Dict(p.name => p for p in compressed.products)
|
product_name_to_product = Dict(p.name => p for p in compressed.products)
|
||||||
@@ -51,4 +51,3 @@ function compress_test()
|
|||||||
@test l1.disposal_cost[p2] ≈ [-10.0]
|
@test l1.disposal_cost[p2] ≈ [-10.0]
|
||||||
@test l1.disposal_cost[p3] ≈ [-10.0]
|
@test l1.disposal_cost[p3] ≈ [-10.0]
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
using RELOG
|
using RELOG
|
||||||
|
|
||||||
function geodb_test()
|
|
||||||
@testset "geodb_query (2018-us-county)" begin
|
@testset "geodb_query (2018-us-county)" begin
|
||||||
region = RELOG.geodb_query("2018-us-county:17043")
|
region = RELOG.geodb_query("2018-us-county:17043")
|
||||||
@test region.centroid.lat == 41.83956
|
@test region.centroid.lat == 41.83956
|
||||||
@@ -24,4 +23,3 @@ function geodb_test()
|
|||||||
@test region.centroid.lon == -89.50414
|
@test region.centroid.lon == -89.50414
|
||||||
@test region.population == 12_671_821
|
@test region.population == 12_671_821
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
using RELOG
|
using RELOG
|
||||||
|
|
||||||
function parse_test()
|
|
||||||
@testset "parse" begin
|
@testset "parse" begin
|
||||||
instance = RELOG.parsefile(fixture("instances/s1.json"))
|
basedir = dirname(@__FILE__)
|
||||||
|
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||||
|
|
||||||
centers = instance.collection_centers
|
centers = instance.collection_centers
|
||||||
plants = instance.plants
|
plants = instance.plants
|
||||||
@@ -40,14 +40,7 @@ function parse_test()
|
|||||||
@test plant.sizes[2].fixed_operating_cost == [30, 30]
|
@test plant.sizes[2].fixed_operating_cost == [30, 30]
|
||||||
@test plant.sizes[2].variable_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"]
|
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"]
|
p3 = product_name_to_product["P3"]
|
||||||
@test length(plant.output) == 2
|
@test length(plant.output) == 2
|
||||||
@test plant.output[p2] == 0.2
|
@test plant.output[p2] == 0.2
|
||||||
@@ -78,7 +71,8 @@ function parse_test()
|
|||||||
end
|
end
|
||||||
|
|
||||||
@testset "parse (geodb)" begin
|
@testset "parse (geodb)" begin
|
||||||
instance = RELOG.parsefile(fixture("instances/s2.json"))
|
basedir = dirname(@__FILE__)
|
||||||
|
instance = RELOG.parsefile("$basedir/../../instances/s2.json")
|
||||||
|
|
||||||
centers = instance.collection_centers
|
centers = instance.collection_centers
|
||||||
@test centers[1].name == "C1"
|
@test centers[1].name == "C1"
|
||||||
@@ -87,6 +81,6 @@ function parse_test()
|
|||||||
end
|
end
|
||||||
|
|
||||||
# @testset "parse (invalid)" begin
|
# @testset "parse (invalid)" begin
|
||||||
# @test_throws ErrorException RELOG.parsefile(fixture("s1-wrong-length.json"))
|
# basedir = dirname(@__FILE__)
|
||||||
|
# @test_throws ErrorException RELOG.parsefile("$basedir/../fixtures/s1-wrong-length.json")
|
||||||
# end
|
# end
|
||||||
end
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
# Copyright (C) 2020 Argonne National Laboratory
|
# Copyright (C) 2020 Argonne National Laboratory
|
||||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
using RELOG, HiGHS, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||||
|
|
||||||
function model_build_test()
|
|
||||||
@testset "build" begin
|
@testset "build" begin
|
||||||
instance = RELOG.parsefile(fixture("instances/s1.json"))
|
basedir = dirname(@__FILE__)
|
||||||
|
instance = RELOG.parsefile("$basedir/../../instances/s1.json")
|
||||||
graph = RELOG.build_graph(instance)
|
graph = RELOG.build_graph(instance)
|
||||||
model = RELOG.build_model(instance, graph, HiGHS.Optimizer)
|
model = RELOG.build_model(instance, graph, Cbc.Optimizer)
|
||||||
|
set_optimizer_attribute(model, "logLevel", 0)
|
||||||
|
|
||||||
process_node_by_location_name =
|
process_node_by_location_name =
|
||||||
Dict(n.location.location_name => n for n in graph.process_nodes)
|
Dict(n.location.location_name => n for n in graph.process_nodes)
|
||||||
@@ -16,23 +17,22 @@ function model_build_test()
|
|||||||
(n.location.location_name, n.product.name) => n for n in graph.plant_shipping_nodes
|
(n.location.location_name, n.product.name) => n for n in graph.plant_shipping_nodes
|
||||||
)
|
)
|
||||||
|
|
||||||
@test length(model[1, :open_plant]) == 12
|
@test length(model[:flow]) == 76
|
||||||
@test length(model[2, :flow]) == 76
|
@test length(model[:dispose]) == 16
|
||||||
@test length(model[2, :plant_dispose]) == 16
|
@test length(model[:open_plant]) == 12
|
||||||
@test length(model[2, :capacity]) == 12
|
@test length(model[:capacity]) == 12
|
||||||
@test length(model[2, :expansion]) == 12
|
@test length(model[:expansion]) == 12
|
||||||
|
|
||||||
# l1 = process_node_by_location_name["L1"]
|
l1 = process_node_by_location_name["L1"]
|
||||||
# v = model[2, :capacity][l1.index, 1]
|
v = model[:capacity][l1, 1]
|
||||||
# @test lower_bound(v) == 0.0
|
@test lower_bound(v) == 0.0
|
||||||
# @test upper_bound(v) == 1000.0
|
@test upper_bound(v) == 1000.0
|
||||||
|
|
||||||
# v = model[2, :expansion][l1.index, 1]
|
v = model[:expansion][l1, 1]
|
||||||
# @test lower_bound(v) == 0.0
|
@test lower_bound(v) == 0.0
|
||||||
# @test upper_bound(v) == 750.0
|
@test upper_bound(v) == 750.0
|
||||||
|
|
||||||
# v = model[2, :plant_dispose][shipping_node_by_loc_and_prod_names["L1", "P2"].index, 1]
|
v = model[:dispose][shipping_node_by_loc_and_prod_names["L1", "P2"], 1]
|
||||||
# @test lower_bound(v) == 0.0
|
@test lower_bound(v) == 0.0
|
||||||
# @test upper_bound(v) == 1.0
|
@test upper_bound(v) == 1.0
|
||||||
end
|
|
||||||
end
|
end
|
||||||
11
test/model/resolve_test.jl
Normal file
11
test/model/resolve_test.jl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (C) 2020 Argonne National Laboratory
|
||||||
|
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
|
using RELOG
|
||||||
|
|
||||||
|
@testset "Resolve" begin
|
||||||
|
# Shoud not crash
|
||||||
|
filename = "$(pwd())/../instances/s1.json"
|
||||||
|
solution_old, model_old = RELOG.solve(filename, return_model = true)
|
||||||
|
solution_new = RELOG.resolve(model_old, filename)
|
||||||
|
end
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
# Copyright (C) 2020 Argonne National Laboratory
|
# Copyright (C) 2020 Argonne National Laboratory
|
||||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
using RELOG, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||||
|
|
||||||
basedir = dirname(@__FILE__)
|
basedir = dirname(@__FILE__)
|
||||||
|
|
||||||
function model_solve_test()
|
|
||||||
@testset "solve (exact)" begin
|
@testset "solve (exact)" begin
|
||||||
solution = RELOG.solve(fixture("instances/s1.json"))
|
solution_filename_a = tempname()
|
||||||
|
solution_filename_b = tempname()
|
||||||
|
solution = RELOG.solve("$basedir/../../instances/s1.json", output = solution_filename_a)
|
||||||
|
|
||||||
solution_filename = tempname()
|
@test isfile(solution_filename_a)
|
||||||
RELOG.write(solution, solution_filename)
|
|
||||||
@test isfile(solution_filename)
|
RELOG.write(solution, solution_filename_b)
|
||||||
|
@test isfile(solution_filename_b)
|
||||||
|
|
||||||
@test "Costs" in keys(solution)
|
@test "Costs" in keys(solution)
|
||||||
@test "Fixed operating (\$)" in keys(solution["Costs"])
|
@test "Fixed operating (\$)" in keys(solution["Costs"])
|
||||||
@@ -24,29 +26,20 @@ function model_solve_test()
|
|||||||
@test "F2" in keys(solution["Plants"])
|
@test "F2" in keys(solution["Plants"])
|
||||||
@test "F3" in keys(solution["Plants"])
|
@test "F3" in keys(solution["Plants"])
|
||||||
@test "F4" 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
|
end
|
||||||
|
|
||||||
@testset "solve (heuristic)" begin
|
@testset "solve (heuristic)" begin
|
||||||
# Should not crash
|
# Should not crash
|
||||||
solution = RELOG.solve(fixture("instances/s1.json"), heuristic = true)
|
solution = RELOG.solve("$basedir/../../instances/s1.json", heuristic = true)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @testset "solve (infeasible)" begin
|
@testset "solve (infeasible)" begin
|
||||||
# json = JSON.parsefile(fixture("instances/s1.json"))
|
json = JSON.parsefile("$basedir/../../instances/s1.json")
|
||||||
# for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
|
for (location_name, location_dict) in json["products"]["P1"]["initial amounts"]
|
||||||
# location_dict["amount (tonne)"] *= 1000
|
location_dict["amount (tonne)"] *= 1000
|
||||||
# end
|
end
|
||||||
# @test_throws ErrorException("No solution available") RELOG.solve(RELOG.parse(json))
|
@test_throws ErrorException("No solution available") RELOG.solve(RELOG.parse(json))
|
||||||
# end
|
end
|
||||||
|
|
||||||
@testset "solve (with storage)" begin
|
@testset "solve (with storage)" begin
|
||||||
basedir = dirname(@__FILE__)
|
basedir = dirname(@__FILE__)
|
||||||
@@ -66,20 +59,3 @@ function model_solve_test()
|
|||||||
@test solution["Costs"]["Storage (\$)"] == [100.0, 75.0, 0.0]
|
@test solution["Costs"]["Storage (\$)"] == [100.0, 75.0, 0.0]
|
||||||
@test solution["Costs"]["Total (\$)"] == [600.0, 75.0, 100.0]
|
@test solution["Costs"]["Total (\$)"] == [600.0, 75.0, 100.0]
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "solve (stochastic)" begin
|
|
||||||
# Should not crash
|
|
||||||
solutions = RELOG.solve_stochastic(
|
|
||||||
scenarios=[
|
|
||||||
fixture("instances/case3_p010_s1.00.json"),
|
|
||||||
fixture("instances/case3_p010_s1.25.json"),
|
|
||||||
],
|
|
||||||
probs=[0.5, 0.5],
|
|
||||||
optimizer=optimizer_with_attributes(
|
|
||||||
HiGHS.Optimizer,
|
|
||||||
"log_to_console" => false,
|
|
||||||
),
|
|
||||||
method=:lshaped,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
using RELOG, JSON, GZip
|
using RELOG, JSON, GZip
|
||||||
|
|
||||||
basedir = @__DIR__
|
|
||||||
|
|
||||||
function reports_test()
|
|
||||||
@testset "Reports" begin
|
@testset "Reports" begin
|
||||||
@testset "from solve" begin
|
@testset "from solve" begin
|
||||||
solution = RELOG.solve(fixture("instances/s1.json"))
|
solution = RELOG.solve("$(pwd())/../instances/s1.json")
|
||||||
tmp_filename = tempname()
|
tmp_filename = tempname()
|
||||||
# The following should not crash
|
# The following should not crash
|
||||||
RELOG.write_plant_emissions_report(solution, tmp_filename)
|
RELOG.write_plant_emissions_report(solution, tmp_filename)
|
||||||
@@ -20,4 +17,3 @@ function reports_test()
|
|||||||
RELOG.write_transportation_report(solution, tmp_filename)
|
RELOG.write_transportation_report(solution, tmp_filename)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
@@ -2,46 +2,21 @@
|
|||||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||||
|
|
||||||
using Test
|
using Test
|
||||||
using RELOG
|
|
||||||
using Revise
|
|
||||||
|
|
||||||
includet("instance/compress_test.jl")
|
|
||||||
includet("instance/geodb_test.jl")
|
|
||||||
includet("instance/parse_test.jl")
|
|
||||||
includet("graph/build_test.jl")
|
|
||||||
includet("model/build_test.jl")
|
|
||||||
includet("model/solve_test.jl")
|
|
||||||
includet("reports_test.jl")
|
|
||||||
|
|
||||||
function fixture(path)
|
|
||||||
for candidate in [
|
|
||||||
"fixtures/$path",
|
|
||||||
"test/fixtures/$path"
|
|
||||||
]
|
|
||||||
if isfile(candidate)
|
|
||||||
return candidate
|
|
||||||
end
|
|
||||||
end
|
|
||||||
error("Fixture not found: $path")
|
|
||||||
end
|
|
||||||
|
|
||||||
function runtests()
|
|
||||||
@testset "RELOG" begin
|
@testset "RELOG" begin
|
||||||
@testset "Instance" begin
|
@testset "Instance" begin
|
||||||
compress_test()
|
include("instance/compress_test.jl")
|
||||||
geodb_test()
|
include("instance/geodb_test.jl")
|
||||||
parse_test()
|
include("instance/parse_test.jl")
|
||||||
end
|
end
|
||||||
@testset "Graph" begin
|
@testset "Graph" begin
|
||||||
graph_build_test()
|
include("graph/build_test.jl")
|
||||||
|
include("graph/dist_test.jl")
|
||||||
end
|
end
|
||||||
@testset "Model" begin
|
@testset "Model" begin
|
||||||
model_build_test()
|
include("model/build_test.jl")
|
||||||
model_solve_test()
|
include("model/solve_test.jl")
|
||||||
|
include("model/resolve_test.jl")
|
||||||
end
|
end
|
||||||
reports_test()
|
include("reports_test.jl")
|
||||||
end
|
end
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
runtests()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user