10 Commits

18 changed files with 511 additions and 167 deletions

View File

@@ -1,4 +1,4 @@
name: CI name: Build & Test
on: on:
- push - push
- pull_request - pull_request
@@ -8,8 +8,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
version: version: ['1.3', '1.4', '1.5', 'nightly']
- '1.3'
os: os:
- ubuntu-latest - ubuntu-latest
arch: arch:
@@ -20,5 +19,5 @@ jobs:
with: with:
version: ${{ matrix.version }} version: ${{ matrix.version }}
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
- uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@latest - uses: julia-actions/julia-runtest@v1

View File

@@ -1,3 +1,7 @@
# Version 0.5.0 (TBD)
- Allow plants to store input material for processing in later years
# Version 0.4.0 (Sep 18, 2020) # Version 0.4.0 (Sep 18, 2020)
- Generate simplified solution reports (CSV) - Generate simplified solution reports (CSV)

View File

@@ -1,15 +1,15 @@
JULIA := julia --color=yes --project=@. JULIA := julia --color=yes --project=@.
SRC_FILES := $(wildcard src/*.jl test/*.jl) SRC_FILES := $(wildcard src/*.jl test/*.jl)
VERSION := 0.4 VERSION := 0.5
all: docs test all: docs test
build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml
mkdir -p build
$(JULIA) src/sysimage.jl $(JULIA) src/sysimage.jl
build/test.log: $(SRC_FILES) build/sysimage.so build/test.log: $(SRC_FILES) build/sysimage.so
@echo Running tests... cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl
cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl | tee ../build/test.log
clean: clean:
rm -rf build/* rm -rf build/*

View File

@@ -1,7 +1,7 @@
name = "RELOG" name = "RELOG"
uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008" uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008"
authors = ["Alinson S Xavier <axavier@anl.gov>"] authors = ["Alinson S Xavier <axavier@anl.gov>"]
version = "0.4.0" version = "0.5.0"
[deps] [deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
@@ -19,6 +19,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[compat] [compat]

View File

@@ -1,16 +1,28 @@
RELOG: Reverse Logistics Optimization <h1 align="center">RELOG: Reverse Logistics Optimization</h1>
===================================== <p align="center">
<a href="https://github.com/ANL-CEEESA/RELOG/actions">
<img src="https://github.com/ANL-CEEESA/RELOG/workflows/CI/badge.svg">
</a>
<a href="https://doi.org/10.5281/zenodo.4302341">
<img src="https://zenodo.org/badge/DOI/10.5281/zenodo.4302341.svg">
</a>
<a href="https://github.com/ANL-CEEESA/RELOG/releases/">
<img src="https://img.shields.io/github/v/release/ANL-CEEESA/RELOG?include_prereleases&label=pre-release">
</a>
</p>
**RELOG** is a supply chain optimization package focusing on reverse logistics and reverse manufacturing. For example, the package can be used to determine where to build recycling plants, what sizes should they have and which customers should be served by which plants. The package supports customized reverse logistics pipelines, with multiple types of plants, multiple types of product and multiple time periods. **RELOG** is a supply chain optimization package focusing on reverse logistics and reverse manufacturing. For example, the package can be used to determine where to build recycling plants, what sizes should they have and which customers should be served by which plants. The package supports customized reverse logistics pipelines, with multiple types of plants, multiple types of product and multiple time periods.
<img src="https://anl-ceeesa.github.io/RELOG/0.4/images/ex_transportation.png" width="1000px"/>
<img src="https://anl-ceeesa.github.io/RELOG/0.5/images/ex_transportation.png" width="1000px"/>
### Documentation ### Documentation
* [Usage](https://anl-ceeesa.github.io/RELOG/0.4/usage) * [Usage](https://anl-ceeesa.github.io/RELOG/0.5/usage)
* [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.4/format) * [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.5/format)
* [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.4/reports) * [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.5/reports)
* [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.4/model) * [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.5/model)
### Authors ### Authors

View File

@@ -107,7 +107,15 @@ Each type of plant is associated with a set of potential locations where it can
| `latitude (deg)` | The latitude of the location, in degrees. | `latitude (deg)` | The latitude of the location, in degrees.
| `longitude (deg)` | The longitude of the location, in degrees. | `longitude (deg)` | The longitude of the location, in degrees.
| `disposal` | A dictionary describing what products can be disposed locally at the plant. | `disposal` | A dictionary describing what products can be disposed locally at the plant.
| `capacities (tonne)` | A dictionary describing what plant sizes are allowed, and their characteristics. | `storage` | A dictionary describing the plant's storage.
| `capacities (tonne)` | A dictionary describing what plant sizes are allowed, and their characteristics.
The `storage` dictionary should contain the following keys:
| Key | Description
|:------------------------|---------------|
| `cost ($/tonne)` | The cost to store a tonne of input product for one time period. Must be a time series.
| `limit (tonne)` | The maximum amount of input product this plant can have in storage at any given time.
The keys in the `disposal` dictionary should be the names of the products. The values are dictionaries with the following keys: The keys in the `disposal` dictionary should be the names of the products. The values are dictionaries with the following keys:
@@ -151,11 +159,15 @@ The keys in the `capacities (tonne)` dictionary should be the amounts (in tonnes
"limit (tonne)": [1.0, 1.0] "limit (tonne)": [1.0, 1.0]
} }
}, },
"storage": {
"cost ($/tonne)": [5.0, 5.3],
"limit (tonne)": 100.0,
},
"capacities (tonne)": { "capacities (tonne)": {
"100": { "100": {
"opening cost ($)": [500, 530], "opening cost ($)": [500, 530],
"fixed operating cost ($)": [300.0, 310.0], "fixed operating cost ($)": [300.0, 310.0],
"variable operating cost ($/tonne)": [5.0, 5.2] "variable operating cost ($/tonne)": [5.0, 5.2],
}, },
"500": { "500": {
"opening cost ($)": [750, 760], "opening cost ($)": [750, 760],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -21,9 +21,11 @@ In this page, we describe the precise mathematical optimization model used by RE
* $c^\text{f-base}_{pt}$ - Fixed cost of keeping plant $p$ open during time period $t$ (`$`) * $c^\text{f-base}_{pt}$ - Fixed cost of keeping plant $p$ open during time period $t$ (`$`)
* $c^\text{f-exp}_{pt}$ - Increase in fixed cost for each additional tonne of capacity (`$/tonne`) * $c^\text{f-exp}_{pt}$ - Increase in fixed cost for each additional tonne of capacity (`$/tonne`)
* $c^\text{var}_{pt}$ - Variable cost of processing one tonne of input at plant $p$ at time $t$ (`$/tonne`) * $c^\text{var}_{pt}$ - Variable cost of processing one tonne of input at plant $p$ at time $t$ (`$/tonne`)
* $c^\text{store}_{pt}$ - Cost of storing one tonne of original material at plant $p$ at time $t$ (`$/tonne`)
* $m^\text{min}_p$ - Minimum capacity of plant $p$ (`tonne`) * $m^\text{min}_p$ - Minimum capacity of plant $p$ (`tonne`)
* $m^\text{max}_p$ - Maximum capacity of plant $p$ (`tonne`) * $m^\text{max}_p$ - Maximum capacity of plant $p$ (`tonne`)
* $m^\text{disp}_{pmt}$ - Maximum amount of material $m$ that plant $p$ can dispose of during time $t$ (`tonne`) * $m^\text{disp}_{pmt}$ - Maximum amount of material $m$ that plant $p$ can dispose of during time $t$ (`tonne`)
* $m^\text{store}_p$ - Maximum amount of original material that plant $p$ can store for later processing.
**Products:** **Products:**
@@ -42,7 +44,9 @@ In this page, we describe the precise mathematical optimization model used by RE
* $w_{pt}$ - Extra capacity (amount above the minimum) added to plant $p$ during time $t$ (`tonne`) * $w_{pt}$ - Extra capacity (amount above the minimum) added to plant $p$ during time $t$ (`tonne`)
* $x_{pt}$ - Binary variable that equals 1 if plant $p$ is operational at time $t$ (`bool`) * $x_{pt}$ - Binary variable that equals 1 if plant $p$ is operational at time $t$ (`bool`)
* $y_{lpt}$ - Amount of product sent from location $l$ to plant $p$ during time $t$ (`tonne`) * $y_{lpt}$ - Amount of product sent from location $l$ to plant $p$ during time $t$ (`tonne`)
* $z_{mpt}$ - Amount of material $m$ disposed of by plant $p$ during time $t$ (`tonne`) * $z^{\text{disp}}_{mpt}$ - Amount of material $m$ disposed of by plant $p$ during time $t$ (`tonne`)
* $z^{\text{store}}_{pt}$ - Amount of original material in storage at plant $p$ by the end of time period $t$ (`tonne`)
* $z^{\text{proc}}_{mpt}$ - Amount of original material processed by plant $p$ during time period $t$ (`tonne`)
### Objective function ### Objective function
@@ -58,16 +62,22 @@ RELOG minimizes the overall capital, production and transportation costs:
c^{\text{exp}}_{pt} w_{pt} c^{\text{exp}}_{pt} w_{pt}
\right] + \\ \right] + \\
& &
\sum_{t \in T} \sum_{l \in L} \sum_{p \in P} \left[ \sum_{t \in T} \sum_{p \in P} \left[
c^{\text{tr}}_t d_{lp} + c^{\text{var}}_{pt} c^{\text{store}}_{pt} z^{\text{store}}_{pt} +
\right] y_{lpt} + \\ c^{\text{proc}}_{pt} z^{\text{proc}}_{pt}
\right] + \\
&
\sum_{t \in T} \sum_{l \in L} \sum_{p \in P}
c^{\text{tr}}_t d_{lp} y_{lpt}
\\
& &
\sum_{t \in T} \sum_{p \in P} \sum_{m \in M} c^{\text{disp}}_{pmt} z_{pmt} \sum_{t \in T} \sum_{p \in P} \sum_{m \in M} c^{\text{disp}}_{pmt} z_{pmt}
\end{align*} \end{align*}
In the first line, we have (i) opening costs, if plant starts operating at time $t$, (ii) fixed operating costs, if plant is operational, (iii) additional fixed operating costs coming from expansion performed in all previous time periods up to the current one, and finally (iv) the expansion costs during the current time period. In the first line, we have (i) opening costs, if plant starts operating at time $t$, (ii) fixed operating costs, if plant is operational, (iii) additional fixed operating costs coming from expansion performed in all previous time periods up to the current one, and finally (iv) the expansion costs during the current time period.
In the second line, we have the transportation costs and the variable operating costs. In the second line, we have storage and variable processing costs.
In the third line, we have the disposal costs. In the third line, we have transportation costs.
In the fourth line, we have the disposal costs.
### Constraints ### Constraints
@@ -78,10 +88,29 @@ In the third line, we have the disposal costs.
& \forall l \in L, t \in T & \forall l \in L, t \in T
\end{align} \end{align}
* Plants have a limited capacity: * Amount received equals amount processed plus stored. Furthermore, all original material should be processed by the end of the simulation.
\begin{align} \begin{align}
& \sum_{l \in L} y_{lpt} \leq m^\text{min}_p x_p + \sum_{i=1}^t w_p & \sum_{l \in L} y_{lpt} + z^{\text{store}}_{p,t-1}
= z^{\text{proc}}_{pt} + z^{\text{store}}_{p,t}
& \forall p \in P, t \in T \\
& z^{\text{store}}_{p,0} = 0
& \forall p \in P \\
& z^{\text{store}}_{p,t^{\max}} = 0
& \forall p \in P
\end{align}
* Plants have a limited processing capacity. Furthermore, if a plant is closed, it has zero processing capacity:
\begin{align}
& z^{\text{proc}}_{pt} \leq m^\text{min}_p x_p + \sum_{i=1}^t w_p
& \forall p \in P, t \in T
\end{align}
* Plants have limited storage capacity. Furthermore, if a plant is closed, is has zero storage capacity:
\begin{align}
& z^{\text{store}}_{pt} \leq m^\text{store}_p x_p
& \forall p \in P, t \in T & \forall p \in P, t \in T
\end{align} \end{align}
@@ -92,10 +121,10 @@ In the third line, we have the disposal costs.
& \forall p \in P, t \in T & \forall p \in P, t \in T
\end{align} \end{align}
* Amount of recovered material is proportional to the plant input: * Amount of recovered material is proportional to amount processed:
\begin{align} \begin{align}
& q_{mpt} = \alpha_{pm} \sum_{l \in L} y_{lpt} & q_{mpt} = \alpha_{pm} z^{\text{proc}}_{pt}
& \forall m \in M, p \in P, t \in T & \forall m \in M, p \in P, t \in T
\end{align} \end{align}
@@ -129,50 +158,8 @@ In the third line, we have the disposal costs.
& \forall p \in P, t \in T \\ & \forall p \in P, t \in T \\
& y_{lpt} \geq 0 & y_{lpt} \geq 0
& \forall l \in L, p \in P, t \in T \\ & \forall l \in L, p \in P, t \in T \\
& m^\text{disp}_{mpt} \geq z_{mpt} \geq 0 & z^{\text{store}}_{pt} \geq 0
& p \in P, t \in T \\
& z^{\text{disp}}_{mpt}, z^{\text{proc}}_{mpt} \geq 0
& \forall m \in M, p \in P, t \in T & \forall m \in M, p \in P, t \in T
\end{align} \end{align}
### Complete optimization model
\begin{align*}
\text{minimize} \;\; &
\sum_{t \in T} \sum_{p \in P} \left[
c^\text{open}_{pt} u_{pt} +
c^\text{f-base}_{pt} x_{pt} +
\sum_{i=1}^t c^\text{f-exp}_{pt} w_{pi} +
c^{\text{exp}}_{pt} w_{pt}
\right] + \\
&
\sum_{t \in T} \sum_{l \in L} \sum_{p \in P} \left[
c^{\text{tr}}_t d_{lp} + c^{\text{var}}_{pt}
\right] y_{lpt} + \\
&
\sum_{t \in T} \sum_{p \in P} \sum_{m \in M} c^{\text{disp}}_{pmt} z_{pmt} \\
\text{subject to } & \sum_{p \in P} y_{lpt} = m^\text{initial}_{lt}
& \forall l \in L, t \in T \\
& \sum_{l \in L} y_{lpt} \leq m^\text{min}_p x_p + \sum_{i=1}^t w_p
& \forall p \in P, t \in T \\
& \sum_{i=1}^t w_p \leq m^\text{max}_p x_p
& \forall p \in P, t \in T \\
& q_{mpt} = \alpha_{pm} \sum_{l \in L} y_{lpt}
& \forall m \in M, p \in P, t \in T \\
& q_{mpt} = z_{mpt}
& \forall m \in M, p \in P, t \in T \\
& x_{pt} = x_{p,t-1} + u_{pt}
& \forall p \in P, t \in T \setminus \{1\} \\
& x_{p,1} = u_{p,1}
& \forall p \in P \\
& q_{mpt} \geq 0
& \forall m \in M, p \in P, t \in T \\
& u_{pt} \in \{0,1\}
& \forall p \in P, t \in T \\
& w_{pt} \geq 0
& \forall p \in P, t \in T \\
& x_{pt} \in \{0,1\}
& \forall p \in P, t \in T \\
& y_{lpt} \geq 0
& \forall l \in L, p \in P, t \in T \\
& m^\text{disp}_{mpt} \geq z_{mpt} \geq 0
& \forall m \in M, p \in P, t \in T
\end{align*}

View File

@@ -18,13 +18,16 @@ Generated by `RELOG.write_plants_report(solution, filename)`. For a concrete exa
| `latitude (deg)` | Latitude of the plant. | `latitude (deg)` | Latitude of the plant.
| `longitude (deg)` | Longitude of the plant. | `longitude (deg)` | Longitude of the plant.
| `capacity (tonne)` | Capacity of the plant at this point in time. | `capacity (tonne)` | Capacity of the plant at this point in time.
| `amount processed (tonne)` | Amount of input material received by the plant this year. | `amount received (tonne)` | Amount of input material received by the plant this year.
| `amount processed (tonne)` | Amount of input material processed by the plant this year.
| `amount in storage (tonne)` | Amount of input material in storage at the end of the year.
| `utilization factor (%)` | Amount processed by the plant this year divided by current plant capacity. | `utilization factor (%)` | Amount processed by the plant this year divided by current plant capacity.
| `energy (GJ)` | Amount of energy expended by the plant this year. | `energy (GJ)` | Amount of energy expended by the plant this year.
| `opening cost ($)` | Amount spent opening the plant. This value is only positive if the plant became operational this year. | `opening cost ($)` | Amount spent opening the plant. This value is only positive if the plant became operational this year.
| `expansion cost ($)` | Amount spent this year expanding the plant capacity. | `expansion cost ($)` | Amount spent this year expanding the plant capacity.
| `fixed operating cost ($)` | Amount spent for keeping the plant operational this year. | `fixed operating cost ($)` | Amount spent for keeping the plant operational this year.
| `variable operating cost ($)` | Amount spent for processing the input material this year. | `variable operating cost ($)` | Amount spent this year to process the input material.
| `storage cost ($)` | Amount spent this year on storage.
| `total cost ($)` | Sum of all previous plant costs. | `total cost ($)` | Sum of all previous plant costs.
@@ -79,7 +82,7 @@ Generated by `RELOG.write_plant_outputs_report(solution, filename)`. For a concr
| `plant type` | Plant type. | `plant type` | Plant type.
| `location name` | Location name. | `location name` | Location name.
| `year` | What year this row corresponds to. This reports includes one row for each year in the simulation. | `year` | What year this row corresponds to. This reports includes one row for each year in the simulation.
| `product` | Product being produced. | `product name` | Product being produced.
| `amount produced (tonne)` | Amount of product produced this year. | `amount produced (tonne)` | Amount of product produced this year.
| `amount sent (tonne)` | Amount of product produced by this plant and sent to another plant for further processing this year. | `amount sent (tonne)` | Amount of product produced by this plant and sent to another plant for further processing this year.
| `amount disposed (tonne)` | Amount produced produced by this plant and immediately disposed of locally this year. | `amount disposed (tonne)` | Amount produced produced by this plant and immediately disposed of locally this year.

View File

@@ -29,6 +29,8 @@ A **product** is any material that needs to be recycled, any intermediary produc
* The model assumes that some products are initially available at user-specified locations (described by their latitude, longitude and the amount available), while other products only become available during the recycling process. * The model assumes that some products are initially available at user-specified locations (described by their latitude, longitude and the amount available), while other products only become available during the recycling process.
* Products that are initially available must be sent to a plant for processing during the same time period they became available.
* Transporting products from one location to another incurs a transportation cost (`$/km/tonne`), spends some amount of energy (`J/km/tonne`) and may generate multiple types of emissions (`tonne/tonne`). All these parameters are user-specified and may be product- and time-specific. * Transporting products from one location to another incurs a transportation cost (`$/km/tonne`), spends some amount of energy (`J/km/tonne`) and may generate multiple types of emissions (`tonne/tonne`). All these parameters are user-specified and may be product- and time-specific.
A **plant** is a facility that converts one type of product to another. RELOG assumes that each plant receives a single type of product as input and converts this input into multiple types of products. Multiple types of plants, with different inputs, outputs and performance characteristics, may be specified. In the NiMH battery recycling study case, for example, one type of plant could be a *disassembly plant*, which converts *batteries* into *cathode* and *anode*. Another type of plant could be *anode recycling plant*, which converts *anode* into *rare-earth elements* and *scrap metals*. A **plant** is a facility that converts one type of product to another. RELOG assumes that each plant receives a single type of product as input and converts this input into multiple types of products. Multiple types of plants, with different inputs, outputs and performance characteristics, may be specified. In the NiMH battery recycling study case, for example, one type of plant could be a *disassembly plant*, which converts *batteries* into *cathode* and *anode*. Another type of plant could be *anode recycling plant*, which converts *anode* into *rare-earth elements* and *scrap metals*.
@@ -37,7 +39,9 @@ A **plant** is a facility that converts one type of product to another. RELOG as
* Plants can be built at user-specified potential locations. Opening a plant incurs a one-time opening cost (`$`) which may be region- and time-specific. Plants also have a limited capacity (in `tonne`), which indicates the maximum amount of input material they are able to process per year. When specifying potential locations for each type of plant, it is also possible to specify the minimum and maximum capacity of the plants that can be built at that particular location. Different plants sizes may have different opening costs and fixed operating costs. After a plant is built, it can be further expanded in the following years, up to its maximum capacity. * Plants can be built at user-specified potential locations. Opening a plant incurs a one-time opening cost (`$`) which may be region- and time-specific. Plants also have a limited capacity (in `tonne`), which indicates the maximum amount of input material they are able to process per year. When specifying potential locations for each type of plant, it is also possible to specify the minimum and maximum capacity of the plants that can be built at that particular location. Different plants sizes may have different opening costs and fixed operating costs. After a plant is built, it can be further expanded in the following years, up to its maximum capacity.
* All products that are initially available must be sent to a plant for processing. All products that are generated by a plant can either be sent to another plant for further processing, or disposed of locally for either a profit or a loss (`$/tonne`). To model environmental regulations, it is also possible to specify the maximum amount of each product that can be disposed of at each location. * Products received by a plant can be either processed immediately or stored for later processing. Plants have a maximum storage capacity (`tonne`). Storage costs (`$/tonne`) can also be specified.
* All products generated by a plant can either be sent to another plant for further processing, or disposed of locally for either a profit or a loss (`$/tonne`). To model environmental regulations, it is also possible to specify the maximum amount of each product that can be disposed of at each location.
All user parameters specified above must be provided to RELOG as a JSON file, which is fully described in the [data format page](format.md). All user parameters specified above must be provided to RELOG as a JSON file, which is fully described in the [data format page](format.md).
@@ -79,3 +83,20 @@ RELOG.solve("instance.json",
output="solution.json", output="solution.json",
optimizer=gurobi) optimizer=gurobi)
``` ```
### 4.2 Multi-period heuristics
For large-scale instances, it may be too time-consuming to find an exact optimal solution to the multi-period version of the problem. For these situations, RELOG includes a heuristic solution method, which proceeds as follows:
1. First, RELOG creates a single-period version of the problem, in which most values are replaced by their averages. This single-period problem is typically much easier to solve.
2. After solving the simplified problem, RELOG resolves the multi-period version of the problem, but considering only candidate plant locations that were selected by the optimal solution to the single-period version of the problem. All remaining candidate plant locations are removed.
To solve an instance using this heuristic, use the option `heuristic=true`, as shown below.
```julia
using RELOG
solution = RELOG.solve("/home/user/instance.json",
heuristic=true)
```

View File

@@ -2,7 +2,11 @@
# 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 JSON, JSONSchema, Printf using DataStructures
using JSON
using JSONSchema
using Printf
using Statistics
mutable struct Product mutable struct Product
@@ -44,6 +48,8 @@ mutable struct Plant
sizes::Array{PlantSize} sizes::Array{PlantSize}
energy::Array{Float64} energy::Array{Float64}
emissions::Dict{String, Array{Float64}} emissions::Dict{String, Array{Float64}}
storage_limit::Float64
storage_cost::Array{Float64}
end end
@@ -55,6 +61,7 @@ mutable struct Instance
building_period::Array{Int64} building_period::Array{Int64}
end end
function validate(json, schema) function validate(json, schema)
result = JSONSchema.validate(json, schema) result = JSONSchema.validate(json, schema)
if result !== nothing if result !== nothing
@@ -77,7 +84,7 @@ function parsefile(path::String)::Instance
end end
function parse(json::Dict)::Instance function parse(json)::Instance
basedir = dirname(@__FILE__) basedir = dirname(@__FILE__)
json_schema = JSON.parsefile("$basedir/schemas/input.json") json_schema = JSON.parsefile("$basedir/schemas/input.json")
validate(json, Schema(json_schema)) validate(json, Schema(json_schema))
@@ -179,6 +186,15 @@ function parse(json::Dict)::Instance
length(sizes) > 1 || push!(sizes, sizes[1]) length(sizes) > 1 || push!(sizes, sizes[1])
sort!(sizes, by = x -> x.capacity) sort!(sizes, by = x -> x.capacity)
# Storage
storage_limit = 0
storage_cost = zeros(T)
if "storage" in keys(location_dict)
storage_dict = location_dict["storage"]
storage_limit = storage_dict["limit (tonne)"]
storage_cost = storage_dict["cost (\$/tonne)"]
end
# Validation: Capacities # Validation: Capacities
if length(sizes) != 2 if length(sizes) != 2
throw("At most two capacities are supported") throw("At most two capacities are supported")
@@ -198,7 +214,9 @@ function parse(json::Dict)::Instance
disposal_cost, disposal_cost,
sizes, sizes,
energy, energy,
emissions) emissions,
storage_limit,
storage_cost)
push!(plants, plant) push!(plants, plant)
end end
@@ -209,3 +227,55 @@ function parse(json::Dict)::Instance
return Instance(T, products, collection_centers, plants, building_period) return Instance(T, products, collection_centers, plants, building_period)
end end
"""
_compress(instance::Instance)
Create a single-period instance from a multi-period one. Specifically,
replaces every time-dependent attribute, such as initial_amounts,
by a list with a single element, which is either a sum, an average,
or something else that makes sense to that specific attribute.
"""
function _compress(instance::Instance)::Instance
T = instance.time
compressed = deepcopy(instance)
compressed.time = 1
compressed.building_period = [1]
# Compress products
for p in compressed.products
p.transportation_cost = [mean(p.transportation_cost)]
p.transportation_energy = [mean(p.transportation_energy)]
for (emission_name, emission_value) in p.transportation_emissions
p.transportation_emissions[emission_name] = [mean(emission_value)]
end
end
# Compress collection centers
for c in compressed.collection_centers
c.amount = [maximum(c.amount) * T]
end
# Compress plants
for plant in compressed.plants
plant.energy = [mean(plant.energy)]
for (emission_name, emission_value) in plant.emissions
plant.emissions[emission_name] = [mean(emission_value)]
end
for s in plant.sizes
s.capacity *= T
s.variable_operating_cost = [mean(s.variable_operating_cost)]
s.opening_cost = [s.opening_cost[1]]
s.fixed_operating_cost = [sum(s.fixed_operating_cost)]
end
for (prod_name, disp_limit) in plant.disposal_limit
plant.disposal_limit[prod_name] = [sum(disp_limit)]
end
for (prod_name, disp_cost) in plant.disposal_cost
plant.disposal_cost[prod_name] = [mean(disp_cost)]
end
end
return compressed
end

View File

@@ -35,6 +35,15 @@ function create_vars!(model::ManufacturingModel)
upper_bound=n.location.disposal_limit[n.product][t]) upper_bound=n.location.disposal_limit[n.product][t])
for n in values(graph.plant_shipping_nodes), t in 1:T) for n in values(graph.plant_shipping_nodes), t in 1:T)
vars.store = Dict((n, t) => @variable(mip,
lower_bound=0,
upper_bound=n.location.storage_limit)
for n in values(graph.process_nodes), t in 1:T)
vars.process = Dict((n, t) => @variable(mip,
lower_bound = 0)
for n in values(graph.process_nodes), t in 1:T)
vars.open_plant = Dict((n, t) => @variable(mip, binary=true) vars.open_plant = Dict((n, t) => @variable(mip, binary=true)
for n in values(graph.process_nodes), t in 1:T) for n in values(graph.process_nodes), t in 1:T)
@@ -82,7 +91,6 @@ function create_objective_function!(model::ManufacturingModel)
# Transportation and variable operating costs # Transportation and variable operating costs
for a in n.incoming_arcs for a in n.incoming_arcs
c = n.location.input.transportation_cost[t] * a.values["distance"] c = n.location.input.transportation_cost[t] * a.values["distance"]
c += n.location.sizes[1].variable_operating_cost[t]
add_to_expression!(obj, c, vars.flow[a, t]) add_to_expression!(obj, c, vars.flow[a, t])
end end
@@ -101,6 +109,16 @@ function create_objective_function!(model::ManufacturingModel)
slope_fix_oper_cost(n.location, t), slope_fix_oper_cost(n.location, t),
vars.expansion[n, t]) vars.expansion[n, t])
# Processing costs
add_to_expression!(obj,
n.location.sizes[1].variable_operating_cost[t],
vars.process[n, t])
# Storage costs
add_to_expression!(obj,
n.location.storage_cost[t],
vars.store[n, t])
# Expansion costs # Expansion costs
if t < T if t < T
add_to_expression!(obj, add_to_expression!(obj,
@@ -113,9 +131,13 @@ function create_objective_function!(model::ManufacturingModel)
end end
end end
# Disposal costs # Shipping node costs
for n in values(graph.plant_shipping_nodes), t in 1:T for n in values(graph.plant_shipping_nodes), t in 1:T
add_to_expression!(obj, n.location.disposal_cost[n.product][t], vars.dispose[n, t])
# Disposal costs
add_to_expression!(obj,
n.location.disposal_cost[n.product][t],
vars.dispose[n, t])
end end
@objective(mip, Min, obj) @objective(mip, Min, obj)
@@ -143,6 +165,7 @@ function create_shipping_node_constraints!(model::ManufacturingModel)
sum(vars.flow[a, t] for a in n.outgoing_arcs) + vars.dispose[n, t]) sum(vars.flow[a, t] for a in n.outgoing_arcs) + vars.dispose[n, t])
end end
end end
end end
@@ -150,13 +173,14 @@ function create_process_node_constraints!(model::ManufacturingModel)
mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time mip, vars, graph, T = model.mip, model.vars, model.graph, model.instance.time
for t in 1:T, n in graph.process_nodes for t in 1:T, n in graph.process_nodes
# Output amount is implied by input amount
input_sum = AffExpr(0.0) input_sum = AffExpr(0.0)
for a in n.incoming_arcs for a in n.incoming_arcs
add_to_expression!(input_sum, 1.0, vars.flow[a, t]) add_to_expression!(input_sum, 1.0, vars.flow[a, t])
end end
# Output amount is implied by amount processed
for a in n.outgoing_arcs for a in n.outgoing_arcs
@constraint(mip, vars.flow[a, t] == a.values["weight"] * input_sum) @constraint(mip, vars.flow[a, t] == a.values["weight"] * vars.process[n, t])
end end
# If plant is closed, capacity is zero # If plant is closed, capacity is zero
@@ -168,8 +192,8 @@ function create_process_node_constraints!(model::ManufacturingModel)
# Capacity is linked to expansion # Capacity is linked to expansion
@constraint(mip, vars.capacity[n, t] <= n.location.sizes[1].capacity + vars.expansion[n, t]) @constraint(mip, vars.capacity[n, t] <= n.location.sizes[1].capacity + vars.expansion[n, t])
# Input sum must be smaller than capacity # Can only process up to capacity
@constraint(mip, input_sum <= vars.capacity[n, t]) @constraint(mip, vars.process[n, t] <= vars.capacity[n, t])
if t > 1 if t > 1
# Plant capacity can only increase over time # Plant capacity can only increase over time
@@ -177,6 +201,18 @@ function create_process_node_constraints!(model::ManufacturingModel)
@constraint(mip, vars.expansion[n, t] >= vars.expansion[n, t-1]) @constraint(mip, vars.expansion[n, t] >= vars.expansion[n, t-1])
end end
# Amount received equals amount processed plus stored
store_in = 0
if t > 1
store_in = vars.store[n, t-1]
end
if t == T
@constraint(mip, vars.store[n, t] == 0)
end
@constraint(mip,
input_sum + store_in == vars.store[n, t] + vars.process[n, t])
# Plant is currently open if it was already open in the previous time period or # Plant is currently open if it was already open in the previous time period or
# if it was built just now # if it was built just now
if t > 1 if t > 1
@@ -197,7 +233,9 @@ default_lp_optimizer = optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
function solve(instance::Instance; function solve(instance::Instance;
optimizer=nothing, optimizer=nothing,
output=nothing) output=nothing,
marginal_costs=true,
)
milp_optimizer = lp_optimizer = optimizer milp_optimizer = lp_optimizer = optimizer
if optimizer == nothing if optimizer == nothing
@@ -224,38 +262,67 @@ function solve(instance::Instance;
return OrderedDict() return OrderedDict()
end end
@info "Re-optimizing with integer variables fixed..." if marginal_costs
all_vars = JuMP.all_variables(model.mip) @info "Re-optimizing with integer variables fixed..."
vals = OrderedDict(var => JuMP.value(var) for var in all_vars) all_vars = JuMP.all_variables(model.mip)
JuMP.set_optimizer(model.mip, lp_optimizer) vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
for var in all_vars JuMP.set_optimizer(model.mip, lp_optimizer)
if JuMP.is_binary(var) for var in all_vars
JuMP.unset_binary(var) if JuMP.is_binary(var)
JuMP.fix(var, vals[var]) JuMP.unset_binary(var)
JuMP.fix(var, vals[var])
end
end end
JuMP.optimize!(model.mip)
end end
JuMP.optimize!(model.mip)
@info "Extracting solution..." @info "Extracting solution..."
solution = get_solution(model) solution = get_solution(model, marginal_costs=marginal_costs)
if output != nothing if output != nothing
@info "Writing solution: $output" write(solution, output)
open(output, "w") do file
JSON.print(file, solution, 2)
end
end end
return solution return solution
end end
function solve(filename::String; kwargs...) function solve(filename::AbstractString;
heuristic=false,
kwargs...,
)
@info "Reading $filename..." @info "Reading $filename..."
instance = RELOG.parsefile(filename) instance = RELOG.parsefile(filename)
return solve(instance; kwargs...) if heuristic && instance.time > 1
@info "Solving single-period version..."
compressed = _compress(instance)
csol = solve(compressed;
output=nothing,
marginal_costs=false,
kwargs...)
@info "Filtering candidate locations..."
selected_pairs = []
for (plant_name, plant_dict) in csol["Plants"]
for (location_name, location_dict) in plant_dict
push!(selected_pairs, (plant_name, location_name))
end
end
filtered_plants = []
for p in instance.plants
if (p.plant_name, p.location_name) in selected_pairs
push!(filtered_plants, p)
end
end
instance.plants = filtered_plants
@info "Solving original version..."
end
sol = solve(instance; kwargs...)
return sol
end end
function get_solution(model::ManufacturingModel)
function get_solution(model::ManufacturingModel;
marginal_costs=true,
)
mip, vars, eqs, graph, instance = model.mip, model.vars, model.eqs, model.graph, model.instance mip, vars, eqs, graph, instance = model.mip, model.vars, model.eqs, model.graph, model.instance
T = instance.time T = instance.time
@@ -269,6 +336,7 @@ function get_solution(model::ManufacturingModel)
"Transportation (\$)" => zeros(T), "Transportation (\$)" => zeros(T),
"Disposal (\$)" => zeros(T), "Disposal (\$)" => zeros(T),
"Expansion (\$)" => zeros(T), "Expansion (\$)" => zeros(T),
"Storage (\$)" => zeros(T),
"Total (\$)" => zeros(T), "Total (\$)" => zeros(T),
), ),
"Energy" => OrderedDict( "Energy" => OrderedDict(
@@ -291,15 +359,17 @@ function get_solution(model::ManufacturingModel)
end end
# Products # Products
for n in graph.collection_shipping_nodes if marginal_costs
location_dict = OrderedDict{Any, Any}( for n in graph.collection_shipping_nodes
"Marginal cost (\$/tonne)" => [round(abs(JuMP.shadow_price(eqs.balance[n, t])), digits=2) location_dict = OrderedDict{Any, Any}(
for t in 1:T], "Marginal cost (\$/tonne)" => [round(abs(JuMP.shadow_price(eqs.balance[n, t])), digits=2)
) for t in 1:T]
if n.product.name keys(output["Products"]) )
output["Products"][n.product.name] = OrderedDict() if n.product.name keys(output["Products"])
output["Products"][n.product.name] = OrderedDict()
end
output["Products"][n.product.name][n.location.name] = location_dict
end end
output["Products"][n.product.name][n.location.name] = location_dict
end end
# Plants # Plants
@@ -336,10 +406,22 @@ function get_solution(model::ManufacturingModel)
) )
end) end)
for t in 1:T], for t in 1:T],
"Process (tonne)" => [JuMP.value(vars.process[process_node, t])
for t in 1:T],
"Variable operating cost (\$)" => [JuMP.value(vars.process[process_node, t]) *
plant.sizes[1].variable_operating_cost[t]
for t in 1:T],
"Storage (tonne)" => [JuMP.value(vars.store[process_node, t])
for t in 1:T],
"Storage cost (\$)" => [JuMP.value(vars.store[process_node, t]) *
plant.storage_cost[t]
for t in 1:T],
) )
output["Costs"]["Fixed operating (\$)"] += plant_dict["Fixed operating cost (\$)"] output["Costs"]["Fixed operating (\$)"] += plant_dict["Fixed operating cost (\$)"]
output["Costs"]["Variable operating (\$)"] += plant_dict["Variable operating cost (\$)"]
output["Costs"]["Opening (\$)"] += plant_dict["Opening cost (\$)"] output["Costs"]["Opening (\$)"] += plant_dict["Opening cost (\$)"]
output["Costs"]["Expansion (\$)"] += plant_dict["Expansion cost (\$)"] output["Costs"]["Expansion (\$)"] += plant_dict["Expansion cost (\$)"]
output["Costs"]["Storage (\$)"] += plant_dict["Storage cost (\$)"]
# Inputs # Inputs
for a in process_node.incoming_arcs for a in process_node.incoming_arcs
@@ -353,14 +435,19 @@ function get_solution(model::ManufacturingModel)
"Distance (km)" => a.values["distance"], "Distance (km)" => a.values["distance"],
"Latitude (deg)" => a.source.location.latitude, "Latitude (deg)" => a.source.location.latitude,
"Longitude (deg)" => a.source.location.longitude, "Longitude (deg)" => a.source.location.longitude,
"Transportation cost (\$)" => a.source.product.transportation_cost .* vals .* a.values["distance"], "Transportation cost (\$)" => a.source.product.transportation_cost .*
"Variable operating cost (\$)" => plant.sizes[1].variable_operating_cost .* vals, vals .*
"Transportation energy (J)" => vals .* a.values["distance"] .* a.source.product.transportation_energy, a.values["distance"],
"Transportation energy (J)" => vals .*
a.values["distance"] .*
a.source.product.transportation_energy,
"Emissions (tonne)" => OrderedDict(), "Emissions (tonne)" => OrderedDict(),
) )
emissions_dict = output["Emissions"]["Transportation (tonne)"] emissions_dict = output["Emissions"]["Transportation (tonne)"]
for (em_name, em_values) in a.source.product.transportation_emissions for (em_name, em_values) in a.source.product.transportation_emissions
dict["Emissions (tonne)"][em_name] = em_values .* dict["Amount (tonne)"] .* a.values["distance"] dict["Emissions (tonne)"][em_name] = em_values .*
dict["Amount (tonne)"] .*
a.values["distance"]
if em_name keys(emissions_dict) if em_name keys(emissions_dict)
emissions_dict[em_name] = zeros(T) emissions_dict[em_name] = zeros(T)
end end
@@ -380,7 +467,6 @@ function get_solution(model::ManufacturingModel)
plant_dict["Input"][plant_name][location_name] = dict plant_dict["Input"][plant_name][location_name] = dict
plant_dict["Total input (tonne)"] += vals plant_dict["Total input (tonne)"] += vals
output["Costs"]["Transportation (\$)"] += dict["Transportation cost (\$)"] output["Costs"]["Transportation (\$)"] += dict["Transportation cost (\$)"]
output["Costs"]["Variable operating (\$)"] += dict["Variable operating cost (\$)"]
output["Energy"]["Transportation (GJ)"] += dict["Transportation energy (J)"] / 1e9 output["Energy"]["Transportation (GJ)"] += dict["Transportation energy (J)"] / 1e9
end end

View File

@@ -5,7 +5,7 @@
using DataFrames using DataFrames
using CSV using CSV
function plants_report(solution::Dict)::DataFrame function plants_report(solution)::DataFrame
df = DataFrame() df = DataFrame()
df."plant type" = String[] df."plant type" = String[]
df."location name" = String[] df."location name" = String[]
@@ -14,34 +14,35 @@ function plants_report(solution::Dict)::DataFrame
df."longitude (deg)" = Float64[] df."longitude (deg)" = Float64[]
df."capacity (tonne)" = Float64[] df."capacity (tonne)" = Float64[]
df."amount processed (tonne)" = Float64[] df."amount processed (tonne)" = Float64[]
df."amount received (tonne)" = Float64[]
df."amount in storage (tonne)" = Float64[]
df."utilization factor (%)" = Float64[] df."utilization factor (%)" = Float64[]
df."energy (GJ)" = Float64[] df."energy (GJ)" = Float64[]
df."opening cost (\$)" = Float64[] df."opening cost (\$)" = Float64[]
df."expansion cost (\$)" = Float64[] df."expansion cost (\$)" = Float64[]
df."fixed operating cost (\$)" = Float64[] df."fixed operating cost (\$)" = Float64[]
df."variable operating cost (\$)" = Float64[] df."variable operating cost (\$)" = Float64[]
df."storage cost (\$)" = Float64[]
df."total cost (\$)" = Float64[] df."total cost (\$)" = Float64[]
T = length(solution["Energy"]["Plants (GJ)"]) T = length(solution["Energy"]["Plants (GJ)"])
for (plant_name, plant_dict) in solution["Plants"] for (plant_name, plant_dict) in solution["Plants"]
for (location_name, location_dict) in plant_dict for (location_name, location_dict) in plant_dict
var_cost = zeros(T)
for (src_plant_name, src_plant_dict) in location_dict["Input"]
for (src_location_name, src_location_dict) in src_plant_dict
var_cost += src_location_dict["Variable operating cost (\$)"]
end
end
var_cost = round.(var_cost, digits=2)
for year in 1:T for year in 1:T
opening_cost = round(location_dict["Opening cost (\$)"][year], digits=2)
expansion_cost = round(location_dict["Expansion cost (\$)"][year], digits=2)
fixed_cost = round(location_dict["Fixed operating cost (\$)"][year], digits=2)
total_cost = round(var_cost[year] + opening_cost + expansion_cost + fixed_cost, digits=2)
capacity = round(location_dict["Capacity (tonne)"][year], digits=2) capacity = round(location_dict["Capacity (tonne)"][year], digits=2)
processed = round(location_dict["Total input (tonne)"][year], digits=2) received = round(location_dict["Total input (tonne)"][year], digits=2)
processed = round(location_dict["Process (tonne)"][year], digits=2)
in_storage = round(location_dict["Storage (tonne)"][year], digits=2)
utilization_factor = round(processed / capacity * 100.0, digits=2) utilization_factor = round(processed / capacity * 100.0, digits=2)
energy = round(location_dict["Energy (GJ)"][year], digits=2) energy = round(location_dict["Energy (GJ)"][year], digits=2)
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)
opening_cost = round(location_dict["Opening cost (\$)"][year], digits=2)
expansion_cost = round(location_dict["Expansion cost (\$)"][year], digits=2)
fixed_cost = round(location_dict["Fixed operating cost (\$)"][year], digits=2)
var_cost = round(location_dict["Variable operating cost (\$)"][year], digits=2)
storage_cost = round(location_dict["Storage cost (\$)"][year], digits=2)
total_cost = round(opening_cost + expansion_cost + fixed_cost +
var_cost + storage_cost, digits=2)
push!(df, [ push!(df, [
plant_name, plant_name,
location_name, location_name,
@@ -50,12 +51,15 @@ function plants_report(solution::Dict)::DataFrame
longitude, longitude,
capacity, capacity,
processed, processed,
received,
in_storage,
utilization_factor, utilization_factor,
energy, energy,
opening_cost, opening_cost,
expansion_cost, expansion_cost,
fixed_cost, fixed_cost,
var_cost[year], var_cost,
storage_cost,
total_cost, total_cost,
]) ])
end end
@@ -64,7 +68,7 @@ function plants_report(solution::Dict)::DataFrame
return df return df
end end
function plant_outputs_report(solution::Dict)::DataFrame function plant_outputs_report(solution)::DataFrame
df = DataFrame() df = DataFrame()
df."plant type" = String[] df."plant type" = String[]
df."location name" = String[] df."location name" = String[]
@@ -119,7 +123,7 @@ function plant_outputs_report(solution::Dict)::DataFrame
end end
function plant_emissions_report(solution::Dict)::DataFrame function plant_emissions_report(solution)::DataFrame
df = DataFrame() df = DataFrame()
df."plant type" = String[] df."plant type" = String[]
df."location name" = String[] df."location name" = String[]
@@ -146,7 +150,7 @@ function plant_emissions_report(solution::Dict)::DataFrame
end end
function transportation_report(solution::Dict)::DataFrame function transportation_report(solution)::DataFrame
df = DataFrame() df = DataFrame()
df."source type" = String[] df."source type" = String[]
df."source location name" = String[] df."source location name" = String[]
@@ -181,10 +185,10 @@ function transportation_report(solution::Dict)::DataFrame
round(dst_location_dict["Longitude (deg)"], digits=6), round(dst_location_dict["Longitude (deg)"], digits=6),
dst_location_dict["Input product"], dst_location_dict["Input product"],
year, year,
round(src_location_dict["Distance (km)"][year], digits=2), round(src_location_dict["Distance (km)"], digits=2),
round(src_location_dict["Amount (tonne)"][year], digits=2), round(src_location_dict["Amount (tonne)"][year], digits=2),
round(src_location_dict["Amount (tonne)"][year] * round(src_location_dict["Amount (tonne)"][year] *
src_location_dict["Distance (km)"][year], src_location_dict["Distance (km)"],
digits=2), digits=2),
round(src_location_dict["Transportation cost (\$)"][year], digits=2), round(src_location_dict["Transportation cost (\$)"][year], digits=2),
round(src_location_dict["Transportation energy (J)"][year] / 1e9, digits=2), round(src_location_dict["Transportation energy (J)"][year] / 1e9, digits=2),
@@ -198,7 +202,7 @@ function transportation_report(solution::Dict)::DataFrame
end end
function transportation_emissions_report(solution::Dict)::DataFrame function transportation_emissions_report(solution)::DataFrame
df = DataFrame() df = DataFrame()
df."source type" = String[] df."source type" = String[]
df."source location name" = String[] df."source location name" = String[]
@@ -234,10 +238,10 @@ function transportation_emissions_report(solution::Dict)::DataFrame
round(dst_location_dict["Longitude (deg)"], digits=6), round(dst_location_dict["Longitude (deg)"], digits=6),
dst_location_dict["Input product"], dst_location_dict["Input product"],
year, year,
round(src_location_dict["Distance (km)"][year], digits=2), round(src_location_dict["Distance (km)"], digits=2),
round(src_location_dict["Amount (tonne)"][year], digits=2), round(src_location_dict["Amount (tonne)"][year], digits=2),
round(src_location_dict["Amount (tonne)"][year] * round(src_location_dict["Amount (tonne)"][year] *
src_location_dict["Distance (km)"][year], src_location_dict["Distance (km)"],
digits=2), digits=2),
emission_name, emission_name,
round(emission_amount[year], digits=2), round(emission_amount[year], digits=2),
@@ -251,6 +255,12 @@ function transportation_emissions_report(solution::Dict)::DataFrame
return df return df
end end
function write(solution::AbstractDict, filename::AbstractString)
@info "Writing solution: $filename"
open(filename, "w") do file
JSON.print(file, solution, 2)
end
end
write_plants_report(solution, filename) = write_plants_report(solution, filename) =
CSV.write(filename, plants_report(solution)) CSV.write(filename, plants_report(solution))

View File

@@ -61,6 +61,17 @@
] ]
} }
}, },
"storage": {
"type": "object",
"properties": {
"cost ($/tonne)": { "$ref": "#/definitions/TimeSeries" },
"limit (tonne)": { "type": "number" }
},
"required": [
"cost ($/tonne)",
"limit (tonne)"
]
},
"capacities (tonne)": { "capacities (tonne)": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {

39
test/fixtures/storage.json vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"parameters": {
"time horizon (years)": 3
},
"products": {
"battery": {
"initial amounts": {
"Chicago": {
"latitude (deg)": 0.0,
"longitude (deg)": 0.0,
"amount (tonne)": [100.0, 0.0, 0.0]
}
},
"transportation cost ($/km/tonne)": [0.01, 0.01, 0.01]
}
},
"plants": {
"mega plant": {
"input": "battery",
"locations": {
"Chicago": {
"latitude (deg)": 0.0,
"longitude (deg)": 0.0,
"storage": {
"cost ($/tonne)": [2.0, 1.5, 1.0],
"limit (tonne)": 50.0
},
"capacities (tonne)": {
"100": {
"opening cost ($)": [0.0, 0.0, 0],
"fixed operating cost ($)": [0.0, 0.0, 0.0],
"variable operating cost ($/tonne)": [10.0, 5.0, 2.0]
}
}
}
}
}
}
}

View File

@@ -11,7 +11,6 @@ using RELOG
centers = instance.collection_centers centers = instance.collection_centers
plants = instance.plants plants = instance.plants
products = instance.products products = instance.products
location_name_to_plant = Dict(p.location_name => p for p in plants) location_name_to_plant = Dict(p.location_name => p for p in plants)
product_name_to_product = Dict(p.name => p for p in products) product_name_to_product = Dict(p.name => p for p in products)
@@ -75,5 +74,54 @@ using RELOG
@testset "validate timeseries" begin @testset "validate timeseries" begin
@test_throws String RELOG.parsefile("fixtures/s1-wrong-length.json") @test_throws String RELOG.parsefile("fixtures/s1-wrong-length.json")
end end
@testset "compress" begin
basedir = dirname(@__FILE__)
instance = RELOG.parsefile("$basedir/../instances/s1.json")
compressed = RELOG._compress(instance)
product_name_to_product = Dict(p.name => p for p in compressed.products)
location_name_to_facility = Dict()
for p in compressed.plants
location_name_to_facility[p.location_name] = p
end
for c in compressed.collection_centers
location_name_to_facility[c.name] = c
end
p1 = product_name_to_product["P1"]
p2 = product_name_to_product["P2"]
p3 = product_name_to_product["P3"]
c1 = location_name_to_facility["C1"]
l1 = location_name_to_facility["L1"]
@test compressed.time == 1
@test compressed.building_period == [1]
@test p1.name == "P1"
@test p1.transportation_cost [0.015]
@test p1.transportation_energy [0.115]
@test p1.transportation_emissions["CO2"] [0.051]
@test p1.transportation_emissions["CH4"] [0.0025]
@test c1.name == "C1"
@test c1.amount [1869.12]
@test l1.plant_name == "F1"
@test l1.location_name == "L1"
@test l1.energy [0.115]
@test l1.emissions["CO2"] [0.051]
@test l1.emissions["CH4"] [0.0025]
@test l1.sizes[1].opening_cost [500]
@test l1.sizes[2].opening_cost [1250]
@test l1.sizes[1].fixed_operating_cost [60]
@test l1.sizes[2].fixed_operating_cost [60]
@test l1.sizes[1].variable_operating_cost [30]
@test l1.sizes[2].variable_operating_cost [30]
@test l1.disposal_limit[p2] [2.0]
@test l1.disposal_limit[p3] [2.0]
@test l1.disposal_cost[p2] [-10.0]
@test l1.disposal_cost[p3] [-10.0]
end
end end

View File

@@ -36,15 +36,21 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
@test lower_bound(v) == 0.0 @test lower_bound(v) == 0.0
@test upper_bound(v) == 1.0 @test upper_bound(v) == 1.0
#dest = FileFormats.Model(format = FileFormats.FORMAT_LP) # dest = FileFormats.Model(format = FileFormats.FORMAT_LP)
#MOI.copy_to(dest, model.mip) # MOI.copy_to(dest, model.mip)
#MOI.write_to_file(dest, "model.lp") # MOI.write_to_file(dest, "model.lp")
end end
@testset "solve" begin @testset "solve (exact)" begin
solution_filename = tempname() solution_filename_a = tempname()
solution_filename_b = tempname()
solution = RELOG.solve("$(pwd())/../instances/s1.json", solution = RELOG.solve("$(pwd())/../instances/s1.json",
output=solution_filename) output=solution_filename_a)
@test isfile(solution_filename_a)
RELOG.write(solution, solution_filename_b)
@test isfile(solution_filename_b)
@test "Costs" in keys(solution) @test "Costs" in keys(solution)
@test "Fixed operating (\$)" in keys(solution["Costs"]) @test "Fixed operating (\$)" in keys(solution["Costs"])
@@ -59,6 +65,12 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
@test "F4" in keys(solution["Plants"]) @test "F4" in keys(solution["Plants"])
end end
@testset "solve (heuristic)" begin
# Should not crash
solution = RELOG.solve("$(pwd())/../instances/s1.json", heuristic=true)
end
@testset "infeasible solve" begin @testset "infeasible solve" begin
json = JSON.parsefile("$(pwd())/../instances/s1.json") json = JSON.parsefile("$(pwd())/../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"]
@@ -67,6 +79,22 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
RELOG.solve(RELOG.parse(json)) RELOG.solve(RELOG.parse(json))
end end
@testset "storage" begin
basedir = dirname(@__FILE__)
filename = "$basedir/fixtures/storage.json"
instance = RELOG.parsefile(filename)
@test instance.plants[1].storage_limit == 50.0
@test instance.plants[1].storage_cost == [2.0, 1.5, 1.0]
solution = RELOG.solve(filename)
plant_dict = solution["Plants"]["mega plant"]["Chicago"]
@test plant_dict["Variable operating cost (\$)"] == [500.0, 0.0, 100.0]
@test plant_dict["Process (tonne)"] == [50.0, 0.0, 50.0]
@test plant_dict["Storage (tonne)"] == [50.0, 50.0, 0.0]
@test plant_dict["Storage cost (\$)"] == [100.0, 75.0, 0.0]
@test solution["Costs"]["Variable operating (\$)"] == [500.0, 0.0, 100.0]
@test solution["Costs"]["Storage (\$)"] == [100.0, 75.0, 0.0]
@test solution["Costs"]["Total (\$)"] == [600.0, 75.0, 100.0]
end
end end

View File

@@ -6,23 +6,36 @@ using RELOG, JSON, GZip
load_json_gz(filename) = JSON.parse(GZip.gzopen(filename)) load_json_gz(filename) = JSON.parse(GZip.gzopen(filename))
function check(func, expected_csv_filename::String) # function check(func, expected_csv_filename::String)
solution = load_json_gz("fixtures/nimh_solution.json.gz") # solution = load_json_gz("fixtures/nimh_solution.json.gz")
actual_csv_filename = tempname() # actual_csv_filename = tempname()
func(solution, actual_csv_filename) # func(solution, actual_csv_filename)
@test isfile(actual_csv_filename) # @test isfile(actual_csv_filename)
if readlines(actual_csv_filename) != readlines(expected_csv_filename) # if readlines(actual_csv_filename) != readlines(expected_csv_filename)
out_filename = replace(expected_csv_filename, ".csv" => "_actual.csv") # out_filename = replace(expected_csv_filename, ".csv" => "_actual.csv")
@error "$func: Unexpected CSV contents: $out_filename" # @error "$func: Unexpected CSV contents: $out_filename"
write(out_filename, read(actual_csv_filename)) # write(out_filename, read(actual_csv_filename))
@test false # @test false
end # end
end # end
@testset "Reports" begin @testset "Reports" begin
check(RELOG.write_plants_report, "fixtures/nimh_plants.csv") # @testset "from fixture" begin
check(RELOG.write_plant_outputs_report, "fixtures/nimh_plant_outputs.csv") # check(RELOG.write_plants_report, "fixtures/nimh_plants.csv")
check(RELOG.write_plant_emissions_report, "fixtures/nimh_plant_emissions.csv") # check(RELOG.write_plant_outputs_report, "fixtures/nimh_plant_outputs.csv")
check(RELOG.write_transportation_report, "fixtures/nimh_transportation.csv") # check(RELOG.write_plant_emissions_report, "fixtures/nimh_plant_emissions.csv")
check(RELOG.write_transportation_emissions_report, "fixtures/nimh_transportation_emissions.csv") # check(RELOG.write_transportation_report, "fixtures/nimh_transportation.csv")
# check(RELOG.write_transportation_emissions_report, "fixtures/nimh_transportation_emissions.csv")
# end
@testset "from solve" begin
solution = RELOG.solve("$(pwd())/../instances/s1.json")
tmp_filename = tempname()
# The following should not crash
RELOG.write_plants_report(solution, tmp_filename)
RELOG.write_plant_outputs_report(solution, tmp_filename)
RELOG.write_plant_emissions_report(solution, tmp_filename)
RELOG.write_transportation_report(solution, tmp_filename)
RELOG.write_transportation_emissions_report(solution, tmp_filename)
end
end end