Tutorial¶
The README file contains installation notes. This tutorial expands on the steps that follow this installation.
This tutorial is a commented walk-through through the script runme.py
,
which is a demonstration user script that can serve as a good basis for one’s
own script.
Initialisation¶
Imports¶
try:
import pyomo.environ
from pyomo.opt.base import SolverFactory
PYOMO3 = False
except ImportError:
import coopr.environ
from coopr.opt.base import SolverFactory
PYOMO3 = True
import os
import shutil
import urbs
from datetime import datetime
Several packages are included.
- the try-except block checks for the version of Coopr/Pyomo installed and imports
the necessary packages for the model creation and solution.
os is a builtin Python module, included here for its os.path submodule that offers operating system independent path manipulation routines. The following code creates the path string
'result/foo'
or'result\\foo'
(depending on the operating system), checks whether it exists and creates the folder(s) if needed. This is used to prepare a new directory for generated result file:result_dir = os.path.join('result', 'foo') if not os.path.exists(result_dir): os.makedirs(result_dir)
urbs is the module whose functions are used mainly in this script. These are
read_excel()
,create_model()
,report()
andplot()
. More functions can be found in the document API reference.`pyomo.opt.base`_ is a utility package by pyomo and provides the function
SolverFactory
that allows creating asolver
object. This objects hides the differences in input/output formats among solvers from the user. More on that in section Solving below.datetime is used to append the current date and time to the result directory name (used in
prepare_result_directory()
)
Settings¶
From here on, the script is best read from the back.:
if __name__ == '__main__':
input_file = 'mimo-example.xlsx'
result_name = os.path.splitext(input_file)[0] # cut away file extension
result_dir = prepare_result_directory(result_name) # name + time stamp
(offset, length) = (4000, 5*24) # time step selection
timesteps = range(offset, offset+length+1)
Variable input_file
defines the input spreadsheet, from which the
optimization problem will draw all its set/parameter data.
Variable timesteps
is the list of timesteps to be simulated. Its members
must be a subset of the labels used in input_file
’s sheets “Demand” and
“SupIm”. It is one of the two function arguments to create_model()
and
accessible directly, because one can quickly reduce the problem size by
reducing the simulation length
, i.e. the number of timesteps to be
optimised.
range()
is used to create a list of consecutive integers. The argument
+1
is needed, because range(a,b)
only includes integers from a
to
b-1
:
>>> range(1,11)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
The following section deals with the definition of different scenarios.
Starting from the same base scenarios, defined by the data in input_file
,
they serve as a short way of defining the difference in input data. If needed,
completely separate input data files could be loaded as well.
In addition to defining scenarios, the scenarios
list allows to select a
subset to be actually run.
Scenario functions¶
A scenario is simply a function that takes the input data
and modifies it
in a certain way. with the required argument data
, the input
data dict
.:
# SCENARIOS
def scenario_base(data):
# do nothing
return data
The simplest scenario does not change anything in the original input file. It usually is called “base” scenario for that reason. All other scenarios are defined by 1 or 2 distinct changes in parameter values, relative to this common foundation.:
def scenario_stock_prices(data):
# change stock commodity prices
co = data['commodity']
stock_commodities_only = (co.index.get_level_values('Type') == 'Stock')
co.loc[stock_commodities_only, 'price'] *= 1.5
return data
For example, scenario_stock_prices()
selects all stock commodities from
the DataFrame
commodity
, and increases their price value by 50%.
See also pandas documentation Selection by label
for more information about the .loc
function to access fields. Also note
the use of Augmented assignment statements (*=
) to modify data
in-place.:
def scenario_co2_limit(data):
# change global CO2 limit
hacks = data['hacks']
hacks.loc['Global CO2 limit', 'Value'] *= 0.05
return data
Scenario scenario_co2_limit()
shows the simple case of changing a single
input data value. In this case, a 95% CO2 reduction compared to the base
scenario must be accomplished. This drastically limits the amount of coal and
gas that may be used by all three sites.:
def scenario_north_process_caps(data):
# change maximum installable capacity
pro = data['process']
pro.loc[('North', 'Hydro plant'), 'cap-up'] *= 0.5
pro.loc[('North', 'Biomass plant'), 'cap-up'] *= 0.25
return data
Scenario scenario_north_process_caps()
demonstrates accessing single
values in the process
DataFrame
. By reducing the amount of
renewable energy conversion processes (hydropower and biomass), this scenario
explores the “second best” option for this region to supply its demand.:
def scenario_all_together(data):
# combine all other scenarios
data = scenario_stock_prices(data)
data = scenario_co2_limit(data)
data = scenario_north_process_caps(data)
return data
Scenario scenario_all_together()
finally shows that scenarios can also be
combined by chaining other scenario functions, making them dependent. This way,
complex scenario trees can written with any single input change coded at a
single place and then building complex composite scenarios from those.
Scenario selection¶
# select scenarios to be run
scenarios = [
scenario_base,
scenario_stock_prices,
scenario_co2_limit,
scenario_north_process_caps,
scenario_all_together]
scenarios = scenarios[:1] # select by slicing
In Python, functions are objects, so they can be put into data structures just
like any variable could be. In the following, the list scenarios
is used to
control which scenarios are being actually computed.
Run scenarios¶
for scenario in scenarios:
prob = run_scenario(input_file, timesteps, scenario, result_dir)
Having prepared settings, input data and scenarios, the actual computations
happen in the function run_scenario()
of the script. It is executed
for
each of the scenarios included in the scenario list. The following
sections describe the content of function run_scenario()
. In a nutshell,
it reads the input data from its argument input_file
, modifies it with the
supplied scenario
, runs the optimisation for the given timesteps
and
writes report and plots to result_dir
.
Reading input¶
# scenario name, read and modify data for scenario
sce = scenario.__name__
data = urbs.read_excel(input_file)
data = scenario(data)
Function read_excel()
returns a dict data
of six pandas DataFrames
with hard-coded column names that correspond to the parameters of the
optimization problem (like eff
for efficiency or inv-cost-c
for
capacity investment costs). The row labels on the other hand may be freely
chosen (like site names, process identifiers or commodity names). By
convention, it must contain the six keys commodity
, process
,
storage
, transmission
, demand
, and supim
. Each value must be a
pandas.DataFrame
, whose index (row labels) and columns (column labels)
conforms to the specification given by the example dataset in the spreadsheet
mimo-example.xlsx
.
data
is then modified by applying the scenario()
function to it.
Solving¶
# create model
prob = urbs.create_model(data, timesteps)
if PYOMO3:
prob = prob.create()
# refresh time stamp string and create filename for logfile
now = prob.created
log_filename = os.path.join(result_dir, '{}.log').format(sce)
# solve model and read results
optim = SolverFactory('glpk') # cplex, glpk, gurobi, ...
optim = setup_solver(optim, logfile=log_filename)
result = optim.solve(prob, tee=True)
if PYOMO3:
prob.load(result)
else:
prob.solutions.load_from(result)
This section is the “work horse”, where most computation and time is spent. The
optimization problem is first defined (create_model()
), then filled with
values (create
). The SolverFactory
object is an abstract representation
of the solver used. The returned object optim
has a method
set_options()
to set solver options (not used in this tutorial).
The remaining two lines call the solver and read the result
object back
into the prob
object, which is queried to for variable values in the
remaining script file. Argument tee=True
enables the realtime console
output for the solver. If you want less verbose output, simply set it to
False
or remove it.
Reporting¶
# write report to spreadsheet
urbs.report(
prob,
os.path.join(result_dir, '{}-{}.xlsx').format(sce, now),
['Elec'], ['South', 'Mid', 'North'])
The urbs.report()
function creates a spreadsheet from the main results.
Summaries of costs, emissions, capacities (process, transmissions, storage) are
saved to one sheet each. For timeseries, each combination of the given
sites
and commodities
are summarised both in sum (in sheet “Energy
sums”) and as individual timeseries (in sheet “… timeseries”). See also
Reporting function explained for a detailed explanation of this function’s implementation.
Plotting¶
# add or change plot colors
my_colors = {
'South': (230, 200, 200),
'Mid': (200, 230, 200),
'North': (200, 200, 230)}
for country, color in my_colors.items():
urbs.COLORS[country] = color
First, the use of the module constant COLORS
for customising plot
colors is demonstrated. All plot colors are user-defineable by adding color
tuple()
(r, g, b)
or modifying existing tuples for commodities and
plot decoration elements. Here, new colors for displaying import/export are
added. Without these, pseudo-random colors are generated in
to_color()
.:
# create timeseries plot for each demand (site, commodity) timeseries
for sit, com in prob.demand.columns:
# create figure
fig = urbs.plot(prob, com, sit)
# change the figure title
ax0 = fig.get_axes()[0]
nice_sce_name = sce.replace('_', ' ').title()
new_figure_title = ax0.get_title().replace(
'Energy balance of ', '{}: '.format(nice_sce_name))
ax0.set_title(new_figure_title)
# save plot to files
for ext in ['png', 'pdf']:
fig_filename = os.path.join(
result_dir, '{}-{}-{}-{}.{}').format(sce, com, sit, now, ext)
fig.savefig(fig_filename, bbox_inches='tight')
Finally, for each demand commodity (only Elec
in this case), a plot over
the whole optimisation period is created. If timesteps
were longer, a
shorter plotting period could be defined and given as an optional argument to
plot()
.
The paragraph “change figure title” shows exemplarily how to use matplotlib
manually to modify some aspects of a plot without having to recreate the
plotting function from scratch. For more ideas for adaptations, look into
plot()
’s code or the matplotlib documentation.
The last paragraph uses the savefig()
method
to save the figure as a pixel png
(raster) and pdf
(vector) image. The
bbox_inches='tight'
argument eliminates whitespace around the plot.
Note
savefig()
has some more interesting
arguments. For example dpi=600
can be used to create higher resolution
raster output for use with printing, in case the preferable vector images
cannot be used. The filename extension or the optional format
argument
can be used to set the output format. Available formats depend on the used
plotting backend. Most backends support png, pdf, ps, eps and svg.