11 Commits

18 changed files with 549 additions and 169 deletions

View File

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

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)
- Generate simplified solution reports (CSV)

View File

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

View File

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

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

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.
| `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

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-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*}

View File

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

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

View File

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

View File

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

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)": {
"type": "object",
"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
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

View File

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

View File

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