8. REISE.jl¶
REISE.jl (Renewable Energy Integration Simulation Engine) is the simulation engine developed by BES to run power-flow studies in the U.S. electric grid. REISE.jl is an open-source package written in Julia that is available on GitHub. It can be interfaced with the BES software ecosystem (see PowerSimData) or used in a standalone mode. In both cases you will need an external optimization solver to solve the DCOPF problem.
You will find in this documentation all the information needed to install this package and use it. We also provide the formulation of the objective function along with its constraints.
8.1. System Requirements¶
Large simulations can require significant amounts of RAM. The amount of necessary RAM is proportional to both the size of the electric grid and the duration of the interval considered for the simulation.
As a general estimate, 1-2 GB of RAM is needed per hour in the interval in a simulation across the entire USA grid. For example, a 24-hour interval would require 24-48 GB of RAM; if only 16 GB of RAM is available, consider using a time interval of 8 hours or less as that would take 8-16 GB of RAM.
The memory necessary would also be proportional to the size of grid used. Since the Western interconnect is roughly 8 times smaller than the entire USA grid, a simulation ran in this interconnect with a 24-hour interval would require ~3-6 GB of RAM.
8.2. Installation¶
There are two options, either install all the dependencies yourself or setup the engine within a Docker image. Whatever option you choose, you will need an external solver to run optimizations. We recommend Gurobi, though any other solver compatible with JuMP can be used. Note that Gurobi is a commercial solver and hence a license file is required. This may be either a local license, a cloud license or a free license for academic use. Check their Software Downloads and License Center page for more details.
Start by cloning the repository locally:
git clone https://github.com/Breakthrough-Energy/REISE.jl
You will also need to download some input data in order to run simulations. Sample data are available on Zenodo. You will find there hourly time series for the hydro/solar/wind generators and a MAT-file enclosing all the information related to the electrical grid in accordance with the MATPOWER case file format.
8.2.1. Native Installation¶
Installation will depend on your operating system. Some examples are provided for Unix-like platforms.
8.2.1.1. Julia¶
Download Julia 1.5 and install it following the instructions located on their Platform Specific Instructions for Official Binaries page. This should be straightforward:
Choose a destination directory. For shared installation,
/opt
is recommended.cd /opt
Download and unzip the package in the chosen directory:
wget -q https://julialang-s3.julialang.org/bin/linux/x64/1.5/julia-1.5.3-linux-x86_64.tar.gz tar -xf julia-1.5.3-linux-x86_64.tar.gz
Expand the
PATH
environment variable. For bash users edit the .bashrc file in your$HOME
folder:export PATH="$PATH:/opt/julia-1.5.3/bin"
8.2.1.2. Gurobi¶
If you plan on using Gurobi as a solver, you will need to download and install it first so it can be accessed by Jump. Installation of Gurobi depends on both the operating system and the license type. Detailed instructions can be found in the Gurobi Installation Guide. For Unix-like platforms, this will look like:
Choose a destination directory. For shared installation,
/opt
is recommended.cd /opt
Download and unzip the package in the chosen directory:
wget https://packages.gurobi.com/9.1/gurobi9.1.0_linux64.tar.gz tar -xvfz gurobi9.1.0_linux64.tar.gz
This will create the
/opt/gurobi910/linux64
subdirectory in which the complete distribution is located.Set environments variables. For bash users edit the .bashrc file in your
$HOME
folder:export GUROBI_HOME="/opt/gurobi910/linux64" export PATH="${PATH}:${GUROBI_HOME}/bin" export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${GUROBI_HOME}/lib"
The Gurobi license needs to be download and installed. Download a copy of your Gurobi license from the account portal, and copy it into the parent directory of
$GUROBI_HOME
.cd gurobi.lic /opt/gurobi910/gurobi.lic
To verify that Gurobi is properly installed, run the gurobi.sh shell script:
.$GUROBI_HOME/bin/gurobi.sh
8.2.1.3. REISE.jl¶
The package will need to be added to each user’s default Julia environment. This can be
done by launching Julia and typing ]
to access the Pkg
(the built-in package
manager) REPL environment that easily allows operations such as installing, updating
and removing packages.
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.5.3 (2020-11-09)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> ]
pkg>
From here, we recommend that you create an environment and install the dependencies in the same state specified in the manifest (Manifest.toml):
activate /PATH/TO/REISE.jl
instantiate
Note that the Julia packages for the user’s desired solvers need to be installed separately. For instance, if you want to use GLPK, the GNU Linear Programming Kit library, you will need to run:
import Pkg
Pkg.add("GLPK")
Then, create a JULIA_PROJECT
environment variable that points to
PATH/TO/REISE.jl
.
To verify that the package has been successfully installed, open a new instance of Julia and verify that the REISE package can load without any errors with the following command:
using REISE
8.2.1.4. Python¶
We strongly recommend that you install Python in order to be able to use the command line interface we developed to run simulations but most importantly to extract the data generated by the simulation.
The scripts located in pyreisejl
depend on several packages. Those are specified
in the requirements.txt, file and can be installed using:
pip install -r requirements.txt
To verify that the Python scripts can successfully run, open a Python interpreter and run the following commands:
from julia.api import Julia
Julia(compiled_modules=False)
from julia import REISE
Note that the final import of REISE may take a couple of minutes to complete.
8.2.2. Docker¶
The easiest way to setup this engine is within a Docker image.
There is an included Dockerfile that can be used to build the Docker image. With the Docker daemon installed and running, do:
docker build . -t reisejl
To run the Docker image, you will need to mount two volumes; one containing the Gurobi license file and another containing the necessary input files for the engine.
docker run -it `
-v /path/to/gurobi.lic:/usr/share/gurobi_license `
-v /path/to/input/data:/path/to/input/data `
reisejl bash
You are ready to run simulation as demonstrated in the Usage section.
8.3. Usage¶
REISE.jl can be used within the Julia interpreter or through the Python scripts.
8.3.1. Julia¶
After the installation, the REISE package is registered and can be imported using
import REISE
to call REISE.run_scenario()
or using REISE
to
call run_scenario()
.
Running a scenario requires the following inputs:
interval
: the length of each simulation interval (hours).n_interval
: the number of simulation intervals.start_index
: the hour to start the simulation, representing the row of the time- series profiles in demand.csv, hydro.csv, solar.csv, and wind.csv. Note that unlike some other programming languages, Julia is 1-indexed, so the first index is 1.inputfolder
: the directory from which to load input files.optimizer_factory
: an argument that can be passed toJuMP.Model
to create a new model instance with an attached solver. Be sure to pass the factory itself (e.g.GLPK.Optimizer
) rather than an instance (e.g.GLPK.Optimizer()
). See theJuMP.Model
documentation for more information.
To illustrate, to run a scenario that starts at the 1st hour of the year, runs in 3
intervals of 24 hours each, using input data located in working directory (pwd()
)
and using the GLPK solver, call:
import REISE
import GLPK
REISE.run_scenario(;
interval=24, n_interval=3, start_index=1, inputfolder=pwd(),
optimizer_factory=GLPK.Optimizer
)
Optional arguments include:
outputfolder
: a directory in which to store results files. The default is a subdirectory “output” within the input directory (created if it does not already exist).threads
: the number of threads to be used by the solver. The default is to let the solver decide.solver_kwargs
: a dictionary of String => value pairs to be passed to the solver.
Default settings for running using Gurobi can be accessed if Gurobi.jl has already
been imported using the REISE.run_scenario_gurobi
function:
using REISE
using Gurobi
REISE.run_scenario_gurobi(;
interval=24, n_interval=3, start_index=1, inputfolder=pwd(),
)
Optional arguments for REISE.run_scenario
can still be passed as desired.
8.3.2. Python¶
There are two main Python scripts included in pyreisejl
:
pyreisejl/utility/call.py
pyreisejl/utility/extract_data.py
The first of these scripts transforms more descriptive input parameters into the ones necessary for the Julia engine while also performing some additional input validation. The latter, which can be set to automatically occur after the simulation has completed, extracts key metrics from the resulting .mat files to .pkl files.
8.3.2.1. Running a simulation¶
A simulation can be run as follows:
pyreisejl/utility/call.py -s '2016-01-01' -e '2016-01-07' -int 24 -i '/PATH/TO/INPUT/DATA'
It will solve the DCOPF problem in our grid model by interval of 24h using hourly data
located in /PATH/TO/INPUT/DATA
from January 1st to January 7th 2016. Note that
the start and end dates need to match dates contained in the input profiles (demand,
hydro, solar, wind). By default Gurobi will be used as the solver and the output data
(.mat files) will be saved in an output
folder created in the given input
directory.
The full list of arguments can be accessed via pyreisejl/utility/call.py --help
:
usage: call.py [-h] [-s START_DATE] [-e END_DATE] [-int INTERVAL] [-i INPUT_DIR] [-t THREADS] [-d] [-o OUTPUT_DIR] [-k]
[--solver SOLVER] [-j JULIA_ENV]
scenario_id
Run REISE.jl simulation.
positional arguments:
scenario_id Scenario ID only if using PowerSimData.
optional arguments:
-h, --help show this help message and exit
-s START_DATE, --start-date START_DATE
The start date for the simulation in format 'YYYY-MM-DD', 'YYYY-MM-DD HH',
'YYYY-MM-DD HH:MM', or 'YYYY-MM-DD HH:MM:SS'.
-e END_DATE, --end-date END_DATE
The end date for the simulation in format 'YYYY-MM-DD',
'YYYY-MM-DD HH', 'YYYY-MM-DD HH:MM', or 'YYYY-MM-DD HH:MM:SS'.
If only the date is specified (without any hours), the entire
end-date will be included in the simulation.
-int INTERVAL, --interval INTERVAL
The length of each interval in hours.
-i INPUT_DIR, --input-dir INPUT_DIR
The directory containing the input data files. Required files
are 'grid.pkl', 'demand.csv', 'hydro.csv', 'solar.csv', and
'wind.csv'.
-t THREADS, --threads THREADS
The number of threads to run the simulation with. This is
optional and defaults to Auto.
-d, --extract-data If this flag is used, the data generated by the simulation
after the engine has finished running will be automatically
extracted into .pkl files, and the result.mat files will be
deleted. The extraction process can be memory intensive. This
is optional and defaults to False if the flag is omitted.
-o OUTPUT_DIR, --output-dir OUTPUT_DIR
The directory to store the extracted data. This is optional
and defaults to a folder in the input directory. This flag is
only used if the extract-data flag is set.
-k, --keep-matlab The result.mat files found in the execute directory will be
kept instead of deleted after extraction. This flag is only
used if the extract-data flag is set.
--solver SOLVER Specify the solver to run the optimization. Will default to
gurobi. Current solvers available are clp,glpk,gurobi.
-j JULIA_ENV, --julia-env JULIA_ENV
The path to the julia environment within which to run
REISE.jl. This is optional and defaults to the default julia
environment.
Different solvers can be used (--solver
).
There is another optional flag that specifies the number of threads to use for the
simulation run in Gurobi (--threads
). If the number of threads specified is higher
than the number of logical processor count available, a warning will be generated but
the simulation will still run.
Finally, you can use --extract-data
to automatically extract the data after a
simulation run without having to manually initiate it. Note that the extraction process
can be memory intensive
8.3.2.2. Extracting Simulation Results¶
After the simulation has completed and if the --extract-data
is set in the
call.py script, the extraction can be run using the same start and end dates as
were used to run the simulation:
pyreisejl/utility/extract_data.py -s '2016-01-01' -e '2016-01-07' -i '/PATH/TO/INPUT/DATA'
The full list of arguments can be accessed via
pyreisejl/utility/extract-data.py --help
:
usage: extract_data.py [-h] [-s START_DATE] [-e END_DATE] [-i INPUT_DIR] [-o OUTPUT_DIR] [-f FREQUENCY] [-k] scenario_id
Extract data from the results of the REISE.jl simulation.
positional arguments:
scenario_id Scenario ID only if using PowerSimData.
optional arguments:
-h, --help show this help message and exit
-s START_DATE, --start-date START_DATE
The start date as provided to run the simulation. Supported
formats are 'YYYY-MM-DD', 'YYYY-MM-DD HH', 'YYYY-MM-DD HH:MM',
or 'YYYY-MM-DD HH:MM:SS'.
-e END_DATE, --end-date END_DATE
The end date as provided to run the simulation. Supported
formats are 'YYYY-MM-DD', 'YYYY-MM-DD HH', 'YYYY-MM-DD HH:MM',
or 'YYYY-MM-DD HH:MM:SS'.
-i INPUT_DIR, --input-dir INPUT_DIR
The directory containing the input data files. Required files
are 'grid.pkl', 'demand.csv', 'hydro.csv', 'solar.csv', and
'wind.csv'.
-o OUTPUT_DIR, --output-dir OUTPUT_DIR
The directory to store the results. This is optional and
defaults to a folder in the input directory.
-f FREQUENCY, --frequency FREQUENCY
The frequency of data points in the original profile csvs as a
Pandas frequency string. This is optional and defaults to an
hour.
-k, --keep-matlab If this flag is used, the result.mat files found in the execute
directory will be kept instead of deleted.
When manually running the extract_data process, the script assumes the frequency of the
input profiles are hourly and will construct the timestamps for the resulting data
accordingly. If a different frequency was used for the input data, it must be specified
via --frequency
. Also, other parameters can be invoked to handle output data.
When the script has finished running, the following .pkl files will be available:
PF.pkl (power flow)
PG.pkl (power generated)
LMP.pkl (locational marginal price)
CONGU.pkl (congestion, upper flow limit)
CONGL.pkl (congestion, lower flow limit)
AVERAGED_CONG.pkl (time averaged congestion)
If the grid used in the simulation contains DC lines, energy storage devices, or flexible demand resources, the following files will also be extracted as necessary:
PF_DCLINE.pkl (power flow on DC lines)
STORAGE_PG.pkl (power generated by storage units)
STORAGE_E.pkl (energy state of charge)
LOAD_SHIFT_DN.pkl (demand that is curtailed)
LOAD_SHIFT_UP.pkl (demand that is added)
If one or more intervals of the simulation were found to be infeasible without shedding load, the following file will also be extracted:
LOAD_SHED.pkl (load shed profile for each load bus)
8.3.2.3. Compatibility with our Software Ecosystem¶
Both pyreisejl/utility/call.py and pyreisejl/utility/extract_data.py can be called using a positional argument that corresponds to a scenario id as generated by PowerSimData. Using this invocation assumes you have installed our software ecosystem. See Installation Guide ) if you are interested.
8.4. Formulation¶
8.4.1. Sets¶
\(B\): Set of buses indexed by \(b\).
\(I\): Set of generators indexed by \(i\).
\(L\): Set of transmission network branches indexed by \(l\).
\(S\): Set of generation cost curve segments indexed by \(s\).
\(T\): Set of time periods indexed by \(t\)
8.4.1.1. Subsets¶
\(I^{\rm H}\): Set of hydro generators.
\(I^{\rm S}\): Set of solar generators.
\(I^{\rm W}\): Set of wind generators.
8.4.2. Variables¶
\(E_{b,\,t}\): Energy available in energy storage devices at bus \(b\) at time \(t\).
\(f_{l,\,t}\): Power flowing on branch \(l\) at time \(t\).
\(g_{i,\,t}\): Power injected by each generator \(i\) at time \(t\).
\(g_{i,\,s,\,t}\): Power injected by each generator \(i\) from cost curve segment \(i\) at time \(t\).
\(J^{\rm chg}_{b,\,t}\): Charging power of energy storage devices at bus \(b\) at time \(t\).
\(J^{\rm dis}_{b,\,t}\): Discharging power of energy storage devices at bus \(b\) at time \(t\).
\(s_{b,\,t}\): Load shed at bus \(b\) at time \(t\).
\(v_{l,\,t}\): Branch limit violation for branch \(l\) at time \(t\).
\(\delta^{\rm down}_{b,\,t}\): Amount of flexible demand curtailed at bus \(b\) at time \(t\).
\(\delta^{\rm up}_{b,\,t}\): Amount of flexible demand added at bus \(b\) at time \(t\).
\(\theta_{b,\,t}\): Voltage angle of bus \(b\) at time \(t\).
8.4.3. Parameters¶
\(a^{\rm shed}\): Binary parameter, whether load shedding is enabled.
\(a^{\rm viol}\): Binary parameter, whether transmission limit violation is enabled.
\(c_{i,\,s}\): Cost coefficient for segment \(s\) of generator \(i\).
\(c^{\rm min}_{i}\): Cost of running generator \(i\) at its minimum power level.
\(d_{b,\,t}\): Power demand at bus \(b\) at time \(t\).
\(E_{b,\,0}\): Initial energy available in energy storage devices at bus \(b\).
\(E^{\rm max}_{b}\): Maximum energy stored in energy storage devices at bus \(b\).
\(f^{\rm max}_{l}\): Maximum flow over branch \(l\).
\(g^{\rm min}_{i}\): Minimum generation for generator \(i\).
\(g^{\rm max}_{i,\,s}\): Width of cost curve segment \(s\) of generator \(i\).
\(J^{max}_{b}\): Maximum charging/discharging power of energy storage devices at bus \(b\).
- \(m^{\rm line}_{l,\,b}\): Mapping of branches to buses.
\(m^{\rm line}_{l,\,b} = 1\) if branch \(l\) starts at bus \(b\),
\(m^{\rm line}_{l,\,b} = -1\) if branch \(l\) ends at bus \(b\),
\(m^{\rm line}_{l,\,b} = 0\) otherwise.
- \(m^{\rm unit}_{i,\,b}\): Mapping of generators to buses.
\(m^{\rm unit}_{i,\,b} = 1\) if generator \(i\) is located at bus \(b\),
\(m^{\rm unit}_{i,\,b} = 0\) otherwise.
\(M\): An arbitrarily-large constant, used in ‘big-M’ constraints to either constrain to \(0\), or relax constraint.
\(p^{\rm e}\): Value of stored energy at beginning/end of interval (so that optimization does not automatically drain the storage by end-of-interval).
\(p^{\rm s}\): Load shed penalty factor.
\(p^{\rm v}\): Transmission violation penalty factor.
\(r^{\rm up}_{i}\): Ramp-up limit for generator \(i\).
\(r^{\rm down}_{i}\): Ramp-down limit for generator \(i\).
\(w_{i,\,t}\): Power available at time \(t\) from time-varying generator (hydro, wind, solar) \(i\).
\(x_{l}\): Impedance of branch \(l\).
\(\underline{\delta}_{b,\,t}\): Demand flexibility curtailments available (in MW) at bus \(b\) at time \(t\).
\(\overline{\delta}_{b,\,t}\): Demand flexibility additions available (in MW) at bus \(b\) at time \(t\).
\(\Delta^{\rm balance}\): The length of the rolling load balance window (in hours), used to account for the duration that flexible demand is deviating from the base demand.
\(\eta^{\rm chg}_{b}\): Charging efficiency of storage device at bus \(b\).
\(\eta^{\rm dis}_{b}\): Discharging efficiency of storage device at bus \(b\).
8.4.4. Constraints¶
All equations apply over all entries in the indexed sets unless otherwise listed.
\(0 \le g_{i,\,s,\,t} \le g^{\rm max}_{i,\,s,\,t}\): Generator segment power is non-negative and less than the segment width.
\(0 \le s_{b,\,t} \le a^{\rm shed} \cdot \left ( d_{b,\,t} + \delta^{\rm up}_{b,\,t} - \delta^{\rm down}_{b,\,t} \right )\): Load shed is non-negative and less than the demand at that bus (including the impact of demand flexibility), if load shedding is enabled. If not, load shed is fixed to \(0\).
\(0 \le v_{b,\,t} \le a^{\rm viol} \cdot M\): Transmission violations are non- negative, if they are enabled (\(M\) is a sufficiently large constant that there is no effective upper limit when \(a^{\rm shed} = 1\)). If not, they are fixed to \(0\).
\(0 \le J_{b,\,t}^{\rm chg} \le J_{b}^{\rm max}\): Storage charging power is non-negative and limited by the maximum charging power at that bus.
\(0 \le J_{b,\,t}^{\rm dis} \le J_{b}^{\rm max}\): Storage discharging power is non-negative and limited by the maximum discharging power at that bus.
\(0 \le E_{b,\,t} \le E_{b}^{\rm max}\): Storage state-of-charge is non-negative and limited by the maximum state of charge at that bus.
\(g_{i,\,t} = w_{i,\,t} \quad \forall i \in I^{\rm H}\): Hydro generator power is fixed to the profiles.
\(0 \le g_{i,\,t} \le w_{i,\,t} \quad \forall i \in I^{\rm S} \cup I^{\rm W}\): Solar and wind generator power is non-negative and not greater than the availability profiles.
\(\sum_{i \in I} m_{i,\,b}^{\rm unit} g_{i,\,t} + \sum_{l \in L} m_{l,\,b}^{\rm line} f_{l,\,t} + J_{b,\,t}^{\rm dis} + s_{b,\, t} + \delta_{b,\, t}^{\rm down} = d_{b,\,t} + J_{b,\,t}^{\rm chg} + \delta_{b,\,t}^{\rm up}\): Power balance at each bus \(b\) at time \(t\).
\(g_{i,\,t} = g_{i}^{\rm min} + \sum_{s \in \rm S} g_{i,\,s,\,t}\): Total generator power is equal to the minimum power plus the power from each segment.
\(E_{b,\,t} = E_{b,\,t-1} + \eta_{b}^{\rm chg} J_{b,\, t}^{\rm chg} - \frac{1}{\eta_{b}^{\rm dis}} J_{b,\,t}^{\rm dis}\): Conservation of energy for energy storage state-of-charge.
\(g_{i,\,t} - g_{i,\,t-1} \le r_{i}^{\rm up}\): Ramp-up constraint.
\(g_{i,\,t} - g_{i,\,t-1} \ge r_{i}^{\rm down}\): Ramp-down constraint.
\(-\left ( f_{l}^{\rm max} + v_{l,\,t} \right ) \le f_{l,\,t} \le \left ( f_{l}^{\rm max} + v_{l,\,t} \right )\): Power flow over each branch is limited by the branch power limit, and can only exceed this value by using the ‘violation’ variable (if enabled), which is penalized in the objective function.
\(f_{l,\,t} = \frac{1}{x_{l}} \sum_{b \in B} m_{l,\,b}^{\rm line} \theta_{b,\,t}\): Power flow over each branch is proportional to the admittance and the angle difference.
\(0 \le \delta_{b,\,t}^{\rm down} \le \underline{\delta}_{b,\,t}\): Bound on the amount of demand that flexible demand resources can curtail.
\(0 \le \delta_{b,\,t}^{\rm up} \le \overline{\delta}_{b,\,t}\): Bound on the amount of demand that flexible demand resources can add.
\(\sum_{t = k}^{k + \Delta^{\rm balance}} \delta_{b,\,t}^{\rm up} - \delta_{b,\,t}^{\rm down} \ge 0, \quad \forall b \in B, \quad k = 1, ..., |T| - \Delta^{\rm balance}\): Rolling load balance for flexible demand resources; used to restrict the time that flexible demand resources can deviate from the base demand.
\(\sum_{t \in T} \delta_{b,\,t}^{\rm up} - \delta_{b,\,t}^{\rm down} \ge 0, \quad \forall b \in B\): Interval load balance for flexible demand resources.
8.4.5. Objective Function¶
\(\min \left [ \sum_{t \in T} \sum_{i \in I} \left [ C_{i}^{\rm min} + \sum_{s \in \rm S} c_{i,\,s} g_{i,\,s,\,t} \right ] + p^{\rm s} \sum_{t \in T} \sum_{b \in B} s_{b,\,t} + p^{\rm v} \sum_{t \in T} \sum_{l \in L} v_{l,\,t} + p^{\rm e} \sum_{b \in B} \left [ E_{b,\,0} - E_{b,\,|T|} \right ] \right ]\)
There are four main components to the objective function:
\(\sum_{t \in T} \sum_{i \in I} [ C_{i}^{\rm min} + \sum_{s \in \rm S} c_{i,\,s} g_{i,\,s,\,t} ]\): The cost of operating generators, fixed costs plus variable costs, which can consist of several cost curve segments for each generator.
\(p^{\rm s} \sum_{t \in T} \sum_{b \in B} s_{b,\,t}\): Penalty for load shedding (if load shedding is enabled).
\(p^{\rm v} \sum_{t \in T} \sum_{l \in L} v_{l,\,t}\): Penalty for transmission line limit violations (if transmission violations are enabled).
\(p^{\rm e} \sum_{b \in B} \left [ E_{b,\,0} - E_{b,\,|T|} \right ]\): Penalty for ending the interval with less stored energy than the start, or reward for ending with more.