mirror of
https://github.com/ANL-CEEESA/RELOG.git
synced 2025-12-05 23:38:52 -06:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d91ac27d4 | |||
| 4098057cc1 | |||
| b91ca9e6cb | |||
| d870f028c0 | |||
| a518e3d3d6 | |||
| be5e09a4ec | |||
| 77cd5871a2 | |||
| a6846be9eb | |||
| fe9cacef24 | |||
| 0f60b847dd |
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Build & Test
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
@@ -8,8 +8,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
version:
|
||||
- '1.3'
|
||||
version: ['1.3', '1.4', '1.5', 'nightly']
|
||||
os:
|
||||
- ubuntu-latest
|
||||
arch:
|
||||
@@ -20,5 +19,5 @@ jobs:
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
arch: ${{ matrix.arch }}
|
||||
- uses: julia-actions/julia-buildpkg@latest
|
||||
- uses: julia-actions/julia-runtest@latest
|
||||
- uses: julia-actions/julia-buildpkg@v1
|
||||
- uses: julia-actions/julia-runtest@v1
|
||||
|
||||
@@ -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)
|
||||
|
||||
- Generate simplified solution reports (CSV)
|
||||
|
||||
6
Makefile
6
Makefile
@@ -1,15 +1,15 @@
|
||||
JULIA := julia --color=yes --project=@.
|
||||
SRC_FILES := $(wildcard src/*.jl test/*.jl)
|
||||
VERSION := 0.4
|
||||
VERSION := 0.5
|
||||
|
||||
all: docs test
|
||||
|
||||
build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml
|
||||
mkdir -p build
|
||||
$(JULIA) src/sysimage.jl
|
||||
|
||||
build/test.log: $(SRC_FILES) build/sysimage.so
|
||||
@echo Running tests...
|
||||
cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl | tee ../build/test.log
|
||||
cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl
|
||||
|
||||
clean:
|
||||
rm -rf build/*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name = "RELOG"
|
||||
uuid = "a2afcdf7-cf04-4913-85f9-c0d81ddf2008"
|
||||
authors = ["Alinson S Xavier <axavier@anl.gov>"]
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
|
||||
[deps]
|
||||
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
|
||||
@@ -19,6 +19,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
|
||||
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
|
||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
|
||||
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
|
||||
[compat]
|
||||
|
||||
26
README.md
26
README.md
@@ -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.
|
||||
|
||||
<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
|
||||
|
||||
* [Usage](https://anl-ceeesa.github.io/RELOG/0.4/usage)
|
||||
* [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.4/format)
|
||||
* [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.4/reports)
|
||||
* [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.4/model)
|
||||
* [Usage](https://anl-ceeesa.github.io/RELOG/0.5/usage)
|
||||
* [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.5/format)
|
||||
* [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.5/reports)
|
||||
* [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.5/model)
|
||||
|
||||
### Authors
|
||||
|
||||
|
||||
@@ -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.
|
||||
| `longitude (deg)` | The longitude of the location, in degrees.
|
||||
| `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:
|
||||
|
||||
@@ -151,11 +159,15 @@ The keys in the `capacities (tonne)` dictionary should be the amounts (in tonnes
|
||||
"limit (tonne)": [1.0, 1.0]
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"cost ($/tonne)": [5.0, 5.3],
|
||||
"limit (tonne)": 100.0,
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
"100": {
|
||||
"opening cost ($)": [500, 530],
|
||||
"fixed operating cost ($)": [300.0, 310.0],
|
||||
"variable operating cost ($/tonne)": [5.0, 5.2]
|
||||
"variable operating cost ($/tonne)": [5.0, 5.2],
|
||||
},
|
||||
"500": {
|
||||
"opening cost ($)": [750, 760],
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 29 KiB |
@@ -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-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{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{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{store}_p$ - Maximum amount of original material that plant $p$ can store for later processing.
|
||||
|
||||
**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`)
|
||||
* $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`)
|
||||
* $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
|
||||
@@ -57,17 +61,23 @@ RELOG minimizes the overall capital, production and transportation costs:
|
||||
\sum_{i=1}^t c^\text{f-exp}_{pt} w_{pi} +
|
||||
c^{\text{exp}}_{pt} w_{pt}
|
||||
\right] + \\
|
||||
&
|
||||
\sum_{t \in T} \sum_{p \in P} \left[
|
||||
c^{\text{store}}_{pt} z^{\text{store}}_{pt} +
|
||||
c^{\text{proc}}_{pt} z^{\text{proc}}_{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_{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}
|
||||
\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 second line, we have the transportation costs and the variable operating costs.
|
||||
In the third line, we have the disposal costs.
|
||||
In the second line, we have storage and variable processing costs.
|
||||
In the third line, we have transportation costs.
|
||||
In the fourth line, we have the disposal costs.
|
||||
|
||||
### Constraints
|
||||
|
||||
@@ -78,10 +88,29 @@ In the third line, we have the disposal costs.
|
||||
& \forall l \in L, t \in T
|
||||
\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}
|
||||
& \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
|
||||
\end{align}
|
||||
|
||||
@@ -92,10 +121,10 @@ In the third line, we have the disposal costs.
|
||||
& \forall p \in P, t \in T
|
||||
\end{align}
|
||||
|
||||
* Amount of recovered material is proportional to the plant input:
|
||||
* Amount of recovered material is proportional to amount processed:
|
||||
|
||||
\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
|
||||
\end{align}
|
||||
|
||||
@@ -129,50 +158,8 @@ In the third line, we have the disposal costs.
|
||||
& \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
|
||||
& 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
|
||||
\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*}
|
||||
|
||||
@@ -18,13 +18,16 @@ Generated by `RELOG.write_plants_report(solution, filename)`. For a concrete exa
|
||||
| `latitude (deg)` | Latitude of the plant.
|
||||
| `longitude (deg)` | Longitude of the plant.
|
||||
| `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.
|
||||
| `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.
|
||||
| `expansion cost ($)` | Amount spent this year expanding the plant capacity.
|
||||
| `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.
|
||||
|
||||
|
||||
@@ -79,7 +82,7 @@ Generated by `RELOG.write_plant_outputs_report(solution, filename)`. For a concr
|
||||
| `plant type` | Plant type.
|
||||
| `location name` | Location name.
|
||||
| `year` | What year this row corresponds to. This reports includes one row for each year in the simulation.
|
||||
| `product` | Product being produced.
|
||||
| `product name` | Product being produced.
|
||||
| `amount produced (tonne)` | Amount of product produced this year.
|
||||
| `amount sent (tonne)` | Amount of product produced by this plant and sent to another plant for further processing this year.
|
||||
| `amount disposed (tonne)` | Amount produced produced by this plant and immediately disposed of locally this year.
|
||||
|
||||
@@ -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.
|
||||
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
* 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).
|
||||
|
||||
@@ -79,3 +83,20 @@ RELOG.solve("instance.json",
|
||||
output="solution.json",
|
||||
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)
|
||||
```
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# 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
|
||||
@@ -44,6 +48,8 @@ mutable struct Plant
|
||||
sizes::Array{PlantSize}
|
||||
energy::Array{Float64}
|
||||
emissions::Dict{String, Array{Float64}}
|
||||
storage_limit::Float64
|
||||
storage_cost::Array{Float64}
|
||||
end
|
||||
|
||||
|
||||
@@ -55,6 +61,7 @@ mutable struct Instance
|
||||
building_period::Array{Int64}
|
||||
end
|
||||
|
||||
|
||||
function validate(json, schema)
|
||||
result = JSONSchema.validate(json, schema)
|
||||
if result !== nothing
|
||||
@@ -77,7 +84,7 @@ function parsefile(path::String)::Instance
|
||||
end
|
||||
|
||||
|
||||
function parse(json::Dict)::Instance
|
||||
function parse(json)::Instance
|
||||
basedir = dirname(@__FILE__)
|
||||
json_schema = JSON.parsefile("$basedir/schemas/input.json")
|
||||
validate(json, Schema(json_schema))
|
||||
@@ -179,6 +186,15 @@ function parse(json::Dict)::Instance
|
||||
length(sizes) > 1 || push!(sizes, sizes[1])
|
||||
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
|
||||
if length(sizes) != 2
|
||||
throw("At most two capacities are supported")
|
||||
@@ -198,7 +214,9 @@ function parse(json::Dict)::Instance
|
||||
disposal_cost,
|
||||
sizes,
|
||||
energy,
|
||||
emissions)
|
||||
emissions,
|
||||
storage_limit,
|
||||
storage_cost)
|
||||
|
||||
push!(plants, plant)
|
||||
end
|
||||
@@ -209,3 +227,55 @@ function parse(json::Dict)::Instance
|
||||
|
||||
return Instance(T, products, collection_centers, plants, building_period)
|
||||
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
|
||||
|
||||
162
src/model.jl
162
src/model.jl
@@ -35,6 +35,15 @@ function create_vars!(model::ManufacturingModel)
|
||||
upper_bound=n.location.disposal_limit[n.product][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)
|
||||
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
|
||||
for a in n.incoming_arcs
|
||||
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])
|
||||
end
|
||||
|
||||
@@ -101,6 +109,16 @@ function create_objective_function!(model::ManufacturingModel)
|
||||
slope_fix_oper_cost(n.location, 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
|
||||
if t < T
|
||||
add_to_expression!(obj,
|
||||
@@ -113,9 +131,13 @@ function create_objective_function!(model::ManufacturingModel)
|
||||
end
|
||||
end
|
||||
|
||||
# Disposal costs
|
||||
# Shipping node costs
|
||||
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
|
||||
|
||||
@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])
|
||||
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
|
||||
|
||||
for t in 1:T, n in graph.process_nodes
|
||||
# Output amount is implied by input amount
|
||||
input_sum = AffExpr(0.0)
|
||||
for a in n.incoming_arcs
|
||||
add_to_expression!(input_sum, 1.0, vars.flow[a, t])
|
||||
end
|
||||
|
||||
# Output amount is implied by amount processed
|
||||
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
|
||||
|
||||
# If plant is closed, capacity is zero
|
||||
@@ -168,14 +192,26 @@ function create_process_node_constraints!(model::ManufacturingModel)
|
||||
# Capacity is linked to expansion
|
||||
@constraint(mip, vars.capacity[n, t] <= n.location.sizes[1].capacity + vars.expansion[n, t])
|
||||
|
||||
# Input sum must be smaller than capacity
|
||||
@constraint(mip, input_sum <= vars.capacity[n, t])
|
||||
# Can only process up to capacity
|
||||
@constraint(mip, vars.process[n, t] <= vars.capacity[n, t])
|
||||
|
||||
if t > 1
|
||||
# Plant capacity can only increase over time
|
||||
@constraint(mip, vars.capacity[n, t] >= vars.capacity[n, t-1])
|
||||
@constraint(mip, vars.expansion[n, t] >= vars.expansion[n, t-1])
|
||||
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
|
||||
# if it was built just now
|
||||
@@ -197,7 +233,9 @@ default_lp_optimizer = optimizer_with_attributes(Clp.Optimizer, "LogLevel" => 0)
|
||||
|
||||
function solve(instance::Instance;
|
||||
optimizer=nothing,
|
||||
output=nothing)
|
||||
output=nothing,
|
||||
marginal_costs=true,
|
||||
)
|
||||
|
||||
milp_optimizer = lp_optimizer = optimizer
|
||||
if optimizer == nothing
|
||||
@@ -224,38 +262,67 @@ function solve(instance::Instance;
|
||||
return OrderedDict()
|
||||
end
|
||||
|
||||
@info "Re-optimizing with integer variables fixed..."
|
||||
all_vars = JuMP.all_variables(model.mip)
|
||||
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
|
||||
JuMP.set_optimizer(model.mip, lp_optimizer)
|
||||
for var in all_vars
|
||||
if JuMP.is_binary(var)
|
||||
JuMP.unset_binary(var)
|
||||
JuMP.fix(var, vals[var])
|
||||
if marginal_costs
|
||||
@info "Re-optimizing with integer variables fixed..."
|
||||
all_vars = JuMP.all_variables(model.mip)
|
||||
vals = OrderedDict(var => JuMP.value(var) for var in all_vars)
|
||||
JuMP.set_optimizer(model.mip, lp_optimizer)
|
||||
for var in all_vars
|
||||
if JuMP.is_binary(var)
|
||||
JuMP.unset_binary(var)
|
||||
JuMP.fix(var, vals[var])
|
||||
end
|
||||
end
|
||||
JuMP.optimize!(model.mip)
|
||||
end
|
||||
JuMP.optimize!(model.mip)
|
||||
|
||||
@info "Extracting solution..."
|
||||
solution = get_solution(model)
|
||||
solution = get_solution(model, marginal_costs=marginal_costs)
|
||||
|
||||
if output != nothing
|
||||
@info "Writing solution: $output"
|
||||
open(output, "w") do file
|
||||
JSON.print(file, solution, 2)
|
||||
end
|
||||
write(solution, output)
|
||||
end
|
||||
|
||||
return solution
|
||||
end
|
||||
|
||||
function solve(filename::String; kwargs...)
|
||||
function solve(filename::AbstractString;
|
||||
heuristic=false,
|
||||
kwargs...,
|
||||
)
|
||||
@info "Reading $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
|
||||
|
||||
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
|
||||
T = instance.time
|
||||
|
||||
@@ -269,6 +336,7 @@ function get_solution(model::ManufacturingModel)
|
||||
"Transportation (\$)" => zeros(T),
|
||||
"Disposal (\$)" => zeros(T),
|
||||
"Expansion (\$)" => zeros(T),
|
||||
"Storage (\$)" => zeros(T),
|
||||
"Total (\$)" => zeros(T),
|
||||
),
|
||||
"Energy" => OrderedDict(
|
||||
@@ -291,15 +359,17 @@ function get_solution(model::ManufacturingModel)
|
||||
end
|
||||
|
||||
# Products
|
||||
for n in graph.collection_shipping_nodes
|
||||
location_dict = OrderedDict{Any, Any}(
|
||||
"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 marginal_costs
|
||||
for n in graph.collection_shipping_nodes
|
||||
location_dict = OrderedDict{Any, Any}(
|
||||
"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()
|
||||
end
|
||||
output["Products"][n.product.name][n.location.name] = location_dict
|
||||
end
|
||||
output["Products"][n.product.name][n.location.name] = location_dict
|
||||
end
|
||||
|
||||
# Plants
|
||||
@@ -336,10 +406,22 @@ function get_solution(model::ManufacturingModel)
|
||||
)
|
||||
end)
|
||||
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"]["Variable operating (\$)"] += plant_dict["Variable operating cost (\$)"]
|
||||
output["Costs"]["Opening (\$)"] += plant_dict["Opening cost (\$)"]
|
||||
output["Costs"]["Expansion (\$)"] += plant_dict["Expansion cost (\$)"]
|
||||
output["Costs"]["Storage (\$)"] += plant_dict["Storage cost (\$)"]
|
||||
|
||||
# Inputs
|
||||
for a in process_node.incoming_arcs
|
||||
@@ -353,14 +435,19 @@ function get_solution(model::ManufacturingModel)
|
||||
"Distance (km)" => a.values["distance"],
|
||||
"Latitude (deg)" => a.source.location.latitude,
|
||||
"Longitude (deg)" => a.source.location.longitude,
|
||||
"Transportation cost (\$)" => a.source.product.transportation_cost .* vals .* a.values["distance"],
|
||||
"Variable operating cost (\$)" => plant.sizes[1].variable_operating_cost .* vals,
|
||||
"Transportation energy (J)" => vals .* a.values["distance"] .* a.source.product.transportation_energy,
|
||||
"Transportation cost (\$)" => a.source.product.transportation_cost .*
|
||||
vals .*
|
||||
a.values["distance"],
|
||||
"Transportation energy (J)" => vals .*
|
||||
a.values["distance"] .*
|
||||
a.source.product.transportation_energy,
|
||||
"Emissions (tonne)" => OrderedDict(),
|
||||
)
|
||||
emissions_dict = output["Emissions"]["Transportation (tonne)"]
|
||||
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)
|
||||
emissions_dict[em_name] = zeros(T)
|
||||
end
|
||||
@@ -380,7 +467,6 @@ function get_solution(model::ManufacturingModel)
|
||||
plant_dict["Input"][plant_name][location_name] = dict
|
||||
plant_dict["Total input (tonne)"] += vals
|
||||
output["Costs"]["Transportation (\$)"] += dict["Transportation cost (\$)"]
|
||||
output["Costs"]["Variable operating (\$)"] += dict["Variable operating cost (\$)"]
|
||||
output["Energy"]["Transportation (GJ)"] += dict["Transportation energy (J)"] / 1e9
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
using DataFrames
|
||||
using CSV
|
||||
|
||||
function plants_report(solution::Dict)::DataFrame
|
||||
function plants_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
@@ -14,34 +14,35 @@ function plants_report(solution::Dict)::DataFrame
|
||||
df."longitude (deg)" = Float64[]
|
||||
df."capacity (tonne)" = Float64[]
|
||||
df."amount processed (tonne)" = Float64[]
|
||||
df."amount received (tonne)" = Float64[]
|
||||
df."amount in storage (tonne)" = Float64[]
|
||||
df."utilization factor (%)" = Float64[]
|
||||
df."energy (GJ)" = Float64[]
|
||||
df."opening cost (\$)" = Float64[]
|
||||
df."expansion cost (\$)" = Float64[]
|
||||
df."fixed operating cost (\$)" = Float64[]
|
||||
df."variable operating cost (\$)" = Float64[]
|
||||
df."storage cost (\$)" = Float64[]
|
||||
df."total cost (\$)" = Float64[]
|
||||
T = length(solution["Energy"]["Plants (GJ)"])
|
||||
for (plant_name, plant_dict) in solution["Plants"]
|
||||
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
|
||||
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)
|
||||
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)
|
||||
energy = round(location_dict["Energy (GJ)"][year], digits=2)
|
||||
latitude = round(location_dict["Latitude (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, [
|
||||
plant_name,
|
||||
location_name,
|
||||
@@ -50,12 +51,15 @@ function plants_report(solution::Dict)::DataFrame
|
||||
longitude,
|
||||
capacity,
|
||||
processed,
|
||||
received,
|
||||
in_storage,
|
||||
utilization_factor,
|
||||
energy,
|
||||
opening_cost,
|
||||
expansion_cost,
|
||||
fixed_cost,
|
||||
var_cost[year],
|
||||
var_cost,
|
||||
storage_cost,
|
||||
total_cost,
|
||||
])
|
||||
end
|
||||
@@ -64,7 +68,7 @@ function plants_report(solution::Dict)::DataFrame
|
||||
return df
|
||||
end
|
||||
|
||||
function plant_outputs_report(solution::Dict)::DataFrame
|
||||
function plant_outputs_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
@@ -119,7 +123,7 @@ function plant_outputs_report(solution::Dict)::DataFrame
|
||||
end
|
||||
|
||||
|
||||
function plant_emissions_report(solution::Dict)::DataFrame
|
||||
function plant_emissions_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."plant type" = String[]
|
||||
df."location name" = String[]
|
||||
@@ -146,7 +150,7 @@ function plant_emissions_report(solution::Dict)::DataFrame
|
||||
end
|
||||
|
||||
|
||||
function transportation_report(solution::Dict)::DataFrame
|
||||
function transportation_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."source type" = String[]
|
||||
df."source location name" = String[]
|
||||
@@ -181,10 +185,10 @@ function transportation_report(solution::Dict)::DataFrame
|
||||
round(dst_location_dict["Longitude (deg)"], digits=6),
|
||||
dst_location_dict["Input product"],
|
||||
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] *
|
||||
src_location_dict["Distance (km)"][year],
|
||||
src_location_dict["Distance (km)"],
|
||||
digits=2),
|
||||
round(src_location_dict["Transportation cost (\$)"][year], digits=2),
|
||||
round(src_location_dict["Transportation energy (J)"][year] / 1e9, digits=2),
|
||||
@@ -198,7 +202,7 @@ function transportation_report(solution::Dict)::DataFrame
|
||||
end
|
||||
|
||||
|
||||
function transportation_emissions_report(solution::Dict)::DataFrame
|
||||
function transportation_emissions_report(solution)::DataFrame
|
||||
df = DataFrame()
|
||||
df."source type" = 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),
|
||||
dst_location_dict["Input product"],
|
||||
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] *
|
||||
src_location_dict["Distance (km)"][year],
|
||||
src_location_dict["Distance (km)"],
|
||||
digits=2),
|
||||
emission_name,
|
||||
round(emission_amount[year], digits=2),
|
||||
@@ -251,6 +255,12 @@ function transportation_emissions_report(solution::Dict)::DataFrame
|
||||
return df
|
||||
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) =
|
||||
CSV.write(filename, plants_report(solution))
|
||||
|
||||
@@ -61,6 +61,17 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cost ($/tonne)": { "$ref": "#/definitions/TimeSeries" },
|
||||
"limit (tonne)": { "type": "number" }
|
||||
},
|
||||
"required": [
|
||||
"cost ($/tonne)",
|
||||
"limit (tonne)"
|
||||
]
|
||||
},
|
||||
"capacities (tonne)": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
|
||||
39
test/fixtures/storage.json
vendored
Normal file
39
test/fixtures/storage.json
vendored
Normal 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ using RELOG
|
||||
centers = instance.collection_centers
|
||||
plants = instance.plants
|
||||
products = instance.products
|
||||
|
||||
location_name_to_plant = Dict(p.location_name => p for p in plants)
|
||||
product_name_to_product = Dict(p.name => p for p in products)
|
||||
|
||||
@@ -75,5 +74,54 @@ using RELOG
|
||||
@testset "validate timeseries" begin
|
||||
@test_throws String RELOG.parsefile("fixtures/s1-wrong-length.json")
|
||||
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
|
||||
|
||||
|
||||
@@ -36,15 +36,21 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
@test lower_bound(v) == 0.0
|
||||
@test upper_bound(v) == 1.0
|
||||
|
||||
#dest = FileFormats.Model(format = FileFormats.FORMAT_LP)
|
||||
#MOI.copy_to(dest, model.mip)
|
||||
#MOI.write_to_file(dest, "model.lp")
|
||||
# dest = FileFormats.Model(format = FileFormats.FORMAT_LP)
|
||||
# MOI.copy_to(dest, model.mip)
|
||||
# MOI.write_to_file(dest, "model.lp")
|
||||
end
|
||||
|
||||
@testset "solve" begin
|
||||
solution_filename = tempname()
|
||||
@testset "solve (exact)" begin
|
||||
solution_filename_a = tempname()
|
||||
solution_filename_b = tempname()
|
||||
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 "Fixed operating (\$)" in keys(solution["Costs"])
|
||||
@@ -58,6 +64,12 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
@test "F3" in keys(solution["Plants"])
|
||||
@test "F4" in keys(solution["Plants"])
|
||||
end
|
||||
|
||||
|
||||
@testset "solve (heuristic)" begin
|
||||
# Should not crash
|
||||
solution = RELOG.solve("$(pwd())/../instances/s1.json", heuristic=true)
|
||||
end
|
||||
|
||||
@testset "infeasible solve" begin
|
||||
json = JSON.parsefile("$(pwd())/../instances/s1.json")
|
||||
@@ -66,7 +78,23 @@ using RELOG, Cbc, JuMP, Printf, JSON, MathOptInterface.FileFormats
|
||||
end
|
||||
RELOG.solve(RELOG.parse(json))
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -6,23 +6,36 @@ using RELOG, JSON, GZip
|
||||
|
||||
load_json_gz(filename) = JSON.parse(GZip.gzopen(filename))
|
||||
|
||||
function check(func, expected_csv_filename::String)
|
||||
solution = load_json_gz("fixtures/nimh_solution.json.gz")
|
||||
actual_csv_filename = tempname()
|
||||
func(solution, actual_csv_filename)
|
||||
@test isfile(actual_csv_filename)
|
||||
if readlines(actual_csv_filename) != readlines(expected_csv_filename)
|
||||
out_filename = replace(expected_csv_filename, ".csv" => "_actual.csv")
|
||||
@error "$func: Unexpected CSV contents: $out_filename"
|
||||
write(out_filename, read(actual_csv_filename))
|
||||
@test false
|
||||
end
|
||||
end
|
||||
# function check(func, expected_csv_filename::String)
|
||||
# solution = load_json_gz("fixtures/nimh_solution.json.gz")
|
||||
# actual_csv_filename = tempname()
|
||||
# func(solution, actual_csv_filename)
|
||||
# @test isfile(actual_csv_filename)
|
||||
# if readlines(actual_csv_filename) != readlines(expected_csv_filename)
|
||||
# out_filename = replace(expected_csv_filename, ".csv" => "_actual.csv")
|
||||
# @error "$func: Unexpected CSV contents: $out_filename"
|
||||
# write(out_filename, read(actual_csv_filename))
|
||||
# @test false
|
||||
# end
|
||||
# end
|
||||
|
||||
@testset "Reports" begin
|
||||
check(RELOG.write_plants_report, "fixtures/nimh_plants.csv")
|
||||
check(RELOG.write_plant_outputs_report, "fixtures/nimh_plant_outputs.csv")
|
||||
check(RELOG.write_plant_emissions_report, "fixtures/nimh_plant_emissions.csv")
|
||||
check(RELOG.write_transportation_report, "fixtures/nimh_transportation.csv")
|
||||
check(RELOG.write_transportation_emissions_report, "fixtures/nimh_transportation_emissions.csv")
|
||||
# @testset "from fixture" begin
|
||||
# check(RELOG.write_plants_report, "fixtures/nimh_plants.csv")
|
||||
# check(RELOG.write_plant_outputs_report, "fixtures/nimh_plant_outputs.csv")
|
||||
# check(RELOG.write_plant_emissions_report, "fixtures/nimh_plant_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
|
||||
|
||||
Reference in New Issue
Block a user