runscript explained

The runscript can be subdivided into several parts. These will be discussed here in detail.

Imports

The script starts with importing the relevant python libraries as well as the module urbs.

import os
import shutil
import urbs

The included packages have the following functions:

  • os and shutil are builtin Python modules, included here for their data path and copying operations.
  • urbs is the directory which includes the modules, whose functions are used mainly in this script. These are prepare_result_directory(), setup_solver() and run_scenario().

More functions can be found in the document API reference.

In the following sections the user defined input, output and scenario settings are described.

Input Settings

The script starts with the specification of the input files, which is either a single .xlsx file located in the same folder as the runscript or a collection of .xlsx files located in the subfolder Input:

input_files = 'Input'
result_name = 'Mimo-ex'
result_dir = urbs.prepare_result_directory(result_name)  # name + time stamp

# copy input file to result directory
try:
    shutil.copytree(input_files, os.path.join(result_dir, 'Input'))
except NotADirectoryError:
    shutil.copyfile(input_files, os.path.join(result_dir, input_files))
# copy runme.py to result directory
shutil.copy(__file__, result_dir)

The input file/folder and the runscript are automatically copied into the result folder.

Next variables specifying the desired solver and objective function are set:

# choose solver (cplex, glpk, gurobi, ...)
solver = 'glpk'

# objective function
objective = 'cost'  # set either 'cost' or 'CO2' as objective

The solver has to be licensed for the specific user, where the open source solver “glpk” is used as the standard. For the objective function urbs currently allows for two options: “cost” and “CO2” (case sensitive). In the former case the total system cost and in the latter case the total CO2-emissions are minimized.

The model parameters are finalized with a specification of timestep length and modeled time horizon:

# simulation timesteps
(offset, length) = (3500, 168)  # time step selection
timesteps = range(offset, offset+length+1)
dt = 1  # length of each time step (unit: hours)

The 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 function arguments to create_model() and accessible directly, so that one can quickly reduce the problem size by reducing the simulation length, i.e. the number of timesteps to be optimised. Variable dt is the duration of each timestep in the list in hours, where any positiv real value is allowed.

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]

Output Settings

The desired output is also specified by the user in the runscript. It is split into two parts: reporting and plotting. The former is used to generate an excel output file and the latter for standard graphs.

Reporting

urbs by default generates an .xlsx-file as an ouput in result_dir. This file includes all commodities of interest to the user and can be specified as report tuples each consisting of a given year, sites and commodities combination. Information about these commodities is summarized both in sum (in sheet “Energy sums”) and as individual timeseries (in sheet “… timeseries”).

# detailed reporting commodity/sites
report_tuples = [
    (2019, 'North', 'Elec'),
    (2019, 'Mid', 'Elec'),
    (2019, 'South', 'Elec'),
    (2019, ['North', 'Mid', 'South'], 'Elec'),
    (2024, 'North', 'Elec'),
    (2024, 'Mid', 'Elec'),
    (2024, 'South', 'Elec'),
    (2024, ['North', 'Mid', 'South'], 'Elec'),
    (2029, 'North', 'Elec'),
    (2029, 'Mid', 'Elec'),
    (2029, 'South', 'Elec'),
    (2029, ['North', 'Mid', 'South'], 'Elec'),
    (2034, 'North', 'Elec'),
    (2034, 'Mid', 'Elec'),
    (2034, 'South', 'Elec'),
    (2034, ['North', 'Mid', 'South'], 'Elec'),
    ]

# optional: define names for sites in report_tuples report_sites_name = {(‘North’, ‘Mid’, ‘South’): ‘All’}

Optionally it is possible to define clusters of sites for aggregated information and with report_sites_name it is then possible to name these. If they are empty, the default value will be taken.

Plotting

urbs generates default result images. Which images exactly are desired can be set by the user. via the following input lines:

# plotting commodities/sites
plot_tuples = [
    (2019, 'North', 'Elec'),
    (2019, 'Mid', 'Elec'),
    (2019, 'South', 'Elec'),
    (2019, ['North', 'Mid', 'South'], 'Elec'),
    (2024, 'North', 'Elec'),
    (2024, 'Mid', 'Elec'),
    (2024, 'South', 'Elec'),
    (2024, ['North', 'Mid', 'South'], 'Elec'),
    (2029, 'North', 'Elec'),
    (2029, 'Mid', 'Elec'),
    (2029, 'South', 'Elec'),
    (2029, ['North', 'Mid', 'South'], 'Elec'),
    (2034, 'North', 'Elec'),
    (2034, 'Mid', 'Elec'),
    (2034, 'South', 'Elec'),
    (2034, ['North', 'Mid', 'South'], 'Elec'),
    ]

# optional: define names for sites in plot_tuples
plot_sites_name = {('North', 'Mid', 'South'): 'All'}

# plotting timesteps
plot_periods = {
    'all': timesteps[1:]
    }

The logic is similar to the reporting case discussed above. With the setting of plotting timesteps the exact range of the plotted result can be set. In the default case shown this range is all modeled timesteps. For larger optimization timestep ranges this can be impractical and instead the following syntax can be used to hard code which steps are to be plotted exactly.

# plotting timesteps
plot_periods = {
    'win': range(1000:1168),
    'sum': range(5000:5168)
    }

In this example two 1 week long ranges are plotted between the specified time steps. Using this make sure, that the chosen ranges are subsets of the modeled time steps themselves.

The plot colors can be customized using the module constant COLORS. All plot colors are user-definable 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:

Scenarios

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

The scenarios list in the end of the input file allows then to select the scenarios to be actually run.

scenarios = [
             urbs.scenario_base,
             urbs.scenario_stock_prices,
             urbs.scenario_co2_limit,
             urbs.scenario_co2_tax_mid,
             urbs.scenario_no_dsm,
             urbs.scenario_north_process_caps,
             urbs.scenario_all_together
            ]

The following scenario functions are specified in the subfolder urbs in script scenarios.py.

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.

Run scenarios

This now finally is the function that gets everything going. It is invoked in the very end of the runscript.

for scenario in scenarios:
prob = urbs.run_scenario(input_files, solver, timesteps, scenario,
                    result_dir, dt, objective,
                    plot_tuples=plot_tuples,
                    plot_sites_name=plot_sites_name,
                    plot_periods=plot_periods,
                    report_tuples=report_tuples,
                    report_sites_name=report_sites_name)

Having prepared settings, input data and scenarios, the actual computations happen in the function run_scenario() of the script runfunctions.py in subfolder urbs. 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 = read_input(input_files,year)
data = scenario(data)
validate_input(data)

Function read_input() returns a dict data of up to 12 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. To then rule out a list of known errors, that accumulate through growing user experience, a variety of validation functions specified in script validate.py in subfolder urbs is run on the dict data.

Solving

# create model
prob = urbs.create_model(data, dt, timesteps)

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

This section is the “work horse”, where most computation and time is spent. The optimization problem is first defined (create_model()) and populated with parameter values with values. 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 line calls the solver and reads 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.